Overhead für die Index-Eindeutigkeit


14

Ich habe mit verschiedenen Entwicklern in meinem Büro eine ständige Debatte über die Kosten eines Index geführt und darüber, ob die Eindeutigkeit vorteilhaft oder kostspielig ist (wahrscheinlich beides). Der Kern des Problems sind unsere konkurrierenden Ressourcen.

Hintergrund

Ich habe zuvor in einer Diskussion gelesen, dass ein UniqueIndex keine zusätzlichen Kosten verursacht, da eine InsertOperation implizit prüft, wo er in den B-Baum passt, und einen Eindeutiger anhängt, wenn ein Duplikat in einem nicht eindeutigen Index gefunden wird das Ende des Schlüssels, fügt aber sonst direkt ein. In dieser Abfolge von Ereignissen entstehen für einen UniqueIndex keine zusätzlichen Kosten.

Mein Kollege bekämpft diese Aussage, indem er sagt, dass dies Uniqueals zweite Operation nach der Suche nach der neuen Position im B-Baum erzwungen wird und daher in der Pflege teurer ist als ein nicht eindeutiger Index.

Im schlimmsten Fall habe ich Tabellen mit einer Identitätsspalte (von Natur aus eindeutig) gesehen, die der Clustering-Schlüssel der Tabelle ist, aber ausdrücklich als nicht eindeutig angegeben wird. Auf der anderen Seite ist meine Besessenheit von der Eindeutigkeit am schlimmsten, und alle Indizes werden als eindeutig erstellt. Wenn es nicht möglich ist, eine explizit eindeutige Beziehung zu einem Index zu definieren, hänge ich die PK der Tabelle an das Ende des Index an, um sicherzustellen, dass der Index eindeutig ist Einzigartigkeit ist garantiert.

Ich bin häufig an Codeüberprüfungen für das Entwicklerteam beteiligt, und ich muss in der Lage sein, allgemeine Richtlinien zu geben, denen sie folgen können. Ja, jeder Index sollte ausgewertet werden. Wenn Sie jedoch fünf Server mit jeweils Tausenden von Tabellen und bis zu zwanzig Indizes in einer Tabelle haben, müssen Sie in der Lage sein, einige einfache Regeln anzuwenden, um ein bestimmtes Qualitätsniveau sicherzustellen.

Frage

Hat die Einzigartigkeit zusätzliche Kosten im InsertVergleich zu den Kosten für die Aufrechterhaltung eines nicht eindeutigen Index? Was ist zweitens falsch daran, den Primärschlüssel einer Tabelle an das Ende eines Indexes anzuhängen, um die Eindeutigkeit sicherzustellen?

Beispiel Tabellendefinition

create table #test_index
    (
    id int not null identity(1, 1),
    dt datetime not null default(current_timestamp),
    val varchar(100) not null,
    is_deleted bit not null default(0),
    primary key nonclustered(id desc),
    unique clustered(dt desc, id desc)
    );

create index
    [nonunique_nonclustered_example]
on #test_index
    (is_deleted)
include
    (val);

create unique index
    [unique_nonclustered_example]
on #test_index
    (is_deleted, dt desc, id desc)
include
    (val);

Beispiel

Ein Beispiel , warum ich das hinzufügen würde UniqueSchlüssel zum Ende eines Index ist in einem unsere Faktentabellen. Es gibt ein Primary Key, auf dem eine IdentitySpalte. Das Clustered Indexist jedoch stattdessen die Partitionierungsschema-Spalte, gefolgt von drei Fremdschlüsseldimensionen ohne Eindeutigkeit. Die Leistung bei der Auswahl dieser Tabelle ist miserabel, und ich erhalte häufig bessere Suchzeiten, wenn ich die Primary Keymit einer Schlüsselsuche verwende, anstatt die zu nutzen Clustered Index. Andere Tabellen, die ein ähnliches Design aufweisen, jedoch Primary Keyam Ende angehängt sind, weisen eine erheblich bessere Leistung auf.

