在SQL SERVER中导致索引查找变成索引扫描的问题分析

 更新时间:2015年09月16日 11:04:57   作者:潇湘隐者  
SQL Server 中什么情况会导致其执行计划从索引查找(Index Seek)变成索引扫描(Index Scan)呢? 下面从几个方面结合上下文具体场景做了下测试、总结、归纳。需要的朋友可以参考下本文

SQL Server 中什么情况会导致其执行计划从索引查找(Index Seek)变成索引扫描(Index Scan)呢? 下面从几个方面结合上下文具体场景做了下测试、总结、归纳。

1:隐式转换会导致执行计划从索引查找(Index Seek)变为索引扫描(Index Scan)

Implicit Conversion will cause index scan instead of index seek. While implicit conversions occur in SQL Server to allow data evaluations against different data types, they can introduce performance problems for specific data type conversions that result in an index scan occurring during the execution.  Good design practices and code reviews can easily prevent implicit conversion issues from ever occurring in your design or workload. 

如下示例,AdventureWorks2014数据库的HumanResources.Employee表,由于NationalIDNumber字段类型为NVARCHAR,下面SQL发生了隐式转换,导致其走索引扫描(Index Scan)

SELECT NationalIDNumber, LoginID 
FROM HumanResources.Employee 
WHERE NationalIDNumber = 112457891 

clipboard

我们可以通过两种方式避免SQL做隐式转换:

    1:确保比较的两者具有相同的数据类型。

    2:使用强制转换(explicit conversion)方式。

我们通过确保比较的两者数据类型相同后,就可以让SQL走索引查找(Index Seek),如下所示

SELECT nationalidnumber,
    loginid
FROM  humanresources.employee
WHERE nationalidnumber = N'112457891' 

clipboard[1]

注意:并不是所有的隐式转换都会导致索引查找(Index Seek)变成索引扫描(Index Scan),Implicit Conversions that cause Index Scans 博客里面介绍了那些数据类型之间的隐式转换才会导致索引扫描(Index Scan)。如下图所示,在此不做过多介绍。

clipboard[2]

clipboard[3]

避免隐式转换的一些措施与方法

    1:良好的设计和代码规范(前期)

    2:对发布脚本进行Rreview(中期)

    3:通过脚本查询隐式转换的SQL(后期)

