Indice Unicità Overhead


14

Ho avuto un dibattito in corso con vari sviluppatori nel mio ufficio sul costo di un indice e se l'unicità è vantaggiosa o costosa (probabilmente entrambi). Il nocciolo del problema sono le nostre risorse concorrenti.

sfondo

Ho già letto una discussione in cui si afferma che un Uniqueindice non è un costo aggiuntivo da mantenere, poiché Insertun'operazione controlla implicitamente dove si adatta all'albero B e, se si trova un duplicato in un indice non univoco, aggiunge un unificatore a la fine della chiave, ma altrimenti inserisce direttamente. In questa sequenza di eventi, un Uniqueindice non ha costi aggiuntivi.

Il mio collega combatte questa affermazione dicendo che Uniqueviene applicato come una seconda operazione dopo la ricerca della nuova posizione nell'albero B, e quindi è più costoso da mantenere rispetto a un indice non univoco.

Nel peggiore dei casi, ho visto tabelle con una colonna identità (intrinsecamente univoca) che è la chiave di clustering della tabella, ma esplicitamente dichiarata come non univoca. Dall'altro lato del peggio c'è la mia ossessione per l'unicità, e tutti gli indici sono creati come unici, e quando non è possibile definire una relazione esplicitamente unica con un indice, aggiungo il PK della tabella alla fine dell'indice per garantire che l'unicità è garantita.

Sono spesso coinvolto nelle revisioni del codice per il team di sviluppo e devo essere in grado di fornire linee guida generali che dovranno seguire. Sì, ogni indice dovrebbe essere valutato, ma quando si hanno cinque server con migliaia di tabelle ciascuno e fino a venti indici su una tabella, è necessario essere in grado di applicare alcune semplici regole per garantire un certo livello di qualità.

Domanda

L'unicità ha un costo aggiuntivo sul back-end di un Insert confronto rispetto al costo di mantenimento di un indice non univoco? In secondo luogo, cosa c'è di sbagliato nell'aggiungere la chiave primaria di una tabella alla fine di un indice per garantire l'univocità?

Definizione della tabella di esempio

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);

Esempio

Un esempio del perché aggiungerei la Uniquechiave alla fine di un indice è in una delle nostre tabelle dei fatti. C'è un Primary Keyche è una Identitycolonna. Tuttavia, Clustered Indexè invece la colonna dello schema di partizionamento, seguita da tre dimensioni di chiave esterna senza unicità. Selezionare le prestazioni su questa tabella è spaventoso e spesso ottengo tempi di ricerca migliori usando il Primary Keytasto con una ricerca chiave invece di sfruttare il Clustered Index. Altre tabelle che seguono un design simile, ma che sono state Primary Keyaggiunte alla fine, hanno prestazioni notevolmente migliori.

-- 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

Risposte:


16

Sono spesso coinvolto nelle revisioni del codice per il team di sviluppo e devo essere in grado di fornire linee guida generali che dovranno seguire.

L'ambiente in cui sono attualmente coinvolto ha 250 server con 2500 database. Ho lavorato su sistemi con 30.000 database . Le linee guida per l'indicizzazione dovrebbero ruotare attorno alla convenzione di denominazione, ecc., Non essere "regole" per quali colonne includere in un indice - ogni singolo indice dovrebbe essere progettato per essere l'indice corretto per quella specifica regola o codice aziendale che tocca la tabella.

L'unicità ha un costo aggiuntivo sul back-end di unInsert confronto rispetto al costo di mantenimento di un indice non univoco? In secondo luogo, cosa c'è di sbagliato nell'aggiungere la chiave primaria di una tabella alla fine di un indice per garantire l'univocità?

L'aggiunta della colonna chiave primaria alla fine di un indice non univoco per renderlo univoco mi sembra un anti-pattern. Se le regole aziendali dettano che i dati devono essere univoci, quindi aggiungere un vincolo univoco alla colonna; che creerà automaticamente un indice univoco. Se stai indicizzando una colonna per le prestazioni , perché dovresti aggiungere una colonna all'indice?

Anche se la tua supposizione che far rispettare l'unicità non aggiunga alcun sovraccarico è corretta (cosa che non è per alcuni casi), cosa stai risolvendo complicando inutilmente l'indice?

Nell'istanza specifica dell'aggiunta della chiave primaria alla fine della chiave di indice in modo che sia possibile includere la UNIQUEmodifica dell'indice nella definizione dell'indice , in realtà non fa differenza per la struttura dell'indice fisico sul disco. Ciò è dovuto alla natura della struttura delle chiavi degli indici B-tree, in quanto devono sempre essere uniche.

Come ha detto David Browne in un commento:

