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

У нас есть приложение, которое хранит данные в базе данных SQL-сервера. (В настоящее время мы поддерживаем SQL Server 2005 и выше). В нашей БД более 400 таблиц. Структура базы данных не идеальна. Самая большая проблема заключается в том, что у нас есть много таблиц с GUID (NEWID()) в качестве первичных CLUSTERED ключей. Когда я спросил нашего главного архитектора базы данных «почему?», он ответил: «это из-за репликации». Наша БД должна поддерживать репликацию транзакций. Изначально все первичные ключи были INT IDENTITY(1,1) CLUSTERED. Но позже, когда дело дошло до поддержки репликации, эти поля были заменены на UNIQUEIDENTIFIER DEFAULT NEWID(). Он сказал, что «иначе было бы кошмарно иметь дело с репликацией». NEWSEQUENTIALID() в то время не поддерживался SQL 7/2000. Итак, теперь у нас есть таблицы со следующей структурой:

CREATE TABLE Table1(
        Table1_PID uniqueidentifier DEFAULT NEWID() NOT NULL,
        Field1 varchar(50) NULL,
        FieldN varchar(50) NULL,
        CONSTRAINT PK_Table1 PRIMARY KEY CLUSTERED (Table1_PID)
    )
    GO

CREATE TABLE Table2(
    Table2_PID uniqueidentifier DEFAULT NEWID() NOT NULL,
    Table1_PID uniqueidentifier NULL,
    Field1 varchar(50) NULL,
    FieldN varchar(50) NULL,
    CONSTRAINT PK_Table2 PRIMARY KEY CLUSTERED (Table2_PID),
    CONSTRAINT FK_Table2_Table1 FOREIGN KEY (Table1_PID) REFERENCES Table1 (Table1_PID)
)
GO

Все таблицы на самом деле имеют очень много полей (до 35) и до 15 некластеризованных индексов.

Я знаю, что GUID, который не является последовательным, например тот, значения которого сгенерированы в клиенте (с использованием .NET) ИЛИ сгенерированы функцией NEWID() SQL (как в нашем случае), является ужасно плохим выбором для кластерного индекса для две причины:

  1. фрагментация
  2. размер

Я также знаю, что ХОРОШИЙ ключ кластеризации заключается в том, что он:

  1. уникальный,
  2. узкий,
  3. статический,
  4. постоянно увеличивающийся,
  5. необнуляемый,
  6. и фиксированная ширина

Дополнительные сведения о причинах этого см. в следующем замечательном видео: http://technet.microsoft.com/en-us/sqlserver/gg508879.aspx.

Итак, INT IDENTITY действительно лучший выбор. BIGINT IDENTITY также хорош, но обычно INT с 2+ миллиардами строк должно быть достаточно для подавляющего большинства таблиц.

Когда наши клиенты начали страдать от фрагментации, было принято решение сделать первичные ключи НЕкластерными. В результате эти таблицы остались без кластеризованного индекса. Другими словами, эти таблицы были превращены в HEAPS. Лично мне это решение не нравится, потому что я уверен, что таблицы кучи не являются частью хорошей структуры базы данных. Пожалуйста, ознакомьтесь с этой статьей с рекомендациями по использованию SQL Server: http://technet.microsoft.com/en-us/library/cc917672.aspx.

В настоящее время мы рассматриваем два варианта улучшения структуры базы данных:

Первый вариант — заменить DEFAULT NEWID() на DEFAULT NEWSEQUENTIALID() для первичного кластерного ключа:

CREATE TABLE Table1_GUID (
  Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
  Field1 varchar(50) NULL,
  FieldN varchar(50) NULL,
  CONSTRAINT PK_Table1 PRIMARY KEY CLUSTERED (Table1_PID)
)
GO

Второй вариант — добавить столбец INT IDENTITY в каждую таблицу и сделать его индексом CLUSTERED UNIQUE, оставив первичный ключ НЕ кластеризованным. Таким образом, Table1 будет выглядеть так:

CREATE TABLE Table1_INT (
  Table1_ID int IDENTITY(1,1) NOT NULL,
  Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
  Field1 varchar(50) NULL,
  FieldN varchar(50) NULL,
  CONSTRAINT PK_Table1 PRIMARY KEY NONCLUSTERED (Table1_PID),
  CONSTRAINT UK_Table1 UNIQUE CLUSTERED (Table1_ID)
)
GO

