Индексация PK GUID в SQL Server 2012

13

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

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

Итак, мое первое желание состояло в том, чтобы переместить кластеризованный индекс в созданное поле, которое представляет собой bigint представление DateTime. Тем не менее, единственный способ сделать CX уникальным - это включить столбец GUID в этот CX, но сначала упорядочить его.

Это сделает ключ кластеризации слишком широким и увеличит ли производительность записи? Чтения тоже важны, но в данный момент, вероятно, больше интересует запись.

njkroes
источник
1
Как создаются GUID? NEWID или NEWSEQUENTIALID?
swasheck
6
Работа с кластеризованными направляющими
2
Возьмите этих разработчиков на обед и объясните им, что если они снова используют NEWID () в качестве первичного ключа, вы будете обвинять их в низкой производительности. Они очень быстро спросят вас, что делать, чтобы предотвратить это. В этот момент вы говорите использовать IDENTITY (1,1) вместо этого. (возможно, небольшое упрощение, но в 9 раз из 10 это сработает).
Макс Вернон
3
Причиной нашей ненависти к guid является то, что они широкие (16 байт) и, если они не созданы, newsequentialidявляются случайными. Кластерные ключи лучше всего подходят, когда они узкие и увеличиваются. У GUID все наоборот: толстый и случайный. Представьте книжную полку, почти полную книг. Входит OED и из-за случайности направляющих он вставляется в середину полки. Чтобы упорядочить вещи, правая половина книг должна быть переведена в новое место, что требует много времени. Это то, что GUID делает с вашей базой данных и убивает производительность.
billinkc
7
Чтобы решить проблему с использованием уникальных идентификаторов, нужно вернуться к чертежной доске и не использовать уникальные идентификаторы . Они не страшны, если система небольшая, но если у вас есть по крайней мере несколько миллионов таблиц строк (или любая таблица больше этого размера), вы просто потеряете голову, используя уникальные идентификаторы для ключей.
Джон Зигель

Ответы:

20

Основными проблемами с GUID, особенно непоследовательными, являются:

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

Так что это значит для вашей ситуации? Все сводится к вашему дизайну. Если ваша система просто предназначена для записи, и вы не беспокоитесь о получении данных, то подход, изложенный Томасом К., точен. Однако вы должны помнить, что, следуя этой стратегии, вы создаете много потенциальных проблем для чтения этих данных и их хранения. Как отмечает Джон Зигель , вы также будете занимать больше места и, по сути, будете раздувать память.

Основной вопрос, связанный с GUID, - насколько они необходимы. Разработчики любят их, потому что они обеспечивают глобальную уникальность, но это редкий случай, когда такая уникальность необходима. Но учтите, что если ваше максимальное количество значений меньше 2 147 483 647 (максимальное значение 4-байтового целого числа со знаком), то вы, вероятно, не используете соответствующий тип данных для своего ключа. Даже при использовании BIGINT (8 байт) ваше максимальное значение составляет 9 223 372 036 854 775 807. Этого обычно достаточно для любой неглобальной базы данных (и многих глобальных), если вам нужно некоторое автоинкрементное значение для уникального ключа.

Наконец, что касается использования кучи по сравнению с кластерным индексом, если вы просто записываете данные, то куча будет наиболее эффективной, поскольку вы минимизируете издержки на вставки. Однако кучи в SQL Server крайне неэффективны для извлечения данных. Мой опыт показывает, что кластерный индекс всегда желателен, если у вас есть возможность объявить его. Я видел, что добавление кластеризованного индекса в таблицу (4 миллиарда + записей) улучшило общую производительность выбора в 6 раз.

Дополнительная информация:

Майк Фал
источник
13

Нет ничего плохого в GUID в качестве ключей и кластеров в системе OLTP (если только у вас нет МНОГО индексов в таблице, которые страдают от увеличенного размера кластера). На самом деле они гораздо более масштабируемы, чем столбцы IDENTITY.

Широко распространено мнение, что GUID представляют собой большую проблему в SQL Server - во многом это просто неправильно. На самом деле, GUID может быть значительно более масштабируемым на блоках с более чем 8 ядрами:

Извините, но ваши разработчики правы. Беспокойство о других вещах, прежде чем беспокоиться о GUID.

Да, и наконец: зачем вам кластерный индекс? Если вас интересует OLTP-система с множеством небольших индексов, вам, скорее всего, лучше с кучей.

Давайте теперь рассмотрим, что фрагментация (которую вводит GUID) делает с вашими чтениями. Есть три основных проблемы с фрагментацией:

  1. Страница разделяет стоимость дискового ввода-вывода
  2. Половина полных страниц не так эффективна, как полные страницы
  3. Это приводит к тому, что страницы хранятся не по порядку, что снижает вероятность последовательного ввода-вывода

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

