Индекс уникальности накладных расходов

14

В моем офисе ведутся постоянные дебаты о стоимости индекса, а также о том, является ли уникальность выгодной или дорогой (вероятно, и то и другое). Суть проблемы - наши конкурирующие ресурсы.

Фон

Ранее я читал обсуждение, в котором говорилось, что Uniqueиндекс не требует дополнительных затрат на поддержание, поскольку Insertоперация неявно проверяет, подходит ли она к B-дереву, и, если дубликат найден в неуникальном индексе, добавляет к ним уникальный код конец ключа, но в противном случае вставляется напрямую. В этой последовательности событий Uniqueиндекс не имеет дополнительных затрат.

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

В худшем случае я видел таблицы со столбцом идентификаторов (изначально уникальным), который является ключом кластеризации таблицы, но явно указан как неуникальный. С другой стороны, хуже всего - моя одержимость уникальностью, и все индексы создаются как уникальные, и когда невозможно определить явно уникальную связь с индексом, я добавляю PK таблицы в конец индекса, чтобы обеспечить Уникальность гарантирована.

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

Вопрос

Есть ли у уникальности дополнительные расходы на бэкэнд по 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
Solonotix
источник

Ответы:

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

Я посмотрел на вывод с использованием Beyond Compare, и за исключением очевидных различий в идентификаторах страниц размещения и т. Д., Две структуры индекса идентичны.

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

Вы можете принять вышесказанное, чтобы обозначить, что включение первичного ключа в каждый индекс и определение его как уникального - это A Good Thing ™, поскольку это все равно происходит под прикрытием. Я бы не делал этого предположения и предложил бы определять индекс как уникальный, если на самом деле естественные данные в индексе уже уникальны.

В Interwebz есть несколько отличных ресурсов на эту тему, в том числе:

К вашему сведению, простое присутствие identityстолбца не гарантирует уникальность. Вам нужно определить столбец как первичный ключ или с уникальным ограничением, чтобы гарантировать, что значения, хранящиеся в этом столбце, на самом деле уникальны. Оператор SET IDENTITY_INSERT schema.table ON;позволит вам вставить неуникальные значения в столбец, определенный как identity.

Макс Вернон
источник
5

Просто дополнение к отличному ответу Макса .

Когда дело доходит до создания неуникального кластерного индекса, SQL Server Uniquifierв любом случае создает нечто, называемое a в фоновом режиме.

Это 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 миллиарда раз, чтобы увидеть эту ошибку. Таким образом, вряд ли вы столкнетесь с этим ограничением.

Chessbrain
источник
Я понятия не имел, что скрытый юниквитатор может исчерпать пространство ключа, но я думаю, что в некоторых случаях все ограничено. Подобно тому, как Caseи как Ifструктуры ограничены 10 уровнями, имеет смысл, что есть также ограничение для разрешения неуникальных объектов. По вашему утверждению, это звучит так, как будто это относится только к случаям, когда ключ кластеризации не является уникальным. Это проблема для Nonclustered Indexили, если ключ кластеризации, Uniqueто нет проблемы для Nonclusteredиндексов?
Solonotix
Индекс уникальности (насколько я знаю) ограничен размером типа столбца (поэтому, если это тип BIGINT, у вас есть 8 байтов для работы). Кроме того, согласно официальной документации Microsoft, для кластеризованного индекса допустимо не более 900 байт, а для некластеризованного - 1700 байт (поскольку вы можете иметь более одного некластеризованного индекса и только 1 кластеризованный индекс на таблицу). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain
1
@Solonotix - унификатор из кластерного индекса используется . Если вы запустите код в моем примере без первичного ключа (вместо этого создайте кластерный индекс), вы увидите, что выходные данные одинаковы как для неуникальных, так и уникальных индексов.
Макс Вернон
-2

Я не собираюсь вдаваться в вопрос о том, должен ли индекс быть уникальным или нет, и есть ли дополнительные издержки в этом подходе или в этом. Но пара вещей беспокоила меня в вашем общем дизайне

  1. дата не время по умолчанию (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

Похоже, вы просто используете PK для создания альтернативного, меньшего индекса. Следовательно, производительность на нем быстрее.

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

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

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

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

Итак, просто проведите анализ и выясните, можете ли вы взять один кластеризованный индекс и разбить его на более мелкие, целевые индексы, необходимые для конкретных заданий.

Тогда вам придется анализировать производительность с точки зрения «один индекс против нескольких индексов», поскольку при создании и обновлении индексов возникают накладные расходы. Но вы должны проанализировать это с общей точки зрения.

Например: он может быть менее ресурсоемким для одного массивного кластерного индекса и более ресурсоемким, чтобы иметь несколько меньших целевых индексов. Но если вы тогда сможете выполнять целевые запросы на внутреннем сервере гораздо быстрее, экономя время (и деньги), это может стоить того.

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

Я просто чувствую, что вы неправильно используете идентификатор PK. Но вы можете использовать систему баз данных, которая допускает только 1 индекс (?), Но вы можете использовать другую, если вы используете PK (в наши дни каждая реляционная база данных автоматически индексирует PK). Однако большинство современных РСУБД должны допускать создание нескольких индексов; не должно быть никаких ограничений на количество индексов, которые вы можете создать (в отличие от ограничения в 1 PK).

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

Это не значит, что вашему столу не нужен PK. В SOP DB 101 говорится, что «в каждой таблице должен быть PK». Но в ситуации с хранилищем данных или тому подобном ... наличие PK на столе может быть просто дополнительными издержками, которые вам не нужны. Или это может быть просто посыл, чтобы убедиться, что вы не добавляете двойные записи дважды. Это действительно вопрос того, что ты делаешь и почему ты это делаешь.

Но массивные таблицы определенно выигрывают от наличия индексов. Но если предположить, что один массивный кластеризованный индекс будет наилучшим, это просто ... он может быть БЕЗОПАСНЫМ ... но я бы порекомендовал протестировать тестовую среду, разбив ее на несколько меньших индексов, нацеленных на конкретные сценарии использования.

blahblah
источник