非聚集索引比聚集索引快吗?


9

这两个表具有相同的结构,并且每个表中都有19972行。为了练习索引,我创建了两个具有相同结构的表并创建了

clustered index on persontb(BusinessEntityID)

nonclustered index on Persontb_NC(BusinessEntityId)

和表结构

BusinessEntityID int
FirstName varchar(100)
LastName  varchar(100)                                                                                                                       

 -- Nonclusted key on businessentityid takes 38%
SELECT  BusinessEntityId from Persontb_NC
WHERE businessentityid BETWEEN 400 AND 4000

-- CLustered key businessentityid takes 62%
SELECT BusinessEntityId  from persontb 
WHERE businessentityid BETWEEN 400 AND 4000

在此处输入图片说明

为什么聚集索引占62%,非聚集索引占38%?


1
为什么要投票关闭?

Answers:


10

是的,因为聚簇索引的叶子页必须存储其他两列(FirstNameLastName)的值,所以聚簇索引每页的行数少于非聚簇索引的行数。

NCI的叶子页仅存储值BusinessEntityId和行定位符(如果表是堆则为RID,否则为CI键)。

因此,估计的成本反映了更多的读取次数和IO需求。

如果您要声明NCI为

nonclustered index on Persontb_NC(BusinessEntityId) INCLUDE (FirstName, LastName)

那么它将类似于聚簇索引。


5

聚集索引不仅包含来自列索引的数据,而且还包含来自所有其他列的数据。(每个表只能有一个聚集索引)

非聚集索引仅包含来自索引列的数据,以及指向其余数据所在位置的row_id指针。

因此,该特定的非聚集索引更轻,并且需要较少的读取才能扫描/搜索该索引,并且该特定的查询将更快地工作。

但是,如果您也尝试检索FirstName和LastName,则将有所不同,并且聚集索引的性能应更好。


2

查询计划之间的百分比对于完全比较毫无意义。您必须对查询进行基准测试,以进行有效比较。此外,较小的行数倾向于隐藏索引策略之间的性能差异。通过将行数增加到1000万,您可以更清楚地了解性能差异。

有一个示例脚本可创建3个表,上面两个表,而第三个表同时具有聚集索引和非聚集索引。

USE [tempdb]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[t1](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [c1] [varchar](200) NULL
) ON [PRIMARY]

CREATE TABLE [dbo].[t2](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [c1] [varchar](200) NULL
) ON [PRIMARY]

CREATE TABLE [dbo].[t3](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [c1] [varchar](200) NULL
) ON [PRIMARY]

GO

CREATE CLUSTERED INDEX CIX_t1 ON t1(id)

CREATE NONCLUSTERED INDEX IX_t2 ON t2(id)

CREATE CLUSTERED INDEX CIX_t3 ON t3(id)
CREATE NONCLUSTERED INDEX IX_t3 ON t3(id)

用一千万行填充表

DECLARE @i INT
DECLARE @j int
DECLARE @t DATETIME
SET NOCOUNT ON
SET @t = CURRENT_TIMESTAMP
SET @i = 0
WHILE @i < 10000000
BEGIN
--populate with strings with a length between 100 and 200 
INSERT INTO t1 (c1) VALUES (REPLICATE('x', 101+ CAST(RAND(@i) * 100 AS INT)))
SET @i = @i + 1
END

PRINT 'Time to populate t1: '+ CAST(DATEDIFF(ms, @t, CURRENT_TIMESTAMP) AS VARCHAR(10)) + ' ms'
SET @t = CURRENT_TIMESTAMP


SET @i = 0
WHILE @i < 10000000
BEGIN
--populate with strings with a length between 100 and 200 
INSERT INTO t2 (c1) VALUES (REPLICATE('x', 101+ CAST(RAND(@i) * 100 AS INT)))
SET @i = @i + 1
END