下面是在数据库从执行计划中搜索隐式转换的SQL语句

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
DECLARE @dbname SYSNAME 
SET @dbname = QUOTENAME(DB_NAME());
WITH XMLNAMESPACES 
  (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') 
SELECT 
  stmt.value('(@StatementText)[1]', 'varchar(max)'), 
  t.value('(ScalarOperator/Identifier/ColumnReference/@Schema)[1]', 'varchar(128)'), 
  t.value('(ScalarOperator/Identifier/ColumnReference/@Table)[1]', 'varchar(128)'), 
  t.value('(ScalarOperator/Identifier/ColumnReference/@Column)[1]', 'varchar(128)'), 
  ic.DATA_TYPE AS ConvertFrom, 
  ic.CHARACTER_MAXIMUM_LENGTH AS ConvertFromLength, 
  t.value('(@DataType)[1]', 'varchar(128)') AS ConvertTo, 
  t.value('(@Length)[1]', 'int') AS ConvertToLength, 
  query_plan 
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('.//Convert[@Implicit="1"]') AS n(t) 
JOIN INFORMATION_SCHEMA.COLUMNS AS ic 
  ON QUOTENAME(ic.TABLE_SCHEMA) = t.value('(ScalarOperator/Identifier/ColumnReference/@Schema)[1]', 'varchar(128)') 
  AND QUOTENAME(ic.TABLE_NAME) = t.value('(ScalarOperator/Identifier/ColumnReference/@Table)[1]', 'varchar(128)') 
  AND ic.COLUMN_NAME = t.value('(ScalarOperator/Identifier/ColumnReference/@Column)[1]', 'varchar(128)') 
WHERE t.exist('ScalarOperator/Identifier/ColumnReference[@Database=sql:variable("@dbname")][@Schema!="[sys]"]') = 1

2:非SARG谓词会导致执行计划从索引查找(Index Seek)变为索引扫描(Index Scan)

    SARG(Searchable Arguments)又叫查询参数, 它的定义:用于限制搜索的一个操作,因为它通常是指一个特定的匹配,一个值的范围内的匹配或者两个以上条件的AND连接。不满足SARG形式的语句最典型的情况就是包括非操作符的语句,如:NOT、!=、<>;、!<;、!>;NOT EXISTS、NOT IN、NOT LIKE等,另外还有像在谓词使用函数、谓词进行运算等。

2.1:索引字段使用函数会导致索引扫描(Index Scan)

SELECT nationalidnumber,
    loginid
FROM  humanresources.employee
WHERE SUBSTRING(nationalidnumber,1,3) = '112'

clipboard[4]

2.2索引字段进行运算会导致索引扫描(Index Scan)

    对索引字段字段进行运算会导致执行计划从索引查找(Index Seek)变成索引扫描(Index Scan):

SELECT * FROM Person.Person WHERE BusinessEntityID + 10 < 260

clipboard[5]

一般要尽量避免这种情况出现,如果可以的话,尽量对SQL进行逻辑转换(如下所示)。虽然这个例子看起来很简单,但是在实际中,还是见过许多这样的案例,就像很多人知道抽烟有害健康,但是就是戒不掉!很多人可能了解这个,但是在实际操作中还是一直会犯这个错误。道理就是如此!

SELECT * FROM Person.Person WHERE BusinessEntityID < 250

clipboard[6]

2.3 LIKE模糊查询回导致索引扫描(Index Scan)

    Like语句是否属于SARG取决于所使用的通配符的类型, LIKE 'Condition%' 就属于SARG、LIKE '%Condition'就属于非SARG谓词操作

SELECT * FROM Person.Person WHERE LastName LIKE 'Ma%'

clipboard[7]

SELECT * FROM Person.Person WHERE LastName LIKE '%Ma%'

clipboard[8]

3:SQL查询返回数据页(Pages)达到了临界点(Tipping Point)会导致索引扫描(Index Scan)或表扫描(Table Scan)

What is the tipping point?
It's the point where the number of rows returned is "no longer selective enough". SQL Server chooses NOT to use the nonclustered index to look up the corresponding data rows and instead performs a table scan.

    关于临界点(Tipping Point),我们下面先不纠结概念了,先从一个鲜活的例子开始吧:

SET NOCOUNT ON;
DROP TABLE TEST
CREATE TABLE TEST (OBJECT_ID INT, NAME VARCHAR(8));
CREATE INDEX PK_TEST ON TEST(OBJECT_ID)
DECLARE @Index INT =1;
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

如上所示,当我们查询OBJECT_ID=1的数据时,优化器使用索引查找(Index Seek)

clipboard[9]

上面OBJECT_ID=1的数据只有一条,如果OBJECT_ID=1的数据达到全表总数据量的20%会怎么样? 我们可以手工更新2001条数据。此时SQL的执行计划变成全表扫描(Table Scan)了。

UPDATE TEST SET OBJECT_ID =1 WHERE OBJECT_ID<=2000;
UPDATE STATISTICS TEST WITH FULLSCAN;
SELECT * FROM TEST WHERE OBJECT_ID= 1

clipboard[10]

clipboard[11]

临界点决定了SQL Server是使用书签查找还是全表/索引扫描。这也意味着临界点只与非覆盖、非聚集索引有关(重点)。

Why is the tipping point interesting?
It shows that narrow (non-covering) nonclustered indexes have fewer uses than often expected (just because a query has a column in the WHERE clause doesn't mean that SQL Server's going to use that index)
It happens at a point that's typically MUCH earlier than expected… and, in fact, sometimes this is a VERY bad thing!
Only nonclustered indexes that do not cover a query have a tipping point. Covering indexes don't have this same issue (which further proves why they're so important for performance tuning)
You might find larger tables/queries performing table scans when in fact, it might be better to use a nonclustered index. How do you know, how do you test, how do you hint and/or force… and, is that a good thing?

4:统计信息缺失或不正确会导致索引扫描(Index Scan)

     统计信息缺失或不正确,很容易导致索引查找(Index Seek)变成索引扫描(Index Scan)。 这个倒是很容易理解,但是构造这样的案例比较难,一时没有想到,在此略过。

