SQL Server Performance 分析

对网络上的一篇博客做下笔记,适当扩展下对 Performance 各个涉及到的要素。这篇文章讲的是分析性能,老外写的:

How to analyse SQL Server performance

主要的要点有这些:

· How does SQL Server work

· Wait Info for currently executing requests

· Aggregated wait stats: sys.dm_os_wait_stats

· Common wait types

· Analyze disk activity: IO stats

· Analyzing individual query execution

· Identifying problem queries

· Missing Indexes

· TEMPDB Performance

· SQL Server usage performance counters

· IO related performance counters

· Memory related performance counters

· CPU performance counters

· SQL Server blocking related performance counters

· Network performance counters

· Defining your own counters

· Advanced Performance Analysis;

· The USE method

概括一下,就是请求到受理,受理到执行,执行到结果,结果到展现。 每部分都会有些等待,弄清楚这些等待,就知道为什么请求处理得那么缓慢了。最后该改善硬件就改,改重写 SQL 就重新写。所有的等待都可以在这张表里找到 sys.dm_os_wait_stats。

这里面涉及到的硬件层面,无非就是网络,CPU,内存,存储。针对这四大主题,应该对 Performance Counter 多加留意, 也就是这张表 sys.dm_os_performance_counters 里面的性能指标要熟悉; 硬件层面如果没有问题,等待时间比较合理,那么出问题的就该是软件了,这里涉及到的软件,主要也就是 SQL Execution Plan。 执行计划是如何产生的,如何被优化的,有哪些执行树,执行符号等等。另外一个软件方面的重点就是并发,并发会有锁,有锁就有阻塞,锁的应用与并发是一种平衡术,锁越多,并发支持就弱了,想要并发支持多,就要牺牲 ACID 。

先来看下一条简单的SQL 请求是如何在SQL Server 内部流走的,会遇到哪些模块,怎么被处理成一个结果集的。

1 客户端发起请求

2 SQL Server 中的伺服线程接到请求,将其排队

3 Worker 线程被总调度安排来接收一个排队中的请求。这个总调度有个时尚的名儿,叫做 Scheduler.

4 Worker 线程调用 Optimizer, 来编译解析出 Execution Plan, 并将其导入 Query Engine 执行。

5 Worker经过等待资源请求,占用资源做运算,从硬盘读取数据到缓存,最终从缓存来取数据后,返回给客户端。 Worker 被放回线程池 thread pool, 等待执行新的请求。

6 客户端展现

SQL Server Performance 分析_第1张图片

(以上的图来自网络:Understanding how SQL Server executes a query)

还是从问题出发,能问出问题的多少,反映了自己知道的层次,更能翻出自己自己薄弱的地方。

如果有不明白的地方,可以参考这篇文章,写得很详细:Understanding how SQL Server executes a query

1 客户端,在 SQL Server 看来有两个概念,一个是 connection, 一个是 session。 两者的区别,connection 是物理的链路,这条链路可以被不同的 session 公用,在这条链路上,可以跑很多的 session。 Connection 是消费内存的,可以从 sys.dm_os_performance_counters 中找到 Connection 当前消耗的内存。 可以看到成千上万个 connection 是很耗内存的,这也会一方面减低内存对缓存的支持。怎么降低 connection 的消耗呢 ? 使用同一个 connection string , 字符数目,大小写,使用的授权访问模式等都相同。所以 connection 公用是一个最佳实践。

2 请求被排队:首先我么要知道有哪些请求正在排队中,分别排了多长时间了,平均排队时间有多少,排队时间越长,无疑说明了有处理瓶颈;这个队肯定也耗内存, 那么究竟多少内存被用在排队上了?什么样的等待出现,说明队列有压力了?

系统表 sys.dm_exec_requests 罗列了所有发到 SQL Server 端的请求,包含了来不及处理的和正在执行的请求。 已经处理过的请求就不会记录在内了。(好好考虑已经处理过的请求都跑哪里去了,我们该如何找到这些处理过的请求,通过这些请求可以找出曾经干过坏事的那些客户端)。

这里牵涉到两个概念,一个是 request, 一个是 task 。 request 前面已经介绍了,这里讲下 task 的概念。 Task 代表了一个 SQL Batch 从请求处理开始到完成的全程。如果 SQL Batch 里面有多个子 SQL 语句,并且是可以并行执行的,那么会重新建立 Task 并设定在 pending 状态下,等待 worker 来执行。Task 可以从系统表里面查到 sys.dm_os_tasks。 这里的 Task 一定是正在执行的 SQL Batch , 不论这个 task 是在等待资源还是正在执行? 理解错误! 这里的 Task 就是 request 放到服务器时候生成的,还包含了一部分已经完成的 Task.

Task 是由 worker 来执行完成的, work 相当于是一个线程, workers 就是线程池。这个线程池可以设定最大值,在需要的时候自动创建出来执行 Task。 通过 sp_configure 可以设定最大值。

exec sp_configure @configname = 'max worker threads', @configvalue = '65535'

reconfigure

所有正在执行的线程 worker 都可以查询到:

select * from sys.dm_os_workers

where state = 'RUNNING'

我们可以写段多线程的访问程序,看看是不是客户端的线程增加访问量了,SQL SERVER 服务器的 worker 数量也会相应增多。每个 worker 能吃掉多少内存?SQL Server 的内存大部分都是缓存,有数据缓存,也有执行计划缓存,也包括线程池用掉的内存,只不过占比比较小 。

SQL Server Performance 分析_第2张图片

3 接下来就是做语句级别的 Optimize 了。 语句先汇编解析,通常有软解析与硬解析。软解析就是分析下语句的格式,看看是不是参数化了或者有相应的已经编译好的执行树可以用。硬解析就是要重新编译生成执行树了。硬解析会耗 CPU 资源,所以造成 CPU 级别的锁或者等待,给并发造成延迟。那么引起硬解析的因素会有哪些呢? 也就是要分析在解析的过程中,需要做哪些分析来确定是不是要做硬解析了?

硬解析做的事情就是从头编译一边 SQL 语句, 从语法验证开始,SQL 语句没有语法错误,到 SQL 的语义解析,即SQL 引用的表和字段都能找到,都存在,直到最后一步的访问数据。 这里面只要涉及到的表和字段有更新, 执行计划都要重新编译,也就是硬解析了。 罗列下所有会引起硬解析的场景:

· Changes made to a table or view referenced by the query (ALTER TABLE and ALTER VIEW).

· Changes made to a single procedure, which would drop all plans for that procedure from the cache (ALTER PROCEDURE).

· Changes to any indexes used by the execution plan.

· Updates on statistics used by the execution plan, generated either explicitly from a statement, such as UPDATE STATISTICS, or generated automatically.