Poiché ogni indice non cluster viene archiviato come indice univoco, l' inserimento in un indice univoco non comporta costi aggiuntivi . In effetti, l'unico costo aggiuntivo non riuscirebbe a dichiarare una chiave candidata come indice univoco, causando l'aggiunta delle chiavi di indice cluster alle chiavi di indice.

Prendi il seguente esempio minimamente completo e verificabile :

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
);

Aggiungerò due indici identici ad eccezione dell'aggiunta della chiave primaria alla fine della seconda definizione della chiave degli indici:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

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

Successivamente, passeremo diverse righe alla tabella:

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()));

Come puoi vedere sopra, tre righe contengono lo stesso valore per la rowDatecolonna e due righe contengono valori univoci.

Successivamente, esamineremo le strutture di pagina fisiche per ciascun indice, utilizzando il DBCC PAGEcomando non documentato :

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 *****************************************';

Ho esaminato l'output utilizzando Beyond Compare e, fatta eccezione per ovvie differenze tra gli ID della pagina di allocazione, ecc., Le due strutture di indice sono identiche.

inserisci qui la descrizione dell'immagine

Potresti prendere quanto sopra per significare che includere la chiave primaria in ogni indice e definire come unico è A Good Thing ™ poiché è quello che succede comunque sotto le coperte. Non farei questo presupposto e suggerirei di definire un indice come unico se in realtà i dati naturali nell'indice sono già unici.

Ci sono diverse risorse eccellenti in Interwebz su questo argomento, tra cui:

Cordiali saluti, la semplice presenza di una identitycolonna non garantisce l'unicità. È necessario definire la colonna come chiave primaria o con un vincolo univoco per garantire che i valori memorizzati in quella colonna siano in effetti univoci. L' SET IDENTITY_INSERT schema.table ON;istruzione ti consentirà di inserire valori non univoci in una colonna definita come identity.


5

Solo un complemento all'ottima risposta di Max .

Quando si tratta di creare un indice cluster non univoco, SQL Server crea comunque qualcosa chiamato a Uniquifierin background.

Ciò Uniquifierpotrebbe causare potenziali problemi in futuro se la tua piattaforma ha molte operazioni CRUD, poiché Uniquifierè grande solo 4 byte (un intero di base a 32 bit). Quindi, se il tuo sistema ha molte operazioni CRUD è possibile che userai tutti i numeri univoci disponibili e all'improvviso riceverai un errore e non ti permetterà di inserire più dati nelle tue tabelle (perché non ha più valori univoci da assegnare alle righe appena inserite).

Quando ciò accade, riceverai questo errore:

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.

L'errore 666 (l'errore sopra riportato) si verifica quando uniquifierper un singolo set di chiavi non univoche si consumano più di 2.147.483.647 righe.

Quindi, avrai bisogno di avere ~ 2 miliardi di righe per un singolo valore chiave, oppure dovrai aver modificato un singolo valore chiave ~ 2 miliardi di volte per vedere questo errore. Pertanto, non è estremamente probabile che incontrerai questa limitazione.


Non avevo idea che l'unicificatore nascosto potesse esaurire lo spazio delle chiavi, ma immagino che tutto sia limitato in alcuni casi. Proprio come il modo Casee le Ifstrutture sono limitate a 10 livelli, ha senso che esiste anche un limite alla risoluzione di entità non uniche. Secondo la tua affermazione, sembra che si applichi solo ai casi in cui la chiave di clustering non è univoca. È un problema per un Nonclustered Indexo se la chiave di clustering è Uniqueallora non c'è un problema per gli Nonclusteredindici?
Solonotix,

Un indice univoco è (per quanto ne so) limitato dalla dimensione del tipo di colonna (quindi se è un tipo GRANDE, hai 8 byte con cui lavorare). Inoltre, secondo la documentazione ufficiale di Microsoft, è consentito un massimo di 900 byte per un indice cluster e 1700 byte per non cluster (poiché è possibile avere più di un indice non cluster e solo 1 indice cluster per tabella).docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain

1
@Solonotix: l'unicificatore dall'indice cluster viene utilizzato negli indici non cluster. Se esegui il codice nel mio esempio senza la chiave primaria (crea invece un indice cluster), puoi vedere che l'output è lo stesso sia per gli indici non univoci che per quelli univoci.
Max Vernon,

-2

Non soppeserò la questione se un indice debba essere unico o meno e se ci sia più overhead in questo approccio o quello. Ma un paio di cose mi hanno infastidito nel tuo progetto generale

  1. dt datetime non null default (current_timestamp). Datetime è una forma precedente o questa e potresti essere in grado di ottenere almeno alcuni risparmi di spazio usando datetime2 () e sysdatetime ().
  2. create index [nonunique_nonclustered_example] su #test_index (is_deleted) include (val). Questo mi dà fastidio. Dai un'occhiata a come accedere ai dati (scommetto che c'è di più WHERE is_deleted = 0) e guarda usando un indice filtrato. Vorrei anche prendere in considerazione l'utilizzo di 2 indici filtrati, uno per where is_deleted = 0e l'altro perwhere is_deleted = 1