Table1_PID будет использоваться для репликации (поэтому мы оставили его как PK), а Table1_ID вообще не будет реплицироваться.

Короче говоря, после того, как мы провели тесты, чтобы увидеть, какой подход лучше, мы обнаружили, что оба решения не очень хороши:

Первый подход (Table1_GUID) выявил следующие недостатки: хотя последовательные GUID определенно намного лучше, чем обычные случайные GUID, они все же в четыре раза больше, чем INT (16 против 4 байтов), и это фактор в нашем случае, потому что у нас много строк в наших таблицах (до 60 миллионов) и много некластеризованных индексов в этих таблицах (до 15). Ключ кластеризации добавляется к каждому некластеризованному индексу, что значительно увеличивает негативный эффект от размера 16 байтов вместо 4. Больше байтов означает больше страниц на диске и в оперативной памяти SQL Server и, следовательно, больше дискового ввода-вывода и больше работы для SQL Server.

Точнее, после того, как я вставил 25 миллионов строк реальных данных в каждую таблицу, а затем создал 15 некластеризованных индексов для каждой таблицы, я увидел большую разницу в пространстве, используемом таблицами:

EXEC sp_spaceused 'Table1_GUID' -- 14.85 GB
EXEC sp_spaceused 'Table1_INT' -- 11.68 GB

Кроме того, тест показал, что операции INSERT в Table1_GUID выполняются немного медленнее, чем в Table1_INT.

Второй подход (Table1_INT) показал, что в большинстве запросов (SELECT) соединение двух таблиц по плану выполнения Table1_INT.Table1_PID = Table2_INT.Table1_PID стало хуже из-за появления дополнительного оператора Key Lookup.

Теперь вопрос: Я считаю, что для нашей проблемы должно быть лучшее решение. Если бы вы могли порекомендовать мне что-то или указать мне на хороший ресурс, я был бы очень признателен. Заранее спасибо.

Обновлено:

Позвольте мне привести вам пример оператора SELECT, в котором появляется дополнительный оператор Key Lookup:

--Create 2 tables with int IDENTITY(1,1) as CLUSTERED KEY.
--These tables have one-to-many relationship.
CREATE TABLE Table1_INT (
    Table1_ID int IDENTITY(1,1) NOT NULL,
    Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
    Field1 varchar(50) NULL,
    FieldN varchar(50) NULL,
    CONSTRAINT PK_Table1_INT PRIMARY KEY NONCLUSTERED (Table1_PID),
    CONSTRAINT UK_Table1_INT UNIQUE CLUSTERED (Table1_ID)
)
GO

CREATE TABLE Table2_INT(
    Table2_ID int IDENTITY(1,1) NOT NULL,
    Table2_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
    Table1_PID uniqueidentifier NULL,
    Field1 varchar(50) NULL,
    FieldN varchar(50) NULL,
    CONSTRAINT PK_Table2_INT PRIMARY KEY NONCLUSTERED (Table2_PID),
    CONSTRAINT UK_Table2_INT UNIQUE CLUSTERED (Table2_ID),
    CONSTRAINT FK_Table2_Table1_INT FOREIGN KEY (Table1_PID) REFERENCES Table1_INT (Table1_PID)
)
GO

И создайте две другие таблицы для сравнения:

--Create the same 2 tables, BUT with uniqueidentifier NEWSEQUENTIALID() as CLUSTERED KEY.
CREATE TABLE Table1_GUID (
    Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
    Field1 varchar(50) NULL,
    FieldN varchar(50) NULL,
    CONSTRAINT PK_Table1_GUID PRIMARY KEY CLUSTERED (Table1_PID),
)
GO

CREATE TABLE Table2_GUID(
    Table2_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
    Table1_PID uniqueidentifier NULL,
    Field1 varchar(50) NULL,
    FieldN varchar(50) NULL,
    CONSTRAINT PK_Table2_GUID PRIMARY KEY CLUSTERED (Table2_PID),
    CONSTRAINT FK_Table2_Table1_GUID FOREIGN KEY (Table1_PID) REFERENCES Table1_GUID (Table1_PID)
)
GO

Теперь запустите следующие операторы select и посмотрите на план выполнения для сравнения:

SELECT T1.Field1, T2.FieldN
FROM Table1_INT T1 
    INNER JOIN Table2_INT T2 
        ON T1.Table1_PID = T2.Table1_PID;

SELECT T1.Field1, T2.FieldN
FROM Table1_GUID T1 
    INNER JOIN Table2_GUID T2 
        ON T1.Table1_PID = T2.Table1_PID;

План выполнения


person Alex    schedule 18.05.2013    source источник
comment
Какова ваша топология replication? Один издатель - один подписчик? Много подписчиков? Много издателей? Подписчики когда-нибудь обновляют данные? Распространяются ли обновления подписчиков обратно издателю?   -  person Remus Rusanu    schedule 18.05.2013
comment
Что ж, топология репликации непростая. Как я уже упоминал выше, у нас много столов. Эти таблицы разбиты, скажем, на 2 группы. Одна группа публикуется на основном сервере (Publisher) и имеет несколько подписчиков (локальные серверы с подпиской по запросу). Вторая группа таблиц публикуется на локальных серверах и имеет одного подписчика (push подписки) — основного сервера. Извините, я должен разделить свое сообщение на несколько из-за ограничения длины комментариев... (см. мой следующий комментарий).   -  person Alex    schedule 18.05.2013
comment
Вторая группа, вероятно, требует дополнительных пояснений. Подмножество таблиц из второй группы реплицируется с одного локального сервера на другой локальный сервер. НО у нас нет прямой репликации между локальными серверами. Вместо этого данные сначала реплицируются с одного локального сервера на главный сервер, а затем триггер на основном сервере вставляет данные в другую таблицу, которая публикуется на этом основном сервере и реплицируется на другой локальный сервер. В триггере и хранимых процедурах, используемых для репликации, есть специальная логика, которая гарантирует, что данные идут в правильном направлении. Продолжение следует...   -  person Alex    schedule 18.05.2013
comment
Что касается обновлений, данные могут быть изменены (вставлены, обновлены) как на издателях, так и на подписчиках. Насколько мне известно, обновления подписчиков никогда не распространяются обратно на издателей.   -  person Alex    schedule 18.05.2013
comment
Похоже, GUID оправданы...   -  person Remus Rusanu    schedule 18.05.2013


Ответы (1)


Я лично использую INT IDENTITY для большинства своих первичных и кластерных ключей.

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

Ключ кластеризации в SQL Server представляет собой физическую конструкцию, которая используется для физического упорядочения данных, и его гораздо сложнее правильно подобрать. Как правило, Кимберли Трипп, королева индексирования в SQL Server, также требует, чтобы хороший ключ кластеризации был уникальным, стабильным, как можно более узким и, в идеале, постоянно увеличивающимся (что и является INT IDENTITY).

См. ее статьи об индексации здесь:

а также см. статью Джимми Нильссона Стоимость использования GUID в качестве первичного ключа.

GUID — действительно плохой выбор для ключа кластеризации, поскольку он широкий, полностью случайный и, таким образом, приводит к плохой фрагментации индекса и низкой производительности. Кроме того, ключевая строка (строки) кластеризации также хранится в каждой записи каждого некластеризованного (дополнительного) индекса, поэтому вы действительно хотите, чтобы он был небольшим - GUID составляет 16 байтов, а INT - 4 байта, и с несколькими некластеризованными индексами и несколькими миллионами строк это имеет ОГРОМНОЕ значение.

В SQL Server ваш первичный ключ по умолчанию является ключом кластеризации, но это не обязательно. Вы можете легко использовать GUID в качестве НЕкластеризованного первичного ключа, а INT IDENTITY в качестве ключа кластеризации — просто нужно немного знать об этом.

person marc_s    schedule 18.05.2013
comment
Я полностью согласен с вами и К. Триппом в том, что INT IDENTITY — лучший кандидат на роль индекса UNIQUE CLUSTERED. И я хотел бы добавить еще две ссылки в вашу коллекцию: sqlskills.com/BLOGS/PAUL/post/ sqlserverperformance.wordpress.com/2010/03/22/ Однако в моем случае Ситуация осложняется тем, что таблицы должны быть реплицированы. И если я применю второй подход (Table1_INT), производительность операторов SELECT ухудшится, что меня беспокоит. - person Alex; 19.05.2013