RDS SQL Server - 专题分享 - 巧用执行计划缓存之数据类型隐式转换

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
简介: # 摘要 SQL Server数据库基表数据类型隐式转换,会导致Index Scan或者Clustered Index Scan的问题,这篇文章分享如何巧用执行计划缓存来发现数据类型隐式转换的查询语句,从而可以有针对性的优化查询,解决高CPU使用率的问题。 # 问题引入 数据类型转化是导致SQL Server高CPU使用率的又一大杀手,详情参见之前的云栖社区文章:[RDS SQL Serve

摘要

SQL Server数据库基表数据类型隐式转换,会导致Index Scan或者Clustered Index Scan的问题,这篇文章分享如何巧用执行计划缓存来发现数据类型隐式转换的查询语句,从而可以有针对性的优化查询,解决高CPU使用率的问题。

问题引入

数据类型转化是导致SQL Server高CPU使用率的又一大杀手,详情参见之前的云栖社区文章:RDS SQL Server - 最佳实践 - 高CPU使用率系列之数据类型转换。SQL Server对基表数据类型转换会导致Index Scan或者Clustered Index Scan,进而导致IO使用率的大幅上升,最终导致CPU的使用率大幅升高。这篇文章是从执行计划缓存缓存的角度来找出导致数据类型转换的查询语句,进而做有针对性的查询语句优化,来破解高CPU使用率的问题。

测试环境

为了更好的展示从执行计划缓存缓存中找出导致数据类型转化的查询语句,我们先建立测试环境。

-- Create testing database
IF DB_ID('TestDb') IS NULL
    CREATE DATABASE TestDb;
GO

USE TestDb
GO

-- create demo table SalesOrder
IF OBJECT_ID('dbo.SalesOrder', 'U') IS NOT NULL
BEGIN
    TRUNCATE TABLE dbo.SalesOrder
    DROP TABLE dbo.SalesOrder
END
GO

CREATE TABLE dbo.SalesOrder
(
    RowID INT IDENTITY(1,1) NOT NULL
    , OrderID UNIQUEIDENTIFIER NOT NULL
    , ItemID INT NOT NULL
    , UserID INT NOT NULL
    , OrderQty INT NOT NULL
    , Price DECIMAL(8,2) NOT NULL
    , OrderDate DATETIME NOT NULL 
        CONSTRAINT DF_OrderDate DEFAULT(GETDATE())
    , LastUpdateTime DATETIME NULL
    , OrderComment NVARCHAR(100) NULL
    , CONSTRAINT PK_SalesOrder PRIMARY KEY(
        OrderID
    )
);

-- data init for 2 M records.
;WITH a 
AS (
    SELECT * 
    FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a)
), RoundData
AS(
SELECT TOP(2000000)
    OrderID = NEWID()
    ,ItemIDRound = abs(checksum(newid()))
    ,Price = a.a * b.a * 10
    ,OrderQty = a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a
FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h
), DATA
AS(
SELECT 
    OrderID
    ,ItemID = cast(ROUND((1300 * (ItemIDRound*1./cast(replace(ItemIDRound, ItemIDRound, '1' + replicate('0', len(ItemIDRound))) as bigint)) + 101), 0) as int)
    ,UserID = cast(ROUND((500 * (ItemIDRound*1./cast(replace(ItemIDRound, ItemIDRound, '1' + replicate('0', len(ItemIDRound))) as bigint)) + 10000), 0) as int)
    ,OrderQty
    ,Price = cast(Price AS DECIMAL(8,2))
    ,OrderDate = dateadd(day, -cast(ROUND((50 * (ItemIDRound*1./cast(replace(ItemIDRound, ItemIDRound, '1' + replicate('0', len(ItemIDRound))) as bigint)) + 1), 0) as int) ,GETDATE())
FROM RoundData
)
INSERT INTO dbo.SalesOrder(OrderID, ItemID, UserID, OrderQty, Price, OrderDate, LastUpdateTime, OrderComment)
SELECT 
    OrderID
    , ItemID
    , UserID
    , OrderQty
    , Price
    , OrderDate
    , LastUpdateTime = OrderDate
    , OrderComment = N'User ' + CAST(UserID AS NVARCHAR(8)) + N' purchased item ' + CAST(ItemID AS NVARCHAR(8))
FROM DATA;
GO

--===============query

USE [TestDb]
GO
ALTER TABLE dbo.SalesOrder
ALTER COLUMN UserID CHAR(8) NULL
GO

EXEC sys.sp_help 'dbo.SalesOrder'

USE [TestDb]
GO
SELECT
    ItemID, OrderQty, Price
FROM dbo.SalesOrder
WHERE UserID = 10057

SELECT TOP 100
    ItemID, OrderQty, Price
FROM dbo.SalesOrder
WHERE UserID = 10058
    AND OrderDate >= DATEADD(MONTH, -1, GETDATE())
    AND OrderDate <= GETDATE();


USE [TestDb]
GO
SELECT
    ItemID, OrderQty, Price
FROM dbo.SalesOrder
WHERE UserID = 10059

SELECT TOP 100
    ItemID, OrderQty, Price