-- date_int is equivalent to convert(int, convert(varchar, current_timestamp, 112))
if not exists(select * from sys.partition_functions where [name] = N'pf_date_int')
    create partition function 
        pf_date_int (int) 
    as range right for values 
        (19000101, 20180101, 20180401, 20180701, 20181001, 20190101, 20190401, 20190701);
go

if not exists(select * from sys.partition_schemes where [name] = N'ps_date_int')
    create partition scheme 
        ps_date_int
    as partition 
        pf_date_int all 
    to 
        ([PRIMARY]);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.bad_fact_table'))
    create table dbo.bad_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        fk_id int not null,
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_bad_fact_table] clustered (date_int, group_id, group_entity_id, fk_id)
        )
    on ps_date_int(date_int);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.better_fact_table'))
    create table dbo.better_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_better_fact_table] clustered(date_int, group_id, group_entity_id, id)
        )
    on ps_date_int(date_int);
go

Antworten:


16

Ich bin häufig an Codeüberprüfungen für das Entwicklerteam beteiligt, und ich muss in der Lage sein, allgemeine Richtlinien zu geben, denen sie folgen können.

Die Umgebung, in der ich mich gerade befinde, hat 250 Server mit 2500 Datenbanken. Ich habe an Systemen mit 30.000 Datenbanken gearbeitet . Richtlinien für die Indizierung sollten sich auf die Benennungskonvention usw. konzentrieren und nicht auf "Regeln" für die Spalten, die in einen Index aufgenommen werden sollen. Jeder einzelne Index sollte so konstruiert sein, dass er den richtigen Index für die jeweilige Geschäftsregel oder den Code darstellt, der die Tabelle berührt.

Hat die Einzigartigkeit zusätzliche Kosten im InsertVergleich zu den Kosten für die Aufrechterhaltung eines nicht eindeutigen Index? Was ist zweitens falsch daran, den Primärschlüssel einer Tabelle an das Ende eines Indexes anzuhängen, um die Eindeutigkeit sicherzustellen?

Das Hinzufügen der Primärschlüsselspalte an das Ende eines nicht eindeutigen Index, um ihn eindeutig zu machen, scheint mir ein Anti-Pattern zu sein. Wenn Geschäftsregeln vorschreiben, dass die Daten eindeutig sein sollen, fügen Sie der Spalte eine eindeutige Einschränkung hinzu. Dadurch wird automatisch ein eindeutiger Index erstellt. Wenn Sie eine Spalte für die Leistung indizieren , warum sollten Sie dem Index eine Spalte hinzufügen?

Selbst wenn Ihre Annahme, dass die Durchsetzung der Eindeutigkeit keinen zusätzlichen Aufwand verursacht (was in bestimmten Fällen nicht der Fall ist), richtig ist , was lösen Sie, indem Sie den Index unnötig komplizieren?

In dem speziellen Fall, dass Sie den Primärschlüssel an das Ende Ihres Indexschlüssels UNIQUEanfügen, damit die Indexdefinition den Modifikator enthält, ändert sich die physische Indexstruktur auf der Festplatte tatsächlich nicht. Dies liegt an der Art der Struktur von B-Tree-Indexschlüsseln, da diese immer eindeutig sein müssen.

Wie David Browne in einem Kommentar erwähnte:

Da jeder nicht gruppierte Index als eindeutiger Index gespeichert wird, entstehen beim Einfügen in einen eindeutigen Index keine zusätzlichen Kosten. In der Tat der einzige zusätzliche Kosten würden in Ermangelung eines Kandidatenschlüssel als eindeutigen Index zu erklären, die die gruppierten Indexschlüssel verursachen würde den Indexschlüssel angehängt werden.

Nehmen Sie das folgende minimal vollständige und überprüfbare Beispiel :

USE tempdb;

DROP TABLE IF EXISTS dbo.IndexTest;
CREATE TABLE dbo.IndexTest
(
    id int NOT NULL
        CONSTRAINT IndexTest_pk
        PRIMARY KEY
        CLUSTERED
        IDENTITY(1,1)
    , rowDate datetime NOT NULL
);

Ich werde zwei Indizes hinzufügen, die bis auf das Hinzufügen des Primärschlüssels am Ende der Schlüsseldefinition des zweiten Index identisch sind:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

CREATE UNIQUE INDEX IndexTest_rowDate_ix02
ON dbo.IndexTest(rowDate, id);

Als nächstes werden wir mehrere Zeilen zur Tabelle hinzufügen:

INSERT INTO dbo.IndexTest (rowDate)
VALUES (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 1, GETDATE()))
     , (DATEADD(SECOND, 2, GETDATE()));

Wie Sie oben sehen können, enthalten drei Zeilen denselben Wert für die rowDateSpalte und zwei Zeilen eindeutige Werte.

Als Nächstes untersuchen wir die physischen Seitenstrukturen für jeden Index mit dem DBCC PAGEBefehl undocumented :

DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE @indexid int;

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix01'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix02'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';

Ich habe mir die Ausgabe mit Beyond Compare angesehen und abgesehen von offensichtlichen Unterschieden bei den Zuordnungsseiten-IDs usw. sind die beiden Indexstrukturen identisch.

Bildbeschreibung hier eingeben

Sie können das Obige so verstehen, dass das Einschließen des Primärschlüssels in jeden Index und das Definieren von als eindeutig A Good Thing ™ ist, da dies sowieso im Verborgenen geschieht. Ich würde diese Annahme nicht machen und nur vorschlagen, einen Index als eindeutig zu definieren, wenn die natürlichen Daten im Index tatsächlich bereits eindeutig sind.

Zu diesem Thema gibt es im Interwebz mehrere hervorragende Ressourcen, darunter:

Zu Ihrer Information: Das bloße Vorhandensein einer identitySpalte garantiert keine Eindeutigkeit. Sie müssen die Spalte als Primärschlüssel oder mit einer eindeutigen Einschränkung definieren, um sicherzustellen , dass die in dieser Spalte gespeicherten Werte tatsächlich eindeutig sind. Mit der SET IDENTITY_INSERT schema.table ON;Anweisung können Sie nicht eindeutige Werte in eine Spalte einfügen, die als definiert ist identity.


5

Nur eine Ergänzung zu Max 'hervorragender Antwort .

Wenn es darum geht, einen nicht eindeutigen Clustered-Index zu erstellen, erstellt SQL Server Uniquifiersowieso etwas, das als Hintergrund bezeichnet wird.

Dies Uniquifierkönnte in Zukunft zu potenziellen Problemen führen, wenn Ihre Plattform viele CRUD-Operationen ausführt, da diese Uniquifiernur 4 Byte groß sind (eine einfache 32-Bit-Ganzzahl). Wenn Ihr System viele CRUD-Operationen ausführt, verbrauchen Sie möglicherweise alle verfügbaren eindeutigen Zahlen, und plötzlich wird eine Fehlermeldung angezeigt, und Sie können keine Daten mehr in Ihre Tabellen einfügen (da dies der Fall ist) Sie haben keine eindeutigen Werte mehr, die Sie Ihren neu eingefügten Zeilen zuweisen können.

In diesem Fall erhalten Sie folgende Fehlermeldung:

The maximum system-generated unique value for a duplicate group 
was exceeded for index with partition ID (someID). 

Dropping and re-creating the index may resolve this;
otherwise, use another clustering key.

Der Fehler 666 (der obige Fehler) tritt auf, wenn der uniquifierfür einen einzelnen Satz nicht eindeutiger Schlüssel mehr als 2.147.483.647 Zeilen belegt.

Sie müssen also entweder ~ 2 Milliarden Zeilen für einen einzelnen Schlüsselwert haben, oder Sie müssen einen einzelnen Schlüsselwert ~ 2 Milliarden Mal modifizieren, um diesen Fehler zu sehen. Daher ist es nicht sehr wahrscheinlich, dass Sie auf diese Einschränkung stoßen.


Ich hatte keine Ahnung, dass der versteckte Uniquifier nicht mehr über genügend Speicherplatz verfügt, aber ich denke, alle Dinge sind in einigen Fällen begrenzt. Ähnlich wie die Art Caseund Weise und die IfStrukturen auf 10 Ebenen begrenzt sind, ist es sinnvoll, dass auch die Auflösung nicht eindeutiger Entitäten begrenzt ist. Ihrer Aussage nach scheint dies nur in Fällen zuzutreffen, in denen der Clustering-Schlüssel nicht eindeutig ist. Ist dies ein Problem für einen Nonclustered Indexoder liegt der Clustering-Schlüssel vor, Uniquegibt es kein Problem für NonclusteredIndizes?
Solonotix

Ein eindeutiger Index ist (soweit ich weiß) durch die Größe des Spaltentyps begrenzt (wenn es sich also um einen BIGINT-Typ handelt, müssen Sie mit 8 Byte arbeiten). Gemäß der offiziellen Dokumentation von Microsoft sind maximal 900 Byte für einen Clustered-Index und 1700 Byte für einen nicht geclusterten Index zulässig (da Sie mehr als einen nicht geclusterten Index und nur einen geclusterten Index pro Tabelle haben können). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain

1
@Solonotix - Der Uniquifier aus dem Clustered-Index wird in den Nicht-Clustered-Indizes verwendet. Wenn Sie den Code in meinem Beispiel ohne den Primärschlüssel ausführen (stattdessen einen Clustered-Index erstellen), sehen Sie, dass die Ausgabe sowohl für den nicht eindeutigen als auch für den eindeutigen Index gleich ist.
Max Vernon

-2

Ich werde mich nicht mit der Frage auseinandersetzen, ob ein Index eindeutig sein sollte oder nicht und ob dieser oder jener Ansatz mehr Aufwand mit sich bringt. Aber ein paar Dinge haben mich an Ihrem allgemeinen Design gestört

  1. dt datetime nicht null default (current_timestamp). Datetime ist eine ältere Form oder so, und Sie können möglicherweise durch die Verwendung von datetime2 () und sysdatetime () mindestens einen Teil des Speicherplatzes einsparen.
  2. create index [nonunique_nonclustered_example] für #test_index (is_deleted) include (val). Das stört mich. Sehen Sie sich an, wie auf die Daten zugegriffen werden soll (ich wette, es gibt mehr als WHERE is_deleted = 0), und verwenden Sie einen gefilterten Index. Ich würde sogar in Betracht ziehen, 2 gefilterte Indizes zu verwenden, einen für where is_deleted = 0und einen fürwhere is_deleted = 1

Grundsätzlich sieht dies eher nach einer Codierungsübung aus, mit der eine Hypothese getestet werden soll, als nach einem echten Problem / einer echten Lösung, aber diese beiden Muster sind definitiv etwas, nach dem ich in Codeüberprüfungen Ausschau halte.


Das meiste, was Sie mit datetime2 anstelle von datetime einsparen, ist 1 Byte. Wenn Ihre Genauigkeit unter 3 liegt, bedeutet dies, dass Sie die Genauigkeit in Bruchteilen von Sekunden verlieren, was nicht immer eine praktikable Lösung ist. Was den Beispielindex angeht, so wurde das Design einfach gehalten, um sich auf meine Frage zu konzentrieren. In einem NonclusteredIndex wird der Clustering-Schlüssel an das Ende der Datenzeile angehängt, damit Schlüssel intern gesucht werden können. Als solches sind die beiden Indizes physisch gleich, was der Punkt meiner Frage war.
Solonotix

Bei der Waage laufen wir darauf hinaus, ein oder zwei Bytes zu sparen, was sich schnell summiert. Und ich war davon ausgegangen, dass wir die Genauigkeit verringern könnten, da Sie die ungenaue Datumszeit verwenden. Für die Indizes werde ich wieder sagen, dass Bitspalten als führende Spalten für Indizes ein Muster sind, das ich als schlechte Wahl betrachte. Wie bei allen Dingen kann Ihr Kilometerstand variieren. Leider die Nachteile eines ungefähren Modells.
Toby

