介绍
在上一篇SQL Server 聚集索引概述 中,我们探讨了 SQL Server 中索引和聚集索引的要求。
在我们继续之前,让我们快速总结一下 SQL Server 聚集索引:
非聚集索引对于查询性能和优化也很有用,具体取决于查询工作负载。在本文中,让我们探索非聚集索引及其内部结构。
在非聚集索引中,叶节点不包含实际数据。它由一个指向实际数据的指针组成。
在下图中,我们可以查看指向聚集索引中数据页的非聚集索引的叶节点:
我们可以在 SQL 表中有多个非聚集索引,因为它是一个逻辑索引,与聚集索引相比,它不会对数据进行物理排序。
让我们通过一个例子来了解 SQL Server 中的非聚集索引。
CREATE TABLE dbo.Employee
(EmpID INT,
EMpName VARCHAR(50),
EmpAge INT,
EmpContactNumber VARCHAR(10)
);
Insert into Employee values(1,'Raj',32,8474563217)
Insert into Employee values(2,'kusum',30,9874563210)
Insert into Employee values(3,'Akshita',28,9632547120)
Select * from Employee where EmpID=2
它执行表扫描(Table Scan),因为我们在该表上没有任何索引:
CREATE UNIQUE CLUSTERED INDEX IX_Clustered_Empployee ON dbo.Employee(EmpID);
在这个执行计划中,我们可以注意到表扫描(Table Scan)变成了聚集索引查找(Cluster Index Seek):
让我们执行另一个 SQL 查询来搜索具有特定联系号码的 Employee:
Select * from Employee where EmpContactNumber='9874563210'
我们在EmpContactNumber列上没有索引,因此查询优化器使用聚集索引,但它扫描整个索引以检索记录:
右键单击执行计划并选择Show Execution Plan XML:
它在新查询窗口中打开 XML 执行计划。在这里,我们注意到它使用聚集索引键并读取各个行以检索结果:
让我们使用以下脚本在 Employee 表中插入更多记录:
Insert into Employee values(4,'Manoj',38,7892145637)
Insert into Employee values(5,'John',33,7900654123)
Insert into Employee values(6,'Priya',18,9603214569)
我们在这个表中有六个员工的记录。现在,再次执行 select 语句以检索具有特定联系号码的员工记录:
它再次根据指定的条件扫描所有六行的结果。想象一下,表中有数百万条记录。如果 SQL Server 必须读取所有索引键行,这将是一项资源和耗时的任务。
我们可以按照下图以 B 树格式表示聚集索引(不是实际表示):
在前面的查询中,SQL Server 读取根节点页并检索每个叶节点页和行以进行数据检索。
现在让我们在 SQL Server的EmpContactNumber列上的Employee表上 创建一个唯一的非聚集索引作为索引键:
CREATE UNIQUE NONCLUSTERED INDEX IX_NonClustered_Employee ON dbo.Employee(EmpContactNumber);
在我们解释这个索引之前,重新运行 SELECT 语句并查看实际的执行计划:
在这个执行计划中,我们可以看到两个组件:
要了解这些组件,我们需要查看 SQL Server 设计中的非聚集索引。在这里,您可以看到叶节点包含非聚集索引键(EmpContactNumber)和聚集索引键(EmpID):
现在,如果重新运行 SELECT 语句,它将使用非聚集索引键遍历并指向具有聚集索引键的页面:
它表明它使用聚集索引键和非聚集索引键的组合检索记录。您可以看到 SELECT 语句的完整逻辑,如下所示:
之前,我们看到它读取六行来检索匹配的行并在输出中返回一行。让我们看一下使用非聚集索引的执行计划:
我们可以在一个 SQL 表中有多个非聚集索引。以前,我们在 EmpContactNumber 列上创建了唯一的非聚集索引。
在创建索引之前,执行以下查询,以便我们在EmpAge列中有重复的值 :
Update Employee set EmpAge=32 where EmpID=2
Update Employee set EmpAge=38 where EmpID=6
Update Employee set EmpAge=38 where EmpID=3
让我们对非唯一非聚集索引执行以下查询。在查询语法中,我们没有指定唯一关键字,它告诉 SQL Server 创建一个非唯一索引:
CREATE NONCLUSTERED INDEX NCIX_Employee_EmpAge ON dbo.Employee(EmpAge);
众所周知,索引的键应该是唯一的。在这种情况下,我们要添加一个非唯一键。问题出现了:SQL Server 如何使这个键成为唯一的?
SQL Server 为它做以下事情:
让我们再次查看以下查询的以下实际执行计划:
Select * from Employee
where EmpContactNumber='8474563217'
它包括索引查找和键查找运算符,如上图所示:
对于大型表,嵌套循环可能是一个代价高昂的运算符。我们可以使用非聚集索引非键列来降低成本。我们使用 索引子句指定非聚集索引中的非键列。
让我们使用包含数据列的方式重新创建非聚集索引:
DROP INDEX [IX_NonClustered_Employee] ON [dbo].[Employee]
GO
CREATE UNIQUE NONCLUSTERED INDEX [IX_NonClustered_Employee] ON [dbo].[Employee]
(
[EmpContactNumber] ASC
)
INCLUDE(EmpName,EmpAge)
包含的列是索引树中叶节点的一部分。它有助于从索引本身获取数据,而不是进一步遍历以检索数据。
在下图中,我们将包含的列EmpName和EmpAge作为叶节点的一部分:
现在重新执行SELECT语句,查看实际执行计划。我们在这个执行计划中没有键查找(Index Seek)和嵌套循环(Nested Loops):
让我们将光标悬停在索引查找上并查看输出列列表。SQL Server 可以使用此非聚集索引查找找到所有列:
借助包含的非键数据列,我们可以使用覆盖索引提高查询性能。然而,这并不意味着我们应该在索引定义中所有非键列。我们在设计索引时要小心,在生产环境中部署之前应该测试索引行为。
在本文中,我们探讨了 SQL Server 中的非聚集索引及其与聚集索引的结合使用。我们应该根据工作负载和查询行为仔细设计索引。