Как стать автором
Обновить

Комментарии 32

Насколько я знаю в mysql innodb записи автоматически сортируются по первичному ключу при вставке. Могут быть проблемы с uuid.

НЛО прилетело и опубликовало эту надпись здесь
Проблема архитектурная: периодически заказчики присылали справочные данные с целью включения их в новую версию дистрибутива. Иногда справочники содержали первичные ключи уже имеющиеся в нашей базе. Приходилось устранять коллизии в процессе агрегирования данных. На этом проблемы не заканчивались: при разворачивании нового дистрибутива периодически возникали обратные коллизии.

Есть простейшее решение подобных проблем с репликацией — использование разных диапазонов айдишников. Например, шаг увеличения счетчика не 1, а 1000 и у каждой из удаленных баз данных счетчик начинается со своего значения из диапазона 1-999, а не с 1.

Или составные ключи вида namespace + id

Где namespace уникален и у каждой связанной базы свой

В этом случае придется вести реестр заказчиков для того чтобы разные заказчики случайно не получили один и тот же начальный идентификатор. С UUID-ом мне об этом думать не надо.

Так его в любом случае придется вести, хотя-бы для бухучета :)

Что касается второй проблемы, меня терзали смутные сомнения по поводу автоинкрементных полей в фаерберде 1.5 (ибо я тоже его активно юзал в свое время и помнил, что по синтаксису это был младший брат Оракла, в котором были сиквенсы вместо автоинкрементных полей), так вот, а автоинкрементные поля появились только в 3-й версии, в 1.5 их не было. :)

Для генерации айдишника на стороне сервера использовались генераторы не привязанные к каким-либо таблицам, а значит и вы их использовали, если вам в итоге приходилось получать новое значение айдишника. И была вполне нормальная практика получить новое значение генератора на клиента и использовать это значение для вставки из клиента в разные таблицы, когда, например, одна сущность, это записи в разных таблицах и при создании оной надо добавлять несколько разписей в разные места.
И у Дельфи с этим проблем тоже не было, по-сути Интербейс это своя родная БД для Дельфи, а Фаерберд был ее опенсорсным клоном. Надо было вам в 2003 просто сходить на форум Дельфимастерс, или на форум sql.ru, или на форум rdsn.ru (кажись ничего не забыл популярного в те времена) и сказать: «а я делаю select max(id) чтоб получить последний айдишник». Вам бы там сначала надавали по голове канделябром, а потом показали как правильно делать. А может и сразу-бы показали, тогда трава была зеленее, а сообщества добрее.

Действительно, в FB есть sequence, они еще странно называются: "генераторы". В принципе, все бы работало как Вы и описываете, если бы не одно "но". На 2003 год помимо FB мы уже поддерживали MSSQL и Oracle. Из соображений унификации было принято решение перейти на ODBC драйверы. А их функциональность по сравнению с родными драйверами достаточно ограниченна (обратная сторона универсальности). В дельфи-компонентах это тоже проявляется. Отсюда и конструкции вида select max(id). Я посчитал эту деталь недостаточно существенной, поэтому не упомянул ее в первой части статьи. Видимо зря. Исходил из тех соображений, что статься не о преимуществах или недостатках ODBC или дельфи-компонентов, а о Qt-драйверах способных работать с uuid-ами в нативном для базы формате.
Про канделябры: в 2003 году мы на самом деле были "студентами", много не знали, с другой стороны: а есть профессионалы, которые миновали эту стадию?! Решение о переходе на UUID-ы мы принимали коллегиально, командой, а не волей одного человека. Конечно, команды тоже могут ошибаться, впрочем, как и большинство ;)
P.S. По прошествии времени думаю: "Как классно, что мы тогда сделали такой выбор!". Пользуюсь им до сих пор!

Проблема программная: чтобы получить доступ к вставленной записи нужно было выполнить дополнительный SELECT-запрос, который возвращал максимальное значение первичного ключа (значение для только что вставленной записи). Причем этот процесс должен был проходить в пределах одной транзакции. Далее можно было обновлять или корректировать запись. Это сейчас я знаю, что некоторые драйверы БД возвращают значение первичного ключа для вставленной записи, но в 2003 году мы такими знаниями не обладали, да и не припомню что бы Делфи-компоненты возвращали что-то подобное.


М-да, вот что значит студенты взялись программировать БД. Даже в 2000 году было множество FAQ в которых говорилось используйте sequence и никогда select max(id)!!! Но виноваты конечно компоненты Делфи

А дальше автор продолжает побеждать выдуманную проблему «Один коннект — одна транзакция»

При использовании UUID в качестве первичного ключа есть еще пара преимуществ:


  • Бывает такой относительно редкий кейс, когда нужно слить две базы данных. При использовании serial скорее всего будут коллизии ключей, и придется долго и муторно изменять значения у первичных и внешних ключей одной из баз. С UUID такой проблемы нет, коллизия оооочень маловероятна.
  • Более удобная подготовка данных. В случае с serial мы можем получить идентификатор записи только при запросе к базе — то есть нужно все операции оборачивать в транзакцию и выполнять последовательно, что в некоторых случаях может привести к долгим транзакциям в БД. В случае с UUID можно заранее сгенерировать случайный идентификатор и использовать его везде, где нужно, и только потом сохранить пачку уже полностью подготовленных данных в базе.
