Frais généraux d'unicité d'index

14

J'ai eu un débat en cours avec divers développeurs dans mon bureau sur le coût d'un indice, et si oui ou non l'unicité est bénéfique ou coûteuse (probablement les deux). Le nœud du problème réside dans nos ressources concurrentes.

Contexte

J'ai déjà lu une discussion qui a déclaré qu'un Uniqueindex n'est pas un coût supplémentaire à maintenir, car une Insertopération vérifie implicitement où il s'inscrit dans l'arbre B et, si un doublon est trouvé dans un index non unique, ajoute un uniquificateur à la fin de la clé, mais sinon insère directement. Dans cette séquence d'événements, un Uniqueindex n'a aucun coût supplémentaire.

Mon collègue combat cette affirmation en disant qu'elle Uniqueest appliquée en tant que deuxième opération après la recherche de la nouvelle position dans l'arbre B, et est donc plus coûteuse à maintenir qu'un index non unique.

Au pire, j'ai vu des tables avec une colonne d'identité (intrinsèquement unique) qui est la clé de clustering de la table, mais explicitement déclarée comme non unique. De l'autre côté du pire est mon obsession de l'unicité, et tous les index sont créés comme uniques, et lorsqu'il n'est pas possible de définir une relation explicitement unique à un index, j'ajoute le PK de la table à la fin de l'index pour garantir la l'unicité est garantie.

Je suis souvent impliqué dans les revues de code pour l'équipe de développement, et je dois être en mesure de donner des directives générales à suivre. Oui, chaque index doit être évalué, mais lorsque vous avez cinq serveurs avec des milliers de tables chacun et jusqu'à vingt index sur une table, vous devez être en mesure d'appliquer des règles simples pour garantir un certain niveau de qualité.

Question

L'unicité a-t-elle un coût supplémentaire sur le back-end d'un Insertpar rapport au coût de maintien d'un indice non unique? Deuxièmement, qu'y a-t-il de mal à ajouter la clé primaire d'une table à la fin d'un index pour garantir l'unicité?

Exemple de définition de table

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

Exemple

Un exemple de la raison pour laquelle j'ajouterais la Uniqueclé à la fin d'un index se trouve dans l'une de nos tables de faits. Il y a un Primary Keyqui est une Identitycolonne. Cependant, Clustered Indexc'est à la place la colonne du schéma de partitionnement, suivie de trois dimensions de clé étrangère sans unicité. Les performances de sélection sur ce tableau sont épouvantables, et j'obtiens souvent de meilleurs temps de recherche en utilisant le Primary Keyavec une recherche de clé plutôt que de tirer parti du Clustered Index. Les autres tableaux qui suivent une conception similaire, mais qui sont Primary Keyannexés à la fin, ont des performances considérablement meilleures.

-- 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
Solonotix
la source

Réponses:

16

Je suis souvent impliqué dans les revues de code pour l'équipe de développement, et je dois être en mesure de donner des directives générales à suivre.

L'environnement dans lequel je suis actuellement impliqué compte 250 serveurs avec 2500 bases de données. J'ai travaillé sur des systèmes avec 30 000 bases de données . Les lignes directrices pour l'indexation devraient tourner autour de la convention de dénomination, etc., ne pas être des "règles" pour les colonnes à inclure dans un index - chaque index individuel devrait être conçu pour être l'index correct pour cette règle ou code métier spécifique touchant la table.

L'unicité a-t-elle un coût supplémentaire sur le back-end d'un Insertpar rapport au coût de maintien d'un indice non unique? Deuxièmement, qu'y a-t-il de mal à ajouter la clé primaire d'une table à la fin d'un index pour garantir l'unicité?

L'ajout de la colonne de clé primaire à la fin d'un index non unique pour le rendre unique me semble être un anti-modèle. Si les règles métier imposent que les données soient uniques, ajoutez une contrainte unique à la colonne; qui créera automatiquement un index unique. Si vous indexez une colonne pour les performances , pourquoi ajouteriez-vous une colonne à l'index?

Même si votre supposition selon laquelle l'application de l'unicité n'ajoute pas de surcharge supplémentaire est correcte (ce qui n'est pas le cas dans certains cas), que résolvez-vous en compliquant inutilement l'index?

Dans le cas spécifique de l'ajout de la clé primaire à la fin de votre clé d'index afin que vous puissiez faire en sorte que la définition d'index inclue le UNIQUEmodificateur, cela ne fait aucune différence dans la structure d'index physique sur le disque. Cela est dû à la nature de la structure des clés d'index B-tree, en ce qu'elles doivent toujours être uniques.

Comme David Browne l'a mentionné dans un commentaire:

Étant donné que chaque index non cluster est stocké en tant qu'index unique, il n'y a aucun coût supplémentaire à insérer dans un index unique. En fait, le seul coût supplémentaire serait de ne pas déclarer une clé candidate comme index unique, ce qui entraînerait l'ajout des clés d'index cluster aux clés d'index.

