Нецелые первичные ключевые соображения

16

контекст

Я проектирую базу данных (на PostgreSQL 9.6), которая будет хранить данные из распределенного приложения. Из-за распределенной природы приложения я не могу использовать целые числа с автоинкрементом в SERIALкачестве основного ключа из-за потенциальных условий гонки.

Естественным решением является использование UUID или глобального уникального идентификатора. Postgres поставляется со встроенным UUIDтипом , который идеально подходит.

У меня проблема с UUID связана с отладкой: это не-дружественная для человека строка. Идентификатор ff53e96d-5fd7-4450-bc99-111b91875ec5ничего не говорит мне, тогда как ACC-f8kJd9xKCd, хотя он не гарантированно уникален, он говорит, что я имею дело с ACCобъектом.

С точки зрения программирования, обычно отлаживаются запросы приложений, относящиеся к нескольким различным объектам. Предположим, что программист неправильно ищет объект ACC(аккаунт) в ORDтаблице (заказ). С помощью удобочитаемого идентификатора программист мгновенно выявляет проблему, а используя UUID, он потратит некоторое время на выяснение того, что было не так.

Мне не нужна «гарантированная» уникальность UUID; Я действительно нужна комната для генерации ключей без конфликтов, но UUID является излишеством. Кроме того, в худшем случае, это не будет концом света, если произойдет столкновение (база данных отклоняет его, и приложение может восстановиться). Таким образом, с учетом компромиссов, меньший, но удобный для человека идентификатор был бы идеальным решением для моего варианта использования.

Идентификация объектов приложения

Придуманный мной идентификатор имеет следующий формат:, {domain}-{string}где {domain}заменяется доменом объекта (аккаунт, заказ, продукт) и {string}представляет собой случайно сгенерированную строку. В некоторых случаях может иметь смысл вставить {sub-domain}перед случайной строкой. Давайте проигнорируем длину {domain}и {string}с целью обеспечения уникальности.

Формат может иметь фиксированный размер, если он помогает производительности индексации / запросов.

Проблема

Знаю это:

  • Я хочу иметь первичные ключи с форматом, как ACC-f8kJd9xKCd.
  • Эти первичные ключи будут частью нескольких таблиц.
  • Все эти ключи будут использоваться в нескольких соединениях / отношениях в базе данных 6NF.
  • Большинство таблиц имеют размер от среднего до большого (в среднем ~ 1 млн строк; самые большие с ~ 100 млн строк).

Что касается производительности, как лучше хранить этот ключ?

Ниже приведены четыре возможных решения, но, поскольку у меня мало опыта работы с базами данных, я не уверен, какое (если есть) лучшее.

Рассмотренные решения

1. Сохранить как строку ( VARCHAR)

(Postgres не делает разницы между CHAR(n)и VARCHAR(n), поэтому я игнорирую CHAR).

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

2. Хранить в двоичном виде ( bytea)

В отличие от Postgres, MySQL не имеет собственного UUIDтипа. Есть несколько постов, объясняющих, как хранить UUID, используя 16-байтовое BINARYполе вместо 36-байтового VARCHAR. Эти посты дали мне идею хранить ключ как бинарный ( byteaна Postgres).

Это экономит размер, но меня больше заботит производительность. Мне не повезло найти объяснение того, какое сравнение быстрее: двоичное или строковое. Я считаю, что двоичные сравнения быстрее. Если они есть, то bytea, вероятно, лучше VARCHAR, даже если программист теперь должен каждый раз кодировать / декодировать данные.

Я могу ошибаться, но я думаю, что и то byteaи другое VARCHARбудет сравнивать (равенство) побайтно (или символ за символом). Есть ли способ «пропустить» это пошаговое сравнение и просто сравнить «все»? (Я так не думаю, но это не стоит проверять).

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

«Креативные» решения

Я придумал два очень «креативных» решения, которые могли бы работать, я просто не уверен, в какой степени (то есть, если у меня возникнут проблемы с масштабированием их до нескольких тысяч строк в таблице).

3. Хранить как, UUIDно с прикрепленным к нему ярлыком

Основная причина не использовать UUID состоит в том, чтобы программисты могли лучше отлаживать приложение. Но что, если мы можем использовать оба: база данных хранит все ключи UUIDтолько как s, но оборачивает объект до / после выполнения запросов.

Например, программист запрашивает ACC-{UUID}, база данных игнорирует ACC-деталь, извлекает результаты и возвращает их все как {domain}-{UUID}.

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

  • Является ли это (удаление / добавление домена при каждом запросе) существенными накладными расходами?
  • Это вообще возможно?

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

4. (Мой любимый) Хранить как IPv6 cidr

Да, вы правильно прочитали. Оказывается, формат адреса IPv6 отлично решает мою проблему .

  • Я могу добавить домены и субдомены в первых нескольких октетах и ​​использовать оставшиеся в качестве случайной строки.
  • В шансы столкновения ОК. (Я бы не стал использовать 2 ^ 128, но все равно все в порядке.)
  • Сравнение равенства (надеюсь) оптимизировано, поэтому я могу получить лучшую производительность, чем просто использовать bytea.
  • На самом деле я могу выполнить несколько интересных сравнений, например contains, в зависимости от того, как представлены домены и их иерархия.

