Les clés naturelles fournissent-elles des performances supérieures ou inférieures dans SQL Server par rapport aux clés de substitution?

25

Je suis fan des clés de substitution. Il y a un risque que mes conclusions soient biaisées par la confirmation.

De nombreuses questions que j'ai vues ici et sur http://stackoverflow.com utilisent des clés naturelles au lieu de clés de substitution basées sur des IDENTITY()valeurs.

Mon expérience dans les systèmes informatiques me dit que toute opération comparative sur un entier sera plus rapide que la comparaison de chaînes.

Ce commentaire m'a fait remettre en question mes croyances, j'ai donc pensé créer un système pour étudier ma thèse selon laquelle les entiers sont plus rapides que les chaînes à utiliser comme clés dans SQL Server.

Puisqu'il est probable qu'il y ait très peu de différence perceptible dans les petits ensembles de données, j'ai immédiatement pensé à une configuration à deux tables où la table principale a 1 000 000 lignes et la table secondaire a 10 lignes pour chaque ligne de la table principale pour un total de 10 000 000 lignes dans la table secondaire. La prémisse de mon test est de créer deux ensembles de tables comme celui-ci, un à l'aide de clés naturelles et un à l'aide de clés entières, et d'exécuter des tests de synchronisation sur une requête simple comme:

SELECT *
FROM Table1
    INNER JOIN Table2 ON Table1.Key = Table2.Key;

Voici le code que j'ai créé comme banc d'essai:

USE Master;
IF (SELECT COUNT(database_id) FROM sys.databases d WHERE d.name = 'NaturalKeyTest') = 1
BEGIN
    ALTER DATABASE NaturalKeyTest SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE NaturalKeyTest;
END
GO
CREATE DATABASE NaturalKeyTest 
    ON (NAME = 'NaturalKeyTest', FILENAME = 
        'C:\SQLServer\Data\NaturalKeyTest.mdf', SIZE=8GB, FILEGROWTH=1GB) 
    LOG ON (NAME='NaturalKeyTestLog', FILENAME = 
        'C:\SQLServer\Logs\NaturalKeyTest.mdf', SIZE=256MB, FILEGROWTH=128MB);
GO
ALTER DATABASE NaturalKeyTest SET RECOVERY SIMPLE;
GO
USE NaturalKeyTest;
GO
CREATE VIEW GetRand
AS 
    SELECT RAND() AS RandomNumber;
GO
CREATE FUNCTION RandomString
(
    @StringLength INT
)
RETURNS NVARCHAR(max)
AS
BEGIN
    DECLARE @cnt INT = 0
    DECLARE @str NVARCHAR(MAX) = '';
    DECLARE @RandomNum FLOAT = 0;
    WHILE @cnt < @StringLength
    BEGIN
        SELECT @RandomNum = RandomNumber
        FROM GetRand;
        SET @str = @str + CAST(CHAR((@RandomNum * 64.) + 32) AS NVARCHAR(MAX)); 
        SET @cnt = @cnt + 1;
    END
    RETURN @str;
END;
GO
CREATE TABLE NaturalTable1
(
    NaturalTable1Key NVARCHAR(255) NOT NULL 
        CONSTRAINT PK_NaturalTable1 PRIMARY KEY CLUSTERED 
    , Table1TestData NVARCHAR(255) NOT NULL 
);
CREATE TABLE NaturalTable2
(
    NaturalTable2Key NVARCHAR(255) NOT NULL 
        CONSTRAINT PK_NaturalTable2 PRIMARY KEY CLUSTERED 
    , NaturalTable1Key NVARCHAR(255) NOT NULL 
        CONSTRAINT FK_NaturalTable2_NaturalTable1Key 
        FOREIGN KEY REFERENCES dbo.NaturalTable1 (NaturalTable1Key) 
        ON DELETE CASCADE ON UPDATE CASCADE
    , Table2TestData NVARCHAR(255) NOT NULL  
);
GO

/* insert 1,000,000 rows into NaturalTable1 */
INSERT INTO NaturalTable1 (NaturalTable1Key, Table1TestData) 
    VALUES (dbo.RandomString(25), dbo.RandomString(100));
GO 1000000 

/* insert 10,000,000 rows into NaturalTable2 */
INSERT INTO NaturalTable2 (NaturalTable2Key, NaturalTable1Key, Table2TestData)
SELECT dbo.RandomString(25), T1.NaturalTable1Key, dbo.RandomString(100)
FROM NaturalTable1 T1
GO 10 

CREATE TABLE IDTable1
(
    IDTable1Key INT NOT NULL CONSTRAINT PK_IDTable1 
    PRIMARY KEY CLUSTERED IDENTITY(1,1)
    , Table1TestData NVARCHAR(255) NOT NULL 
    CONSTRAINT DF_IDTable1_TestData DEFAULT dbo.RandomString(100)
);
CREATE TABLE IDTable2
(
    IDTable2Key INT NOT NULL CONSTRAINT PK_IDTable2 
        PRIMARY KEY CLUSTERED IDENTITY(1,1)
    , IDTable1Key INT NOT NULL 
        CONSTRAINT FK_IDTable2_IDTable1Key FOREIGN KEY 
        REFERENCES dbo.IDTable1 (IDTable1Key) 
        ON DELETE CASCADE ON UPDATE CASCADE
    , Table2TestData NVARCHAR(255) NOT NULL 
        CONSTRAINT DF_IDTable2_TestData DEFAULT dbo.RandomString(100)
);
GO
INSERT INTO IDTable1 DEFAULT VALUES;
GO 1000000
INSERT INTO IDTable2 (IDTable1Key)
SELECT T1.IDTable1Key
FROM IDTable1 T1
GO 10

Le code ci-dessus crée une base de données et 4 tables et remplit les tables de données, prêtes à être testées. Le code de test que j'ai exécuté est:

USE NaturalKeyTest;
GO
DECLARE @loops INT = 0;
DECLARE @MaxLoops INT = 10;
DECLARE @Results TABLE (
    FinishedAt DATETIME DEFAULT (GETDATE())
    , KeyType NVARCHAR(255)
    , ElapsedTime FLOAT
);
WHILE @loops < @MaxLoops
BEGIN
    DBCC FREEPROCCACHE;
    DBCC FREESESSIONCACHE;
    DBCC FREESYSTEMCACHE ('ALL');
    DBCC DROPCLEANBUFFERS;
    WAITFOR DELAY '00:00:05';
    DECLARE @start DATETIME = GETDATE();
    DECLARE @end DATETIME;
    DECLARE @count INT;
    SELECT @count = COUNT(*) 
    FROM dbo.NaturalTable1 T1
        INNER JOIN dbo.NaturalTable2 T2 ON T1.NaturalTable1Key = T2.NaturalTable1Key;
    SET @end = GETDATE();
    INSERT INTO @Results (KeyType, ElapsedTime)
    SELECT 'Natural PK' AS KeyType, CAST((@end - @start) AS FLOAT) AS ElapsedTime;

    DBCC FREEPROCCACHE;
    DBCC FREESESSIONCACHE;
    DBCC FREESYSTEMCACHE ('ALL');
    DBCC DROPCLEANBUFFERS;
    WAITFOR DELAY '00:00:05';
    SET @start = GETDATE();
    SELECT @count = COUNT(*) 
    FROM dbo.IDTable1 T1
        INNER JOIN dbo.IDTable2 T2 ON T1.IDTable1Key = T2.IDTable1Key;
    SET @end = GETDATE();
    INSERT INTO @Results (KeyType, ElapsedTime)
    SELECT 'IDENTITY() PK' AS KeyType, CAST((@end - @start) AS FLOAT) AS ElapsedTime;

    SET @loops = @loops + 1;
END
SELECT KeyType, FORMAT(CAST(AVG(ElapsedTime) AS DATETIME), 'HH:mm:ss.fff') AS AvgTime 
FROM @Results
GROUP BY KeyType;

Voici les résultats:

entrez la description de l'image ici

Suis-je en train de faire quelque chose de mal ici, ou les touches INT sont-elles 3 fois plus rapides que les clés naturelles à 25 caractères?

Remarque, j'ai écrit une question de suivi ici .

Max Vernon
la source
1
Eh bien, l'INT est de 4 octets et le NVARCHAR efficace (25) est environ 14 fois plus long (y compris les données système telles que la longueur), donc en termes d'index seul, je pense que vous auriez un index PK beaucoup plus large et plus profond et donc plus I / O est nécessaire, ce qui aura un impact sur le temps de traitement. Cependant, un entier naturel (peut-être même un chiffre de contrôle) serait à peu près le même INT que nous pensons utiliser pour une colonne d'identité de substitution. Ainsi, la "clé naturelle" peut-être un INT, BIGINT, CHAR, NVARCHAR et que tout compte.
RLF
7
Je pense que le gain de performances @ MikeSherrill'Catcall 'voulait dire que vous n'avez pas réellement besoin de la jointure contre la table de "recherche" lorsque vous utilisez une clé naturelle. Comparez une requête pour obtenir la valeur de recherche avec une jointure, avec une requête où la valeur est déjà stockée dans la table principale. Vous pouvez obtenir un "gagnant" différent en fonction de la longueur de clé naturelle et du nombre de lignes dans la table de recherche.
Mikael Eriksson
3
Ce que @MikaelEriksson a dit plus les cas où vous avez une jointure entre plus de 2 tables (disons 4) où avec les substituts vous devrez joindre les tables A à D à B et C tandis qu'avec les clés naturelles vous pourriez joindre A à D directement
ypercubeᵀᴹ

Réponses:

18

En général, SQL Server utilise des arborescences B + pour les index. Le coût d'une recherche d'index est directement lié à la longueur de la clé dans ce format de stockage. Par conséquent, une clé de substitution surpasse généralement une clé naturelle sur les indices recherchés.

Par défaut, SQL Server regroupe une table sur la clé primaire. La clé d'index cluster est utilisée pour identifier les lignes, elle est donc ajoutée en tant que colonne incluse à tous les autres index. Plus cette clé est large, plus chaque indice secondaire est grand.

Pire encore, si les index secondaires ne sont pas explicitement définis car UNIQUEla clé d'index clusterisée fait automatiquement partie de la clé de chacun d'entre eux. Cela s'applique généralement à la plupart des index, car les index ne sont généralement déclarés comme uniques que lorsque l'exigence est d'imposer l'unicité.

Donc, si la question est, l'index groupé naturel contre substitut, le substitut gagnera presque toujours.

D'un autre côté, vous ajoutez cette colonne de substitution à la table, ce qui rend la table en soi plus grande. Cela rendra les analyses d'index en cluster plus coûteuses. Donc, si vous n'avez que très peu d'index secondaires et que votre charge de travail nécessite de regarder souvent toutes (ou la plupart des) lignes, vous pourriez en fait être mieux avec une clé naturelle qui enregistre ces quelques octets supplémentaires.

Enfin, les clés naturelles facilitent souvent la compréhension du modèle de données. Tout en utilisant plus d'espace de stockage, les clés primaires naturelles conduisent à des clés étrangères naturelles qui à leur tour augmentent la densité des informations locales.

Ainsi, comme si souvent dans le monde des bases de données, la vraie réponse est "ça dépend". Et - testez toujours dans votre propre environnement avec des données réalistes.

Sebastian Meine
la source
10

Je crois que le meilleur se situe au milieu .

Présentation des touches naturelles:

  1. Ils rendent le modèle de données plus évident car ils proviennent du domaine et non de la tête de quelqu'un.
  2. Les clés simples (une colonne, entre CHAR(4)et CHAR(20)) économisent des octets supplémentaires, mais vous devez surveiller leur cohérence ( ON UPDATE CASCADEdevient critique pour ces clés, qui pourraient être modifiées).
  3. Un grand nombre de cas, lorsque les clés naturelles sont complexes: se compose de deux colonnes ou plus. Si une telle clé peut migrer vers une autre entité en tant que clé étrangère, elle ajoutera une surcharge de données (les indices et les colonnes de données peuvent devenir volumineux) et les performances perdent.
  4. Si la clé est une chaîne de grande taille, elle perdra probablement toujours une clé entière, car une condition de recherche simple devient une comparaison de tableaux d'octets dans un moteur de base de données, qui dans la plupart des cas est plus lente, qu'une comparaison entière.
  5. Si la clé est une chaîne multilingue, vous devez également regarder les classements.

Avantages: 1 et 2.

Watchouts: 3, 4 et 5.


Présentation des clés d'identité artificielles:

  1. Vous n'avez pas à vous soucier de leur création et de leur gestion (dans la plupart des cas) car cette fonctionnalité est gérée par le moteur de base de données. Ils sont uniques par défaut et ne prennent pas beaucoup de place. Les opérations personnalisées comme ON UPDATE CASCADEpeuvent être omises, car les valeurs de clé ne changent pas.

  2. Ils sont (souvent) les meilleurs candidats à la migration en tant que clés étrangères car:

    2.1. se compose d'une colonne;

    2.2. en utilisant un type simple qui a un petit poids et agit rapidement pour les opérations de comparaison.

  3. Pour une association, dont les clés ne migrent nulle part, cela peut devenir une pure surcharge de données, car son utilité est perdue. La clé primaire naturelle complexe (s'il n'y a pas de colonnes de chaîne) sera plus utile.