-4

Es sieht so aus, als würden Sie einfach PK verwenden, um einen alternativen, kleineren Index zu erstellen. Daher ist die Leistung schneller.

Sie sehen dies bei Unternehmen mit massiven Datentabellen (z. B. Stammdatentabellen). Jemand entscheidet sich für einen massiven Clustered-Index, der die Anforderungen verschiedener Berichtsgruppen erfüllen soll.

Eine Gruppe benötigt jedoch möglicherweise nur wenige Teile dieses Index, während eine andere Gruppe andere Teile benötigt. Daher hilft es nicht wirklich, wenn der Index in jede Spalte unter der Sonne geschlagen wird, um die Leistung zu optimieren.

In der Zwischenzeit wird das Problem häufig dadurch gelöst, dass mehrere kleinere Zielindizes erstellt werden.

Und das scheint das zu sein, was du tust. Sie haben diesen massiven Clustered-Index mit schrecklicher Leistung, dann verwenden Sie PK, um einen weiteren Index mit weniger Spalten zu erstellen, der (keine Überraschung) eine bessere Leistung aufweist.

Führen Sie einfach eine Analyse durch und finden Sie heraus, ob Sie den einzelnen Clustered-Index in kleinere, zielgerichtete Indizes aufteilen können, die für bestimmte Jobs erforderlich sind.

Sie müssten dann die Leistung von einem Standpunkt aus analysieren, bei dem es um einen "Einzelindex vs. Mehrfachindex" geht, da das Erstellen und Aktualisieren von Indizes einen Mehraufwand bedeutet. Sie müssen dies jedoch aus einer Gesamtperspektive analysieren.

EG: Es ist möglicherweise weniger ressourcenintensiv für einen massiven Clustered-Index, und es ist ressourcenintensiver, wenn mehrere kleinere Zielindizes vorhanden sind. Wenn Sie jedoch gezielte Abfragen im Back-End schneller ausführen können und dabei Zeit (und Geld) sparen, lohnt es sich möglicherweise.

Sie müssten also eine End-to-End-Analyse durchführen. Sehen Sie sich nicht nur an, wie sich dies auf Ihre eigene Welt auswirkt, sondern auch, wie sich dies auf Endbenutzer auswirkt.

Ich habe nur das Gefühl, dass Sie die PK-Kennung falsch verwenden. Möglicherweise verwenden Sie jedoch ein Datenbanksystem, das nur einen Index (?) Zulässt. Sie können jedoch einen anderen einschleichen, wenn Sie PK verwenden (in diesen Tagen scheint jedes relationale Datenbanksystem die PK automatisch zu indizieren). Die meisten modernen RDBMS sollten jedoch die Erstellung mehrerer Indizes ermöglichen. Die Anzahl der Indizes, die Sie erstellen können, sollte unbegrenzt sein (im Gegensatz zu einem Limit von 1 PK).

Wenn Sie also eine PK erstellen, die sich wie ein Alt-Index verhält, verbrauchen Sie Ihre PK. Dies kann erforderlich sein, wenn die Tabelle später in ihrer Rolle erweitert wird.

Das heißt nicht, dass Ihre Tabelle keine PK benötigt. SOP DBs 101 sagen, dass "jede Tabelle eine PK haben sollte". Aber in einer Data-Warehousing-Situation oder einer ähnlichen Situation kann es sein, dass eine PK auf einem Tisch zusätzlichen Aufwand verursacht, den Sie nicht benötigen. Oder es könnte von Gott gesandt werden, um sicherzustellen, dass Sie keine doppelten Dupe-Einträge hinzufügen. Es ist wirklich eine Frage dessen, was Sie tun und warum Sie es tun.

Massive Tabellen profitieren jedoch definitiv von Indizes. Angenommen, ein einzelner massiver Clustered-Index ist der beste, ist aber möglicherweise der beste. Ich würde jedoch empfehlen, einen Test durchzuführen, bei dem der Index in mehrere kleinere Indizes aufgeteilt wird, die auf bestimmte Anwendungsszenarien abzielen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.