基本概念
锁
锁是SQL Srv 数据库引擎用来控制多个用户对同一个数据块并发访问的一种机制。SQL Srv等数据库(Informix、DB2等)中的锁不会对行、页、表或索引等资源有实际影响。锁由数据库引擎的一个部件(称为“锁管理器”)在内部管理,它更像一个预订系统。应用程序一般不直接向数据库引擎请求锁,而是通过锁管理器从数据库预订某些资源。
锁管理器为目前已加锁的每个数据项维护一个列表每个请求为链表中一条记录,按请求到达的顺序排序。它使用一个以数据项名称为索引的散列表来查找链表中的数据项。一个数据项的链表中每一条记录表是有那个事务提出的请求,以及它请求什么类型的锁,该记录还表示该请求是否已授予锁。图1是一个锁表示例,该表包含5个不同数据项I4、I7、I23、I44和I912的锁,例如,图中T23在数据项I912和I7上已被授予锁,并且正在等待I4上加锁。
图1 锁表
锁粒度
SQL Srv 数据库引擎可以多粒度锁定,允许一个事务锁定不同类型的资源。为了尽量减少锁定的开销,数据库引擎自动将资源锁定在适合任务的级别。 锁定在较小的粒度(例如行)可以提高并发度,但开销较高,因为如果锁定了许多行,则需要持有更多的锁。锁定在较大的粒度(例如表)会降低了并发度,因为锁定整个表限制了其他事务对表中任意部分的访问。但其开销较低,因为需要维护的锁较少。
数据库引擎通常必须获取多粒度级别上的锁才能完整地保护资源。这组多粒度级别上的锁称为锁层次结构。 例如,为了完整地保护对索引的读取,数据库引擎实例可能必须获取行上的共享锁以及页和表上的意向共享锁。
下表列出了数据库引擎可以锁定的资源。
资源 | 说明 |
RID | 用于锁定堆中的单个行的行标识符。 |
KEY | 索引中用于保护可序列化事务中的键范围的行锁。 |
PAGE | 数据库中的 8 KB 页,例如数据页或索引页。 |
EXTENT | 一组连续的八页,例如数据页或索引页。 |
HoBT | 用于保护没有聚集索引的表中的 B 树(索引)或堆数据页的锁。 |
TABLE | 包括所有数据和索引的整个表。 |
FILE | 数据库文件。 |
APPLICATION | 应用程序专用的资源。 |
METADATA | 元数据锁。 |
ALLOCATION_UNIT | 分配单元。 |
DATABASE | 整个数据库。 |
锁模式
SQL Srv 数据库引擎使用不同的锁模式锁定资源,这些锁模式确定了并发事务访问资源的方式。下表显示了数据库引擎使用的资源锁模式。
锁模式 | 说明 |
共享 (S) | 用于不更改或不更新数据的读取操作,如 SELECT 语句。 |
更新 (U) | 用于可更新的资源中。 防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。 |
排他 (X) | 用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。 确保不会同时对同一资源进行多重更新。 |
意向(I) | 用于建立锁的层次结构。 意向锁包含三种类型:意向共享 (IS)、意向排他 (IX) 和意向排他共享 (SIX)。 |
架构 | 在执行依赖于表架构的操作时使用。 架构锁包含两种类型:架构修改 (Sch-M) 和架构稳定性 (Sch-S)。 |
大容量更新 | 在向表进行大容量数据复制且指定了 TABLOCK 提示时使用。 |
键范围 | 当使用可序列化事务隔离级别时保护查询读取的行的范围。 确保再次运行查询时其他事务无法插入符合可序列化事务的查询的行。 |
下面重点介绍该文讨论的并发问题涉及到的共享锁、排他锁和意向锁。
共享锁(S锁)
允许并发事务在封闭式并发控制下读取 (SELECT)资源。资源上存在共享锁(S锁)时,任何其他事务都不能修改数据。读取操作完成,就立即释放资源上的共享锁(S锁),除非将事务隔离级别设置为可重复读或更高级别,或者在事务持续时间内用锁定提示保留共享锁(S锁)。
排他锁(X锁)
防止并发事务对资源进行访问,使用排他锁(X锁)时,任何其他事务都无法修改数据;仅在使用NOLOCK 提示或未提交读隔离级别时才会进行读取操作。
意向锁(I锁)
意向锁来保护共享锁(S锁)或排他锁(X锁)放置在锁层次结构的底层资源上。之所以命名为意向锁,是因为在较低级别锁前可获取它们,因此会通知意向将锁放置在较低级别上。
ü 当SQL Srv获取低级别的锁时,还将在包含更低级别对象的对象上放置意向锁;
ü 当锁定行或索引键范围时,SQL Srv将在包含这些行或键的页上放置意向锁;
ü 当锁定页时,SQL Srv将在包含这些页的更高级别的对象上放置意向锁。
锁升级
SQL Srv锁升级是将许多较细粒度的锁转换成数量更少的较粗粒度的锁的过程,以更好的使用锁内存资源以减少系统开销,但却增加了并发争用的可能性。
数据库引擎可以为同一语句执行行锁定和页锁定,以最大程度地减少锁的数量,并降低需要进行锁升级的可能性每个升级事件主要在单个 Transact-SQL 语句级别上操作。当事件启动时,只要活动语句满足升级阈值的要求,数据库引擎就会尝试升级当前事务在活动语句所引用的任何表中持有的所有锁。
如果没有使用 ALTER TABLE SET LOCK_ESCALATION 选项来禁用表的锁升级并且满足以下任一条件时,则将触发锁升级:
l 单个Transact-SQL语句在单个无分区表或索引上获得至少5,000个锁;
l 单个 Transact-SQL 语句在已分区表的单个分区上获得至少5,000个锁,并且 ALTER TABLE SET LOCK_ESCALATION 选项设为 AUTO;
l SQL Srv实例中的锁的数量超出了内存或配置阈值。
如果由于锁冲突导致无法升级锁,则数据库引擎每当获取 1,250 个新锁时便会触发锁升级。当数据库引擎对每1250 个新获取的锁检查是否存在升级时,仅当 Transact-SQL 语句在表的单个引用上获取至少5,000 个锁时,触发锁升级。Transact-SQL语句在表的单个引用上获取至少 5,000 个锁时,触发锁升级。例如,如果该语句在一个索引上获取 3,000 个锁,在同一表中的另一个索引上获取3,000 个锁,这种情况下不会触发锁升级。同样,如果语句中含有表的自联接,并且表的每一个引用仅在表中获取 3,000 个锁,则不会触发锁升级。
每当锁的数量大于锁升级的内存阈值时,数据库引擎都会触发锁升级。内存阈值取决于 locks 配置选项的设置:
l 设置为 0:当锁对象使用的内存是数据库引擎使用的内存的 24%时,将达到锁升级阈值。用于表示锁的数据结构有96个字节长。该阈值是动态的,因为数据库引擎动态地获得和释放内存来针对变化的工作负荷进行调整。
l 设置为非 0:锁升级阈值是 locks 选项的值的 40%(或者更低,如果存在内存不足的压力)。
数据库引擎可以为升级选择任何会话中的活动语句,而且,只要实例中使用的锁内存保持在阈值之上,每获取 1,250 个新锁,它就会为升级选择语句。
锁升级时,数据库引擎将尝试将表上的意向锁改为对应的全锁,例如,将意向排他锁(IX 锁)改为排他锁(X 锁),或将意向共享锁(IS 锁)改为共享锁(S 锁)。如果锁升级尝试成功并获取全表锁,将释放事务在堆或索引上所持有的所有堆或 B 树锁、页锁(PAGE 锁)或行级锁(RID 锁)。如果无法获取全锁,当时不会发生锁升级,而数据库引擎将继续获取行、键或页锁。
数据库引擎不会将行锁或键范围锁升级到页锁,而是将它们直接升级到表锁。同样,页锁始终升级到表锁。在 SQL Srv 2008 中,已分区表的锁定可以升级到 HoBT 级别,而不是表锁。
分区表
为什么要分区
什么是分区?为什么要使用分区?简单的回答是:为了改善大型表以及具有各种访问模式的表的可伸缩性和可管理性。在表变大的情况下,表的性能、可伸缩性和可管理性需要重新加以设计和考虑。
除了大小之外,当表中的不同行集拥有不同的使用模式时,具有不同访问模式的表也可能会影响性能和可用性。例如,零层中当前月份的数据是可读写的,但以往月份的数据(通常占表数据的大部分)是只读的。
分区可以带来什么帮助?当表和索引变得非常大时,分区可以将数据分为更小、更容易管理的部分,从而提供一定的帮助。此文重点介绍横向分区,在横向分区中,大量的行组存储在多个相互独立的分区中。
此外,如果具有多个 CPU 的系统中存在一个大型表,则对该表进行分区可以通过并行操作获得更好的性能。通过对各个并行子集执行多项操作,可以改善在极大型数据集(例如数百万行)中执行大规模操作的性能。
定义和术语
SQL Srv 2005 及以后版本的分区表和索引在存储表的方式和位置方面就有了多种选择。在SQL Srv中可以根据分区架构创建表和索引。分区架构可以将对象映射到一个或多个文件组。为了确定数据的相应物理位置,分区架构将使用分区函数。分区函数定义了用来定向行的算法,而架构则将分区与其相应的物理位置(即文件组)相关联。
范围分区
范围分区是按照特定和可定制的数据范围定义的表分区。范围分区的边界由开发人员选择,还可以随着数据使用模式的变化而变化。通常,这些范围是根据日期、机构排序后的数据组进行划分的。
范围分区主要用于数据存档(日期)和并发处理(机构)。SQL Srv分区表和索引的最大优点就是可以管理特定范围内的数据。通过范围分区,可以非常快速地存档和替换旧的数据,不同分区间并发处理数据。当数据访问通常用于对大范围数据的决策支持时,最适合使用范围分区。在这种情况下,数据所在的具体位置至关重要,这样才能在需要时只访问相应的分区。另外,由于事务数据已经可用,因此可以轻松快捷地添加数据。范围分区最初定义起来很复杂,因为需要为每个分区定义边界条件。此外,还需要创建一个架构,将每个分区映射到一个或多个文件组。但是,它们通常具有一致的模式,因此,定义后很容易通过编程方式进行维护(参见图2)。
图2 36个分区的范围分区表
分区键
对表和索引进行分区的第一步就是定义分区的关键数据。分区键必须作为一个列存在于表中,还必须满足一定的条件。分区函数定义键(也称为数据的逻辑分离)所基于的数据类型。函数只定义键,而不定义数据在磁盘上的物理位置。数据的位置由分区架构决定。换句话说,架构将数据映射到一个或多个文件组,文件组将数据映射到特定的文件,文件又将数据映射到磁盘。分区架构通常使用函数来实现此目的:如果函数定义了五个分区,则架构必须使用五个文件组。文件组不需要各不相同;但是,如果拥有多个磁盘(最好是多个 CPU),使用不同的文件组可以获得更好的性能。将架构与表一起使用时,需要定义用作分区函数的参数的列。
索引分区
除了对表的数据集进行分区之外,还可以对索引进行分区。使用相同的函数对表及其索引进行分区通常可以优化性能。当索引和表按照相同的顺序使用相同的分区函数和列时,表和索引将对齐。如果在已经分区的表中建立索引,SQL Srv 会自动将新索引与该表的分区架构对齐,除非该索引的分区明显不同。当表及其索引对齐后,SQL Srv 则可以更有效地将分区移入和移出分区表,因为所有相关的数据和索引都使用相同的算法进行划分。
如果定义表和索引时不仅使用了相同的分区函数,还使用了相同的分区架构,则这些表和索引将被认为是按存储位置对齐。按存储位置对齐的一个优点是,相同边界内的所有数据都位于相同的物理磁盘上。在这种情况下,可以单独在某个时间段内执行备份操作,还可以根据数据的变化在备份频率和备份类型方面改变您的策略。如果连接或收集了相同文件或文件组中的表和索引,则可以发现更多的好处。SQL Srv 可以通过在多个分区中并行操作来获益。在按存储位置对齐和多 CPU 的情况下,每个处理器都可以直接处理特定的文件或文件组,而不会与数据访问产生任何冲突,因为所有需要的数据都位于同一个磁盘上。这样,可以并行运行多个进程,而不会相互干扰。
分区表实现步骤
逻辑流程如下:
图3 分区表实现逻辑图
确定分区对象
虽然分区可以带来众多的好处,但也增加了实现对象的管理费用和复杂性,这可能是得不偿失的。尤其是可能不需要为较小的表或目前满足性能和维护要求的表分区。
分区键和分区数
如果业务模式为多机构并发处理数据,则可以通过机构键的范围分区减少数据争用的情况,同时通过日期键的范围分区减少只读数据不需要分区时的维护工作。要确定分区数,应先评估分区键或并发机构数目。
多文件组
为了有助于优化性能和维护,应使用文件组分离数据。文件组的数目一定程度上由硬件资源决定:一般情况下,文件组数最好与分区数相同,并且这些文件组通常位于不同的磁盘上。但是,这主要适用于打算对整个数据集进行分析的系统。如果有多个 CPU,SQL Srv 则可以并行处理多个分区,从而大大缩短处理大量复杂报表和分析的总体时间。这种情况下,可以获得并行处理的好处。
创建文件组
如果需要为多个文件放置一个分区表以获得更好的 I/O 平衡,则至少需要创建一个文件组。文件组可以由一个或多个文件构成,而每个分区必须映射到一个文件组。一个文件组可以由多个分区使用,但是为了更好地管理数据(例如,为了获得更精确的备份控制),应该对分区表进行设计,以便只有相关数据或逻辑分组的数据位于同一个文件组中。使用ALTERDATABASE可以添加逻辑文件组名,然后添加文件。要为AdventureWorks数据库创建名为2003Q3的文件组:
ALTER DATABASE AdventureWorks ADD FILEGROUP [2003Q3]
创建文件组后,使用ALTER DATABASE将文件添加到该文件组中。
ALTER DATABASE AdventureWorks ADDFILE(NAME=N'2003Q3',
FILENAME = N'C:\AdventureWorks\2003Q3.ndf', SIZE= 5MB,MAXSIZE= 100MB,FILEGROWTH= 5MB)TOFILEGROUP[2003Q3]
通过在 CREATE TABLE 的ON 子句中指定一个文件组,可以为文件创建一个表。但是,如果表未分区,则不能为多个文件组创建一个表。要为一个文件组创建表,请使用 CREATE TABLE 的ON 子句。要创建分区表,必须先确定分区的功能机制。进行分区的标准以分区函数的形式从逻辑上与表相分离。此分区函数作为独立于表的定义存在,而这种物理分离将起到帮助作用,因为多个对象都可以使用该分区函数。因此,为表分区的第一步是创建分区函数。
创建分区函数
在范围分区中首先定义边界点:如果存在五个分区,则定义四个边界点值,并指定每个值是第一个分区的上边界 (LEFT) 还是第二个分区的下边界(RIGHT)。根据 LEFT 或 RIGHT 指定,始终有一个空分区,因为该分区没有明确定义的边界点。
具体来讲,如果分区函数的第一个值(或边界条件)是 '20001001',则边界分区中的值将是:
l 对于 LEFT:
ü 第一个分区是所有小于或等于 '20001001' 的数据
ü 第二个分区是所有大于 '20001001' 的数据
l 对于 RIGHT:
ü 第一个分区是所有小于 '20001001' 的数据
ü 第二个分区是所有大于或等于 '20001001' 数据
要在四个活动分区(每个分区代表一个日历季度)中存储四分之一的 Orders 数据,并创建第五个分区以备将来使用(还是作为占位符,用于在分区表中移入和移出数据),请将 LEFT 分区函数与以下四个边界条件结合使用:
CREATE PARTITION FUNCTION OrderDateRangePFN(datetime)AS RANGELEFTFORVALUES('20000930 23:59:59.997','20001231 23:59:59.997','20010331 23:59:59.997','20010630 23:59:59.997')
有关详细信息,请参见SQL Srv Books Online的“Transact-SQL Reference”中的“Date and Time”部分。
创建分区架构
创建分区函数后,必须将其与分区架构相关联,以便将分区定向至特定的文件组。定义分区架构时,即使多个分区位于同一个文件组中,也必须为每个分区指定一个文件组。对于前面创建的范围分区 (OrderDateRangePFN),存在五个分区;最后一个空分区将在PRIMARY 文件组中创建。因为此分区永远不包含数据,所以不需要指定特殊的位置。
CREATE PARTITION SCHEME OrderDatePSchemeAS PARTITION OrderDateRangePFNTO ([2000Q3],[2000Q4],[2001Q1],[2001Q2],[PRIMARY])
创建分区表
定义分区函数(逻辑结构)和分区架构(物理结构)后,即可创建表来利用它们。表定义应使用的架构,而架构又定义函数。要将这三者结合起来,必须指定应该应用分区函数的列。范围分区始终只映射到表中的一列,此列应与分区函数中定义的边界条件的数据类型相匹配。
CREATETABLE[dbo].[OrdersRange] (
[PurchaseOrderID][int]NOTNULL,
[EmployeeID][int]NULL,
[VendorID][int]NULL,
[TaxAmt][money]NULL,
[Freight][money]NULL,
[SubTotal][money]NULL,
[Status][tinyint]NOTNULL,
[RevisionNumber][tinyint]NULL,
[ModifiedDate][datetime]NULL,
[ShipMethodID][tinyint]NULL,
[ShipDate][datetime]NOTNULL,
[OrderDate][datetime]NOTNULL,
[TotalDue][money]NULL
)ONOrderDatePScheme(OrderDate)
索引分区
默认情况下,分区表中创建的索引也使用相同的分区架构和分区列。如果属于这种情况,索引将与表对齐。尽管未作要求,但将表与其索引对齐可以使管理工作更容易进行,对于滑动窗口方案尤其如此。
例如,要创建唯一的索引,分区列必须是一个关键列;这将确保对相应的分区进行验证,以保证索引的唯一性。因此,如果需要在一列上对表进行分区,而必须在另一个列上创建唯一的索引,这些表和索引将无法对齐。在这种情况下,可以在唯一的列(如果是多列的唯一键,则可以是任一关键列)中对索引进行分区,或者根本就不进行分区。
注意:如果打算使用现有数据加载表并立即在其中添加索引,则通常可以通过以下方式获得更好的性能:先加载到未分区、未建立索引的表中,然后在加载数据后创建分区索引。通过为分区架构定义群集索引,可以在加载数据后更有效地为表分区。这也是为现有表分区的不错方法。要创建与未分区表相同的表并创建与已分区群集索引相同的群集索引,可用一个文件组目标位置替换创建表中的 ON 子句。然后,在加载数据之后为分区架构创建群集索引。
分区锁机制
以下通过一个实验,来窥探分区锁的使用方法。
SQL Srv 2005及其以前的数据库版本锁升级粒度比较粗,并没有分区锁的概念,因而在不同分区间查询、更新等操作会导致锁粒度由行锁或页锁直接升级为表锁,使得整张表被某个进程锁定,并发性能严重下降。SQL Srv 2008细化了锁升级粒度,可以从行锁、页锁升级为分区锁,而不是直接升级为表锁,因而最大限度地加大了分区间的并发性。
只有在表创建以后,通过ALTER TABLE语句设置表升级级别,而不能在建表时指定表升级级别,锁升级的选项设置值有以下三种:
• TABLE – 锁升级一旦发生,将直接升级为表锁(默认升级选项);
• AUTO – 对于已分区的表,锁将升级分区锁,否则升级为表锁;
• DISABLE – 锁升级关闭,但并不能保证锁升级不会发生(某些情况下可能会发生)。
表的锁升级级别可以通过sys.tables视图查询。
SELECT lock_escalation_desc FROM sys.tablesWHEREname='TableName';
实验数据库表采用区间分区方案划分为三个分区,取值分别为(-∞,7999],[80000,15999],[160000,+∞)。
CREATEDATABASELockEscalationTest;
GO
USE LockEscalationTest;
GO
--创建分区函数
CREATEPARTITIONFUNCTIONMyPartitionFunction(INT)ASRANGERIGHTFORVALUES(8000, 16000);
GO
--创建分区方案
CREATEPARTITIONSCHEMEMyPartitionSchemeASPARTITIONMyPartitionFunctionALLTO([PRIMARY]);
GO
--创建分区表
CREATETABLEMyPartitionedTable(c1INT);
GO
--创建分区聚集索引
CREATECLUSTEREDINDEXMPT_ClustONMyPartitionedTable(c1)ONMyPartitionScheme(c1);
GO
--表数据填充
SET NOCOUNT ON;
GO
DECLARE@aINT= 1;
WHILE (@a < 17000)
BEGIN
INSERTINTOMyPartitionedTableVALUES(@a);
SELECT@a=@a+ 1;
END;
GO
实验数据准备好后,我们可以看到MyPartitionedTable表默认的锁升级级别为Table:
SELECT lock_escalation_desc FROM sys.tablesWHEREname='MyPartitionedTable';
此时我们对分区一进行更新操作:
BEGIN TRAN
UPDATE MyPartitionedTable SET c1 = c1 WHERE c1 < 7500;
GO
查看 MyPartitionedTable 表上锁定情况:
SELECT
[resource_type],[resource_associated_entity_id],[request_mode],[request_type],[request_status]
FROM sys.dm_tran_locksWHERE[resource_type]<>'DATABASE';
GO
表锁情况如下:
我们可以看到,访问分区一的会话使锁粒度升级为排他锁,即在整张表上加表锁,此时任何其它会话对该表的操作(包括读操作)都将被阻塞。
为了说明分区锁的作用,我们回滚刚才的更新操作,同时将表升级级别设置为分区锁,再对分区一做同样的更新操作。
查看分区属性及表锁情况:
SELECT [partition_id], [object_id],[index_id],[partition_number]
FROM sys.partitionsWHEREobject_id=OBJECT_ID('MyPartitionedTable');
GO
分区属性情况如下:
SELECT [resource_type], [resource_associated_entity_id], [request_mode],[request_type],[request_status]
FROM sys.dm_tran_locksWHERE[resource_type]<>'DATABASE';
GO
表锁情况如下:
我们可以看到,分区一加了排他锁,表加了意向排他锁,而其他两个分区上并未添加任何锁,此时,其它会话对分区一的所有操作(包括读操作)会被阻塞,而对其它分区的任何操作不会受到任何影响。
接下来,我们在另一会话对另一分区进行更新操作,看看会发生什么。
BEGIN TRAN UPDATE MyPartitionedTable set c1 = c1
WHERE c1 > 8100 AND c1 < 15900;
GO
分区属性情况如下:
表锁情况如下:
分区一、分区二上加了排他锁,而表仍然只有意向排他锁,而分区三未添加任何锁,此时,其它会话对分区一、分区二的所有操作(包括读操作)会被阻塞,而对分区三的任何操作不会受到任何影响。
其他说明
l 每个分区表或索引的分区数最多为1000个,SQL Srv 2012在默认情况下支持多达 15,000 个分区;
l 避免全表查询或处理的语句,因为该语句会迫使SQL Srv进行全表锁定,因而阻止其它会话对某个分区的访问,造成死锁;
l 尽量一个会话访问一个分区,避免会话访问多个分区。
补充材料
l SQL Srv 2008新增可识别分区的操作
在 SQL Server 2008 中已分区表将作为一个多列索引呈现给查询处理器,其中 PartitionID 是第一列。PartitionID 是一个隐藏的计算列,用于在内部表示包含特定行的分区的 ID。例如,假设一个定义为 T(a, b,c) 的表 T 在 a 列进行了分区,并在 b 列的聚集索引。在 SQL Server 2008 中,此分区表在内部被视为一个具有架构 T(PartitionID, a, b, c) 的未分区表,并具有组合键 (PartitionID, b) 的聚集索引。这样查询优化器便可以基于PartitionID 对任何已分区表或索引执行查找操作。
现在,分区的排除任务已在此查找操作中完成。
此外,查询优化器的功能也得以扩展,可以针对 PartitionID(作为逻辑首列)以及其他可能的索引键列执行某一条件下的查找或扫描操作,然后,对于符合第一级查找操作的条件的每个不同值,再针对一个或多个其他列执行不同条件下的二级查找。也就是说,这种称为“跳跃扫描”的操作允许查询优化器基于某一条件来执行查找或扫描操作以确定要访问的分区,然后在该运算符内执行一个二级索引查找操作以返回这些分区中符合另一个不同条件的行。例如,请考虑以下查询。
SELECT * FROM T WHERE a < 10 and b = 2;
对于本示例,假设定义为 T(a, b, c) 的表 T 对 a 列进行了分区,并具有 b 的聚集索引。表 T 的分区边界由以下分区函数定义:
CREATE PARTITION FUNCTION myRangePF1 (int) AS RANGE LEFT FOR VALUES(3, 7, 10);
为求解该查询,查询处理器将执行第一级查找操作以查找包含符合条件 T.a < 10 的行的每个分区。这将标识要访问的分区。然后,在所标识的每个分区内,处理器将针对 b 列的聚集索引执行一个二级查找以查找符合条件 T.b = 2 和 T.a <10 的行。
下图所示为跳跃扫描操作的逻辑表示形式,其中显示了在 a 列和 b 列中包含数据的表 T。分区编号为 1 到 4,分区边界由垂直虚线表示。对分区执行的第一级查找操作(图中未显示)已确定分区 1、2 和 3 符合查找条件(由为该表定义的分区和 a 列的谓词指示),即 T.a < 10。曲线指示了跳跃扫描操作的二级查找部分所遍历的路径。实际上,跳跃扫描操作将在这些分区的每个分区中查找符合条件 b = 2 的行。跳跃扫描操作的总开销等于三个单独索引查找之和。
附图1 分区查询示意图
l 已分区对象的并行查询执行策略
查询处理器对从已分区对象选择的查询使用查询执行策略。作为执行策略的一部分,查询处理器会确定查询所需的表分区,以及要分配给每个分区的线程比例。在大多数情况下,查询处理器会为每个分区分配数量相等或几乎相等的线程,然后在这些分区中并行地执行查询。以下几段更详细地介绍了线程分配情况。
如果线程数小于分区数,则查询处理器会将每个线程分配给一个不同的分区,最初会有一个或多个分区没有获得分配的线程。当线程完成在一个分区上的执行时,查询处理器会将它分配给下一个分区,直到每个分区都分配有一个线程。这是查询处理器将线程重新分配给其他分区的唯一情况。
附图2 线程数小于分区数
如果线程数与分区数相等,则查询处理器会为每个分区分配一个线程。当线程完成时,不会重新分配给另一个分区。
附图3 线程数等于分区数
如果线程数大于分区数,则查询处理器会为每个分区分配相等数量的线程。如果线程数并非恰好是分区数的倍数,则查询处理器会为某些分区额外分配一个线程,以使用所有可用线程。请注意,如果只有一个分区,则会将所有线程都分配给该分区。在下图中,有四个分区和 14 个线程。每个分区都分配有 3 个线程,两个分区具有一个额外的线程,总共分配了 14 个线程。当线程完成时,不会重新分配给另一个分区。
附图4 线程数大于分区数
尽管以上示例指出了一种分配线程的简单方式,但实际策略要复杂一些,并需要考虑在查询执行过程中出现的其他变化因素。例如,如果表已分区,并在 A 列上有一个聚集索引,并且查询有谓词子句 WHEREA IN (13, 17, 25),则查询处理器将为这三个查找值(A=13、A=17 和 A=25))各分配一个或多个线程,而不是为每个表分区分配一个或多个线程。只需在包含这些值的分区中执行查询,并且如果所有这些查找谓词都恰好在同一个表分区中,则所有线程都将分配给同一个表分区。
为了举出另一个示例,假定表在 A 列上有四个分区(边界点为 (10, 20,30)),在 B 列上有一个索引,并且查询有一个谓词子句 WHERE B IN (50, 100, 150)。因为表分区是基于值 A,所以值 B 可以出现在任何表分区中。这样,查询处理器将分别在四个表分区中查找三个 B 值 (50, 100, 150) 中的每一个值。查询处理器将按比例分配线程,以便它可以并行执行 12 个查询扫描中的每一个扫描。
参考资料
[1] Abraham Silberschatz, Henry F.Korth and S.Sudarshan. Database System Concepts (6thEdition)
[2] Kalen Delaney.InsideMicrosoft SQL Server 2005: The Storage Engine.
[3] Itzik Ben-Gan. Inside Microsoft SQL Server 2008: T-SQL Querying.
[4] Itzik Ben-Gan. Inside Microsoft SQL Server 2008: The T-SQL Programming.
[5] Kalen Delaney. Inside Microsoft SQLServer 2005 Query Tuning and Optimization.
[6]徐海蔚. Microsoft SQL Server企业级平台管理实践.2010.
[7] T.Kyte. ExpertOracle Database Architecture Oracle Database Programming 9i, 10g, and 11g Techniquesand Solutions, Second Edition.
[8] http://msdn.microsoft.com/zh-cn/library/ms345599(v=sql.105).aspx
[9] http://msdn.microsoft.com/zh-cn/library/ms345146(v=sql.90).aspx