FROM dbo.SalesOrder
WHERE UserID = 10061
    AND OrderDate >= DATEADD(MONTH, -1, GETDATE())
    AND OrderDate <= GETDATE();

查看SalesOrder表结构,我们很清楚的看到UserID数据类型是CHAR(8),而查询WHERE语句中的WHERE UserID = XXXX中,等号右边的数据类型为INT,这会导致SQL Server将数据类型优先级低的数据类型CHAR转化为INT,SQL Server需要将这个表中的200万条记录的UserID从CHAR(8)转化为INT。所以,必须进行Scan操作,从而导致高CPU使用率。
01.png

从执行计划来看,也的确导致了数据类型的隐式转换:
02.png

解决方法

从测试环境部分,我们发现的确导致了数据类型的隐式转换。以下短短100行代码,可以从执行计划缓存中找出导致数据类型隐式转化的查询语句和执行计划的详细信息。

USE testdb
GO
DECLARE
    @db_name SYSNAME
;
SET
    @db_name = QUOTENAME(DB_NAME())
;

;WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
, planCache
AS(
    SELECT 
       stmt.value('(@StatementText)[1]', 'varchar(max)') AS stmt, 
       n.t.value('(ScalarOperator/Identifier/ColumnReference/@Database)[1]', 'sysname') AS DatabaseName,
       REPLACE(REPLACE(n.t.value('(ScalarOperator/Identifier/ColumnReference/@Schema)[1]', 'sysname'), '[', ''), ']', '') AS SchemaName, 
       REPLACE(REPLACE(n.t.value('(ScalarOperator/Identifier/ColumnReference/@Table)[1]', 'sysname'), '[', ''), ']', '') AS ObjectName, 
       REPLACE(REPLACE(n.t.value('(ScalarOperator/Identifier/ColumnReference/@Column)[1]', 'sysname'), '[', ''), ']', '') AS ColumnName, 
       n.t.value('(@DataType)[1]', 'sysname') AS ConvertTo, 
       n.t.value('(@Length)[1]', 'int') AS ConvertToLength, 
       PhysicalOperator.value('(.//Predicate/ScalarOperator/@ScalarString)[1]','nvarchar(max)') AS ScalarString,
       query_plan,
       cp.plan_handle
    FROM sys.dm_exec_cached_plans AS cp 
        CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qp
        CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS batch(stmt)
        CROSS APPLY stmt.nodes('//ScalarOperator/Compare/ScalarOperator/Convert[@Implicit="1"]') AS n(t) 
        CROSS APPLY batch.stmt.nodes('.//RelOp[@PhysicalOp=''Index Scan'' or @PhysicalOp=''Clustered Index Scan'']') as RelOp(PhysicalOperator)
    WHERE n.t.exist('ScalarOperator/Identifier/ColumnReference[@Database=sql:variable("@db_name")][@Schema != ''[sys]'']') = 1
)
SELECT 
    pc.stmt,
    pc.DatabaseName,
    pc.SchemaName,
    pc.ObjectName,
    pc.ColumnName,
    ps.UsedPageCount,
    ix.name AS IndexName,
    CAST(ps.UsedPageCount/ 128. AS decimal(12,2)) AS UsedSizeMB,
    ps.TotalRowCount,
    qs.execution_count * UsedPageCount AS MostLogicalRead,
    cols.DATA_TYPE AS ConvertFrom, 
    cols.CHARACTER_MAXIMUM_LENGTH AS ConvertFromLength,
    pc.ConvertTo,
    pc.ConvertToLength,
    pc.ScalarString,
    pc.query_plan,
    qs.creation_time
    ,qs.last_execution_time
    ,qs.execution_count
    ,qs.total_worker_time
    ,qs.last_worker_time
    ,qs.min_worker_time
    ,qs.max_worker_time
    ,qs.total_physical_reads
    ,qs.last_physical_reads
    ,qs.min_physical_reads
    ,qs.max_physical_reads
    ,qs.total_logical_writes
    ,qs.last_logical_writes
    ,qs.min_logical_writes
    ,qs.max_logical_writes
    ,qs.total_logical_reads
    ,qs.last_logical_reads
    ,qs.min_logical_reads
    ,qs.max_logical_reads
    ,qs.total_clr_time
    ,qs.last_clr_time
    ,qs.min_clr_time
    ,qs.max_clr_time
    ,qs.total_elapsed_time
    ,qs.last_elapsed_time
    ,qs.min_elapsed_time
    ,qs.max_elapsed_time
    ,qs.total_rows
    ,qs.last_rows
    ,qs.min_rows
    ,qs.max_rows
