文章选自:《Pro SQL Server Internals, 2nd edition》CHAPTER 2 Tables and Indexes
作者:Dmitri Korotkevitch
一个聚簇索引表明表中数据的物理顺序,该顺序是根据聚簇索引键排序的。一个表只能定义一个聚簇索引。
假设你想要在带有数据的堆表上创建聚簇索引。首先第一步,如图2-5所示,SQL Server会先创建一个数据副本,然后根据聚簇索引键的值进行排序。所有的数据页都被连接到双向链表中,它们每个页面都包含指向链中的下一个和上一个页面的指针。这个列表称为索引的叶级,并且它也包含目前表格的数据
图2-5 对象:叶级
注意:页面上的排序顺序由槽阵列控制。页面上的实际数据没有排序
当叶级包含很多页面时,SQL Server就会创建一个索引的中间层级,如图2-6所示。
图2-6 聚簇索引结构:中间级和叶级
中间级把每个叶级页面存储为一行。它存储了两条信息:物理地址和它引用的页面对应索引键的最小值。会出现一种异常是当第一页只有一行的时候,SQL Server存储的值是NULL而不是最小索引键值。通过这种优化,当你要在最小索引值的表中插一行时,SQL Server就不需要再更新非叶级级别的行。
中间级上的这些页面也会链接到双向链表中。SQL Server会一直添加中间级,直到有一个级别只包含单个页面为止。这个层级就叫做根级,作为索引的入口点,如图2-7所示
图2-7 聚簇索引结构:根级
就像你看到的一样,索引总是具有一个叶级、一个根级、零个或多个中间级。当索引数据输入单个页面时会出现一种异常。在这种情况下,SQL Server不会创建单独的根层级页面,索引也只包含单个叶级页面。
索引中的级别数主要取决于行和索引键大小。例如,4字节的整数列上的索引在中间和根层级上每行需要13个字节。这13个字节包括一个2字节的槽阵列条目、一个4字节的索引键值、一个6字节的页指针和一个1字节的行开销,这已经够用了,因为索引键不包含可变长度和空列。
因此,每页可以容纳8060字节/ 13字节=620行。这意味着,每一个中间层最多可以存储620 * 620 = 384,400叶级页的信息。如果你的数据行大小为200字节,而且只有三个层级,那么你的每个叶级页面可以存储40行,那你最多可以存储15,376,000行在索引中。增加另外的中间层级到索引中则基本将覆盖掉所有的可用整数值。
注意:在现实生活中,索引碎片化将把这些数字分解。我们将在第6章中讨论索引碎片化。
SQL Server可以通过三种不同的方式从索引中读取数据。第一个是有序扫描。假设我们想执行SELECT Name FROM dbo.Customers ORDER BY CustomerId.的查询。索引页根层级上的数据就会根据CustomerId列值排序存储。因此,SQL Server可以从第一个页面到最后一个页面扫描索引中的叶级,并按照它们存储的顺序返回行
SQL Server从索引的根页面开始读取第一行。该行应用的是来自表的最小键值的中间页面。SQL Server读取该页面并重复该过程,直到它找到叶子层级的第一个页面。然后,SQL Server开始逐个读取行,遍历页面的链表,直到读取了所有行。图2-8说明了这个过程。
图2-8 有序索引扫描
前面查询的执行方案也说明了聚簇索引扫描将其有序属性设置为true的操作,如图2-9所示
图2-9 有序索引扫描执行计划
值得一提的是,order by子句不需要触发有序扫描。有序扫描意味着SQL Server根据索引键的顺序读取数据。
SQL Server可以全方位查询索引,包括向前和向后。但是,必须记住一个重要的点:SQL Server在向后索引扫描期间不执行并行性。
提示:你可以通过检查执行计划中的索引扫描或索引查找操作符属性来检查扫描方向。但是记住,管理工厂在执行计划时不会的表示在图表中显示这些属性。你需要打开属性窗口,通过在执行计划中选择操作符并选择视图/属性窗口菜单项或按F4键来查看它。
SQL Server的企业版有一个称为旋转木马扫描的优化特性,它允许多个任务共享相同的索引扫描。假设会话S1扫描索引。在扫描过程中,另一个会话S2也会执行一个查询,该查询扫描相同的索引。通过旋转木马扫描,S2在当前扫描位置加入S1。SQL Server只读取每个页面一次,将行传递给两个会话。
当S1扫描到达索引的末尾时,S2从索引的开头开始扫描数据,直到S2扫描开始的那一点。旋转木马扫描是说明为什么不能依赖索引键的顺序,以及为什么在关键的时候应该使用order BY子句的一个例子。
排序扫描之后的下一个访问方法称为分配顺序扫描。SQL Server通过IAM页面访问表数据,这与它通过堆表访问表数据的方式类似。SELECT Name FROM dbo.Customers WITH (NOLOCK) 这种查询和图2-10说明了这种方法。图2-11说明了查询执行计划。
图2-10 分配顺序扫描
图2-11 分配顺序扫描执行计划
不幸的是,SQL Server在使用分配顺序扫描时不容易检测到。尽管执行计划中的有序属性显示为false,它也表明SQL Server并不在意是否按照索引键的顺序读取行,也不关心是否使用了分配顺序扫描。分配顺序扫描可以更快的扫描大型表,尽管它有较高的启动成本。当表很小时,SQL Server不使用这种访问方法。
另一个要重要的考虑因素是数据一致性。SQL Server在聚簇索引的表中不使用转发指针,分配顺序扫描可能产生不同的结果。由于页面分割导致的数据移动,行可能会被多次跳过或读取。因此,SQL Server通常不使用分配顺序扫描,除非它在未提交或可序列化事务隔离级别读取数据
注意:我们将在第6章“索引碎片化”中讨论页面分割和碎片,并在第3部分“锁定、阻塞和并发”中讨论锁定和数据一致性。
最后一种索引访问方法称为索引查找。SELECT Name FROM dbo.Customers WHERE CustomerId BETWEEN 4 AND 7的查询和图2-12说明了这个过程。
图2-12 索引查找
为了从表中读取行范围,SQL Server需要从范围中找到键值最小行,就是4。SQL Server从根页面开始,第二行引用键值最小为350的页面。它比我们正在找的键值(4)要大,SQL Server读取被根页引用的中层数据页(1:170)
类似地,中层级页面将SQL Server带到第一个叶级页面(1:176)。SQL Server读取该页面,然后读取CustomerIds中等于4和5的行,最后剩下的两行读取到第二页。
执行计划如图2-13所示。
图2-13 执行计划的索引查找
就像你假设的那样,索引查找比索引扫描更有效,因为SQL Server只处理行和数据页的子集,而不是扫描整个表。
从技术上讲,有两种索引查找操作。第一个称为单例查找,有时也称为点查找,意味着SQL Server只查找和返回一行。你可以想成是操作WHERE CustomerId = 2这样的例子。另一种类型的索引查找操作称为范围扫描,它要求SQL Server找到键的最低或最高值,并去扫描(向前或向后)这些行直到扫描范围结束。可以认为是WHERE CustomerId BETWEEN 4 AND 7这样语句导致的范围扫描。这两种情况都显示为执行计划中的索引查找操作
正如我们猜测的,范围扫描可能会强制SQL Server处理来自索引的大量甚至所有数据页。例如,如果将查询更改为使用WHERE CustomerId >0,SQL Server将读取所有行/页,即使在执行计划中显示了索引查找操作符。你必须记住这种行为,并在查询性能调优期间始终分析范围扫描的效率。
关系数据库中有一个概念叫保留谓词,它代表搜索参数的。如果SQL Server可以使用索引查找操作(如果存在索引),则谓词是保留。简而言之,当SQL Server能够隔离要处理的单个值或索引键值范围时,谓词是可保留的,从而限制了谓词计算期间的搜索。显然,使用保留谓词编写查询并在任何可能的情况下使用索引搜索是有益的。
保留谓词包括以下操作符:=、>、>=、<、<=、IN、BETWEEN和LIKE(在前缀匹配的情况下)。非保留操作符包括NOT、<>、LIKE(在非前缀匹配的情况下)和NOT in。
使谓词不可保留的另一种情况是对表列使用函数或数学计算。SQL Server必须为它处理的每一行调用函数或执行计算。幸运的是,在某些情况下,您可以重构查询,使这些谓词成为可保留。表2-1显示了一些例子。
你必须记住的另一个重要因素是类型转换。在某些情况下,您可以使用不正确的数据类型使谓词不可保留。让我们使用varchar列创建一个表,并用一些数据填充它,如列表2-6所示。
列表2 - 6。保留谓词和数据类型:测试表的创建
create table dbo.Data
(
VarcharKey varchar(10) not null,
Placeholder char(200)
);
create unique clustered index IDX_Data_VarcharKey
on dbo.Data(VarcharKey);
;with N1(C) as (select 0 union all select 0 union all select 0) -- 2 rows
,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows
,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows
,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows
,IDs(ID) as (select row_number() over (order by (select null)) from N5)
insert into dbo.Data(VarcharKey)
select convert(varchar(10),ID) from IDs;
聚簇索引键列被定义为varchar,尽管它存储整数值。现在,让我们运行两个选择,如列表2-7所示,并查看执行计划。
列表2 - 7。保留谓词和数据类型:使用整型参数进行选择
declare
@IntParam int = ‘200‘
select * from dbo.Data where VarcharKey = @IntParam;
select * from dbo.Data where VarcharKey = convert(varchar(10),@IntParam);
如图2-14所示,对于integer参数,SQL Server扫描集群索引,将varchar转换为每一行的整数。在第二种情况下,SQL Server在开始时将整型参数转换为varchar,并使用更高效的聚簇索引查找操作。
图2 - 14。保留谓词和数据类型:具有整型参数的执行计划
提示:注意连接谓词的列数据类型。隐式或显式数据类型转换会显著降低查询的性能。
在unicode字符串参数的情况下,您将观察到非常类似的行为。让我们运行列表2-8所示的查询。图2-15显示了语句的执行计划。
列表2 - 8。保留谓词和数据类型:使用字符串参数进行选择
图2-15。保留谓词和数据类型:带有string参数的执行计划
正向你看到的,对于varchar列,unicode字符串参数是不可保留的。这是一个比看上去大得多的问题。虽然很少以这种方式编写查询,如清单2-8所示,但是现在大多数应用程序开发环境都将字符串视为unicode。结果,SQL Server客户端库为字符串对象生成unicode (nvarchar)参数,除非参数数据类型明确指定为varchar。这使得谓词不可保留,由于不必要的扫描在索引varchar列时,就会导致性能下降。
重要:总是在客户机应用程序指定参数的数据类型。例如,在ADO中,使用Parameters.Add("@ParamName",SqlDbType.Varchar, <Size>).Value = stringVariable代替
Parameters.Add("@ParamName").Value = stringVariable重载。在ORM框架中使用映射指定类中的非unicode属性。
值得一提的是,对于nvarchar unicode数据列,varchar参数是可保留的。
原文地址:https://www.cnblogs.com/JKOBGHJ/p/10111753.html