Объявление 1) Если вы хотите масштабировать, то можете позволить себе купить I / O. Даже дешевый твердотельный накопитель Samsung / Intel 512 ГБ (по несколько долларов США / ГБ) обеспечит вам более 100 000 операций ввода-вывода в секунду. Вы не будете потреблять это в ближайшее время в системе с 2 сокетами. И если вы столкнетесь с этим, купите еще один, и вы настроены

Объявление 2) Если вы удалите данные из своей таблицы, у вас все равно будет половина полных страниц. И даже если вы этого не сделаете, память дешевая и для всех, кроме самых больших OLTP-систем - горячие данные должны соответствовать им. Желание упаковать больше данных на страницы является субоптимизацией, когда вы ищете масштаб.

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

Что касается объединения, существует два основных типа соединения, которые вы, вероятно, увидите в рабочей нагрузке, подобной OLTP: хэш и цикл. Давайте посмотрим на каждого по очереди:

Хеш-соединение: Хеш-соединение предполагает, что небольшая таблица сканируется, а более крупная обычно ищется. Небольшие таблицы, скорее всего, будут в памяти, поэтому ввод / вывод не является для вас проблемой. Мы уже затронули тот факт, что поиск во фрагментарных индексах имеет такую ​​же стоимость, как и в нефрагментированном индексе.

Соединение петли: внешний стол будет найден. Та же стоимость

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

Теперь у вас могут быть некоторые законные сканирования диапазона (особенно при соединении по внешним ключам), и в этом случае фрагментированные данные менее "упакованы" по сравнению с не фрагментированными данными. Но давайте рассмотрим, какие объединения вы, вероятно, увидите в хорошо проиндексированных данных 3NF:

  1. Объединение из таблицы, которая имеет ссылку внешнего ключа на первичный ключ таблицы, на которую она ссылается

  2. Наоборот

Объявление 1) В этом случае вы идете на один поиск первичного ключа - присоединение n к 1. Фрагментация или нет, та же стоимость (один поиск)

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

Рассмотрим эти внешние ключи на мгновение. Даже если вы «совершенно» последовательно заложили наши первичные ключи - все, что указывает на этот ключ, все равно будет непоследовательным.

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

Томас Кейсер
источник
1
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Пол Уайт 9
5

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

Я особенно согласен с тем, что чистое представление БД ужасно неправильно. Делать ваше приложение медленным и не масштабируемым, чтобы улучшить только производительность БД, может быть весьма ошибочно.

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

Но я склонен использовать альтернативу GUID. Мое личное предпочтение для типа данных здесь - это глобально уникальный BIGINT, генерируемый приложением. Как можно это сделать? В самом тривиальном примере вы добавляете небольшую, ОЧЕНЬ легковесную функцию в ваше приложение для хеширования GUID. Предполагая, что ваша хеш-функция работает быстро и относительно быстро (см. Пример CityHash от Google: http://google-opensource.blogspot.in/2011/04/introduction-cityhash.html - убедитесь, что вы правильно выполнили все шаги компиляции, или FNV1a-вариант http://tools.ietf.org/html/draft-eastlake-fnv-03 для простого кода), что дает вам преимущество как сгенерированных приложением уникальных идентификаторов, так и 64-битного значения ключа, с которым ЦП работают лучше ,

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

Марк Стейси
источник
2
Я предлагаю вам отредактировать свой ответ как ответ на вопрос ОП, а не (как сейчас) как ответ Томаса. Вы все еще можете выделить различия между Томасом (MikeFal's) и вашим предложением.
ypercubeᵀᴹ
2
Пожалуйста, ответьте на свой вопрос. Если вы этого не сделаете, мы удалим его для вас.
JNK
2
Спасибо за комментарии Марк. Когда вы редактируете свой ответ (который, я думаю, обеспечивает очень хороший контекст), я бы изменил одну вещь: IDENTITY не требует дополнительного обхода на сервер, если вы осторожны с INSERT. Вы всегда можете вернуть SCOPE_IDENTITY () в пакете, который вызывает INSERT ..
Томас Кейсер
1
Что касается «это ужасно медленно, так как для создания ключа требуется обратная поездка в БД» - вы можете получить столько, сколько вам нужно, за одну поездку.
AK
Что касается «вы можете получить столько, сколько вам нужно в одном цикле» - вы не можете сделать это со столбцами IDENTITY или любым другим методом, где вы в основном используете DEFAULT на уровне базы данных.
Ави Черри