Pull to refresh

Comments 72

Использование генерации GUID на стороне базы данных в разы медленнее, чем генерации на стороне клиента. Скорее всего, это связано с затратами на чтение только что добавленного идентификатора.

Не очень ясно почему, неужели именно указанная причина будет оказывать такое влияние?
Планирую разобраться с этим более детально и добавить результаты исследования в статью.
прочел статью, и нигде не нашел упоминания негативного эффекта использования случайной строки (GUID) в качестве primary key на индексацию таблицы. Primary key это же clustered index, те добавляя такую запись мы вынуждаем сиквел сервер копаться в таблице, чтобы разместить новую запись в правильное место. Для табличек размером в тысячи записей — плевать, а для милионов?
Во-первых, primary key — не обязательно кластерный. А во-вторых, именно для решения этой проблемы и берут псевдопоследовательные гуиды.
Странно, статья как раз в том числе и об этом. Вот эти абзацы например:

Привычная генерация уникальных идентификаторов в том же .NET через Guid.NewGuid() дает множество значений, не связанных друг с другом никакой закономерностью. Если ряд GUID-ов, полученных из этой функции, держать в отсортированном списке, то каждое новое добавляемое значение может «попадать» в любую его часть.


и далее по тексту. Потом:

Над последним пунктом стоит задуматься. Из-за чего может происходить замедление работы при использовании непоследовательных GUID-ов, кроме как частого разделения страниц? Скорее всего — из-за частого чтения «случайных» страниц с диска.


и далее
Добавил информацию в конце статьи о том, почему получается такая разница
Я конечно понимаю, что шанс сгенерировать два одинаковых GUID'a очень мал, но что будет если это все-таки произойдет? Отловить ошибку и попытаться вставить с новым GUID'ом? А если и тогда сгенерирует существующий?
В общем случае — повторить попытку. Для пользовательской транзакции это может выражаться в попытке сохранить изменения еще раз, для обработчика сообщений из очереди — сделать автоматический retry несколько раз, прежде чем поместить сообщение в очередь «проблемных» и т.д. Вероятность того, что GUID генератор подряд даст два одинаковых значения практически равна 0. Мне на практике не встречалось ни одного случая коллизий у GUID
Ну тут следует учесть что шанс этот напрямую зависит от кол-ва записей в базе. Если у вас в базе уже есть несколько сотен миллионов GUID'ов, то шанс нарваться на коллизию значительно выше чем в случае «сгенерировать два идентичных ключа». Интересно кстати было бы посчитать вероятности
Если очень быстро генерить GUID, то можно получить один и тот же два раза подряд.
В моей практике был очень неприятный случай.
Диск был SSD-шный, а данных на запись мало.
Вылетело необработанное исключение, не предусмотренное логикой программы.
Так как приложение было «почти enterprise» уровня, на орехи досталось всем.
Поставили костыль, обработку исключения и попытку получить новый GUID, после трех не успешных попыток добавлять интервал Sleep(100) между следующими тремя попытками. И если и это не помогло, то корректно откатывать транзакцию и показывать пользователю фигу.
Кроме всего прочего noname-сетевые платы могут содержать один и тот же MAC-адрес на всю партию, а он используется при генерации GUID.
В общем, ни в коем случае не стоит расслабляться.
Если очень быстро генерить GUID, то можно получить один и тот же два раза подряд.

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

Кроме всего прочего noname-сетевые платы могут содержать один и тот же MAC-адрес на всю партию, а он используется при генерации GUID.

У вас генератор первой версии, что ли, был? MAC-и (и время) использовались только в первой версии.
Да скорее всего первый.
В то время про версии GUID-а мало кто что слышал, около 2003-2004 годов.
Сейчас то конечно с уникальностью уже получше и от сетевых карт отказались.
Так может не стоит это переносить на существующие реалии?
Переносить наверно не стоит, но обжегшийся на молоке на воду дует.
Я же на всякий случай try… catch… буду ставить. Так спокойней спится.
А да диск был не ссд-шный, там рам-диск был, чего это с памятью то моей…
Проблема, понимаете ли, в том, что в ситуации, когда вам действительно нужен сгенеренный на клиенте GUID, try/catch вам не поможет, потому что данные уже давно улетели. Вам надо либо всю архитектуру строить исходя из того, что уникальные идентификаторы генерятся рядом с базой (что в определенных случаях просто существенно дороже), либо… доверять гуидам.
Далее с символьного сервера Microsoft при
помощи WinDBG вытянул rpcrt4.dbg и rpcrt4.pdb. Скормив их IDA Pro, получил картину, поразившую меня.
Ниже схематично представлен алгоритм, как я его понял — от вершины и до функций нижнего уровня.