FROM planCache AS pc
    INNER JOIN INFORMATION_SCHEMA.COLUMNS as cols WITH(NOLOCK)
    ON pc.SchemaName = cols.TABLE_SCHEMA
    AND pc.ObjectName = cols.TABLE_NAME
    AND pc.ColumnName = cols.COLUMN_NAME
    INNER JOIN sys.tables as tb WITH(NOLOCK)
    ON tb.schema_id = schema_id(pc.SchemaName)
        AND tb.name = pc.ObjectName
    INNER JOIN sys.indexes as ix WITH(NOLOCK)
    ON tb.object_id = ix.object_id
    LEFT JOIN (
                SELECT 
                    object_id,
                    index_id,
                    sum(used_page_count) AS UsedPageCount,
                    sum(row_count) AS TotalRowCount 
                FROM sys.dm_db_partition_stats as dps WITH(NOLOCK)
                GROUP BY object_id,Index_id
                ) as ps 
    ON ix.object_id = ps.object_id and ix.index_id = ps.index_id
    left join sys.dm_exec_query_stats qs on pc.plan_handle= qs.plan_handle
--DBCC freeproccache

查询结果的一步截图如下:
03.png

总结

这篇文章分享了如何从执行计划缓存中找到导致SQL Server数据类型隐式转化的查询语句,为我们针对特定查询语句的优化提供了基础,最终破解高CPU使用率的问题。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
13天前
|
SQL 存储 缓存
浅析MySQL中的SQL执行过程
本文探讨了MySQL的体系结构、SQL执行流程及SQL执行时间分析方法。首先介绍了MySQL由连接层、SQL层和存储引擎层构成;接着详细描述了SQL从客户端发送到服务器执行的具体流程;最后,通过启用profiling功能,展示了如何分析SQL执行时间,并说明了MySQL 8.0版本后移除查询缓存的原因。
浅析MySQL中的SQL执行过程
|
28天前
|
SQL 关系型数据库 MySQL
【MySQL】根据binlog日志获取回滚sql的一个开发思路
【MySQL】根据binlog日志获取回滚sql的一个开发思路
|
28天前
|
SQL 关系型数据库 MySQL
在MySQL中,什么是结构化查询语言 (SQL)
【8月更文挑战第20天】在MySQL中,什么是结构化查询语言 (SQL)
33 1
|
28天前
|
SQL 存储 关系型数据库
【MySQL核心】MySQL 数据恢复-ibd2sql
【MySQL核心】MySQL 数据恢复-ibd2sql
|
3天前
|
存储 SQL 关系型数据库
【MySQL调优】如何进行MySQL调优?从参数、数据建模、索引、SQL语句等方向,三万字详细解读MySQL的性能优化方案(2024版)
MySQL调优主要分为三个步骤:监控报警、排查慢SQL、MySQL调优。 排查慢SQL:开启慢查询日志 、找出最慢的几条SQL、分析查询计划 。 MySQL调优: 基础优化:缓存优化、硬件优化、参数优化、定期清理垃圾、使用合适的存储引擎、读写分离、分库分表; 表设计优化:数据类型优化、冷热数据分表等。 索引优化:考虑索引失效的11个场景、遵循索引设计原则、连接查询优化、排序优化、深分页查询优化、覆盖索引、索引下推、用普通索引等。 SQL优化。
【MySQL调优】如何进行MySQL调优?从参数、数据建模、索引、SQL语句等方向,三万字详细解读MySQL的性能优化方案(2024版)
|
3天前
|
存储 关系型数据库 MySQL
MySQL高级篇——覆盖索引、前缀索引、索引下推、SQL优化、主键设计
覆盖索引、前缀索引、索引下推、SQL优化、EXISTS 和 IN 的区分、建议COUNT(*)或COUNT(1)、建议SELECT(字段)而不是SELECT(*)、LIMIT 1 对优化的影响、多使用COMMIT、主键设计、自增主键的缺点、淘宝订单号的主键设计、MySQL 8.0改造UUID为有序
MySQL高级篇——覆盖索引、前缀索引、索引下推、SQL优化、主键设计
|
1天前
|
SQL 监控 关系型数据库
MySQL数据库中如何检查一条SQL语句是否被回滚
检查MySQL中的SQL语句是否被回滚需要综合使用日志分析、事务状态监控和事务控制语句。理解和应用这些工具和命令,可以有效地管理和验证数据库事务的执行情况,确保数据的一致性和系统的稳定性。此外,熟悉事务的ACID属性和正确设置事务隔离级别对于预防数据问题和解决事务冲突同样重要。
9 2
|
4天前
|
SQL 关系型数据库 MySQL
SQL和MySQL
SQL和MySQL
14 1
|
24天前
|
缓存 NoSQL 算法
【Azure Redis 缓存】Redis性能指标之Server Load
【Azure Redis 缓存】Redis性能指标之Server Load
|
24天前
|
SQL 关系型数据库 MySQL
【MySQL 慢查询秘籍】慢SQL无处遁形!实战指南:一步步教你揪出数据库性能杀手!
【8月更文挑战第24天】本文以教程形式深入探讨了MySQL慢SQL查询的分析与优化方法。首先介绍了如何配置MySQL以记录执行时间过长的SQL语句。接着,利用内置工具`mysqlslowlog`及第三方工具`pt-query-digest`对慢查询日志进行了详细分析。通过一个具体示例展示了可能导致性能瓶颈的查询,并提出了相应的优化策略,包括添加索引、缩小查询范围、使用`EXPLAIN`分析执行计划等。掌握这些技巧对于提升MySQL数据库性能具有重要意义。
50 1