Prenons l'exemple minimalement complet et vérifiable suivant :

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

J'ajouterai deux index identiques à l'exception de l'ajout de la clé primaire à la fin de la deuxième définition de clé d'index:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

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

Ensuite, nous allons plusieurs lignes dans le tableau:

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

Comme vous pouvez le voir ci-dessus, trois lignes contiennent la même valeur pour la rowDatecolonne et deux lignes contiennent des valeurs uniques.

Ensuite, nous allons examiner les structures de page physiques pour chaque index, à l'aide de la DBCC PAGEcommande non documentée :

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

J'ai regardé la sortie en utilisant Beyond Compare, et à l'exception des différences évidentes autour des ID de page d'allocation, etc., les deux structures d'index sont identiques.

entrez la description de l'image ici

Vous pouvez considérer ce qui précède comme signifiant qu'inclure la clé primaire dans chaque index et définir comme unique est A Good Thing ™ puisque c'est ce qui se passe sous le couvert de toute façon. Je ne ferais pas cette hypothèse et suggérerais de définir uniquement un index comme unique si en fait les données naturelles de l'index sont déjà uniques.

Il existe plusieurs excellentes ressources dans Interwebz sur ce sujet, notamment:

Pour info, la simple présence d'un identity colonne ne garantit pas l' unicité. Vous devez définir la colonne en tant que clé primaire ou avec une contrainte unique pour vous assurer que les valeurs stockées dans cette colonne sont en fait uniques. L' SET IDENTITY_INSERT schema.table ON;instruction vous permettra d'insérer des valeurs non uniques dans une colonne définie comme identity.

Max Vernon
la source
5

Juste un complément à l'excellente réponse de Max .

Lorsqu'il s'agit de créer un index cluster non unique, SQL Server crée quelque chose appelé Uniquifier en arrière-plan.