Заголовок спойлера
RPC_STATUS __stdcall UuidCreate(UUID *Uuid){

rc4_safe_select(x,x,x);

NewGenRandom(x,x,x,x);

rc4_safe_key(x,x,x,x);

}

__stdcall NewGenRandom(x,x,x,x)

{

NewGenRandomEx(x,x,x);

}

__stdcall NewGenRandomEx(x,x,x){

InitRand(x,x);

InitializeRNG(x);

GenRandom(x,x,x);

}

__stdcall InitRand(x,x){

InitCircularHash(x,x,x,x);

ReadSeed(x,x);

}

int __stdcall ReadSeed(LPBYTE lpData,DWORD cbData){

AccessSeed(x,x);

RegQueryValueEx(«SOFTWARE\Microsoft\Cryptography\RNG\Seed»);

}

int __stdcall AccessSeed(REGSAM samDesired,DWORD dwDisposition){

RegCreateKeyEx(«SOFTWARE\Microsoft\Cryptography\RNG\Seed»);

}

__stdcall InitializeRNG(x){

rc4_safe_startup(x);

InterlockedCompareExchangePointerWin95(x,x,x);

}

__stdcall InterlockedCompareExchangePointerWin95(x,x,x){

InterlockedCompareExchange();

}

__stdcall GenRandom(x,x,x){

RandomFillBuffer(x,x);

}

__stdcall RandomFillBuffer(x,x){

UpdateCircularHash(x,x,x);

rc4_safe_select(x,x,x);

GetCircularHashValue(x,x,x);

GatherRandomKey(x,x,x,x);

rc4_safe_key(x,x,x,x);

rc4_safe(x,x,x,x);

}

__stdcall UpdateCircularHash(x,x,x){

MD4Init(x);

MD4Update(x,x,x);

MD4Final(x);

}

int __stdcall GatherRandomKey(LPVOID lpInBuffer,DWORD nInBufferSize,LPVOID lpOutBuffer,LPDWORD lpBytesReturned){

GatherRandomKeyFastUserMode(x,x,x,x);

}

int __stdcall GatherRandomKeyFastUserMode(LPVOID lpInBuffer,DWORD nInBufferSize,LPVOID lpOutBuffer,LPDWORD lpBytesReturned){

IsRNGWinNT();

NtOpenFile("\\Device\\KsecDD");

InterlockedCompareExchangePointerWin95(x,x,x);

}


Таким образом, основа — это значение из реестра SOFTWARE\Microsoft\Cryptography\RNG\Seed, хэш MD4 и потоковый шифр RC4.

NtOpenFile("\\Device\\KsecDD");

обращение к драйверу KsecDD — и есть запрос hardware конфигурации.

именно этот драйвер вызывается при создании отчета «синий экран смерти» и при создании журнала аудита.

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

--In response, DevCon displays the device instance IDs of the USB devices

— The command specifies the Net class and then refines the search by specifying devices in the class whose hardware ID or compatible ID include «ndiswan.»

--In response, DevCon displays the device instance ID of the device and reports the result.

Я на работе у коллег слышал о случае совпадения гуидов в .NET-приложении лет шесть назад.
Но это так, к слову… Руководствоваться следует фактами, а не рассказами людей из интернетов :)
Стандартный механизм генерации GUID какой-то уникальный номер или MAC (детали не знаю) от сетевой карты. Поэтому в природных условиях сгенерировать одинаковые гуиды можно только путем целенаправленного шаманства.
Или путем покупки ноунейм сетевых, где один мак может быть на всю партию.
Как уже говорилось, не используйте генератор гуидов первой версии.
Вроде MS SQL Server гарантирует, что на стороне сервера генерируются именно что уникальные, а вот на клиенте можно нагенерировать чего угодно.
Остается вопрос, как получать последовательные GUID на клиенте? К сожалению, стандартной функции в .NET для этих целей нет, но ее можно сделать, воспользовавшись P/Invoke:

