浅析SQL SERVER执行计划中的各种怪相

在查看执行计划或调优过程当中,执行计划里面有些现象总会让人有些迷惑不解: html

    1:为何同一条SQL语句有时候会走索引查找,有时候SQL脚本又不走索引查找,反而走全表扫描? 数据库

    2:同一条SQL语句,查询条件的取值不一样,它的执行计划会一致吗? app

    3: 同一条SQL语句,其执行计划会变化,为何 性能

    4: 在查询条件的某个或几个字段上建立了索引,执行计划就必定会走该索引吗? 测试

    5:同时存在几个索引,SQL语句会走那个索引? 优化

     ............................................................ spa

有时候若是要跟别人解释清楚这些问题,若是不经过一些案例或例子来解说,很难阐述清楚,一方面是表达能力问题。另一方面,再华丽的语言也难敌眼见为实,毕竟人接受信息大部分经过眼睛,小部分经过耳朵。眼见为实耳听为虚吗! code

下面来看一个简单的例子,为何我在对应的查询字段上建有索引,可是它不走索引反而走全表扫描。htm

DROP TABLE TEST 
   CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(8));
 
   CREATE INDEX PK_TEST ON TEST(OBJECT_ID) 
   DECLARE @Index INT =0;
 
WHILE @Index < 20
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry';
    
    SET @Index = @Index +1;
END
 
 
UPDATE STATISTICS TEST WITH FULLSCAN
 
SELECT * FROM TEST WHERE OBJECT_ID=1

 

clip_image001

 

已经在查询字段OBJECT_ID上创建了索引,为何SQL优化器不走索引,而要走全表扫描呢?为了说明白,那么咱们借助于查询提示(Hints)强制优化器走索引查找来讲明上述状况,对比走索引查找、全表扫描二者的代价开销,从下图,咱们能够看到当前状况下,走全表扫描的开销要小于索引查找。由于当前状况下,走索引须要额外的IO开销,反而不如全表扫描。因此优化器选择了走全表扫描而非索引查找。不少开发人员有种根深蒂固的执拗观念“走索引查找必定要优于全表扫描”(我跟他们解释的时候,不少人不相信,"慷慨激昂"的质疑我,以致于个人解释都显得苍白无力),大多数状况下,走索引查找要优于全表扫描,可是在特定的场景、特定数据状况下,会出现全表扫描优于索引查找的状况。尤为是ORACLE里面,不少作开发的同事一看到SQL执行计划走全表扫描,立马大呼小叫。其实彻底是先入为主的观念做怪。blog

SELECT * FROM TEST WHERE OBJECT_ID=1
 
 
SELECT * FROM TEST WITH(INDEX=PK_TEST) WHERE OBJECT_ID =1
 

 

clip_image002

 

二者开销不一致,其实在IO开销这一块,能够从下面看出逻辑读取的差别。

DBCC FREEPROCCACHE;
DBCC DROPCLEANBUFFERS;
SET STATISTICS IO ON;
 
SELECT * FROM TEST WHERE OBJECT_ID=1

 

 

clip_image003

 

DBCC FREEPROCCACHE;
 
DBCC DROPCLEANBUFFERS;
 
SET STATISTICS IO ON;
 
SELECT * FROM TEST WITH(INDEX=PK_TEST) WHERE OBJECT_ID =1
 

 

 

clip_image004

 

 

那么接下来,咱们将该表的数据从20条记录增加到10000条记录,你以为执行计划会变化吗?你们不妨先思考一下这个问题,再看下文。

TRUNCATE TABLE TEST;
DECLARE @Index INT =0;
 
WHILE @Index < 10000
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry';
    
    SET @Index = @Index +1;
END
 
 
UPDATE STATISTICS TEST WITH FULLSCAN
SELECT * FROM TEST WHERE OBJECT_ID=1

 

以下所示,当数据变化时,优化器认为走索引查找要优于全表扫描,因此选择了索引查找,说到底优化器是基于成本的优化器,在众多的执行计划中,它会选择代价开销最小的一个执行计划。

 

clip_image005

 

此时,强制优化器走全表扫描,对比开销结果,你会发现结果彻底跟上面结果相反。

 

clip_image006

 

我若是更新该表数据,使其分布彻底倾斜,那么你能够看到对于同一个SQL,不一样的取值,它的执行计划也会彻底不一样。

UPDATE TEST SET OBJECT_ID =1 WHERE OBJECT_ID<9999
UPDATE STATISTICS TEST WITH FULLSCAN
 
SELECT OBJECT_ID,COUNT(1) SUM_COUNT FROM TEST GROUP BY OBJECT_ID
OBJECT_ID    SUM_COUNT
----------- -----------
1             9999
9999           1
 
SELECT * FROM TEST WHERE OBJECT_ID=1
SELECT * FROM TEST WHERE OBJECT_ID=9999

 

clip_image007

 

可见同一条SQL语句,查询条件的取值不一样,它的执行计划可能会不同。

这几个例子,其实我想说的是执行计划每每会受数据变化的、数据分布(直方图)的影响,在统计信息正确的状况下,优化器会根据代价来判断选取最优的执行计划。前提是统计信息准确。在调优过程当中,有时候遇到统计信息不正确致使执行计划不好的状况。我没有想到一个好的例子来让你们形象观察统计信息的不正确性致使执行计划的不一样。在此不作详细讨论。