PRINT 'Time to populate t3: '+ CAST(DATEDIFF(ms, @t, CURRENT_TIMESTAMP) AS VARCHAR(10)) + ' ms'
SET @t = CURRENT_TIMESTAMP

SET @i = 0
WHILE @i < 10000000
BEGIN
--populate with strings with a length between 100 and 200 
INSERT INTO t3 (c1) VALUES (REPLICATE('x', 101+ CAST(RAND(@i) * 100 AS INT)))
SET @i = @i + 1
END

PRINT 'Time to populate t3: '+ CAST(DATEDIFF(ms, @t, CURRENT_TIMESTAMP) AS VARCHAR(10)) + ' ms'

我们可以使用sys.dm_db_index_physical_stats来查看索引在磁盘上的大小。

SELECT  OBJECT_NAME(OBJECT_ID) table_name, index_id, index_type_desc, 
record_count, page_count, page_count / 128.0 size_in_mb, avg_record_size_in_bytes
FROM    sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('t1'), NULL, NULL, 'detailed')
WHERE   index_level = 0 
UNION ALL
SELECT  OBJECT_NAME(OBJECT_ID) table_name, index_id, index_type_desc, 
record_count, page_count, page_count / 128.0 size_in_mb, avg_record_size_in_bytes
FROM    sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('t2'), NULL, NULL, 'detailed')
WHERE   index_level = 0 
UNION ALL
SELECT  OBJECT_NAME(OBJECT_ID) table_name, index_id, index_type_desc, 
record_count, page_count, page_count / 128.0 size_in_mb, avg_record_size_in_bytes
FROM    sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('t3'), NULL, NULL, 'detailed')
WHERE   index_level = 0 

结果:

table_name  index_id    page_count  size_in_mb  avg_record_size_in_bytes    index_type_desc
t1  1   211698  1653.890625 167.543 CLUSTERED INDEX
t2  0   209163  1634.085937 165.543 HEAP
t2  2   22272   174.000000  16  NONCLUSTERED INDEX
t3  1   211698  1653.890625 167.543 CLUSTERED INDEX
t3  2   12361   96.570312   8   NONCLUSTERED INDEX

T1的聚集索引大小约为1.6 GB。T2的非聚集索引为170 MB(IO节省90%)。T3的非聚集索引为97 MB,或比T1少IO约95%。

因此,根据所需的IO,原始查询计划应该更多地是10%/ 90%,而不是38%/ 62%。同样,由于非聚集索引很可能完全适合内存,因此差异会更大,因为磁盘IO非常昂贵。


1
推断您的10%/90%身材比身材更准确有点飞跃38%/62%。长度在100到200之间的字符串肯定会高估名字/姓氏对的空间要求,因此您的页面密度将比OP低。当我尝试使用您的示例数据时,估算成本显示为87%/ 13%
马丁·史密斯

1
SQL Server确实会引用data_pagesin sys.allocation_units。您可以从下面CREATE TABLE T1(C INT);CREATE TABLE T2(C INT);UPDATE STATISTICS T1 WITH PAGECOUNT = 1;UPDATE STATISTICS T2 WITH PAGECOUNT = 100的估算费用比较中看出这一点SELECT * FROM T1;SELECT * FROM T2;
Martin Smith

请重新阅读我的回答中的第一句话。直接比较成本是没有意义的。对于OP查询之间的性能差异,可以通过计算索引大小(以及IO数量)的减少来凭经验得出更好的估计,而不是通过优化程序的成本得出。
StrayCatDBA

1
通常来说是肯定的,但是在这种情况下,查询优化程序使聚集索引比非聚集索引花费更多的原因(此问题的主题)正是由于页数不同。
马丁·史密斯

1
根据http://www.qdpma.com/ppt/CostFormulas2.ppt的公式用来成本索引扫描或索引查找没有查找是(版本相关)IO(0.003125 +每页0.00074074)和CPU(0.0001581 + 0.0000011每行)。CI和NCI的固定成本和行数相等,因此唯一的变量是页数。
马丁·史密斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.