Я видел другой вариант — GUID генерировался из двух частей, первая — текущая дата, вторая — рандом.
Ключ должен быть строго возрастающим. Если вторая часть — рандом, то он уже не дает строгого возрастания, если это не какой-то специальный рандом.
Cтрогого возрастания нет, а так ли это нужно? Нужен гарантированно уникальный ключ, а возрастание даёт бонус в производительности, который теряем на P/Invoke www.codeproject.com/Articles/253444/PInvoke-Performance
Внимательно прочитайте статью, там как раз сравнивается перформанс разных вариантов, и затраты на P/Invoke там не видны. Как раз время вставки записей, в которых последовательный GUID ключ генерится через P/Invoke оказывается самым быстрым, потому что затраты на «обработку» непоследовательного ключа на порядки больше. Цитата приведеная выше из моей статьи говорит именно про последовательные гуиды, поэтому я не совсем понял, как предложенный способ генерации ключа соотносится с «последовательным».
Я считаю возможным три варианта: GUID строго последователен, GUID не строго последователен, GUID рандомен. В статье увидел только два варианта: seq и non-seq. Я может чего не понимаю, но для не строго последовательной генерации в большинстве случаев последовательность будет соблюдаться. Вижу только две ситуации когда этот вариант будет хуже — хайлоад и логи.

Вообще будет здорово добавить тест в сравнение, и учесть не только время вставки, но и время генерации самого GUIDа
Вообще будет здорово добавить тест в сравнение, и учесть не только время вставки, но и время генерации самого GUIDа


Время генерации включено во всех случаях (можно посмотреть в коде, который я на гитхабе выложил).
NEWSEQUENTIALID() не дает строго возрастания, он дает возрастание в рамках некой группы, группы периодически ротируются, поэтому в некий момент вы можете получить идентификатор, который меньше ранее созданного.
Согласен. Строго говоря он работает так:
Creates a GUID that is greater than any GUID previously generated by this function on a specified computer since Windows was started. After restarting Windows, the GUID can start again from a lower range, but is still globally unique.

Взято вот отсюда
Я правильно понял, чтобы UuidCreateSequential отработал корректно и запись действительно попала в конец, клиент, генерирующий гуид и ms sql server должны находится на одной машине?
UuidCreateSequential не гарантирует, что запись попадет в конец. Все, что он гарантирует — это то, что какое-то время последовательные вызовы будут давать монотонно возрастающие гуиды.
Реализация comb guid из NHibernate — старшие байты берем из даты-времени (что обеспечивает возрастание), младшие байты из Guid.NewGuid() обеспечивают уникальность.
Но и вероятность коллизии растет, ведь случайных байтов остается сильно меньше.

Хм, только сейчас обратил внимание на время комментария. Прошу прощения, кинули ссылкой, не заметил что год уже немного другой)
Спасибо, полезное исследование. Какая версия SQL Server использовалась? Интересно будут ли результаты отличаться в старых версиях (2008 например) и новых.
Использовался SQL Server 2012, если будет время — попробую разные версии.
Только нужно помнить, что «меньший объем» и «быстрее генерируется» — вещи относительные. Относительные относительно, в этих примерах, размера таблицы и скорости вставки, т.е. в подавляющем большинстве реальных сценариев это ничто, это даже не экономия на спичках.
Возможно, вопрос немного глупый, но что если бы можно было генерировать автоинкремент на клиенте? К примеру, при подключении к БД программа резервирует себе некоторый набор id-шников — [1001-1100] — и расходует при создании новых записей. По окончании запрашивает снова — [1601-1700]. Какие недостатки будут у такой системы?
Как вы предлагаете «резервировать»?
Одна общая последовательность на всю базу. Хотя, как сказали ниже, это будет узким местом.
Вот это и недостаток.
Можно, только зачем? Плюс нужно отслеживать коллизии, когда два параллельных процесса смогли зарезервировать одинаковый набор.
Или жестко лочить доступ к генератору последовательности на этапе получения «резервного» отрезка, что создает bottleneck. Потом, если приложение за время рабочей сессии ничего не добавило, что делать с резервированным отрезком? Если просто игнорировать и всегда получать по возрастанию, то можно быстро дойти до конца 32-битного integer. Можно взять больший тип, но в любом случае решение получается очень хрупкое и плохо масштабируемое.