也许细心的朋友已经发现了我上面测试用例使用的是非汇集索引,也就是说该表是一个堆表。若是我建立的索引是汇集索引,状况会怎么样?以下所示,汇集索引下的执行计划跟非汇集索引状况又不同。

DROP TABLE TEST;
CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(8));
 
CREATE CLUSTERED INDEX PK_TEST ON TEST(OBJECT_ID) 
DECLARE @Index INT =0;
 
WHILE @Index < 20
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry';
    
    SET @Index = @Index +1;
END
UPDATE STATISTICS TEST WITH FULLSCAN;

 

clip_image008

 

以下所示,这种状况下走汇集索引查找与汇集索引扫描的开销几乎接近。

clip_image009

若果我将数据增加到10000条记录后,状况又不一样。这是一个显而易见的结果,仅仅为了说明数据对执行计划的影响。

clip_image010

下面咱们删除TEST表, 新建另一个TEST表, 以下所示

 

DROP TABLE TEST;
SELECT * INTO TEST FROM sys.objects
 
(2014 行受影响)
 
CREATE INDEX IDX_TEST_N1 ON TEST(CREATE_DATE, TYPE);
 
UPDATE STATISTICS TEST WITH FULLSCAN;
 
SELECT CREATE_DATE, TYPE FROM TEST 
WHERE CREATE_DATE >='2013-07-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
  
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-07-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'

下面看看这两个SQL的执行计划的差别,这两个SQL略有差别,查询字段不一样,一个是查询全部字段,一个是查询CREATE_DATE, TYPE两个字段

clip_image011

对比二者的执行计划

clip_image012

这里涉及索引覆盖所,想深刻理解能够参考宋沄剑这篇博客T-SQL查询高级--理解SQL SERVER中非汇集索引的覆盖,链接,交叉和过滤.

在这个简单例子中,咱们能够用查询必须字段代替*,用索引覆盖避免其走RID查找,可是实际环境中每每比较复杂,有时候同一个表上的查询SQL,可能很是多,索引覆盖也每每不可能所有涉及。因此在写SQL代码中,咱们要养成查询必要字段的习惯,不要生成SELECT *的习惯,由于它有下面一些弊端:

1:若是你只须要表中几个字段,SELECT * 会产生额外的IO,消耗额外的带宽资源。当数据库有大量这类SQL,就会产生量变到质变。慢慢影响整个数据库的性能。

2:习惯成必然(不少时候大部分人都是从SELECT * FROM开始的),养成了这样写SQL的习惯。

3:形成额外的书签查找或是由查找变为扫描

4: 产生潜在的BUG 例如 INSERT INTO T (COLUMN1,…… )SELECT * FROM M . 若是M表字段增长、或修改字段类型等都会致使错误。

上面仅仅是题外话,这里要说明的是你的SQL写法也有可能影响执行计划。

下面来看一个例子,忽然某天有这么样一个需求(固然实际状况远比这个复杂),

DROP TABLE TEST;
SELECT * INTO TEST FROM sys.objects
 
CREATE CLUSTERED INDEX PK_TEST ON TEST(OBJECT_ID)
 
UPDATE STATISTICS TEST WITH FULLSCAN
 
 
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'

 

clip_image013

某个开发人员在测试、优化过程当中,发现执行计划走汇集索引扫描,因而想若是给CREATE_DATE和TYPE字段创建一个索引,那么它会不会快一点?结果他发现他添加了索引,但是优化器根本不走他创建的索引,为何呢?

CREATE  INDEX IDX_TEST_N1 ON TEST(CREATE_DATE, TYPE)
UPDATE STATISTICS TEST WITH FULLSCAN
 
 
 
SET SHOWPLAN_ALL  ON
GO
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
GO

clip_image014

 

咱们又要使用查询提示强制其走索引查找,来对比其开销代价

SET SHOWPLAN_ALL  ON
GO
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
GO
SET SHOWPLAN_ALL  OFF;
GO
 
 
SET SHOWPLAN_ALL  ON
GO
SELECT * FROM TEST WITH( INDEX=IDX_TEST_N1)
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
GO
SET SHOWPLAN_ALL  OFF;
GO

 

clip_image015

 

clip_image016

 

优化器发现走汇集索引的开销小于走IDX_TEST_N1索引查找,因此即便你在查询条件上建有索引,执行计划仍是不会走这个索引。若是我建立索引时,覆盖这些字段,那么它就会走索引查找而不会是汇集索引。

DROP INDEX IDX_TEST_N1 ON TEST
 
CREATE NONCLUSTERED INDEX IDX_TEST_N1
ON [dbo].[TEST] ([type],[create_date])
INCLUDE ([name],[object_id],[principal_id],[schema_id],[parent_object_id],[type_desc],[modify_date],[is_ms_shipped],[is_published],[is_schema_published])
GO

clip_image017

 

另外还附上我测试过程当中,查询条件取值不一样,执行计划不一样的案例(否则有些人也会以为迷惑),仍是那句话,数据会影响执行计划的选择。

clip_image018

clip_image019

 

后记:

   生产环境的案例每每比我上面几个简单例子复杂得多,分析优化起来更加麻烦。咱们优化时要透过现象看本质,多思考,多对比才能拨开迷雾见真相!

相关文章
相关标签/搜索