Чи забезпечують природні ключі більш високу чи нижчу продуктивність у 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, і тому більше я / O потрібно, що вплине на час обробки. Однак, натуральне ціле число (можливо, навіть перевірте цифру) було б майже таким же INT, яке ми думаємо використовувати для сурогатного стовпця Ідентичності. Отже, "природний ключ", можливо, INT, BIGINT, CHAR, NVARCHAR і все, що має значення.
RLF

7
Я думаю, що підвищення продуктивності @ MikeSherrill'Catcall 'досягає того, що вам насправді не потрібно з'єднання проти таблиці "пошуку", коли ви використовуєте природний ключ. Порівняйте запит, щоб отримати значення пошуку з приєднанням, із запитом, де значення вже зберігається в головній таблиці. Ви можете отримати іншого "переможця" залежно від довжини природного ключа та кількості рядків у таблиці пошуку.
Мікаель Ерікссон

3
Що сказав @MikaelEriksson, а також випадки, коли ви маєте з'єднання між більш ніж двома таблицями (скажімо, 4), де із сурогатами вам доведеться приєднуватися до таблиць від A до D до B і C, тоді як за допомогою природних ключів ви можете приєднатись від A до D безпосередньо
ypercubeᵀᴹ

Відповіді:


18

Загалом SQL Server використовує B + Дерева для індексів. Витрати на пошук індексу безпосередньо пов'язані з довжиною ключа в цьому форматі зберігання. Отже, сурогатний ключ, як правило, перевершує природний ключ при пошуку індексів.

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, можуть бути вимкнено, оскільки ключові значення не змінюються." Ефект сурогатних ключів полягає в тому, щоб кожен посилання на закордонний ключ був еквівалентний "НА ОНОВЛЕННЯ КАСКАДИ". Ключ не змінюється, але значення, яке він представляє, робить .
Майк Шеррілл 'Відкликання котів'

@ MikeSherrill'Catcall 'Так, звичайно. Однак ON UPDATE CASCADEне використовується, тоді як ключі ніколи не оновлювалися. Але, якщо вони є, то, якщо вони ON UPDATE NO ACTIONналаштовані , це може бути проблемою . Я маю на увазі, що СУБД ніколи його не використовує, тоді як значення стовпців ключових значень не змінюються.
BlitZ

4

Ключовою є логічна особливість бази даних, тоді як продуктивність завжди визначається фізичною реалізацією в сховищі та фізичними операціями, що протидіють цій реалізації. Тому помилково приписувати характеристики продуктивності клавішам.

У цьому конкретному прикладі порівняно один з одним дві можливі реалізації таблиць та запитів. Приклад не дає відповіді на питання, поставлене тут у назві. Проведене порівняння полягає в об'єднанні з використанням двох різних типів даних (ціле число та символ), використовуючи лише один тип індексу (B-дерево). "Очевидним" моментом є те, що якби був використаний хеш-індекс або інший тип індексу, можливо, не було б виміряної різниці в продуктивності між двома реалізаціями. Однак з прикладом є більш фундаментальні проблеми.

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

Важливим моментом сурогатного ключа є те, що він є додатковим атрибутом у таблиці, де в таблиці також є "значущі" ключові атрибути, що використовуються в бізнес-сфері. Саме атрибути, що не сурогатні, є цікавими для того, щоб результати запиту були корисними. Тому реалістичний тест порівнював таблиці з використанням лише природних ключів з альтернативною реалізацією, що мають в одній таблиці як природні, так і сурогатні ключі. Сурогатні ключі зазвичай потребують додаткового зберігання та індексації, а за визначенням потребують додаткових обмежень унікальності. Сурогати потребують додаткової обробки для відображення зовнішніх природних ключових значень на їх сурогати і навпаки.

Тепер порівняйте цей потенційний запит:

A.

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

До його логічного еквівалента, якщо атрибут NaturalTable1Key у Таблиці2 замінено на сурогат IDTable1Key:

Б.

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

Запит B вимагає приєднання; Запит A ні. Це звична ситуація в базах даних, які (над) використовують сурогати. Запити стають зайвими складними і їх значно важче оптимізувати. Бізнес-логіку (особливо обмеження цілісності даних) стає складніше впроваджувати, перевіряти та перевіряти.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.