В реальной жизни в сценариях с репликацией и автоинкрементными ключами на разных репликах вручную прописывают разные диапазоны ключа. Например, на реплике А он стартует с 1, на реплике Б — с 1000000. Но это, опять же, требует ручного вмешательства, плохо масштабируется, и существует вероятность коллизии, когда одна реплика вставит по какой-то причине неожиданно большое количество записей.
Насколько я понимаю, последовательные GUID-ы тоже не на единицу отличаются, если рассматривать его как 128-битное число? Как вы сказали, 32 бита брать необязательно, можно те же 128. Если даже раздавать всем клиентам по 1000 каждый раз, то получится примерно 2^118 вариантов. Если база большая, к примеру расположена на 4 машинах, то можно реализовать такую же схему, как с клиентами — генератор последовательности на каждой машине получает свой большой диапазон у мастер-генератора (назовем его так): [1 — 1000000], [1000001 — 2000000],… Как вы считаете, такая схема реальна, или не стоит заморачиваться? В общем-то, основная причина, по которой я спрашиваю — поиск по целочисленному ключу теоретически должен быть быстрее поиска по GUID (что имеет значение при отказе от join-ов).
Если те же 128, то строго говоря разницы не будет. Что ГУИД, что 128-битовое число — набор байт одинаковой длины. Разница может быть только в порядке сравнения этих байт.

Насчет скорости поиска — я планирую либо сделать вторую часть статьи, либо проапдейтить эту сравнениями производительности выборки. Действительно, это очень нужный в данном случае тест.
У меня подобная реализация несколько лет вполне успешно работает: #
автоинкримент всегда можно увеличивать с определенным шагом, зависящем от количества реплик + какой-нибудь запас.
Главное в этом случае не пользоваться identity(1, 10) и identity(2, 10) ибо если во втором данные обгонят по величине первые и перелить из второго в первое, то в первом новые ключи будут нумероваться с двойкой на конце. Ну или не переливать друг в друга.
для 2 процессов достаточно использовать позитивные и негативные диапазоны
Примерно так и работае HiLo Identity Generator в NHibernate.
Hi (номер cледующего свободного блока) хранится в отдельной таблице в БД, при старте приложение атомарно считывает и инкрементирует текущее значение.
Lo часть генерируется на клиентском приложении.
Id вычисляется как Hi * 32767 + Lo.

Из минусов могу назвать:
1. Int32 действительно быстро кончаются.
2. Неудобно вставлять данные из скриптов — приходится повторять логику по инкрементированию Hi и вычислению Id.
3. Последовательности Id получаются с пропусками.
Такой подход требует, чтобы клиент поддерживал сеанс. Поэтому в случае REST\Web работать не будет.
В Entity Framework 7 появилась возможность получать последовательные айдишники блоками по 10 штук (по умолчанию). Гарантируется, что все блоки не пересекаются, получение потокобезопасно и коллизий возникнуть не может.
С полученными блоками можно работать напрямую, явно указывая id при сохранении объекта.

