Некластерный индекс быстрее, чем кластерный индекс?

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
Зачем голосовать за близких?

Ответы:

10

Да, кластеризованный индекс имеет меньше строк на страницу, чем некластеризованный индекс, поскольку конечные страницы кластерного индекса должны хранить значения для двух других столбцов ( FirstNameи LastName).

Листовые страницы NCI хранят только BusinessEntityIdзначения и локатор строк (RID, если таблица является кучей, или ключ CI в противном случае).

Таким образом, предполагаемые затраты отражают большее количество операций чтения и ввода-вывода.

Если бы вы объявили NCI как

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

тогда это будет похоже на кластерный индекс.

Мартин Смит
источник
5

Кластерный индекс содержит не только данные из столбца индекса, но и данные из всех других столбцов. (В таблице может быть только один кластерный индекс)

Некластеризованный индекс содержит только данные из индексированных столбцов и указатель row_id, где находятся остальные данные.

Поэтому этот конкретный некластеризованный индекс легче, и для его сканирования / поиска требуется меньше чтения, и этот конкретный запрос будет работать быстрее.

Однако, если бы вы попытались получить FirstName и LastName, это было бы по-другому, и кластерный индекс должен работать лучше.

Ненад Живкович
источник
2

Процент между планами запросов не имеет смысла сравнивать напрямую. Вы должны сравнить запросы, чтобы иметь правильное сравнение. Кроме того, небольшое количество строк имеет тенденцию скрывать различия в производительности между стратегиями индексирования. Увеличив количество строк до 10 миллионов, вы сможете получить более четкую картину различий в производительности.

Существует пример сценария, который создает 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)

Заполните таблицы 10 миллионами строк

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 ГБ. Некластеризованный индекс T2 составляет 170 МБ (90% экономии на вводе-выводе). Некластеризованный индекс T3 составляет 97 МБ, или примерно на 95% меньше IO, чем T1.

Таким образом, исходя из требуемого ввода-вывода, исходный план запроса должен был составлять примерно 10% / 90%, а не 38% / 62%. Кроме того, поскольку некластеризованный индекс, вероятно, полностью умещается в памяти, разница может быть еще больше, поскольку дисковый ввод-вывод очень дорогой.

StrayCatDBA
источник
1
Это немного прыжок, чтобы сделать вывод, что ваша 10%/90%фигура является более точной, чем 38%/62%. Строки длиной от 100 до 200, безусловно, будут чрезмерно завышать требования к пространству для пары имя / фамилия, поэтому плотность страниц будет ниже, чем у OP. Когда я пытаюсь сопоставить данные вашего примера, предполагаемые затраты составляют 87% / 13% .
Мартин Смит
1
SQL Server делает aleady обратитесь к data_pagesин 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 лучшую оценку можно получить эмпирически, рассчитав уменьшение размера индексов (и, следовательно, количества операций ввода-вывода), а не затрат оптимизатора.
StrayCatDBA
1
Вообще говоря, это да, но в этом случае причина, по которой оптимизатор запросов стоит кластеризованный индекс как большую, чем некластеризованный индекс (предмет этого вопроса), именно из-за разного количества страниц.
Мартин Смит
1
В соответствии с http://www.qdpma.com/ppt/CostFormulas2.ppt формула используется для стоить сканирование индекса или поиск по индексу без поиска является (зависит от версии) ввод - вывод (0,003125 + 0,00074074 на страницу) и центральный процессор (0,0001581 0,0000011 + за ряд). Фиксированные затраты и строки равны для CI и NCI, поэтому единственной переменной являются страницы.
Мартин Смит