· Dropping an index used by the execution plan.

· An explicit call to sp_recompile.

· Large numbers of changes to keys (generated by INSERT or DELETE statements from other users that modify a table referenced by the query).

· For tables with triggers, if the number of rows in the inserted or deleted tables grows significantly.

· Executing a stored procedure using the WITH RECOMPILE option.

总结起来也就是三大方向, schema 和 data 的更新, 手工重新编译,都能引起硬解析。硬解析会引起 CPU 的过度消耗,所以对 AUTO_UPATE_STATISTICS 这个 database 级别的 Option 需要随时调整。

最后一个有可能引起查询计划重新编译的情况就是,在内存压力情况下,部分执行计划会被从内存中移出。从内存中移出执行计划有两种情况, 一种是手工 DBCC DROPPROCCACHE; 一种是 database engine 检测到某一个执行计划的cost 已经将为0 并且这个时候服务器有内存压力了,那么 database engine 就会帮忙移出这些为 0 的执行计划用来缓解内存压力 。 计划缓存 (plan cache) 一般存有两种缓存, 一是 ad hoc query plan , 这种查询计划的 cost 为0 ,在压力下这种查询计划最早被移出;二是 store procedure query plan, 这种计划的初始 cost 为第一次解析时候的cost, 如果在下一次 database engine examine 的时候,没有被重用,cost 就减 1, 如果被重用,就恢复到初始值。碰到服务器有内存压力了, database engine 就会频繁的去做 examine , 直到压力缓解。

4 Query Memory Grant :worker 是线程, 在执行的时候,肯定需要内存,来存储变量,来存储数据集,那么问题来了,每个线程占用的内存大小该如何计算呢, 存储的临时数据集都存在哪里了呢,会不会放到缓存一份,再拷贝一份到线程中?如果有两份数据缓存,那么锁是如何加载并同步在两份数据集上的呢?据我理解, 锁肯定是加在缓存中的数据页上面,可以加载行级也可以加载页级。然后根据操作符从数据页中加载出相应的记录集,放到 virtual table 里面, SQL Server 有预读取页的特性, 就是在找到对应的记录时候,顺便把页读取到内存的缓存里面。 所以 worker 不大可能再费劲完整的拷贝一份到执行线程中,而是基于预先读取的这些数据页,慢慢得再加载所需要的数据集到 virtual table 里面。

那么内存是怎么被分配到 worker 里面的呢? 如果一下子就分配了足够的内存,那么大容量并发的时候就会占用大量的服务器内存,而导致内存不够用。所以内存是一点一点分配过来的,先分配一个 reservation, 在这个 reservation 限定的范围内,根据运行时需求再分配内存,一般 reservation 的内存是足够用的,如果超出这个范围,就会将数据缓存到 tempdb 里面,这样性能呢将下降。但并不是所有的 worker 线程都会被分配reservation的,只有在大数据读取或者复杂运算的时候,才有这个动作, 比如对大表进行全表扫描,对海量数据进行排序或者汇总计算等等。每一个 worker 的内存分配都可以在 sys.dm_exec_query_memory_grants 中找到。

在探索 optimizer 和 query engine 如何产生最优计划并执行访问数据之前,对底层存储层做一次回顾,加深存储结构的理解也可以体会为什么执行计划这么做是最优的。目前的结构存储只要是针对表和索引的,当然在更高级的版本中有 memory-optimized table 一说,memory-optimized table 有点复杂,具体的限制条件也很多,暂时不讨论了,稍微了解下。

表一级有 heap table 和 clustered index table 区别。太基础的东西先不谈, 我自己对这一级存在的疑问或者说不熟悉的地方还有很多,所以先把这些研究一下。一是表结构设计,谈到表,小表的设计没什么问题,大表的设计就有说法了,表分区,有分区 schema, 有分区函数,自己不常用,所以先把大致的方法下来:先建立一个分区函数(partition function), 在建立分区表的时候,以分区字段调用分区函数。这里有必要谈下分区函数用到的分区字段,这里只允许依照分区字段的离散值来分区,而不能依据分区字段的值区间分区。相比oracle的三种分区函数,SQL SERVER仅支持了 list表分区。再建立分区 scheme(partition scheme), 按照某一个字段(比如时间字段,按照月份作分区)建立 scheme, 比如从 2010年 1月份开始建立到 2020年12月份的分区 scheme。scheme的作用就是将分区函数指定的分区值区间对应到不同的文件组file group 上。

语法:

CREATE PARTITION FUNCTION partition_function_name ( input_parameter_type )
AS RANGE [ LEFT | RIGHT ] 
FOR VALUES ( [ boundary_value [ ,...n ] ] ) 
[ ; ]
CREATE PARTITION SCHEME partition_scheme_name
AS PARTITION partition_function_name
[ ALL ] TO ( { file_group_name | [ PRIMARY ] } [ ,...n ] )
[ ; ]
create partition function Monthly(datetime)

as range left

for values('20160101','20160201','20160301','20160401','20160501')

go

create partition scheme MonthlySch

as partition Monthly

all to ([PRIMARY])

go

CREATE TABLE dbo.FctSalesMonth (OrderMon datetime, OrderAmount int ) on MonthlySch(OrderMon)

go

insert into dbo.FctSalesMonth (OrderMon,OrderAmount) values('2016-01-01',200)

insert into dbo.FctSalesMonth (OrderMon,OrderAmount) values('2016-01-02',200)

insert into dbo.FctSalesMonth (OrderMon,OrderAmount) values('2016-02-01',200)

insert into dbo.FctSalesMonth (OrderMon,OrderAmount) values('2016-03-04',200)

go

select partition_id,object_name(object_id) as tableName,index_id,partition_number,row_count

from sys.dm_db_partition_stats where object_id = object_id(N'dbo.FctSalesMonth')

go

SQL Server Performance 分析_第3张图片

这里要注意的地方就是 AS RANG LEFT|RIGHT 的区别:LEFT 表示以左边第一个值为基准,小于等于这个值的其他记录都放到第一个分区,大于第一个值且小于等于第二个值的记录都放在第二个分区。LEFT与RIGHT是非常绕的两个概念,我是这样理解的:上面列出的这些临界点,把整个平面划成了左右两部分,就是LEFT RANGE 和RIGHT RANGE。当我们需要把这些临界点放在左边的RANGE的时候,我们就用RANGE LEFT,相反我们需要把临界点放在右边这个RANGE的时候,我们就用RANGE RIGHT。

分区的目的无非就是把一张大表拆分成若干个小表存在不同的存储介质上,以减轻磁盘访问的压力,支持大并发。比如我服务器上有三块硬盘,每块1T,一张表有6亿条数据,如果将这一张表都存储在同一块硬盘上,磁盘访问的顺序肯定是按照一定顺序来的,要么是sequential read要么是random read,但如果分成2亿条数据存一块硬盘,那么同一时间可以访问原先3倍的数据。速度孰快一眼便知。