Проблемы те же, что и в NHibernate — пропуски в id, быстрое увеличение значений ключа. По умолчанию по-прежнему ключ генерируется в БД.
Подробнее рассмотрено в видео блоге.
Простите чайниковский вопрос, конечно, но разве в столь продвинутой БД как MS SQL нет сущностей SEQUENCE? Мне отчего-то кажется, что использование последовательностей снимает все вопросы, поднятые в статье, нет?
Есть. Но — никак не снимает. Фактически, использование последовательностей для идентификаторов ничем особо не отличается от Identity.
Хосподи, ну что за статья-обрубок??
«так как для некоторых ситуаций более предпочтительным может оказаться GUID» — ну хотя бы ма-аленько так про ситуации. Ну хоть парочку для каждого случая, чтобы сориентировать про какие ситуации вообще вам известно.
«Приведены преимущества каждого из вариантов...» — вот это вообще не имеет смысла при отсутствии анализа недостатков (что там у нас с джойнами по GUIDу на серьезной базе?).
«На практике возможны и другие, более редкие, типы ключа» — действительно, нафиг всю эту тягомотину с денормализацией и подходами к выбору оптимальных ключей! GUID и автоинкремент — наше все! А все остальное — редкая фигня.

Теперь о тестах (и в продолжение об обрубках). Судя по ним, автор в базу исключительно пишет. Кстати, давайте поиграем со вставкой, когда таблица не пустая, а содержит миллион записей. С индексом, конечно. А с несколькими индексами? А с кластерным по ключу?
А где, кстати, чтение, обновление, удаление? Джойны опять же?
Слова про «ограничение оперативной памяти» есть только на диаграммах. Что сие означает и как (зачем) это было реализовано?
Ну и раз уж речь идет о тестах, было бы любезно указать версию MSSQL и описать железо.

Какая-то абстракция ни о чем. Вот на кой такое писать?
вот это вообще не имеет смысла при отсутствии анализа недостатков (что там у нас с джойнами по GUIDу на серьезной базе?).

Что вы считаете серьезной базой? Десять на десять миллионов записей — все нормально.

Мы же с вами инженеры, правильно? ;)

Если основные задачи на базе — это запросы для построения отчетов и конечный пользователь радуется результату через 5 минут после запроса отчета за годовой период — это нормально. В общем случае при равных условиях джойн по GUIDу (16 байт) будет медленнее джойна по целочисленному (4 байта, например) ключу.
Если у нас онлайн торговая площадка/агрегатор — то здесь речь о нормально может идти при разговоре о сотнях миллисекунд.

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

Да, медленнее. Но вопрос в том, критично ли это замедление для решаемой задачи.

Если у нас онлайн торговая площадка/агрегатор — то здесь речь о нормально может идти при разговоре о сотнях миллисекунд.

Там вообще не факт, что надо РСУБД использовать, так что обсуждение, что в качестве первичного ключа, теряет смысл.
Прежде чем писать в таком тоне, очень рекомендую самому попытаться написать хотя бы одну статью. И прочитать внимательно критикуемую статью, кстати, тоже.

А теперь по пунктам:
«так как для некоторых ситуаций более предпочтительным может оказаться GUID» — ну хотя бы ма-аленько так про ситуации. Ну хоть парочку для каждого случая, чтобы сориентировать про какие ситуации вообще вам известно.

Ниже вообще-то расписываются критерии, по которым предпочтительнее GUID, и даже выводы насчет его производительности в плане вставки есть в конце статьи? Или я должен был написать в стиле «Если у вас каталог для интенет магазина — используйте автоинкремент», если у вас база данных пользователей — используйте ГУИД"?

«Приведены преимущества каждого из вариантов...» — вот это вообще не имеет смысла при отсутствии анализа недостатков (что там у нас с джойнами по GUIDу на серьезной базе?).

Да, это указать можно, и в тексте статьи упоминается, как на скорость вычитки могут повлиять непоследовательные ГУИДы

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

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

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

Про миллион записей — это когда вся таблица не умещается в памяти. И этот кейс рассмотрен.

А где, кстати, чтение, обновление, удаление? Джойны опять же?

Про чтение и джойны я плнаирую, как будет время, написать в part 2.

Слова про «ограничение оперативной памяти» есть только на диаграммах. Что сие означает и как (зачем) это было реализовано?

Если прочитать внимательно, то все можно увидеть.

Ну и раз уж речь идет о тестах, было бы любезно указать версию MSSQL и описать железо.

Цель этого тестирования — сравнительные цифры, а не абсолютные. Сравнение дает понять как в одних и тех же условиях ведут себя разные варианты. Знание конкретного железа чем-то поможет?
Недавно видел пост, о том как чуваки умудрились сделать коллизию, используя sequential id. Так что для распределенных случаев его категорически нельзя использовать. В итоге остается один достойный вариант — autoincrement.

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

Поэтому всегда имеет смысл делать кластерный ключ auto increment. Но если вы хорошо знаете SQL Server, то догадаетесь, что кластерный ключ и первичный ключ таблицы — разные вещи. Можно иметь кластерный ключ — auto increment для оптимизации хранения и первичный ключ guid, если у вас планируется распределенная БД.
И все-таки насчет одного достойного варианта — спорное утверждение. Настройка и поддержка репликации при использовании автоинкрементов — очень неприятное занятие.

Насчет кластерного и первичного — конечно же знаю разницу. И на практике были разные случаи. Например, когда таблица кластеризована по дате изменения объекта, а первичный ключ, естественно, не кластерный.
Встроенный механизм репликации в SQL Server как раз добавляет колонку с GUID (если не было объявлено колонки ROWGUID) и Timestamp. Вообще для репликации гуиду вовсе не надо быть ключом.

Например, когда таблица кластеризована по дате изменения объекта
При каждом обновлении меняется кластерный ключ? Это же гениально!
А если эти изменения редки, а вычитка записей за последние 30 дней идет крайне часто? Все зависит от бизнес кейса ;)
В мульти-мастер кластере MySQL Galera интересно решается проблема с коллизиями автоинкрементов при записи с разных нод: Auto increments in Galera

Каждая нода генерирует свои уникальные непересекающиеся с другими нодами ID используя оффсет и размер инкремента.
Тесты в статье некорректны.
При длине строкового поля размером более 4000 байт, то при вставке строки в страницы IN_ROW_DATA не попадает ничего и данные пишутся в страницы ROW_OVERFLOW_DATA. Это значит, что в тесте GUID произошло по факту гораздо меньше page splits, чем случилось бы в реальной системе.
Да, 8000 байт или 4000 символов в nvarchar.
Т.е. в моих тестах все корретно. Единственное — опечатка на предпоследнем графике (там написано «символов» вместо «байт»), поправлю ее вечером.
Мое REPO на TSQL
Запускал по 3 раза
Microsoft SQL Server 2014 — 12.0.2269.0 (X64)
Jun 10 2015 03:35:45
Copyright © Microsoft Corporation
Developer Edition (64-bit) on Windows NT 6.3 (Build 9600: )
SSD drive

Заголовок спойлера
— IDENTITY
create table TestTable (
   id int identity(1,1) not null primary key clustered,
   sequence int not null,
   data char(250) not null default '');
go


— NEWID
create table TestTable (
   id uniqueidentifier default newid() not null primary key clustered,
   sequence int not null,
   data char(250) not null default '');
go


— NEWSEQUENTIALID
create table TestTable (
   id uniqueidentifier default newsequentialid() not null primary key clustered,
   sequence int not null,
   data char(250) not null default '');
go

тестируем вставкой 50.000 записей

declare @count int;
set @count = 0;
while @count < 50000 begin
   insert TestTable (sequence)
   values (@count);

   set @count = @count + 1;
end;
go

смотрим что получилось:

— Get the number of read / writes for this session…
select reads, writes
from sys.dm_exec_sessions
where session_id = @@spid;


— Get the page fragmentation and density at the leaf level.
select index_type_desc, index_depth, page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent, record_count
from sys.dm_db_index_physical_stats(db_id(), object_id('TestTable'), null, null, 'detailed')
where index_level = 0;
go



________________| inserting..| reads.....| writes.....| page_count...| avg_page_space_used_in_percent...| avg_fragmentation_in_percent
identity.....................| 13s..........| 61411.....| 58110....| 1667.............| 98.9177044724487............................| 0.659868026394721
NEWID()....._______| 12s.........| 57351.....| 56435....| 2498..............| 68.9704472448727............................| 98.7673956262425
newsequentialid.......| 11s..........| 62453.....| 59852....| 1725..............| 99.8882752656289............................| 0.869565217391304

