Nicht gruppierter Index ist schneller als gruppierter Index?

9

Beide Tabellen haben dieselbe Struktur und 19972 Zeilen in jeder Tabelle. Zum Üben der Indizierung habe ich beide Tabellen mit der gleichen Struktur erstellt und erstellt

clustered index on persontb(BusinessEntityID)

und

nonclustered index on Persontb_NC(BusinessEntityId)

und Tabellenstruktur

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

Geben Sie hier die Bildbeschreibung ein

Warum nimmt der Clustered-Index 62% und der Nicht-Clustered-Index 38% ein?


quelle
1
Warum für nah stimmen?

Antworten:

10

Ja, der Clustered-Index enthält weniger Zeilen pro Seite als der Nicht-Clustered-Index, da die Blattseiten des Clustered-Index die Werte für die beiden anderen Spalten ( FirstNameund LastName) speichern müssen .

Die Blattseiten des NCI speichern nur die BusinessEntityIdWerte und einen Zeilenlokator (RID, wenn die Tabelle ein Heap oder der CI-Schlüssel ist).

Die geschätzten Kosten spiegeln also die größere Anzahl von Lesevorgängen und den E / A-Bedarf wider.

Wenn Sie das NCI als deklarieren würden

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

dann wäre es ähnlich wie der Clustered-Index.

Martin Smith
quelle
5

Der Clustered-Index enthält nicht nur Daten aus dem aktivierten Spaltenindex, sondern auch Daten aus allen anderen Spalten. (Es kann nur einen Clustered-Index pro Tabelle geben.)

Der nicht gruppierte Index enthält nur Daten aus indizierten Spalten und einen row_id-Zeiger darauf, wo sich der Rest der Daten befindet.

Daher ist dieser bestimmte nicht gruppierte Index leichter und es ist weniger Lesen erforderlich, um ihn zu scannen / zu durchsuchen, und diese bestimmte Abfrage funktioniert schneller.

Wenn Sie jedoch versucht haben, auch Vorname und Nachname abzurufen, ist dies unterschiedlich und der Clustered-Index sollte eine bessere Leistung erzielen.

Nenad Zivkovic
quelle
2

Die Prozentsätze zwischen den Abfrageplänen sind für einen direkten Vergleich bedeutungslos. Sie müssen die Abfragen vergleichen, um einen gültigen Vergleich zu erhalten. Darüber hinaus neigen kleine Zeilenzahlen dazu, Leistungsunterschiede zwischen Indizierungsstrategien zu verbergen. Wenn Sie die Anzahl der Zeilen auf 10 Millionen erhöhen, erhalten Sie ein klareres Bild der Leistungsunterschiede.

Es gibt ein Beispielskript, das drei Tabellen erstellt, Ihre beiden von oben, und eine dritte mit einem gruppierten und einem nicht gruppierten Index.

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)

Füllen Sie die Tabellen mit 10 Millionen Zeilen

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'

Wir können sys.dm_db_index_physical_stats verwenden, um die Größe der Indizes auf der Festplatte anzuzeigen.

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 

Und die Ergebnisse:

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

Der Clustered-Index von T1 ist ungefähr 1,6 GB groß. Der nicht gruppierte Index von T2 beträgt 170 MB (90% Einsparung an E / A). Der nicht gruppierte Index von T3 beträgt 97 MB oder etwa 95% weniger E / A als T1.

Basierend auf den erforderlichen E / A-Vorgaben sollte der ursprüngliche Abfrageplan eher 10% / 90% und nicht 38% / 62% betragen. Da der nicht gruppierte Index wahrscheinlich vollständig in den Speicher passt, kann der Unterschied noch größer sein, da die Festplatten-E / A sehr teuer ist.

StrayCatDBA
quelle
1
Es ist ein kleiner Sprung, daraus zu schließen, dass Ihre 10%/90%Zahl genauer ist als die 38%/62%. Zeichenfolgen mit einer Länge zwischen 100 und 200 sind sicherlich eine grobe Überschätzung des Speicherplatzbedarfs für ein Paar aus Vorname und Nachname, sodass Sie eine geringere Seitendichte als das OP haben. Wenn ich es mit Ihren Beispieldaten versuche, werden die geschätzten Kosten mit 87% / 13% angezeigt .
Martin Smith
1
SQL Server bezieht sich bereits auf das data_pagesin sys.allocation_units. Sie können dies sehen, CREATE TABLE T1(C INT);CREATE TABLE T2(C INT);UPDATE STATISTICS T1 WITH PAGECOUNT = 1;UPDATE STATISTICS T2 WITH PAGECOUNT = 100wenn Sie dann die geschätzten Kosten vergleichenSELECT * FROM T1;SELECT * FROM T2;
Martin Smith
Bitte lesen Sie den ersten Satz in meiner Antwort noch einmal. Kosten direkt zu vergleichen ist bedeutungslos. Für den Leistungsunterschied zwischen den Abfragen des OP kann eine bessere Schätzung empirisch abgeleitet werden, indem die Verringerung der Größe der Indizes (und damit der Anzahl der E / A) berechnet wird, nicht die Kosten des Optimierers.
StrayCatDBA
1
Im Allgemeinen ist es ja, aber in diesem Fall liegt der Grund, warum der Abfrageoptimierer den Clustered-Index mehr kostet als der Nicht-Clustered-Index (das Thema dieser Frage), genau in der unterschiedlichen Seitenzahl.
Martin Smith
1
Nach http://www.qdpma.com/ppt/CostFormulas2.ppt verwendet die Formel ohne Lookup suchen einen Index - Scan oder Index kosten ist (versionsabhängig) IO (0,003125 + 0,00074074 pro Seite) und CPU (0,0001581 + 0,0000011 pro Zeile). Die Fixkosten und Zeilen sind für CI und NCI gleich, sodass die einzige Variable Seiten sind.
Martin Smith