表的字段表示方式,没有了解过这些字段以什么方式存储在硬盘上的,比如 char(1), 代表的是存储一个字符,这一个字符代表的是一个 ASCII 码,那么就只能存储 255 个字符。那么其实就是一个字节的存储。那其他的比如 datetime,是如何存储的呢?

datetime的存储分为两个integer, 第一个Integer存储的是从1900/1/1/以来经过的天数:

select convert(int,substring(convert(varbinary(8),getutcdate()),1,4)), datediff(day,0,getutcdate())

第二个integer存储的是从午夜开始以来,经过的 tick 数目,单位是毫秒,一个tick代表了300毫秒:

select convert(int,substring(convert(varbinary(8),getutcdate()),5,4))/300

,datediff(s,convert(date,getutcdate()),getutcdate())

索引,sql server 从这个版本其开始有列式索引 columnstore index 了,这种索引带来的优点和使用方法需要重点研究一下;索引的使用限制,就是在什么情况下,索引能被引用到,什么情况下,索引失效。

索引能被引用的地方,这还得分单表查询与多表查询,单键与多建的情况。

单表查询与多表查询对索引的限制,其实就是在where子句或者join子句中,被索引字段不能有函数,(相比较oracle的函数索引就不同)。第二点就是like子句中,通配符%不能在整个字符串之前。举两个不走索引的例子:假设 dbo.sales有orderMonth这个字段的索引,字段是整型,那么convert(varchar(6),orderMonth)=’201601’是不会走索引的,orderMonth like ‘%05’ 也是走不了索引的。并且,如果where子句有其他的限制条件,那么索引的字段一定是要放在第一个位置。因为不放被索引字段在第一个位置,而放了没有被索引的字段在第一个位置,那么肯定走了一遍全表扫描,索引也就失效了。(这里也要考虑查询优化器是不是会识别这种模式,优先生成被索引字段在前的计划)

单键与多键的情况就较复杂了。首先单键与多键的情况也必须满足单表与多表的限制条件才能走索引。其次多键的情况下,还要考虑被索引字段的排列顺序。比如我们把整个数据库的表结构作一张表,按照object_id与column_id做一个符合索引。那么当我们按照object_Id,column_Id作索引查询的时候,分别互换object_id,column_Id,会不会都走索引呢?

select t.object_id, col.column_id, object_name(t.object_id) as table_name, col.name as column_name

into lenistest4.dbo.siebeldbTableSchema

from siebeldb.sys.tables t

inner join siebeldb.sys.columns col on t.object_id = col.object_id

create index idx_obj_col_id on dbo.siebeldbTableSchema (object_id,column_id)

用上面的代码将某一个数据库的表结构保存下来,放到一张表里,然后将这个表的object_id,column_Id顺序建了一个索引。我们分别看下面的查询,执行计划是不是会走索引。

select object_id,column_id

from siebeldbTableSchema

where object_id = 1415428562 and column_id = 11

select object_id,column_id

from siebeldbTableSchema

where column_id = 11 and object_id = 1415428562

两个语句执行之间还需要清空数据库的执行计划缓存:

dbcc freeproccache

select * from sys.dm_exec_query_stats

select * from sys.dm_exec_cached_plans

第一个DBCC用来清空执行计划缓存,后面两个查询语句主要用来确定执行计划缓存是不是清空。上面两个查询的执行计划还是被优化器识别过来了,都走了索引。

那么如果我们的输出列有一列不在索引列里面,需要通过RID LOOKUP去定位堆表里面的某一个字段,是不是也会走索引?

select object_id,column_id ,column_name

from siebeldbTableSchema

where object_id = 1415428562 and column_id = 11

select object_id,column_id ,column_name

from siebeldbTableSchema

where column_id = 11 and object_id = 1415428562

其实同样,也一样走索引。只不过多了一层Nested LOOPS.

接下来我们将没有被索引的字段放在第一个位置,看看是不是走索引?

select object_id,column_id ,column_name

from siebeldbTableSchema

where table_name = 'S_CONTACT' and column_id = 11 and object_id = 1415428562

可见sqlserver 的查询优化器很灵,居然能识别出来有索引的字段将其放在执行计划有力的位置。以上的查询还是顺利地走了索引。

Columnstore index 列式索引也是一大特色,从SQL SERVER 2012起,支持列式索引了。我们要谈一谈列式索引的概念,用法,比较下与row-based index的区别,特别是列式索引的存储。据说列式索引采用了独特的压缩方式。这种压缩方式叫xVelocity(前称VertiPaq),专门用于 Analysis Service和Power Pivot的数据存储,现将其移到relational database storage engine中来。

1) ColumnStore index data Structure: 从物理存储上来说,ColumnStore index 在page之上加了一层抽象,segment。一个segment就是一列索引的字段。如果我们新创建一个ColumnStore Index,就会有两个segment。每个segment会有一个存储的上限,每一个segment都可以包含很多数据页(data page)。一个columnstore index的所有segment,按照从上到下一一对应排序。也就是说,如果我们新建一个2列的columnstore index,第一个segment的第一个行,和第二个segment的第一个行,组成了堆表里面的第一行。

2)按照列来存储,有3个好处:一来存储的都是同质化的(homogenous)数据,压缩采用的函数比较高效;二来针对重复值比较多的列,可以采用 dictionary的方式存储,key部分存储在索引上,value部分放在dictionary 里面,省下很多空间,查询产生的IO就更小了;再一个因为每一个segment存储了单一的值,减少了一些大字段的占用空间,很多预读的数据页就极大减少了不必要字段,IO更有效率。

3)Batch Mode Processing:SQL Server 有三种处理数据集的方式, 一种是 row-based, 一行一行处理,一种是 Batch mode, 一个batch包含了1000条数据,每一个列在这个batch里面被称之为vector,基于vector的处理方法,叫做batch processing。当然我们可以把row-based, batch mode合并起来应用,这是第三种方式。

针对 colunmstore index,借用Robert Sheldon的一张图,可以获得清晰的存储认识:
SQL Server Performance 分析_第4张图片

每一个ROW GROUP都存储了相同数目的行,并且都按相同的行位置排列。针对上面的列式索引,Make + Model + Color, 假设表里第一行的数据是, Audi + S + Red, 那么 Make Segment第一行存储的是Audi, Model Segment第一行存储的就是S,Color Segment第一行存储的就是Red。

如果我们对一张堆表做全表扫描,可以看到I/O Cost的标示
SQL Server Performance 分析_第5张图片

