Обеспечивают ли естественные ключи более высокую или более низкую производительность в SQL Server, чем суррогатные целочисленные ключи?

25

Я фанат суррогатных ключей. Существует риск, что мои выводы являются предвзятым подтверждением.

Многие вопросы, которые я видел здесь и на http://stackoverflow.com, используют естественные ключи вместо суррогатных ключей, основанных наIDENTITY() значениях.

Мой опыт работы в компьютерных системах говорит мне, что выполнение любой сравнительной операции над целым числом будет быстрее, чем сравнение строк.

это комментарий заставил меня усомниться в моём убеждении, поэтому я подумал, что создам систему для проверки моего тезиса о том, что целые числа быстрее, чем строки, для использования в качестве ключей в SQL Server.

Поскольку в небольших наборах данных, вероятно, будет очень мало заметной разницы, я сразу подумал о настройке двух таблиц, в которой первичная таблица имеет 1 000 000 строк, а вторичная таблица имеет 10 строк для каждой строки в первичной таблице, что в общей сложности составляет 10 000 000 строк. вторичный стол. Предпосылка моего теста состоит в том, чтобы создать два набора таблиц, таких как эта, одна с использованием естественных ключей, а другая с целочисленными ключами, и запустить временные тесты для простого запроса, такого как:

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

Ниже приведен код, который я создал в качестве тестового стенда:

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

Приведенный выше код создает базу данных и 4 таблицы и заполняет таблицы данными, готовыми к тестированию. Тестовый код, который я запустил:

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;

Вот результаты:

введите описание изображения здесь

Я что-то не так делаю, или клавиши INT в 3 раза быстрее, чем обычные 25-символьные?

Обратите внимание, я написал дополнительный вопрос здесь .

Макс Вернон
источник
1
Итак, INT составляет 4 байта, а эффективный NVARCHAR (25) примерно в 14 раз длиннее (включая системные данные, такие как длина), поэтому, исходя только из индекса, я считаю, что у вас будет значительно более широкий и глубокий индекс PK и, следовательно, больше I Требуется / O, что повлияет на время обработки. Однако натуральное целое число (возможно, даже контрольная цифра) было бы в значительной степени тем же самым INT, который мы думаем использовать для столбца суррогатной идентичности. Итак, «естественный ключ» может быть INT, BIGINT, CHAR, NVARCHAR, и это все имеет значение.
RLF
7
Я думаю, что прирост производительности, достигнутый @ MikeSherrill'Catcall ', заключается в том, что вам на самом деле не нужно соединение с таблицей «поиска», когда вы используете натуральный ключ. Сравните запрос, чтобы получить значение поиска с объединением, с запросом, в котором значение уже сохранено в основной таблице. Вы можете получить другого «победителя» в зависимости от длины естественного ключа и количества строк в таблице поиска.
Микаэль Эрикссон,
3
То, что сказал @MikaelEriksson, плюс случаи, когда у вас есть соединение между более чем 2 таблицами (скажем, 4), где с суррогатами вам нужно будет объединять таблицы с A по D по B и C, в то время как с естественными ключами вы можете напрямую соединять A с D
ypercubeᵀᴹ

Ответы:

18

В целом, SQL Server использует B + Trees для индексов. Стоимость поиска по индексу напрямую связана с длиной ключа в этом формате хранения. Следовательно, суррогатный ключ обычно превосходит естественный ключ при поиске индекса.

SQL Server кластеризует таблицу по первичному ключу по умолчанию. Ключ кластеризованного индекса используется для идентификации строк, поэтому он добавляется в качестве включенного столбца ко всем остальным индексам. Чем шире этот ключ, тем больше каждый вторичный индекс.

Еще хуже, если вторичные индексы не определены явно, поскольку UNIQUEключ кластеризованного индекса автоматически становится частью ключа каждого из них. Это обычно относится к большинству индексов, так как обычно индексы объявляются уникальными только тогда, когда требуется обеспечить уникальность.

Таким образом, если вопрос в том, естественный или суррогатный кластерный индекс, суррогат почти всегда победит.

С другой стороны, вы добавляете этот суррогатный столбец в таблицу, увеличивая ее размер. Это приведет к удорожанию сканирования кластерных индексов. Таким образом, если у вас очень мало вторичных индексов и ваша рабочая нагрузка требует частого просмотра всех (или большинства) строк, вам, возможно, лучше использовать естественный ключ, сохраняющий эти несколько лишних байтов.

Наконец, естественные ключи часто облегчают понимание модели данных. При использовании большего объема памяти естественные первичные ключи приводят к естественным внешним ключам, которые, в свою очередь, увеличивают локальную плотность информации.

Так что, как это часто бывает в мире баз данных, реальный ответ - «это зависит». И - всегда тестируйте в своей среде с реалистичными данными.

Себастьян Майн
источник
10

Я считаю, что лучшее лежит в середине .