Avantages: 1 et 2.

Attention: 3.


CONCLUSION:

Les clés Arificial sont plus faciles à entretenir, fiables et rapides car elles ont été conçues pour ces fonctionnalités. Mais dans certains cas, ne sont pas nécessaires. Par exemple, le CHAR(4)candidat à colonne unique se comporte dans la plupart des cas comme INT IDENTITY. Il y a donc une autre question ici aussi: maintenabilité + stabilité ou évidence ?

Question "Dois-je injecter une clé artificielle ou non?" dépend toujours de la structure de la clé naturelle:

  • S'il contient une grande chaîne, il est plus lent et ajoutera une surcharge de données s'il migre comme étranger vers une autre entité.
  • S'il se compose de plusieurs colonnes, il est plus lent et ajoutera une surcharge de données s'il migre comme étranger vers une autre entité.
Blitz
la source
5
"Les opérations personnalisées comme ON UPDATE CASCADE peuvent être omises, car les valeurs de clé ne changent pas." Les clés de substitution ont pour effet de faire de chaque référence de clé étrangère l'équivalent de "ON UPDATE CASCADE". La clé ne change pas, mais la valeur qu'elle représente change .
Mike Sherrill 'Cat Recall'
@ MikeSherrill'Catcall 'Oui, bien sûr. Cependant, ON UPDATE CASCADEnon utilisé, alors que les clés n'ont jamais été mises à jour. Mais, s'ils le sont, cela pourrait être un problème s'il ON UPDATE NO ACTIONest configuré. Je veux dire, que le SGBD ne l'utilise jamais, alors que les valeurs des colonnes clés n'ont pas changé.
BlitZ
4

Une clé est une caractéristique logique d'une base de données tandis que les performances sont toujours déterminées par l'implémentation physique dans le stockage et par les opérations physiques exécutées sur cette implémentation. C'est donc une erreur d'attribuer des caractéristiques de performance aux clés.

Cependant, dans cet exemple particulier, deux implémentations possibles de tables et de requêtes sont comparées. L'exemple ne répond pas à la question posée dans le titre ici. La comparaison est faite de jointures utilisant deux types de données différents (entier et caractère) en utilisant un seul type d'index (B-tree). Un point "évident" est que si un index de hachage ou un autre type d'index était utilisé, il n'y aurait probablement aucune différence de performance mesurable entre les deux implémentations. Il y a cependant des problèmes plus fondamentaux avec l'exemple.

Deux requêtes sont comparées pour les performances mais les deux requêtes ne sont pas logiquement équivalentes car elles renvoient des résultats différents! Un test plus réaliste comparerait deux requêtes renvoyant les mêmes résultats mais utilisant des implémentations différentes.

Le point essentiel à propos d'une clé de substitution est qu'il s'agit d'un attribut supplémentaire dans une table où la table a également des attributs de clé "significatifs" utilisés dans le domaine métier. Ce sont les attributs non substituts qui sont utiles pour que les résultats de la requête soient utiles. Un test réaliste comparerait donc les tables utilisant uniquement des clés naturelles avec une implémentation alternative ayant à la fois des clés naturelles et de substitution dans la même table. Les clés de substitution nécessitent généralement un stockage et une indexation supplémentaires et, par définition, nécessitent des contraintes d'unicité supplémentaires. Les mères porteuses nécessitent un traitement supplémentaire pour mapper les valeurs de clé naturelle externes sur leurs substituts et vice versa.

Comparez maintenant cette requête potentielle:

UNE.

SELECT t2.NaturalTable2Key, t2.NaturalTable1Key
FROM Table2 t2;

À son équivalent logique si l'attribut NaturalTable1Key du tableau 2 est remplacé par l'IDTable1Key de substitution:

B.

SELECT t2.NaturalTable2Key, t1.NaturalTable1Key
FROM Table2 t2
INNER JOIN Table1 t1
ON t1.IDTable1Key = t2.IDTable1Key;

La requête B nécessite une jointure; La requête A ne fonctionne pas. Il s'agit d'une situation familière dans les bases de données qui utilisent (sur) des substituts. Les requêtes deviennent inutilement complexes et beaucoup plus difficiles à optimiser. La logique métier (en particulier les contraintes d'intégrité des données) devient plus difficile à mettre en œuvre, à tester et à vérifier.

nvogel
la source