我们对一张表加 columnstore index, 可以看到默认的,执行计划就选择了columnstore index。

create nonclustered columnstore index idx_colstr_sts

on siebeldbTableSchema(object_id,column_id,column_name)

go

SQL Server Performance 分析_第6张图片
Estimated Operator Cost 从1.46 降到了0.27, 而Estimated I/O Cost 从1.25降到了0.06。 I/O这种重型处理一般在分析系统,BI或者数据仓库中大量存在,所以针对fact table或者大数据量的dimension table会比较适用。

5 接下来的重点就是查询优化器的机制,包括查询操作符,查询优化树

1) 逻辑处理顺序logical processing :

Select top (X)|Distinct t1.Field1,t2.Field4,sum(t2.Field6) as Filed6
From dbo.tableone t1 Left|right|full Join dbo.tabletwo t2 on t1.Filed2
= t2.Field1 Where t1.Field3 = ‘XXX’
Order by t2.Field5

这里的重点有三个:多表 join先是做笛卡儿积运算,然后根据Left,Right Join将内部表的不符合记录都删除,在这个例子里,假设dbo.tableone有10条记录,dbo.tabletwo有20条记录,首先会生成有10*20=200条记录的结果集,假设我们用的是left join,那么tableone里面符合Field3=’XXX’的记录都将选出来,而tabletwo里面没有匹配对的记录都将附上值NULL;join on 与where 的作用根据join type的不同也将影响执行顺序;order by 子句是最后第二步执行的,在这个时候,所有的获取数据的操作都已经完成了,排序的操作里面就可以用得字段的alias。

2)影响优化器选择执行计划的因素:

Statistics: 及时更新Statistics

select name,is_auto_update_stats_on from sys.databases where name = 'lenistest4'

alter database lenistest4

set auto_update_statistics on

go

Fragmentation of table and index:碎片会导致随机读(random read),都只知道顺序读(sequential read)是效率最高的。那么怎么去判断我们要执行碎片整理了呢,什么样的碎片整理方法有效呢?在DMV里面有张表,Sys.dm_db_index_physical_stats ,我们要关注的一个字段就是avg_fragmentation_in_percent。

当avg_fragmentation_in_percent 在5%和30%之间的时候,用Alter index Recoganize;当avg_fragmentation_in_percent 大于30%的时候i,用Alter Index Rebuild。当然终极方法 drop index之后create index也是可以的,但是你想想会有什么后果。

Join Type: 关于join type的论述,网上有太多的资料可以查询了。我们在这里可以简单讲讲了:Hash Join, Merge Join, Loop Join,各自的适用范围有所不同, Hash Join 适用在没有索引可用的情况下,Merge join 适用在左右两表都要排序的场景下,Loop join适合小表的应用。至于怎么去改写这三种join的执行计划,下面有提到。

3)执行计划操作符:参考:Showplan Logical and Physical Operators Reference

4)手工干预执行计划的生成: hint

Left|Right|Full{Loop|merge|hash} join:

select top 500000 f.record_date, d.object_name, d.counter_name, d.instance_name , f.cntr_value from dimstatisticscounters d
inner merge join fctstatisticscollection f on f.row_id = d.row_id

With(index(index_name)):

select object_id,column_id ,column_name

from siebeldbTableSchema with(index(idx_obj_col_id))

where column_id = 11 and object_id = 1415428562

Select xxx from table_name option(table tableName index(indexName)):

select object_id,column_id ,column_name

from siebeldbTableSchema

where table_name = 'S_CONTACT' and column_id = 11 and object_id = 1415428562

option( table hint (siebeldbTableSchema,index(idx_colstr_sts)))

这里还有这么一个坑,就是exposed object name必须和 from的表对象引用一致,否则出现类似这个错误:

select object_id,column_id ,column_name

from siebeldbTableSchema

where table_name = 'S_CONTACT' and column_id = 11 and object_id = 1415428562

option( table hint (dbo.siebeldbTableSchema,index(idx_colstr_sts)))

Msg 8723, Level 16, State 1, Line 62

Cannot execute query. Object ‘dbo.siebeldbTableSchema’ is specified in
the TABLE HINT clause, but is not used in the query or does not match
the alias specified in the query. Table references in the TABLE HINT
clause must match the WITH clause.

5)获取执行计划: 执行计划的存储有2种方式,一种XML格式,一种文本格式。可视化的方法都是基于XML来展现的。

Show plan:一次只用一条命令,用完set off

SET SHOWPLAN_TEXT ON
SET SHOWPLAN_ALL ON
SET SHOWPLAN_XML ON
SET STATISTICS PROFILE ON
SET STATISTICS XML ON

这些命令都是捕获接下来要执行的SQL的执行计划。区别在于SHOWPLAN_TEXT,SHOWPLAN_ALL,SHOWPLAN_XML是不执行SQL的,只是做estimate;而statistics profile, statistics xml都是会执行 SQL的。

用这些命令的时候要注意两个地方:

首先,必须先执行set 命令,否则会出现类似这个错误:

set showplan_text on ;

select object_id,column_id ,column_name

from siebeldbTableSchema

where table_name = 'S_CONTACT'

Msg 1067, Level 15, State 2, Line 1

The SET SHOWPLAN statements must be the only statements in the batch.

其次, 要改变执行计划或者说要用另一种set,必须先关闭之前的set option。举个例子,第一次我们使用了 set showplan_text on,那么在使接下来用到的 showplan_xml on 起作用之前,先用set showplan_text off关闭之前的开关。

SSMS: 这个方法一开始用SQL Server的时候就会碰到,没啥好讲。

Extended events: 真有人这样干 :sql server - How do I obtain a Query Execution Plan?。 定义完这个event,就可以在SSMS里面直接起用他,并且获得可视化效果。

/*
    Generated via "Query Detail Tracking" template.
*/
CREATE EVENT SESSION [GetExecutionPlan] ON SERVER 
ADD EVENT sqlserver.query_post_execution_showplan(
    ACTION(package0.event_sequence,sqlserver.plan_handle,sqlserver.query_hash,sqlserver.query_plan_hash,sqlserver.session_id,sqlserver.sql_text,sqlserver.tsql_frame,sqlserver.tsql_stack)),
/* Remove any of the following events (or include additional events) as desired. */
ADD EVENT sqlserver.error_reported(
    ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.database_id,sqlserver.plan_handle,sqlserver.query_hash,sqlserver.query_plan_hash,sqlserver.session_id,sqlserver.sql_text,sqlserver.tsql_frame,sqlserver.tsql_stack)
    WHERE ([package0].[greater_than_uint64]([sqlserver].[database_id],(4)) AND [package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.module_end(SET collect_statement=(1)
    ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.database_id,sqlserver.plan_handle,sqlserver.query_hash,sqlserver.query_plan_hash,sqlserver.session_id,sqlserver.sql_text,sqlserver.tsql_frame,sqlserver.tsql_stack)
    WHERE ([package0].[greater_than_uint64]([sqlserver].[database_id],(4)) AND [package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.rpc_completed(
    ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.database_id,sqlserver.plan_handle,sqlserver.query_hash,sqlserver.query_plan_hash,sqlserver.session_id,sqlserver.sql_text,sqlserver.tsql_frame,sqlserver.tsql_stack)
    WHERE ([package0].[greater_than_uint64]([sqlserver].[database_id],(4)) AND [package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sp_statement_completed(SET collect_object_name=(1)
    ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.database_id,sqlserver.plan_handle,sqlserver.query_hash,sqlserver.query_plan_hash,sqlserver.session_id,sqlserver.sql_text,sqlserver.tsql_frame,sqlserver.tsql_stack)
    WHERE ([package0].[greater_than_uint64]([sqlserver].[database_id],(4)) AND [package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_completed(
    ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.database_id,sqlserver.plan_handle,sqlserver.query_hash,sqlserver.query_plan_hash,sqlserver.session_id,sqlserver.sql_text,sqlserver.tsql_frame,sqlserver.tsql_stack)
    WHERE ([package0].[greater_than_uint64]([sqlserver].[database_id],(4)) AND [package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_statement_completed(
    ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.database_id,sqlserver.plan_handle,sqlserver.query_hash,sqlserver.query_plan_hash,sqlserver.session_id,sqlserver.sql_text,sqlserver.tsql_frame,sqlserver.tsql_stack)
    WHERE ([package0].[greater_than_uint64]([sqlserver].[database_id],(4)) AND [package0].[equal_boolean]([sqlserver].[is_system],(0)))) 
ADD TARGET package0.ring_buffer
WITH (MAX_MEMORY=4096 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=30 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF)
GO

以上针对捕获的SQL执行计划都是基于当前的,要看之前的执行计划,我们可以用DMV:

SELECT UseCounts, Cacheobjtype, Objtype, TEXT, query_plan
FROM sys.dm_exec_cached_plans 
CROSS APPLY sys.dm_exec_sql_text(plan_handle)
CROSS APPLY sys.dm_exec_query_plan(plan_handle)

这里的格式是XML。 并且列出了stored procedure里面,逐行逐句的SQL执行计划。

上面列出的都是我自己头脑里的第一个针对查询优化器的反应。比较零散,不成系统,所以从网络上看看别人尤其是出书的作者们是怎么分析这方面的知识的,有助于自己的梳理,也可以看到别人的知识框架与写作方法。

参考文章来源于 :The SQL Server Query Optimizer

作者的写作手法与我的不同地方有很多,首先他是从框架入手,比如SQL Server 的各个engine 入手,这里涉及到的两个engine 是storage engine, relational engine。接着作者讲解了很多细节性的知识,包括各个组件是怎么衔接,每个组件的输入与输出。

Relational Engine也可以通俗地称为Query Processor,简单来讲,可以由四大步骤组成:

SQL Server Performance 分析_第7张图片

所有被送到 SQL SERVER端的SQL 首先会经过 Parsing 和Binding。 Parsing就是把SQL语句简单的分解成各个逻辑正确的处理单元,我们可以这样理解,比如这条SQL, select * from dbo.Sales。逻辑处理单元就是从表 dbo.Sales取出所有的数据,并且语法都正确。Binding 更多的就是命名空间检查的步骤,所有的对象都要在数据库中能解析到。经过这两步所有的SQL都生成了逻辑上可处理的单元,将被送到 query optimizer做优化处理。这里也就是解析中的第二步,硬解析。如果已经有对应的execution plan存在缓存中了,那么就不会经过这一步了,但是如果没有相应地缓存,优化器即将对送进来的 SQL 语句做进一步的加工。第一步就是生成所有可能的查询计划(其实并不是每一个可能的查询计划,只是每产生一个查询计划,就评估一下这个计划的成本,如果成本比其他的小,就会选择这个最小成本的查询计划),接着根据cost estimated model对这些查询计划作分析,挑选出最优化的计划。最后的输出就是将所有的逻辑处理单元转换成物理处理单元 ,比如刚才的从dbo.Sales取出所有数据,这回就转成了 execution engine可以理解的内容, 每一个逻辑处理单元的操作节点 operation node转变成了physical operation method,这里就转成了 table full scan。

基于以上的原理, SQL SERVER 挑选出最优化的执行计划也是有相当的运气成分在里面,如果我们知道什么的执行计划对于当前的查询有效,可以用hint给优化器一些暗示,让他直接挑选出我们认为优化的查询计划。当然前提是我们知道是优化器花了很长时间或者资源来搜索search space(就是所有可用的执行计划的集合),我们可以通过打开执行计划的 select 操作符,看他的属性窗口,这里记录了compile cpu, compile memory, compile time一些的统计信息,如果过长过高,我们可以用hint来提高这些时间。

SQL Server Performance 分析_第8张图片

作者在文中提出了一个很有意思的话题,就是关于join带来的性能影响。Join最重要的两个概念,join的次序与join的方法种类。Join种类就不详细说了,join order还是有必要细化的。以前没注意这个细节。 当有很多表join的时候 ,并不是等着上面的join做完,底下的join才开始,而是2个表一个join,然后同时开始。所以更改join的次序能有效降低cardinality。

6 性能指标performance counters: performance counters记录的都是当前数据库的一些性能指标计数器,看到performance counters第一反应就是sys.dm_os_performance_counters 这张DMV,它包含了5个字段,object_name可以看作是计数器的命名空间,instance可以看作是一个应用实例,计数器的数值只有在应用实例层面才存在,通常包含了数据库名字,counter_name就是承载各个计数器的名称,cntr_value是性能指标数字,有可能是及时数值,也有可能是累计数值,cntr_type是windows性能架构(windows performance architecture)定义的计数器种类,它也同样用于windows performance monitor(perfmon.exe)和 WMI Performance Counters中 ,具体可以参看 MSDN 的解释 WMI Performance Counter Types :在 WMI Performance Counter中,性能数字可以来自于两个源头,一是RAW DATA, 采样来自于Win32_PerfRawData,另外可以来自于计算好的数据集,采样来自于Win32_PerfFormattedData。两者之间的区别就是数字是否格式化,所谓格式化就是经过某种还原算法,让数字变得可读。这里sys.dm_os_performance_counters毫无疑问,是可读的数字。

针对 Win32_PerfRawData和Win32_PerfFormattedData,我们可以用 powershell 来分别读取一些数值比较一下:

读取RAW Data:

$osClass = New-Object System.Management.ManagementClass Win32_ClassNameHere  
$osClass.Options.UseAmendedQualifiers = $true
$properties = $osClass.Properties  
"This class has {0} properties as follows:" -f $properties.count
foreach ($property in $properties) {  
"Property Name: {0}" -f $property.Name  
"Description:   {0}" -f $($property.Qualifiers["Description"].Value)  
"Type:          {0}" -f $property.Type  
"-------"
}
}

读取Formatted Performance Data:

$osClass = New-Object System.Management.ManagementClass Win32_PerfFormattedData_APPPOOLCountersProvider_APPPOOLWAS  
$osClass.Options.UseAmendedQualifiers = $true  
# Get the Properties in the class  
$properties = $osClass.Properties  
"This class has {0} properties as follows:" -f $properties.count  
# display the Property name, description, type, qualifiers and instance values  
foreach ($property in $properties) {  
"Property Name: {0}" -f $property.Name  
"Description:   {0}" -f $($property.Qualifiers["Description"].Value)  
"Type:          {0}" -f $property.Type  
"-------"  
}

我们还是回到“可读”的性能指标上来, SQL Server 已经帮我们做好这部分格式化的工作了。 刚才我们看到性能指标有自己的命名空间(其实就是归类,类别的意思),那么有多少种性能类别呢,可能第一反应就是 CPU, MEMORY, IO, Network,没错,但是SQL SERVER总该有自己的一套归类方案不是:

select distinct

case

when charindex(':',object_name) >0 then substring(object_name, charindex(':',object_name) + 1 , len(object_name) - charindex(':',object_name))

else object_name

end as object_name

from sys.dm_os_performance_counters

order by 1

for xml path('') – 这里如果要 XML格式转成一行返回,就要去掉column alias object_name

从我的本地机器上来看,总共有43个种类 :

Access Methods,Availability Replica,Batch Resp Statistics,Broker
Activation,Broker Statistics,Broker TO Statistics,Broker/DBM
Transport,Buffer Manager,Buffer Node,CLR,Catalog Metadata,Cursor
Manager Total,Cursor Manager by Type,Database
Replica,Databases,Deprecated Features,Exec
Statistics,FileTable,General Statistics,HTTP
Storage,Latches,Locks,Memory Broker Clerks,Memory Manager,Memory
Node,Plan Cache,Replication Agents,Replication Dist.,Replication
Snapshot,Resource Pool Stats,SQL Errors,SQL
Statistics,Transactions,User Settable,Wait Statistics,Workload Group
Stats,XTP Cursors,XTP Garbage Collection,XTP Phantom Processor,XTP
Storage,XTP Transaction Log,XTP Transactions

我们可能会对 Buffer Manager, Databases, Locks, Memory Manager, XTP Transactions感兴趣,其他的研究方法大同小异。举个例子,找出 buffer manager中对应的计数器:

select distinct

case

when charindex(':',object_name) >0 then substring(object_name, charindex(':',object_name) + 1 , len(object_name) - charindex(':',object_name))

else ltrim(rtrim(object_name ))

end as object_name

, (select ltrim(rtrim(counter_name)) +',' from sys.dm_os_performance_counters iner where iner.object_name = oner.object_name for xml path('')) as counter_name

from sys.dm_os_performance_counters oner

where object_name like '%Buffer Manager%'
Buffer cache hit ratio,Buffer cache hit ratio base,Page lookups/sec,Free list stalls/sec,Database pages,Target pages,Integral Controller Slope,Lazy writes/sec,Readahead pages/sec,Readahead time/sec,Page reads/sec,Page writes/sec,Checkpoint pages/sec,Background writer pages/sec,Page life expectancy,Extension page writes/sec,Extension page reads/sec,Extension outstanding IO counter,Extension page evictions/sec,Extension allocated pages,Extension free pages,Extension in use as percentage,Extension page unreferenced time

这里不言而喻,有个重要指标 Buffer cache hit ratio 。 如果发现这个指标有下降的趋势,说明有可能内存不足了,造成的原因可能是访问量突然加大,也有可能是某个查询读取了大量的数据页,一个一个排除有可能的因素。

select distinct

case

when charindex(':',object_name) >0 then substring(object_name, charindex(':',object_name) + 1 , len(object_name) - charindex(':',object_name))

else ltrim(rtrim(object_name ))

end as object_name

, (select ltrim(rtrim(counter_name)) +',' from sys.dm_os_performance_counters iner where iner.object_name = oner.object_name for xml path('')) as counter_name

from sys.dm_os_performance_counters oner

where object_name like '%Memory Manager%'

External benefit of memory,Connection Memory (KB),Database Cache
Memory (KB),Free Memory (KB),Granted Workspace Memory (KB),Lock Memory
(KB),Lock Blocks Allocated,Lock Owner Blocks Allocated,Lock
Blocks,Lock Owner Blocks,Maximum Workspace Memory (KB),Memory Grants
Outstanding,Memory Grants Pending,Optimizer Memory (KB),Reserved
Server Memory (KB),SQL Cache Memory (KB),Stolen Server Memory (KB),Log
Pool Memory (KB),Target Server Memory (KB),Total Server Memory (KB)

针对内存作监控,可以明显地看到connection, Buffer Pool 用到的内存。

将这些都集成到自定义的性能数据仓库里,随时查看数据库的性能。只是采样频率应该适当控制。

除了在sys.dm_os_performance_counters里面查找性能指标外,我们还可以从perfmon.exe来跟踪性能指标,列举一些常见的performance counters:

具体的原文解释可以参考这里:The Accidental DBA (Day 21 of 30): Essential PerfMon counters

1) CPU: Processor: %Processor Time, %Privileged Time ; Process(sqlserver.exe) : %Processor Time , %Privileged Time。 在虚拟机的时候, processor的计数器不如process(sqlserver.exe)来的精确,因为processor的计数器,其实是记录的VM分配的CPU百分数,而sqlserver.exe是相对比例。

2) Memory:

Memory: Available Mbytes 系统分配的内存中可用内存有多少,以MB为单位。如果系统的可用内存低于64MB, SQL SERVER 会检测到这个信号,释放部分内存给系统。

SQL Server: Buffer Manager

Lazy Writes/sec : Lazy Writer是一个不断检查buffer pool 中可用内存的进程,如果发现需要新的内存来保存新的数据页,那么他会把一些不用的数据页丢掉,也会把不用的脏数据页(比如很长时间没有被访问的脏数据页)写回到数据库里面,以腾出一些空间给新的数据页。

关于Lazy Writer可以参考这篇文章,写的很详细 : http://www.sqlshack.com/sql-server-memory-performance-metrics-part-5-understanding-lazy-writes-free-list-stallssec-memory-grants-pending/

Page Life Expectancy : 数据页在缓存中停留的时间,如果过短,说明有缓存有压力,一直在频繁的切换数据页,不停的从数据库磁盘中拉取数据到buffer pool中,还不停的把数据页写到磁盘或者丢弃。所以分配足够的内存给buffer pool,是能有效提高缓存命中率的。

Page Reads/sec:,Page Writes/sec:如果 page life expectancy过低,而page reads/sec, page writes/sec又过高,说明 buffer pool有压力。这时要和 PLE结合起来看才有说明性。

SQL Server : Memory Manager

Total Server Memory(KB):目前sql server使用的内存。这个指标应当与target server memory联合起来看,主要是看比值, total server memory / target server memory 。理论上两者比值越接近于 1越说明目前的sql server没有内存压力。如果相差太远,说明sql server无法分配到足够的内存,也有可能是maximum server memory指定的太小。比如 服务器250GB 内存,而我们指定了sql server 的maximum server memory的大小为5GB。 那么两者比例明显太小,需要重新调整maximum server memory。 当然更可能的是服务器有内存压力。

使用sp_configure 来检查与修改 maximum server memory。使用reconfigure生效。

Target Server Memory (KB):这个值代表了sql server为了更好的性能,应该使用的理想值。为了验证这个值是不是随时调整的或是根据安装时指定的,我们需要跟踪这个值的变化。也就是说这个值其实是sql server 根据当前的访问量臆想出来的可能需要的理想内存大小。

以上详细的对比可以参考:SQL Server memory performance metrics

3) I/O:Latency延迟是IO主要反映的问题。比如平均读一次的时间是8ms,读了800MB, 而下个平均读一次的时间是10ms,读了200MB,这足可说明延迟高了,出问题了,有可能是执行了一系列的报表程序,也有可能是索引被误删等等。我们主要考察以下几个指标:

Physical Disk :Avg.Disk sec/Read , Avg.Disk Bytest/Read ; Avg.Disk Sec/Write, Avg.Disk Bytes/Write

Paging File: %Usage

SQL Server: Access Methods : Forwarded Records/sec , Full Scan /sec, Index Searches/sec

SSD 上市之后, physical disk的效率变高了,所以我们还要关注 SQL Server Access Methods.

4)罗列一些常见的要时刻监控的性能指标,可以做到我们的监控工具里面:

参考应用:Top 10 SQL Server Counters for Monitoring SQL Server Performance

4.1. SQLServer: Buffer Manager: Buffer cache hit ratio

The buffer cache hit ratio counter represents how often SQL Server is able to find data pages in its buffer cache when a query needs a data page. The higher this number the better, because it means SQL Server was able to get data for queries out of memory instead of reading from disk. You want this number to be as close to 100 as possible. Having this counter at 100 means that 100% of the time SQL Server has found the needed data pages in memory. A low buffer cache hit ratio could indicate a memory problem.

4.2. SQLServer: Buffer Manager: Page life expectancy

The page life expectancy counter measures how long pages stay in the buffer cache in seconds. The longer a page stays in memory, the more likely SQL Server will not need to read from disk to resolve a query. You should watch this counter over time to determine a baseline for what is normal in your database environment. Some say anything below 300 (or 5 minutes) means you might need additional memory.

4.3. SQLServer: SQL Statistics: Batch Requests/Sec

Batch Requests/Sec measures the number of batches SQL Server is receiving per second. This counter is a good indicator of how much activity is being processed by your SQL Server box. The higher the number, the more queries are being executed on your box. Like many counters, there is no single number that can be used universally to indicate your machine is too busy. Today’s machines are getting more and more powerful all the time and therefore can process more batch requests per second. You should review this counter over time to determine a baseline number for your environment.

4.4. SQLServer: SQL Statistics: SQL Compilations/Sec

The SQL Compilations/Sec measure the number of times SQL Server compiles an execution plan per second. Compiling an execution plan is a resource-intensive operation. Compilations/Sec should be compared with the number of Batch Requests/Sec to get an indication of whether or not complications might be hurting your performance. To do that, divide the number of batch requests by the number of compiles per second to give you a ratio of the number of batches executed per compile. Ideally you want to have one compile per every 10 batch requests.

4.5. SQLServer: SQL Statistics: SQL Re-Compilations/Sec

When the execution plan is invalidated due to some significant event, SQL Server will re-compile it. The Re-compilations/Sec counter measures the number of time a re-compile event was triggered per second. Re-compiles, like compiles, are expensive operations so you want to minimize the number of re-compiles. Ideally you want to keep this counter less than 10% of the number of Compilations/Sec.

4.6. SQLServer: General Statistics: User Connections

The user connections counter identifies the number of different users that are connected to SQL Server at the time the sample was taken. You need to watch this counter over time to understand your baseline user connection numbers. Once you have some idea of your high and low water marks during normal usage of your system, you can then look for times when this counter exceeds the high and low marks. If the value of this counter goes down and the load on the system is the same, then you might have a bottleneck that is not allowing your server to handle the normal load. Keep in mind though that this counter value might go down just because less people are using your SQL Server instance.

4.7. SQLServer: Locks: Lock Waits / Sec: _Total

In order for SQL Server to manage concurrent users on the system, SQL Server needs to lock resources from time to time. The lock waits per second counter tracks the number of times per second that SQL Server is not able to retain a lock right away for a resource. Ideally you don’t want any request to wait for a lock. Therefore you want to keep this counter at zero, or close to zero at all times.

4.8. SQLServer: Access Methods: Page Splits / Sec

This counter measures the number of times SQL Server had to split a page when updating or inserting data per second. Page splits are expensive, and cause your table to perform more poorly due to fragmentation. Therefore, the fewer page splits you have the better your system will perform. Ideally this counter should be less than 20% of the batch requests per second.

4.9. SQLServer: General Statistic: Processes Block

The processes blocked counter identifies the number of blocked processes. When one process is blocking another process, the blocked process cannot move forward with its execution plan until the resource that is causing it to wait is freed up. Ideally you don’t want to see any blocked processes. When processes are being blocked you should investigate.

4.10. SQLServer: Buffer Manager: Checkpoint Pages / Sec

The checkpoint pages per second counter measures the number of pages written to disk by a checkpoint operation. You should watch this counter over time to establish a baseline for your systems. Once a baseline value has been established you can watch this value to see if it is climbing. If this counter is climbing, it might mean you are running into memory pressures that are causing dirty pages to be flushed to disk more frequently than normal.

7等待 waits

举个上面提到的常用performance counter, [SQL Server:Locks:Lock Waits/Sec:_Total]。这个 wait讲的是sql server 在秒级别的时间刻度上,当前有多少lock 在等待独占资源,无论是尝试占用硬件资源还是软件资源,无论是在数据库级别还是在ROW级别,当前所有的等待加锁的情况都被计算在内,所以instance就是_Total。 我们可以从sys.dm_os_performance_counters里面找到当前的lock waits值 :

select distinct

case

when charindex(':',object_name) >0 then substring(object_name, charindex(':',object_name) + 1 , len(object_name) - charindex(':',object_name))

else ltrim(rtrim(object_name ))

end as object_name

--, (select ltrim(rtrim(counter_name)) +',' from sys.dm_os_performance_counters iner where iner.object_name = oner.object_name for xml path('')) as counter_name

,counter_name,instance_name,cntr_value

from sys.dm_os_performance_counters oner

where lower(object_name) like '%lock%'

and (lower(counter_name) like '%lock%' or lower(counter_name) like '%wait%%')

结果集里面还包含了lock 等待的时间,申请lock的数量等等,这些性能值都是越小越好,所以看到一个就够了。

除了lock有等待时间外,还有很多其他的wait types,比如log write waits,, memory grant queue waits, Network IO Waits, page latch waits等等:

select distinct

case

when charindex(':',object_name) >0 then substring(object_name, charindex(':',object_name) + 1 , len(object_name) - charindex(':',object_name))

else ltrim(rtrim(object_name ))

end as object_name

--, (select ltrim(rtrim(counter_name)) +',' from sys.dm_os_performance_counters iner where iner.object_name = oner.object_name for xml path('')) as counter_name

,counter_name,instance_name,cntr_value

from sys.dm_os_performance_counters oner

where lower(object_name) like '%wait%'

上面的sql 列举了所有的wait统计信息,针对某一具体的wait,还需要drill down 到具体的DMV去分析,比如lock:

如果有两个session的SQL 互相在等待锁,那么必有一个session是被block了,我们可以这样看:

select session_id,status,command,blocking_session_id,wait_type from sys.dm_exec_requests

当然也可以用standard reports来看,standard reports 是一组SQL SERVER自带的报表系统,方便查看系统的存储,网络状态以及locking。

列举一些常见的wait types:

ASYNC_NETWORK_IO: 客户端的数据程序处理不过来从服务器传来的大量数据;

CXPACKET:这个很有意思。一个大的查询我们可以将其插分成多个小查询,并且可以并行执行,只不过有的线程执行的快,有的执行的慢,那执行快的线程会等待执行慢的,结果其他的需要线程处理数据的request就被刮起在那里了。这个CXPACKKET很大的时候,说明多线程被滥用了,我们需要改进,改进的方法就是限制 MAXDOP, maximum degree of parallelism。 设置的建议,采取微软官方的用法:https://support.microsoft.com/en-us/kb/2806535

LCK*:这个前面举了详细的例子了;

PAGEIOLATCH_*: 通常发生在数据读取到缓存中的时候,解决方法有很多,加索引,碎片整理,分表,分区;

SOS_SCHEDULER_YIELD: SOS, SQL SERVER Operating System。这里明显是CPU压力 。

当数据量小的时候,没看出什么特别的地方,所以我们将数据量扩大2^8倍,再看看:

declare @record_date datetime = '2016-2-1'
while @record_date < '2016-4-30'
begin
insert into dbo.fctdbsize (record_date,type_desc,name,size,size_mb,size_gb,x_flag)
select @record_date as record_date,type_desc,name,size,size_mb,size_gb,x_flag
from dbo.fctdbsize
set @record_date = dateadd(dd,10,@record_date)
select object_name(object_id) as tableName, rows from sys.partitions
where object_id = object_id(N'dbo.fctdbsize') and index_id = 0
end
go

在这个执行过程中,都可以看到这些wait types:
PAGEIOLATCH_EX
PAGEIOLATCH_SH:从硬盘读取数据页到缓存,_SH意味着只读
PAGEIOLATCH_EX,PAGEIOLATCH_UP: _EX, exclusive, _UP, update.所以这是读取数据页到缓存,并且加上X锁,以便更改数据。

SOS_SCHEDULER_YIELD
在request生成的时候,worker就形成task由SOS放到Runnable Queue里面去。这个时候开始计算signal wait time(等待SOS发出信号,由scheduler调用这个task)。 一旦signal 发出,这个request就会被放到processor上面执行,执行时间最长为4millionsenconds,一旦超时,未完成的task就放到Runnable尾部,等到下一次的signal notification时间,这里Runnable Queue严格按照100%的FIFO(First in First Out)原则。超时的时候,就发出SOS_Scheduler_Yield (yield在这里是离开,让步的意思)的等待。在执行的时候,如果碰到有资源不可用,那么就这个task而言,它会被放到wait list里面等到资源被释放,从这一刻起,就开始计算resource wait time。等到资源释放了,这个task 就放到Runnable Queue下面等待执行。这里resource wait time 加上signal wait time 就是所有等待的时间 。sys.dm_os_wait_stats里面只记录了total wait time,signal wait time,需要做减法才能计算出resource wait time。
简单的凭出现SOS_SCHEDULER_YIELD就认为有CPU压力不可靠,只有频繁出现才说明问题。

SLEEP_BPOOL_FLUSH:
是在backup transaction log的job中出现的,命令是checkpoint。大量的物理操作会出现这种等待,BPOOL就是buffer pool.

WRITELOG:写日志

XTP_THREAD_POOL: DISPATCHER_QUEUE_SEMAPHORE 说明系统闲着,等待更多的活按排过来做。
XTP_CKPT_AGENT: WAIT_XTP_HOST_WAIT:系统触发。
XTP_OFFLINE_CKPT: WAIT_XTP_OFFLINE_CKPT_NEW_LOG:等待扫描新的事务日志。
(以上三个XTP_X的等待事件在非常繁忙的系统中是不会出现的,只有在闲置的系统中才一直存在在sys.dm_exec_requests中)

勘误:SQL Server Query Optimizer 还没有那么智能,多个列组成的索引,要想索引起作用,字段顺序是很重要的。文中举的例子不是很好,只是凑巧都走了索引罢了。
1.首先假定我们有3个列组成的索引,A+B+C。我们只放B=X这样是不能走索引的,除非数据量很小,而且字段选取的都被这个ABC索引给覆盖掉。我们说过优化器是不会每个可能的执行计划都去判断一遍成本的 ,遇到一个凑合着能用的,就给execution engine去执行了。
2.想要 B=X走索引,必须先放A=a.value,而且不能有>,<,>=,<=,!这样操作符用在A字段上。
具体下篇文章做详细解答。

欢迎关注个人微信公众号

SQL Server Performance 分析_第9张图片

你可能感兴趣的:(sql,server,SQL,Server,实战笔记)