Например, предположим, что я использую код 0000для представления домена «продукты». Ключ 0000:0db8:85a3:0000:0000:8a2e:0370:7334будет представлять продукт 0db8:85a3:0000:0000:8a2e:0370:7334.

Основной вопрос здесь такой: byteaесть ли какое-то главное преимущество или недостаток в использовании cidrтипа данных?

Ренато Сикейра Массаро
источник
5
Сколько распределенных узлов возможно? Вы знаете их номер (и имена) заранее? Рассматривали ли вы составные (многоколонные) ПК? Домен (в зависимости от моего первого вопроса) плюс простой последовательный столбец может быть самым маленьким, простым и быстрым ...
Эрвин Брандштеттер,
@ Спасибо! @ErwinBrandstetter Что касается приложения, оно разработано для автоматического масштабирования в соответствии с нагрузкой, поэтому впереди очень мало информации. Я думал об использовании (домен, UUID) в качестве PK, но это повторится «домен» во всем, домен все равно будет varcharсреди многих других проблем. Я не знал о доменах pg, о которых приятно узнать. Я вижу домены, используемые для проверки, использует ли данный запрос правильный объект, но он все равно будет опираться на нецелочисленный индекс. Не уверен, что здесь есть «безопасный» способ использования serial(без одного шага блокировки).
Ренато Сикейра Массаро
1
Домен не обязательно должен быть varchar. Попробуйте сделать его FK integerтипом и добавить для него таблицу поиска. Таким образом, вы можете иметь удобочитаемость для человека и защитить свой композит PKот аномалий вставки / обновления (добавление несуществующего домена).
Yemet
1
textпредпочтительно более чем varchar. Посмотрите на depesz.com/2010/03/02/charx-vs-varcharx-vs-varchar-vs-text и postgresql.org/docs/current/static/datatype-character.html
pietrop,
1
« Я хочу иметь первичные ключи в таком формате, как ACC-f8kJd9xKCd. «Похоже, это работа для старого доброго композитного PRIMARY KEY .
MDCCL

Ответы:

5

С помощью ltree

Если IPV6 работает, отлично. Он не поддерживает "ACC". ltreeделает.

Путь метки - это последовательность из нуля или более меток, разделенных точками, например, L1.L2.L3, представляющая путь от корня иерархического дерева до определенного узла. Длина пути метки должна быть не более 65 кБ, но желательно, чтобы она была меньше 2 кБ. На практике это не главное ограничение; например, самый длинный путь метки в каталоге DMOZ ( http://www.dmoz.org ) составляет около 240 байт.

Вы бы использовали это так,

CREATE EXTENSION ltree;
SELECT replace('ACC-f8kJd9xKCd', '-', '.')::ltree;

Мы создаем образцы данных.

SELECT x, (
  CASE WHEN x%7=0 THEN 'ACC'
    WHEN x%3=0 THEN 'XYZ'
    ELSE 'COM'
  END ||'.'|| md5(x::text)
  )::ltree
FROM generate_series(1,10000) AS t(x);

CREATE INDEX ON foo USING GIST (ltree);
ANALYZE foo;


  x  |                ltree                 
-----+--------------------------------------
   1 | COM.c4ca4238a0b923820dcc509a6f75849b
   2 | COM.c81e728d9d4c2f636f067f89cc14862c
   3 | XYZ.eccbc87e4b5ce2fe28308fd9f2a7baf3
   4 | COM.a87ff679a2f3e71d9181a67b7542122c
   5 | COM.e4da3b7fbbce2345d7772b0674a318d5
   6 | XYZ.1679091c5a880faf6fb5e6087eb1b2dc
   7 | ACC.8f14e45fceea167a5a36dedd4bea2543
   8 | COM.c9f0f895fb98ab9159f51fd0297e236d

И альт ..

                                                          QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=103.23..234.91 rows=1414 width=57) (actual time=0.422..0.908 rows=1428 loops=1)
   Recheck Cond: ('ACC'::ltree @> ltree)
   Heap Blocks: exact=114
   ->  Bitmap Index Scan on foo_ltree_idx  (cost=0.00..102.88 rows=1414 width=0) (actual time=0.389..0.389 rows=1428 loops=1)
         Index Cond: ('ACC'::ltree @> ltree)
 Planning time: 0.133 ms
 Execution time: 1.033 ms
(7 rows)

Смотрите документы для получения дополнительной информации и операторов

Если вы создаете идентификаторы продукта, я бы хотел. Если вам нужно что-то для их создания, я бы использовал UUID.

Эван Кэрролл
источник
1

Что касается сравнения производительности с Bytea. Сравнение сети выполняется в 3 этапа: сначала по общим битам сетевой части, затем по длине сетевой части, а затем по всему немаскированному адресу. смотрите: network_cmp_internal

так что это должно быть немного медленнее, чем bytea, который сразу переходит в memcmp. Я выполнил простой тест для таблицы с 10 миллионами строк в поисках одной:

  • используя числовой идентификатор (целое число), это заняло у меня 1000 мс.
  • с использованием CIDR это заняло 1300 мс.
  • с помощью bytea это заняло 1250 мс.

Я не могу сказать, что есть большая разница между bytea и cidr (хотя разрыв оставался неизменным). Просто дополнительное ifутверждение - думаю, это не так уж плохо для 10-метровых кортежей.

Надеюсь, это поможет - хотелось бы услышать, что вы в итоге выбрали.

cohenjo
источник