Унікальність індексу


14

У мене в офісі тривають дискусії з різними розробниками про вартість індексу, і про те, чи є унікальність корисною чи дорогою (напевно, обидва). Суть проблеми - наші конкуруючі ресурси.

Фон

Раніше я читав дискусію, яка заявляє, що Uniqueіндекс не потребує додаткових витрат на підтримання, оскільки Insertоперація неявно перевіряє, куди він вписується у B-дерево, і, якщо дублікат знайдений у не унікальному індексі, додає унікатор до кінець ключа, але в іншому випадку вставляється безпосередньо. У цій послідовності подій, aUnique індекс не має додаткових витрат.

Мій колега бореться з цим твердженням, кажучи, що Uniqueвиконується як друга операція після прагнення до нової позиції в B-дереві, і, таким чином, дорожче підтримувати, ніж не унікальний індекс.

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

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

Питання

Чи має унікальність додаткові витрати на задньому плані Insertпорівняно з витратами на підтримання унікального індексу? По-друге, що не так з додаванням Первинного ключа таблиці до кінця індексу, щоб забезпечити унікальність?

Приклад визначення таблиці

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

Приклад

Приклад того, чому я додав би Uniqueключ до кінця індексу, є в одній із наших таблиць фактів. Існує , Primary Keyщо це Identityстовпець. Однак, Clustered Indexнатомість стовпець схеми розподілу розміщений після чого три зовнішні ключові розміри без унікальності. Вибір продуктивності в цій таблиці є ненормальним, і я часто отримую кращі пошуки часів, використовуючи Primary Keyклавіш пошуку, а не використовуючи Clustered Index. Інші таблиці, які відповідають аналогічному дизайну, але Primary Keyдодані до кінця, мають значно кращі показники.

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

Відповіді:


16

Я часто беру участь у перегляді кодів для команди розробників, і мені потрібно мати можливість дати загальні вказівки щодо їх виконання.

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

Чи має унікальність додаткові витрати на задньому плані Insertпорівняно з витратами на підтримання унікального індексу? По-друге, що не так з додаванням Первинного ключа таблиці до кінця індексу, щоб забезпечити унікальність?

Додавання стовпця первинного ключа до кінця не унікального індексу, щоб зробити його унікальним, я вважаю антидіаграмою. Якщо ділові правила диктують, що дані мають бути унікальними, то додайте до стовпця унікальне обмеження; який автоматично створить унікальний індекс. Якщо ви індексуєте стовпчик для продуктивності , чому б ви додали стовпець до індексу?

Навіть якщо ваше припущення, що застосування унікальності не призводить до додаткових накладних витрат, є правильним (що це не для певних випадків), що ви вирішуєте, зайво ускладнюючи індекс?

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

Як згадував Девід Браун у коментарі:

Оскільки кожен некластеризований індекс зберігається як унікальний індекс, додаткові витрати на вставлення в унікальний індекс не виникають. Насправді, єдині додаткові витрати могли б отримати не оголосити кандидат-ключ унікальним індексом, що призведе до додавання кластеризованих індексних ключів до індексних ключів.

Візьміть наступне мінімально повний і перевірений приклад :

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

Я додаю два індекси, які однакові, за винятком додавання первинного ключа в кінці другого визначення ключових індексів:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

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

Далі ми будемо кілька рядків до таблиці:

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

Як ви бачите вище, три рядки містять однакове значення для rowDate стовпця, а два рядки - унікальні значення.

Далі ми розглянемо фізичні структури сторінок для кожного індексу, використовуючи незадокументовану DBCC PAGEкоманду:

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

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

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

Ви можете сказати вище, щоб означати, що включення первинного ключа у кожен індекс та визначення як унікального є доброю річчю ™, оскільки саме так і відбувається під кришками. Я б не робив цього припущення, і пропоную лише визначити індекс як унікальний, якщо насправді природні дані в індексі вже є унікальними.

В Інтервебзі на цю тему є кілька чудових ресурсів, зокрема:

БЮР, сама наявність identityстовпчика не гарантує унікальності. Вам потрібно визначити стовпчик як первинний ключ або з унікальним обмеженням, щоб переконатися, що значення, збережені в цьому стовпці, є насправді унікальними. SET IDENTITY_INSERT schema.table ON;Заява дозволить вам вставити не-унікальні значення в стовпець визначається як identity.


5

Просто доповнення до відмінної відповіді Макса .

Якщо справа доходить до створення унікального кластерного індексу, SQL Server все одно створює щось, що називається Uniquifierу фоновому режимі.

Це Uniquifierможе спричинити можливі проблеми в майбутньому, якщо на вашій платформі буде багато операцій CRUD, оскільки це Uniquifierлише 4 байти (основне 32-бітове ціле число). Отже, якщо у вашій системі багато операцій із CRUD, можливо, ви використаєте всі наявні унікальні номери, і раптом ви отримаєте помилку, і це не дозволить вам більше вставляти дані у свої таблиці (адже це буде більше не мають унікальних значень для присвоєння вашим щойно вставленим рядкам).

Коли це станеться, ви отримаєте цю помилку:

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.

Помилка 666 (вищезгадана помилка) виникає, коли uniquifierдля одного набору унікальних ключів витрачається більше 2,147,483,647 рядків.

Отже, вам потрібно буде мати ~ 2 мільярди рядків для одного значення ключа, або вам потрібно буде змінити значення одного ключа ~ 2 мільярди разів, щоб побачити цю помилку. Таким чином, не надто ймовірно, що ви зіткнетеся з цим обмеженням.


Я поняття не мав, що прихований уніфікатор може вичерпатися з ключового простору, але, мабуть, у деяких випадках все обмежене. Так само, як обмеження Caseта Ifструктури на 10 рівнях, є сенс, що існує також обмеження у вирішенні не унікальних сутностей. За вашим твердженням, це здається, що це стосується лише випадків, коли ключ кластеризації є унікальним. Це проблема для Nonclustered Indexклавіша кластеризації чи тоді клавіша кластеризації Uniqueне існує проблеми з Nonclusteredіндексами?
Солонотікс

Унікальний індекс (наскільки я знаю) обмежений розміром типу стовпця (тому, якщо це тип BIGINT, у вас є 8 байт для роботи). Крім того, згідно з офіційною документацією мікрософт, для кластеризованого індексу дозволено максимум 900 байт і 1700 байт для некластеризованих (оскільки у вас може бути більше одного некластеризованого індексу і лише 1 кластерний індекс на таблицю). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain

1
@Solonotix - у некластеризованих індексах використовується однозначний показник із кластерного індексу . Якщо ви запускаєте код у моєму прикладі без первинного ключа (замість цього створіть кластерний індекс), ви можете побачити, що вихід є однаковим як для не унікальних, так і для унікальних індексів.
Макс Вернон

-2

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

  1. dt datetime не має нульового значення (current_timestamp). Datetime - це більш стара форма, і ви можете досягти хоча б деякої економії місця за допомогою datetime2 () та sysdatetime ().
  2. створити індекс [nonunique_nonclustered_example] на #test_index (is_deleted) include (val). Це мене турбує. Погляньте, як отримати доступ до даних (я маю на увазі, що їх більше WHERE is_deleted = 0), і подивіться на використання відфільтрованого індексу. Я б навіть подумав про використання 2 відфільтрованих індексів, один для, where is_deleted = 0а другий дляwhere is_deleted = 1

По суті це більше нагадує вправу кодування, розроблену для перевірки гіпотези, а не реальної проблеми / рішення, але ці два зразки, безумовно, те, що я шукаю в оглядах коду.


Найбільше, що ви заощадите, використовуючи datetime2 замість datetime, - це 1 байт, і це якщо ваша точність менше 3, що означатиме втрату точності на дробові секунди, що не завжди є життєздатним рішенням. Що стосується наведеного прикладного індексу, дизайн залишався простим, щоб зосередитись на моєму питанні. NonclusteredІндекс матиме ключ кластерного додається до кінця рядка даних для ключових операцій пошуку внутрішньо. Як такі, два індекси фізично однакові, що було суть мого питання.
Солонотікс

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

-4

Схоже, просто використовуйте ПК, щоб зробити інший менший індекс. Отже, продуктивність на ньому швидша.

Це ви бачите в компаніях, які мають масивні таблиці даних (наприклад: основні таблиці даних). Хтось вирішує мати один масивний кластерний індекс на ньому, очікуючи, що він задовольнить потреби різних груп звітів.

Але одній групі може знадобитися лише декілька частин цього індексу, тоді як іншій потрібні інші частини .. тому індекс просто ляпає в кожен стовпець під сонцем для «оптимізації продуктивності» насправді не допомагає.

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

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

Отже, просто зробіть аналіз і з’ясуйте, чи можете ви взяти єдиний кластерний індекс і розбити його на менші, цільові індекси, які потрібні конкретним робочим місцям.

Тоді вам доведеться проаналізувати показники ефективності з точки зору "єдиного індексу проти кількох індексів", оскільки є великі витрати на складання та оновлення індексів. Але вам доведеться проаналізувати це із загальної точки зору.

EG: це може бути менш ресурсомістким до одного масивного кластерного індексу, і більш ресурсомістким, щоб мати кілька менших цільових індексів. Але якщо ви потім зможете запускати цільові запити на задній план набагато швидше, економлячи час (і гроші) там, можливо, це варто того.

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

Я просто відчуваю, що ви неправильно використовуєте ідентифікатор ПК. Але, можливо, ви використовуєте систему баз даних, яка дозволяє лише 1 індекс (?), Але ви можете проникнути інший, якщо ви PK (b / c кожна реляційна система баз даних в ці дні, здається, автоматично індексує ПК). Однак, більшість сучасних RDBMS 'повинні дозволити створення декількох індексів; не повинно бути обмеження кількості індексів, які ви можете внести (на відміну від ліміту в 1 ПК).

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

Це не означає, що вашій таблиці не потрібна ПК. 101-й БД SOP говорить, що "кожна таблиця повинна мати ПК". Але в ситуації зберігання даних або подібних .. наявність ПК на столі може просто бути додатковими накладними витратами, які вам не потрібні. Або це може бути надіслане богом, щоб переконатися, що ви не подвійно додаєте записи дупу Це справді питання того, що ти робиш і чому ти це робиш.

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

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