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 indiquait qu'un index Unique
n'était pas un coût supplémentaire à maintenir, car une opération Insert
vérifie implicitement où il s'insère dans l'arbre B et, si un doublon est trouvé dans un index non unique, ajoute un uniquificateur à la fin de la clé, mais insère autrement directement. Dans cette séquence d'événements, un index Unique
n'a aucun coût supplémentaire.
Mon collègue combat cette déclaration en disant que Unique
est appliqué comme une deuxième opération après la recherche de la nouvelle position dans l'arbre B, et est donc plus coûteux à 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 fréquemment impliqué dans les révisions 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 assurer un certain niveau de qualité.
Question
L'unicité a-t-elle un coût supplémentaire sur le back-end d'un Insert
par rapport au coût de maintien d'un index 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);
Un exemple de la raison pour laquelle j'ajouterais la clé Unique
à la fin d'un index se trouve dans l'une de nos tables de faits. Il y a un Primary Key
qui est une colonne Identity
. Cependant, le Clustered Index
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 abyssales et j'obtiens souvent de meilleurs temps de recherche en utilisant le Primary Key
avec une recherche de clé plutôt que de tirer parti de Clustered Index
. D'autres tableaux qui suivent une conception similaire, mais qui ont le Primary Key
ajouté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
Je suis fréquemment impliqué dans les révisions 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é a 250 serveurs avec 2500 bases de données. J'ai travaillé sur des systèmes avec 30 000 bases de données . Les directives pour l'indexation devraient tourner autour de la convention de dénomination, etc., ne pas être des "règles" pour quelles 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
Insert
par rapport au coût de maintien d'un index 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; ce 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 que l'application de l'unicité n'ajoute pas de surcharge supplémentaire est correcte (ce qu'elle n'est pas dans certains cas), que résolvez-vous en compliquer inutilement l'indice?
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 modificateur UNIQUE
, 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 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 entraînerait l'échec de déclarer une clé candidate comme index unique, ce qui entraînerait l'ajout des clés d'index cluster à l'index clés.
Prenez l'exemple suivant exemple minimalement complet et vérifiable :
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 colonne rowDate
et deux lignes contiennent des valeurs uniques.
Ensuite, nous allons examiner les structures de page physiques pour chaque index, en utilisant la commande DBCC PAGE
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.
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 uniquement de définir un index comme unique si en fait les données naturelles de l'index sont déjà uniques.
Il existe plusieurs excellentes ressources sur Interwebz à ce sujet, notamment:
Pour info, la simple présence d'une colonne identity
ne garantit pas l'unicité. Vous devez définir la colonne comme clé primaire ou avec une contrainte unique pour vous assurer que les valeurs stockées dans cette colonne sont en fait uniques. L'instruction SET IDENTITY_INSERT schema.table ON;
Vous permettra d'insérer des valeurs non uniques dans une colonne définie comme identity
.
Juste un complément à excellente réponse de Max .
Lorsqu'il s'agit de créer un index cluster non unique, SQL Server crée de toute façon quelque chose appelé Uniquifier
en arrière-plan.
Ce Uniquifier
pourrait causer des problèmes potentiels à l'avenir si votre plate-forme a beaucoup d'opérations CRUD, car ce Uniquifier
ne 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 uniquifier
pour un seul jeu de clés non uniques consomme plus de 2 147 483 647 lignes.
Ainsi, vous devrez 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.