Fondamentalmente questo sembra più un esercizio di codifica progettato per testare un'ipotesi piuttosto che un vero problema / soluzione, ma quei due schemi sono sicuramente qualcosa che cerco nelle recensioni del codice.


Il massimo che risparmierai utilizzando datetime2 anziché datetime è 1 byte, ovvero se la precisione è inferiore a 3, il che significherebbe perdere precisione in secondi frazionari, che non è sempre una soluzione praticabile. Per quanto riguarda l'indice di esempio fornito, il design è stato mantenuto semplice per concentrarmi sulla mia domanda. Un Nonclusteredindice avrà la chiave di clustering aggiunta alla fine della riga di dati per le ricerche di chiavi internamente. Pertanto, i due indici sono fisicamente gli stessi, il che era il punto della mia domanda.
Solonotix,

Alla scala corriamo per salvare rapidamente un byte o due. E avevo ipotizzato che, dato che stavi usando il datetime impreciso, potremmo ridurre la precisione. Per gli indici, ancora una volta affermerò che le colonne di bit come colonne di piombo sugli indici è un modello che considero una scelta sbagliata. Come per tutte le cose, il tuo chilometraggio può variare. Purtroppo gli svantaggi di un modello approssimativo.
Toby,

-4

Sembra che tu stia semplicemente usando PK per creare un indice alternativo più piccolo. Quindi, le prestazioni sono più veloci.

Lo vedi in aziende che hanno enormi tabelle di dati (ad es. Tabelle di dati principali). Qualcuno decide di avere un enorme indice cluster su di esso aspettandosi che soddisfi le esigenze di vari gruppi di reporting.

Ma un gruppo potrebbe aver bisogno solo di alcune parti di quell'indice mentre un altro gruppo ha bisogno di altre parti .. quindi l'indice che schiaffeggia semplicemente in ogni colonna sotto il sole per "ottimizzare le prestazioni" non aiuta davvero.

Nel frattempo, scomporlo per creare indici multipli, più piccoli e mirati, spesso risolve il problema.

E quello sembra essere quello che stai facendo. Hai questo enorme indice cluster con prestazioni terribili, quindi stai usando PK per creare un altro indice con meno colonne che (nessuna sorpresa) ha prestazioni migliori.

Quindi, basta fare un'analisi e capire se è possibile prendere il singolo indice cluster e suddividerlo in indici più piccoli e mirati di cui hanno bisogno lavori specifici.

Dovresti quindi analizzare le prestazioni da un punto di vista "indice singolo vs. indice multiplo", poiché ci sono costi generali nella creazione e nell'aggiornamento degli indici. Ma devi analizzarlo da una prospettiva generale.

Ad esempio: potrebbe essere meno dispendioso in termini di risorse rispetto a un indice cluster elevato e più dispendioso in termini di risorse avere più indici target più piccoli. Ma se sei in grado di eseguire query mirate sul back-end molto più rapidamente, risparmiando tempo (e denaro) lì, potrebbe valerne la pena.

Quindi, dovresti fare un'analisi end-to-end .. non solo guardare come influenza il tuo mondo, ma anche come influenza gli utenti finali.

Sento che stai usando male l'identificatore PK. Ma potresti utilizzare un sistema di database che consente solo 1 indice (?), Ma puoi introdurne un altro se PK (b / c ogni sistema di database relazionale in questi giorni sembra indicizzare automaticamente il PK). Tuttavia, la maggior parte dei RDBMS moderni dovrebbe consentire la creazione di più indici; non ci dovrebbero essere limiti al numero di indici che puoi fare (al contrario di un limite di 1 PK).

Quindi, creando un PK che agisce semplicemente come un indice alt .. stai usando il tuo PK, che potrebbe essere necessario se la tabella viene successivamente espansa nel suo ruolo.

Questo non vuol dire che il tuo tavolo non abbia bisogno di un PK. Il 101 di SOP DB dice "ogni tavolo dovrebbe avere un PK". Ma, in una situazione di archiviazione dei dati o simili ... avere un PK su un tavolo potrebbe essere solo un sovraccarico extra che non ti serve. Oppure, potrebbe essere un god-send per assicurarsi di non aggiungere due volte le voci duplicate. È davvero una questione di cosa stai facendo e perché lo stai facendo.

Ma, enormi tabelle traggono sicuramente vantaggio dall'avere indici. Ma, supponendo che un singolo indice cluster enorme sia il migliore è solo ... potrebbe essere il migliore .. ma consiglierei di provare un env di test suddividendo l'indice in più indici più piccoli destinati a scenari di casi d'uso specifici.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.