Обзор натуральных ключей:

  1. Они делают модель данных более очевидной, потому что они пришли из предметной области, а не из чьей-то головы.
  2. Простые ключи (один столбец, между CHAR(4)и CHAR(20)) сохраняют некоторые дополнительные байты, но вам нужно следить за их согласованностью ( ON UPDATE CASCADEстановится критичным для тех ключей, которые могут быть изменены).
  3. Много случаев, когда естественные ключи сложны: состоит из двух или более столбцов. Если такой ключ может быть перенесен на другой объект в качестве первичного ключа, то это приведет к дополнительным расходам данных (индексы и столбцы данных могут стать большими) и снижению производительности.
  4. Если ключ является большой строкой, то он, вероятно, всегда потеряет целочисленный ключ, потому что простое условие поиска становится сравнением байтового массива в ядре базы данных, которое в большинстве случаев медленнее, чем целочисленное сравнение.
  5. Если ключ - это мультиязычная строка, нужно также посмотреть параметры сортировки.

Преимущества: 1 и 2.

Наблюдения: 3, 4 и 5.


Обзор искусственных ключей идентификации:

  1. Вам не нужно беспокоиться об их создании и обработке (в большинстве случаев), так как эта функция обрабатывается ядром базы данных. Они уникальны по умолчанию и не занимают много места. Пользовательские операции наподобие ON UPDATE CASCADEмогут быть опущены, потому что значения ключей не меняются.

  2. Они (часто) являются лучшими кандидатами на миграцию в качестве внешних ключей, потому что:

    2.1. состоит из одного столбца;

    2.2. используя простой тип, который имеет небольшой вес и действует быстро для операций сравнения.

  3. Для объектов ассоциации, ключи которых никуда не переносятся, это может стать чисто служебной информацией, поскольку ее полезность теряется. Сложный естественный первичный ключ (если там нет строковых столбцов) будет более полезным.

Преимущества: 1 и 2.

Наблюдения: 3.


ВЫВОД:

Официальные ключи более удобны в обслуживании, надежны и быстры, потому что они были разработаны для этих функций. Но в некоторых случаях это не нужно. Например, CHAR(4)кандидат в один столбец в большинстве случаев ведет себя как INT IDENTITY. Так что здесь есть еще один вопрос: ремонтопригодность + стабильность или очевидность ?

Вопрос "Должен ли я вводить искусственный ключ или нет?" всегда зависит от структуры естественного ключа:

  • Если он содержит большую строку, он работает медленнее и добавляет дополнительные данные, если мигрирует как чужой объект.
  • Если он состоит из нескольких столбцов, он работает медленнее и добавляет дополнительные данные, если мигрирует как чужой объект.
блицкриг
источник
5
«Пользовательские операции, такие как ON UPDATE CASCADE, могут быть опущены, поскольку значения ключей не меняются». Эффект суррогатных ключей состоит в том, чтобы сделать ссылку на каждый внешний ключ эквивалентом «ON UPDATE CASCADE». Ключ не меняется, но значение она представляет делает .
Майк Шеррилл 'Cat Recall'
@ MikeSherrill'Catcall 'Да, конечно. Однако ON UPDATE CASCADEне используется, в то время как ключи никогда не обновлялись. Но, если они есть, то это может быть проблемой, если ON UPDATE NO ACTIONнастроен. Я имею в виду, что СУБД никогда не использовала его, пока значения ключевых столбцов не изменились.
BlitZ
4

Ключ - это логическая особенность базы данных, тогда как производительность всегда определяется физической реализацией в хранилище и физическими операциями, выполняемыми с этой реализацией. Поэтому ошибочно приписывать характеристики производительности ключам.

Однако в этом конкретном примере две возможные реализации таблиц и запросов сравниваются друг с другом. Пример не отвечает на вопрос, поставленный здесь в заголовке. Проводится сравнение для объединений, использующих два разных типа данных (целое число и символ), использующих только один тип индекса (B-дерево). «Очевидным» моментом является то, что если бы использовался хеш-индекс или индекс другого типа, вполне вероятно, что между двумя реализациями не было бы заметной разницы в производительности. Однако с примером есть более фундаментальные проблемы.

Два запроса сравниваются по производительности, но два запроса не являются логически эквивалентными, поскольку они возвращают разные результаты! Более реалистичный тест сравнил бы два запроса, возвращающие одинаковые результаты, но с использованием разных реализаций.

Важным моментом в отношении суррогатного ключа является то, что он является дополнительным атрибутом в таблице, где таблица также имеет «значимые» ключевые атрибуты, используемые в бизнес-сфере. Это несуррогатные атрибуты, которые представляют интерес для результатов запроса, чтобы быть полезными. Поэтому реалистичный тест сравнил бы таблицы, использующие только натуральные ключи, с альтернативной реализацией, имеющей как натуральные, так и суррогатные ключи в одной и той же таблице. Суррогатные ключи обычно требуют дополнительного хранения и индексации и по определению требуют дополнительных ограничений уникальности. Суррогаты требуют дополнительной обработки для отображения значений внешних естественных ключей на их суррогаты и наоборот.

Теперь сравните этот потенциальный запрос:

A.

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

В его логическом эквиваленте, если атрибут NaturalTable1Key в Таблице 2 заменен суррогатным IDTable1Key:

B.

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

Запрос B требует объединения; Запроса А нет. Это знакомая ситуация в базах данных, которые (чрезмерно) используют суррогаты. Запросы становятся излишне сложными и их намного сложнее оптимизировать. Бизнес-логику (особенно ограничения целостности данных) становится все труднее реализовать, протестировать и проверить.

nvogel
источник