У каждой таблицы с данными должен быть синтетический ключ id. Ссылки на эту таблицу должны быть по этому ключу.
Два основных подхода:
1. Использовать целочисленный ключ (обычно 1, 2, 4, 8 байтов в зависимости от предполагаемого числа строк), который генерируется в виде глобально возрастающего значения.
2. Использовать 128-битный UUID.
2.1. Генерировать UUID случайным образом.
2.2. Генерировать UUID в виде локально возрастающей последовательности (она возрастает, но может в какой-то момент "переполняться").
2.3. Генерировать UUID в виде глобально возрастающей последовательности (для простоты можно считать, что левые 64 бита это timestamp, правые 64 бита случайны).
На что это влияет:
1. Чем меньше байтов, тем быстрей оно работает. Ну это, наверное, очевидно. И читать меньше и памяти меньше занимает и кешей меньше требует и индексы меньше, и промахов меньше. Разница между типичным 4-байтовым целым и 16-байтовым UUID в 4 раза. Понятно, что упереться в производительность в этом месте — надо постараться, с другой стороны вопрос про то, что будет в каждой таблице, так что какая-то доля процента производительности тут объективно будет.
2. Возрастающие значения в индекс ложатся гораздо быстрей, чем случайные. Возрастающие просто в конец узла B-дерева добавляются, изредка его немного перестраивая. Дерево компактное. Случайные значения летят в случайные узлы, дерево будет более "разбитое". Разница в производительности на больших объёмах вставок тоже объективно будет. Вариант 2.2 собственно только ради этого и придумывался.
3. Запросы order by id. Понятно, что обычно есть что-то вроде created_at, но всё равно удобно иметь возможность упорядочить строки в порядке создания, переиспользуя уже существующий индекс.
4. Читабельность. Ну тут целые числа однозначно приятней глазу, чем UUID.
5. Безопасность. В некоторых сценариях UUID хорош тем, что его нельзя перебирать. Обычно для таких сценариев делают отдельный столбец с UUID, если в базе id целочисленный, но иметь этот столбец уже готовым удобней. Также, когда внешние идентфикаторы всегда UUID, это в целом безопасней, т.к. про безопасность отдельных эндпоинтов не всегда люди думают, тут оно "нахаляву". Однако с вариантом 2.3 с безопасностью есть нюансы — утекает timestamp так или иначе.
6. Генерация ID на стороне клиента. Порой это бывает удобно. Я бы сказал, это вообще всегда удобно. Генерация ID на стороне сервера накладывает достаточно ощутимые ограничения на используемые протоколы и во многих случаях усложняет код.
7. Распределённая БД. К примеру у нас есть древовидная структура БД, когда в каждом филиале стоит отдельная БД (для возможности автономной работы) и данные уходят в центральную БД. В случае UUID всё выглядит гораздо проще. В случае id нужно думать чуть больше.
8. Совпадения id для разных сущностей. К примеру можно сделать join по неправильному полю. Если id не совпадают, запрос ничего не вернёт, что вероятно насторожит и не даст багу идти дальше. В противном случае может вернуться бессмысленный, но ненулевой результат.
Также есть проблема с реализацией вариантов 2.2 и 2.3, это не слишком часто используемые подходы и возможно придется использовать редкие библиотеки или писать самому. Это не великая проблема, но всё же.
Я сейчас работаю над best practices, которыми будем руководствоваться в будущем для создания сервисов. И нужно этот вопрос решить. Плюсы и минусы там и там есть. Скорей склоняюсь к UUID 2.3, т.к. считаю, что проблемы с производительностью будут малозаметны, а удобство читаемых id не перевешивает плюсов UUID. Заранее спасибо за конструктивные отзывы.
Здравствуйте, vsb, Вы писали:
vsb>Наверное набивший оскомину вопрос, извините.
vsb>У каждой таблицы с данными должен быть синтетический ключ id. Ссылки на эту таблицу должны быть по этому ключу.
UUID, т.к. int при репликации и генерации на клиенте создает много проблем.
Здравствуйте, vsb, Вы писали:
vsb>Разница в производительности на больших объёмах вставок тоже объективно будет. Вариант 2.2 собственно только ради этого и придумывался.
Не только на вставках, но и на последующих выборках.
vsb>3. Запросы order by id. Понятно, что обычно есть что-то вроде created_at, но всё равно удобно иметь возможность упорядочить строки в порядке создания, переиспользуя уже существующий индекс.
vsb>4. Читабельность. Ну тут целые числа однозначно приятней глазу, чем UUID.
Если нужен читабельный пользователями ID, лучше завести отдельный ключ, отличный от PK. Даже если PK это такой же int.
vsb>5. Безопасность. vsb>Однако с вариантом 2.3 с безопасностью есть нюансы — утекает timestamp так или иначе.
И 2.2 и 2.3 можно перебирать/подбирать. Чуть сложнее, чем 1, но вполне успешно. Если нужно защититься от подбора, опять же заводим отдельный ключ, видимый "извне".
vsb>Также есть проблема с реализацией вариантов 2.2 и 2.3, это не слишком часто используемые подходы и возможно придется использовать редкие библиотеки или писать самому. Это не великая проблема, но всё же.
Не вижу проблемы. Библиотек полно, и все современные ОС предоставляют подобный сервис.
vsb>Я сейчас работаю над best practices, которыми будем руководствоваться в будущем для создания сервисов. И нужно этот вопрос решить. Плюсы и минусы там и там есть. Скорей склоняюсь к UUID 2.3, т.к. считаю, что проблемы с производительностью будут малозаметны, а удобство читаемых id не перевешивает плюсов UUID.
Главное помнить, что best practices это не догмы, а рекомендации. Подходы вполне можно совмещать, даже в рамках одной БД.
Таблицы-справочники, меняющиеся раз в год, и таблицы-перечисления, на которые ссылается полбазы, вполне могут жить с целочисленными ключами. Особенно если ключи укладываются, скажем, в байт. Проблемы генерации ключа на клиенте (раз в год) их не касаются.
Здравствуйте, wildwind, Вы писали:
vsb>>4. Читабельность. Ну тут целые числа однозначно приятней глазу, чем UUID.
W>Если нужен читабельный пользователями ID, лучше завести отдельный ключ, отличный от PK. Даже если PK это такой же int.
Речь про читабельность для программиста/админа. Например я могу сделать select, увидеть там id=12762 и без копипаста напечатать его же в следующем запросе. С UUID это невозможно.
W>И 2.2 и 2.3 можно перебирать/подбирать. Чуть сложнее, чем 1, но вполне успешно.
Сомневаюсь. Даже 32 бита ты не переберёшь, тебя задолго до этого забанит чего-нибудь. В общем это не то, что обычно понимают под перебором.
vsb>>Также есть проблема с реализацией вариантов 2.2 и 2.3, это не слишком часто используемые подходы и возможно придется использовать редкие библиотеки или писать самому. Это не великая проблема, но всё же.
W>Не вижу проблемы. Библиотек полно, и все современные ОС предоставляют подобный сервис.
Ну приведи пример для postgres. Что за современные ОС, у меня alpine linux, что за сервис ты имеешь в виду?
W>Главное помнить, что best practices это не догмы, а рекомендации. Подходы вполне можно совмещать, даже в рамках одной БД. W>Таблицы-справочники, меняющиеся раз в год, и таблицы-перечисления, на которые ссылается полбазы, вполне могут жить с целочисленными ключами. Особенно если ключи укладываются, скажем, в байт. Проблемы генерации ключа на клиенте (раз в год) их не касаются.
В моём понимании смысл best practices в том, чтобы не отходить от них без существенных причин. Поэтому таблицы справочники точно будут с UUID в такой модели, по крайней мере я не вижу ни одной причины исключать их. А использовать там однобайтовые идентификаторы это каноничный premature optimization, который с очень ощутимой вероятностью выстрелит лет через 5, когда придётся делать миграцию половины таблиц на более широкий тип. Но в целом, конечно, согласен, если очень надо — без проблем, это не догма.
Здравствуйте, vsb, Вы писали:
vsb>Здравствуйте, wildwind, Вы писали:
vsb>>>4. Читабельность. Ну тут целые числа однозначно приятней глазу, чем UUID.
W>>Если нужен читабельный пользователями ID, лучше завести отдельный ключ, отличный от PK. Даже если PK это такой же int.
vsb>Речь про читабельность для программиста/админа. Например я могу сделать select, увидеть там id=12762 и без копипаста напечатать его же в следующем запросе. С UUID это невозможно.
Это вообще не аргумент. Профессионалы могут и скопировать или же просто дописать запрос.
Здравствуйте, BlackEric, Вы писали:
BE>Это вообще не аргумент. Профессионалы могут и скопировать или же просто дописать запрос.
Для меня аргумент. Ещё вариант — саппорт скинул фотографию с телефона интерфейса, на котором видно ID в URL-е, который мне нужно. Да много такого. Соглашусь, что не самый веский аргумент.
Здравствуйте, vsb, Вы писали:
vsb>Сомневаюсь. Даже 32 бита ты не переберёшь, тебя задолго до этого забанит чего-нибудь. В общем это не то, что обычно понимают под перебором.
Опытным путем определяется алгоритм генерации, его входные данные. Если повезет, даже конкретная библиотека. Дальше дело техники.
vsb>Ну приведи пример для postgres. Что за современные ОС, у меня alpine linux, что за сервис ты имеешь в виду?
Здравствуйте, wildwind, Вы писали:
vsb>>Сомневаюсь. Даже 32 бита ты не переберёшь, тебя задолго до этого забанит чего-нибудь. В общем это не то, что обычно понимают под перебором.
W>Опытным путем определяется алгоритм генерации, его входные данные. Если повезет, даже конкретная библиотека. Дальше дело техники.
Ну определил ты, что 48 битов заполняются текущим временем, 80 битов заполняется случайными значениями, чего дальше? 80 битов будешь подбирать?
vsb>>Ну приведи пример для postgres. Что за современные ОС, у меня alpine linux, что за сервис ты имеешь в виду?
W>С первой страницы поиска:
W>https://github.com/tvondra/sequential-uuids
Да ладно, уж как-нибудь сгенерировать 128 битов я смогу без библиотек, я не из того поколения, которым нужен left-pad, чтобы пробелы почистить. Это так, к слову больше было.
Здравствуйте, vsb, Вы писали:
vsb> Наверное набивший оскомину вопрос, извините.
Если сервис будет работать больше чем на localhost, то глобально монотонно возрастающий UUID (нечастые промахи в хвосте последовательности допустимы). Других вариантов нет.
Вообще не обязательно такой длинный как UUID, там вроде 38 символов или типа того.
Я обычно генерю UUID и беру первые 7-9 символов. Для уникальности хватает за глаза. И вполне читаемо
Здравствуйте, wildwind, Вы писали:
W>Если нужен читабельный пользователями ID, лучше завести отдельный ключ, отличный от PK. Даже если PK это такой же int.
Здравствуйте, vsb, Вы писали:
vsb>Я сейчас работаю над best practices, которыми будем руководствоваться в будущем для создания сервисов. И нужно этот вопрос решить. Плюсы и минусы там и там есть. Скорей склоняюсь к UUID 2.3, т.к. считаю, что проблемы с производительностью будут малозаметны, а удобство читаемых id не перевешивает плюсов UUID. Заранее спасибо за конструктивные отзывы.
У тебя в разделе про UUID есть пункты 2.1, еще раз 2.1 и 2.2, а ты ссылаешься на 2.3.
UUID прекрасен тем, что его можно генерировать случайно, и вероятность коллизий очень мала. Если тебе кажется, что 128 бит недостаточно, с легкостью можно подняться до 160-и или 256-и (что, правда, не увеличит читабельности).
В некоторых случаях бывает полезен еще такой вариант, вычислять ID как криптохеш от содержимого записи. Конечно, должен существовать способ однозначным образом представить это содержимое в виде последовательности байт, от которых, собственно, и вычисляется хеш, чтобы разные записи не совпадали а одинаковые не давали разных ID.
Здравствуйте, vsb, Вы писали:
vsb>В моём понимании смысл best practices в том, чтобы не отходить от них без существенных причин. Поэтому таблицы справочники точно будут с UUID в такой модели, по крайней мере я не вижу ни одной причины исключать их.
А мне понравилась идея.
Причины тут те же самые, что ты описал выше, только только для справочников у них веса другие
Т.е. выборка типа
select * from orders where statusId = 2 and cityId=678
(где "2" — это "открыт" а город 678 это Питер и аналитик/саппорт это прекрасно помнит уже)
такая может быть куда чаще требоваться в анализе данных, чем выборка по id собственно заказа и полезно что она может быть записана наглядно и быстро. Т.е. "плохая читабельность" для справочников более большая проблема, чем для таблиц данных, а польза от удобной распределенной генерации для них совершенно несущественна.
При этом проблемы расхода места и памяти для них вполне актуальны, т.к. поля со значением из справочника в таблице очень даже часто индексируются, а более компактный индекс лучше чем менее компактный.
Здравствуйте, Нomunculus, Вы писали:
Н>Вообще не обязательно такой длинный как UUID, там вроде 38 символов или типа того. Н>Я обычно генерю UUID и беру первые 7-9 символов. Для уникальности хватает за глаза. И вполне читаемо
A UUID (Universal Unique Identifier) is a 128-bit value
Здравствуйте, Нomunculus, Вы писали:
Н>Вообще не обязательно такой длинный как UUID, там вроде 38 символов или типа того. Н>Я обычно генерю UUID и беру первые 7-9 символов. Для уникальности хватает за глаза. И вполне читаемо
Ерунда какая-то. UUID это число. Обычно его пишут в символьной форме в шестнадцатеричной системе, но исходно это число, и нормальные БД умеют с ним работать как с числом.
Беря из него "первые 9 символов" ты одновременно теряешь все гарантии уникальности uuid (он хорошо размазан в целом, но если выкусывать из него только часть, то в ней коллизии могут быть и весьма частые ведь)
И одновременно, если ты хранишь его в строковом виде (а как ты еще берешь первые символы?), ты теряешь больше места на их хранении и больше вычислительных ресурсов на сравнение чем чельный uuid.
Ты можешь просто брать псевдослучайное число в диапазоне миллиард — и то будет лучше, наверное.
Здравствуйте, Нomunculus, Вы писали:
Н>Разумеется от задачи зависит. Что какие нудные. Для многих задач уникальности первый 9 символов за глаза, даже с большей веротностью повторяемости.
А почему не использовать просто случайные числа тогда?