с joina-ми тока удвоенная нагрузка на CPU — оно и понятно — сравнение за 2 операции делается, вместо одной int-а.

SQL Repo
IF EXISTS( SELECT * FROM sysobjects WHERE name='ParentTable' AND (xtype='U') )
DROP TABLE dbo.ParentTable
GO
IF EXISTS( SELECT * FROM sysobjects WHERE name='TestTable' AND (xtype='U') )
DROP TABLE dbo.TestTable
GO

create table TestTable ( id int identity(1,1) not null primary key clustered, sequence int not null );
go
create table ParentTable (
id int identity(1,-1) not null primary key clustered,
ParentTableId int not null INDEX idx_ParentTableId
);
go
alter TABLE dbo.ParentTable with nocheck add CONSTRAINT FK_ParentTable FOREIGN KEY (ParentTableId) REFERENCES dbo.TestTable(id);
GO

--— NEWID
create table TestTable ( id uniqueidentifier default newid() not null primary key clustered, sequence int not null );
create table ParentTable (
id uniqueidentifier default newid() not null primary key clustered,
ParentTableId uniqueidentifier not null,
);
go
alter TABLE dbo.ParentTable with nocheck add CONSTRAINT FK_ParentTable FOREIGN KEY (ParentTableId) REFERENCES dbo.TestTable(id);
GO

--— NEWSEQUENTIALID
create table TestTable ( id uniqueidentifier default newsequentialid() not null primary key clustered, sequence int not null );
go
create table ParentTable (
id uniqueidentifier default newsequentialid() not null primary key clustered,
ParentTableId uniqueidentifier not null
);
go
alter TABLE dbo.ParentTable with nocheck add CONSTRAINT FK_ParentTable FOREIGN KEY (ParentTableId) REFERENCES dbo.TestTable(id);
GO

— тестируем вставкой 2,000,000 записей

declare count int = 0;
while count < 2000000 begin
INSERT INTO TestTable (sequence) VALUES ( count);
set count = count + 1;
end;
INSERT INTO ParentTable (ParentTableId) SELECT id FROM TestTable
go

select max(sequence) from ParentTable p join TestTable t on p.ParentTableId = t.id
where exists (select * from TestTable s where p.ParentTableId = s.id)

--смотрим что получилось:

--— Get the number of read / writes for this session…
select reads, writes
from sys.dm_exec_sessions
where session_id = @@spid;

--— Get the page fragmentation and density at the leaf level.
select index_type_desc, index_depth, page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent, record_count
from sys.dm_db_index_physical_stats(db_id(), object_id('TestTable'), null, null, 'detailed')
where index_level = 0;
go



http://www.figvam.ca/cloud/pics/i/7354.jpg

________________| select join | inserting..| writes.....| page_count...| avg_page_space_used_in_percent...| avg_fragmentation_in_percent
identity.....................| 2s.............| 8m56s.....| 161777..| 4202. ............| 99.9429948109711............................| 0.404569252736792
NEWID()....._______| 2s.............| 8m48s.....| 227244..| 9999..............| 71.6404867803311............................| 99.0799079907991
newsequentialid.......| 2s.............| 8m49s.....| 244776..| 7169..............| 99.9306893995552............................| 0.711396289580137
У меня была ситуация, когда клиент вставлял пачками по 10-15М очень маленьких записей. Чтобы ускорить вставку, я пришёл к составному ключу: два поля Int32. Первое — автоинкремент с сервера, сеансовый ключ, который каждый клиент получает при каждом коннекте к серверу. Второе — внутренний инкремент на клиенте. Так у нас будет и последовательность и непротиворечивость, при чём обе гарантированные. Последовательность обеспечивается ещё и тем, что клиент перед заливкой данных, даже если и висел на связи несколько часов, всё равно делает реконнект, чтобы получить свежий сеансовый ключ и его заливка шла в конец базы. Клиентов много, но одновременная заливка двух крайне маловероятна, в основном они читают. Но даже если лить будут два, будем просто иметь перерасход кэша (нужно будет в памяти держать одновременно две страницы, по одной на каждого клиента).
Sign up to leave a comment.

Articles

Change theme settings