5:谓词不是联合索引的第一列会导致索引扫描(Index Scan)

SELECT * INTO Sales.SalesOrderDetail_Tmp FROM Sales.SalesOrderDetail;
CREATE INDEX PK_SalesOrderDetail_Tmp ON Sales.SalesOrderDetail_Tmp(SalesOrderID, SalesOrderDetailID);
UPDATE STATISTICS  Sales.SalesOrderDetail_Tmp WITH FULLSCAN;

下面这个SQL语句得到的结果是一致的,但是第二个SQL语句由于谓词不是联合索引第一列,导致索引扫描

SELECT * FROM Sales.SalesOrderDetail_Tmp
WHERE SalesOrderID=43659 AND SalesOrderDetailID<10

clipboard[12]

SELECT * FROM Sales.SalesOrderDetail_Tmp WHERE SalesOrderDetailID<10

clipboard[13]

相关文章

  • SQL Server 使用 Pivot 和 UnPivot 实现行列转换的问题小结

    SQL Server 使用 Pivot 和 UnPivot 

    对于行列转换的数据,通常也就是在做报表的时候用的比较多,今天就通过本文给大家总结下SQL Server 使用 Pivot 和 UnPivot 实现行列转换的问题小结,感兴趣的朋友一起看看吧
    2022-01-01
  • 模糊查询的通用存储过程

    模糊查询的通用存储过程

    模糊查询的通用存储过程实现语句。
    2009-07-07
  • mybatis-plus的sql语句打印问题小结

    mybatis-plus的sql语句打印问题小结

    这篇文章主要介绍了mybatis-plus的sql语句打印问题,今天将常用的方式拷贝过来之后,发现没有发生效果(开始的时候以为是使用配置中心nacos导致问题,最后经过仔细的检查发现是单词拼错了),所以在这里记录一下
    2022-04-04
  • sql server 性能优化之nolock

    sql server 性能优化之nolock

    在SQL Server数据库查询时,为了提高查询的性能,我们往往会在表后面加一个nolock,或者是with(nolock),让数据库在查询时不锁定表,从而提高查询的速度,接下来,通过本篇文章给大家详解sql server 性能优化之nolock,需要的朋友快来学习吧。
    2015-08-08
  • sql函数实现去除字符串中的相同的字符串

    sql函数实现去除字符串中的相同的字符串

    去除字符串中的相同的字符,此功能在开发过程中很实用,为此本文整理了一些,希望对你了解它有所帮助
    2013-01-01
  • SQL Server清除事务日志的两种方式

    SQL Server清除事务日志的两种方式

    事务日志是一种记录每次数据库修改操作的日志,它记录了每一次事务修改的详细日志,但磁盘容量始终有限制,本文主要介绍了SQL Server清除事务日志的两种方式,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • 新手SqlServer数据库dba需要注意的一些小细节

    新手SqlServer数据库dba需要注意的一些小细节

    这篇文章主要介绍了新手SqlServer数据库dba需要注意的一些小细节,本文讲解了15个小细节、小技巧及需要注意的地方,需要的朋友可以参考下
    2015-02-02
  • SQL LOADER错误小结

    SQL LOADER错误小结

    在使用SQL*LOADER装载数据时,由于平面文件的多样化和数据格式问题总会遇到形形色色的一些小问题,下面是小编抽时间整理的一些错误,感兴趣的朋友一起学习吧
    2015-12-12
  • SqlServer 巧妙解决多条件组合查询

    SqlServer 巧妙解决多条件组合查询

    开发中经常会遇得到需要多种条件组合查询的情况,比如有三个表,年级表Grade(GradeId,GradeName),班级Class(ClassId,ClassName,GradeId),学员表Student(StuId,StuName,ClassId),现要求可以按年级Id、班级Id、学生名,这三个条件可以任意组合查询学员信息
    2012-11-11
  • SQL Server常用存储过程及示例

    SQL Server常用存储过程及示例

    以下是对SQL Server中常用的存储过程进行了介绍。需要的朋友可以过来参考下
    2013-08-08

最新评论