Cette Uniquifier pourrait entraîner des problèmes potentiels à l'avenir si votre plate-forme a beaucoup d'opérations CRUD, car cela Uniquifierne fait que 4 octets (un entier de 32 bits de base). Donc, si votre système a beaucoup d'opérations CRUD, il est possible que vous utilisiez tous les numéros uniques disponibles et tout à coup, vous recevrez une erreur et cela ne vous permettra plus d'insérer des données dans vos tables (car cela n'ont plus de valeurs uniques à affecter à vos lignes nouvellement insérées).

Lorsque cela se produit, vous recevrez cette erreur:

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.

Erreur 666 (l'erreur ci-dessus) se produit lorsque le uniquifierpour un seul jeu de clés non uniques consomme plus de 2 147 483 647 lignes.

Ainsi, vous aurez besoin d'avoir ~ 2 milliards de lignes pour une seule valeur de clé, ou vous devrez avoir modifié une seule valeur de clé ~ 2 milliards de fois pour voir cette erreur. En tant que tel, il est peu probable que vous rencontriez cette limitation.

Chessbrain
la source
Je ne savais pas que l'uniquificateur caché pouvait manquer d'espace clé, mais je suppose que tout est limité dans certains cas. Tout comme la façon dont Caseet les Ifstructures sont limitées à 10 niveaux, il est logique qu'il y ait également une limite à la résolution d'entités non uniques. D'après votre déclaration, cela semble ne s'appliquer qu'aux cas où la clé de clustering n'est pas unique. Est-ce un problème pour un Nonclustered Indexou si la clé de clustering est Uniquealors il n'y a pas de problème pour les Nonclusteredindex?
Solonotix
Un index Unique est (pour autant que je sache) limité par la taille du type de colonne (donc s'il s'agit d'un type BIGINT, vous avez 8 octets pour travailler). En outre, selon la documentation officielle de Microsoft, il y a un maximum de 900 octets autorisés pour un index cluster et 1700 octets pour non cluster (car vous pouvez avoir plus d'un index non cluster et seulement 1 index cluster par table). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain
1
@Solonotix - l'uniquificateur de l' index cluster est utilisé dans les index non cluster. Si vous exécutez le code dans mon exemple sans la clé primaire (créez un index clusterisé à la place), vous pouvez voir que la sortie est la même pour les index non uniques et uniques.
Max Vernon
-2

Je ne vais pas peser sur la question de savoir si un indice doit être unique ou non, et s'il y a plus de frais généraux dans cette approche ou cela. Mais quelques choses m'ont dérangé dans votre conception générale

  1. dt datetime non null par défaut (current_timestamp). Datetime est une forme plus ancienne ou celle-ci, et vous pourrez peut-être réaliser au moins quelques économies d'espace en utilisant datetime2 () et sysdatetime ().
  2. créer un index [nonunique_nonclustered_example] sur #test_index (is_deleted) include (val). Cela me dérange. Jetez un œil à la façon dont les données doivent être accessibles (je parie qu'il y en a plus WHERE is_deleted = 0) et regardez à l'aide d'un index filtré. J'envisagerais même d'utiliser 2 index filtrés, l'un pour where is_deleted = 0et l'autre pourwhere is_deleted = 1

Fondamentalement, cela ressemble plus à un exercice de codage conçu pour tester une hypothèse plutôt qu'un vrai problème / solution, mais ces deux modèles sont certainement quelque chose que je recherche dans les revues de code.

Toby
la source
Le maximum que vous économiserez en utilisant datetime2 au lieu de datetime est de 1 octet, et c'est si votre précision est inférieure à 3, ce qui signifierait une perte de précision sur des fractions de seconde, ce qui n'est pas toujours une solution viable. Quant à l'exemple d'index fourni, la conception est restée simple pour se concentrer sur ma question. Un Nonclusteredindex aura la clé de clustering ajoutée à la fin de la ligne de données pour les recherches de clés en interne. En tant que tels, les deux index sont physiquement les mêmes, ce qui était le point de ma question.
Solonotix
À l'échelle, nous courons à enregistrer un octet ou deux s'additionne rapidement. Et j'avais supposé que puisque vous utilisiez le datetime imprécis, nous pouvions réduire la précision. Pour les index, je dirai encore une fois que les colonnes de bits car les colonnes de tête des index est un modèle que je considère comme un mauvais choix. Comme pour tout, votre kilométrage peut varier. Hélas les inconvénients d'un modèle approximatif.
Toby
-4

Il semble que vous utilisiez simplement PK pour créer un index alternatif plus petit. Par conséquent, les performances sont plus rapides.

Vous voyez cela dans les entreprises qui ont d'énormes tableaux de données (par exemple: tableaux de données de base). Quelqu'un décide d'avoir un index cluster massif sur lui, s'attendant à ce qu'il réponde aux besoins des différents groupes de reporting.

Mais, un groupe peut n'avoir besoin que de quelques parties de cet index alors qu'un autre groupe a besoin d'autres parties.

Pendant ce temps, le décomposer pour créer plusieurs indices plus petits et ciblés résout souvent le problème.

Et cela semble être ce que vous faites. Vous avez cet index cluster massif avec des performances horribles, puis vous utilisez PK pour créer un autre index avec moins de colonnes qui (sans surprise) a de meilleures performances.

Donc, faites une analyse et déterminez si vous pouvez prendre l'index cluster unique et le décomposer en indices plus petits et ciblés dont des emplois spécifiques ont besoin.

Vous devrez alors analyser les performances à partir d'un point de vue "index unique vs index multiple", car il y a des frais généraux dans la création et la mise à jour des index. Mais, vous devez analyser cela dans une perspective globale.

EG: il peut être moins gourmand en ressources pour un seul indice cluster massif, et plus gourmand en ressources pour avoir plusieurs indices ciblés plus petits. Mais, si vous êtes alors en mesure d'exécuter des requêtes ciblées sur le back-end beaucoup plus rapidement, ce qui vous fait gagner du temps (et de l'argent), cela en vaut la peine.

Donc, vous devriez faire une analyse de bout en bout .. non seulement regarder comment cela affecte votre propre monde, mais aussi comment cela affecte les utilisateurs finaux.

J'ai juste l'impression que vous utilisez mal l'identifiant PK. Mais, vous utilisez peut-être un système de base de données qui n'autorise qu'un seul index (?), Mais vous pouvez en introduire un autre si vous PK (b / c chaque système de base de données relationnelle de nos jours semble indexer automatiquement le PK). Cependant, la plupart des SGBDR modernes devraient permettre la création d'index multiples; il ne devrait pas y avoir de limite au nombre d'index que vous pouvez créer (par opposition à une limite de 1 PK).

Donc, en faisant un PK qui agit juste comme un index alt .. vous utilisez votre PK, ce qui peut être nécessaire si la table est développée plus tard dans son rôle.

Cela ne veut pas dire que votre table n'a pas besoin d'un PK. SOP DB 101 dit "chaque table doit avoir un PK". Mais, dans une situation d'entreposage de données ou autre ... avoir un PK sur une table peut être un surcoût supplémentaire dont vous n'avez pas besoin. Ou, cela pourrait être un envoi de Dieu pour vous assurer que vous ne doublez pas les entrées de dupe. C'est vraiment une question de ce que vous faites et pourquoi vous le faites.

Mais, les tables massives bénéficient indéniablement d'avoir des index. Mais, en supposant qu'un seul index massif en cluster sera le meilleur est juste ... il peut être le meilleur .. mais je recommanderais de tester sur un env de test divisant l'index en plusieurs indices plus petits ciblant des scénarios de cas d'utilisation spécifiques.

blahblah
la source