Обеим этим проблемам придумали решения давным-давно, как минимум четверть века назад оно уже было: для этого используются сиквенсы генерируемые на стороне сервера, для решения первой проблемы — генерация значений из разных множеств для разных баз данных.

Извините, но нет. Если сначала получать идентификатор из сиквенса, а потом сохранять запись, то это будет лишний запрос к базе. Разные множества — это дополнительная настройка, причем если использовать те же целочисленные ключи, это не гарантирует невозможности коллизий. Что то, что другое выглядит откровенно костыльно.

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

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

Окей, простой пример: есть два интернет-магазина, которые их владелец решил слить в один. Магазины написаны на каком-нибудь Django, для работы с БД используется ORM. Никто просто не заморачивался с настройкой какого-то глобального sequence для обоих баз, да и вообще не планировалось никакое взаимодействие между этими базами. В результате получаем огромный геморрой… которого можно легко избежать, используя UUID.
Ну и при использовании UUID не нужно предварительно делать никакую вставку (которая в первую очередь является усложнением кода). Просто генерируем новое значение и используем его, а затем в одной транзакции вставляем данные в базу.

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

А тот самый редчайший случай имеет смысл только тогда, когда оба магазина имеют абсолютно одинаковый структуры данных.

Нужно ли говорить насколько это редкий случай?
это будет лишний запрос к базе
Для одной записи незаметно. А при вставке 100500, можно сразу инкрементнуть генератор на 100500 и таким образом, получить за 1 запрос 100500 ключей.

М-м-м, а если заинкрементить 100500 значений, а транзакция откатится, мы потеряем этот диапазон?
Так же полагаю, этот совет будет работать когда количество вставляемых данных изначально известно.

М-м-м, а если заинкрементить 100500 значений, а транзакция откатится, мы потеряем этот диапазон?
Да, а в чём проблема, разве это дефицитный ресурс — sequence, особенно 64-битный?
Так же полагаю, этот совет будет работать когда количество вставляемых данных изначально известно.
Если сохраняем блок данных в транзакции, скорее всего число записей заранее известно.

А если чтение из потока?

Берите из базы по 100 значений ключей, например. 1 лишний запрос генератора на 100 инсертов вообще не будет заметен. А лучше, конечно, пользоваться специальным синтаксисом, который зависит от СУБД. Например, insert… returning… в Firebird.

Мы приходим к тому, о чем как минимум уже пару раз говорилось в этом обсуждении: нужны дополнительные телодвижения для реализации этого функционала. А индивидуальная поддержка СУБД может стать отдельным приключением. Повторюсь: использование UUID-ов позволяет минимизировать эти издержки.

Но честно, я не представляю сценарий записи в базу из потока. Минимум, это какая-то двухзвенка, а сейчас в моде сервисы работающие так или иначе поверх http, у которых 1 запрос известного размера = 1 транзакция, и никак иначе.

Импорт данных из какого-нибудь csv или xml. Если там внутри гигабайты данных — держать их в памяти просто чтобы сосчитать становится накладно.

Обычно я работаю с такими сценариями, когда входной xml/json укладывается в объектную модель и дальше что-то делаеться с объектами.

Допускаю, что какой-то супер-нагруженный веб-сервис не имеет возможности десериализовать входной файл в модель, а должен входной поток сразу лить в базу. Но в этом случае нам и айдишники не нужны. Делаем инсерты в базу, без ID-поля, СУБД сама назначит ID.

1) Данные могут быть связанными, так что ID-поля нужны;

2) И почему веб-сервис, мы, например, этой технологией почти не пользуемся, в основном TCP.

Данные могут быть связанными, так что ID-поля нужны
В таком случае, мы данные держим в памяти, а не делаем один проход по потоку.
И почему веб-сервис, мы, например, этой технологией почти не пользуемся, в основном TCP
Под http много готовой инфраструктуры.

В памяти держится только связка "id объекта в файлу — id объекта в базе".

А зачем это для задачи «загрузка csv в базу»?

Для csv и не нужно (пока файл один), для xml может пригодиться.

Ну, если в XML есть некие «справочники» (в которых мало записей) и «рабочие данные» (которых на порядок больше), то имеет смысл справочники прочитать в память (их всё равно придётся читать первыми) и посчитать число записей. Небольшая разница — хранить в память запись справочника целиком, либо пару: ключ БД — ключ XML.

Кроме того, если справочных данных на порядок меньше и мы всё равно не хотим их держать в памяти, можно позволить для таких записей отдельный запрос ID, всё равно таких записей существенно меньше и затраты будут незаметны (если уж считать байты и такты, у GUID-ов тоже есть накладные расходы: guid не инлайнится в объект, как int/int64, а вероятно потребует аллокации, если хранится строкой).

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


А справочники — да, часто проще прочитать в память целиком.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории