Пошук уроків, статей та іншого контенту
Порівняйте UUID та integer за розміром, індексацією, безпекою, масштабуванням і зручністю інтеграцій.
У PostgreSQL для ідентифікатора запису найчастіше використовують:
integer — 32-бітове ціле число;
bigint — 64-бітове ціле число;
uuid — 128-бітовий універсальний ідентифікатор.
Найтиповіший приклад із цілим числом:
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);Приклад із UUID:
CREATE TABLE users (
id uuid PRIMARY KEY,
email text NOT NULL UNIQUE
);Обидва варіанти коректні. Вибір залежить від способу генерації ідентифікаторів, масштабу системи, вимог до інтеграцій і того, чи можна відкрито показувати ID клієнту.
Фізичний розмір типів у PostgreSQL:
integer — 4 байти;
bigint — 8 байт;
uuid — 16 байт.
integer може зберігати значення приблизно від -2,1 мільярда до 2,1 мільярда. Для ідентифікаторів зазвичай достатньо додатного діапазону до 2 147 483 647.
bigint має значно більший діапазон — до приблизно 9,22 × 10^18 додатних значень. Для більшості централізованих систем цього достатньо на дуже тривалий час.
UUID складається зі 128 бітів, або приблизно 3,4 × 10^38 можливих значень. На практиці випадкові UUID мають настільки великий простір, що зіткнення є вкрай малоймовірним.
Важливо розрізняти:
розмір значення всередині PostgreSQL;
розмір значення під час передачі через API.
UUID займає 16 байт у базі, але у стандартному текстовому форматі зазвичай має 36 символів:
550e8400-e29b-41d4-a716-446655440000Числовий ID, наприклад 48152, у JSON або URL коротший і займає менше місця.
Первинний ключ автоматично створює індекс. Для обох типів PostgreSQL зазвичай використовує B-tree:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
total numeric(12, 2) NOT NULL
);Індекс за bigint зазвичай:
займає менше місця, ніж індекс за uuid;
швидше порівнює значення;
краще використовує кеш процесора;
містить більше записів на одній сторінці індексу.
UUID теж добре індексується, але його значення вдвічі більші за bigint. Для великих таблиць це може призвести до:
більшого розміру індексів;
більшої кількості операцій читання з диска;
більшого тиску на кеш;
трохи дорожчих порівнянь ключів.
Це не означає, що UUID повільний. У багатьох застосунках різниця непомітна на фоні мережевих запитів, обробки бізнес-логіки та інших індексів. Але на дуже великих таблицях або за високого навантаження компактні числові ключі можуть бути вигіднішими.
Послідовні числові ідентифікатори мають вигляд:
1, 2, 3, 4, 5, ...Нові значення зазвичай додаються в кінець індексу. Це добре для локальності даних і зменшує кількість розділень сторінок індексу.
Випадкові UUID, наприклад UUID версії 4, розподілені по всьому діапазону значень. Вставки можуть відбуватися в різні частини B-tree-індексу. На великих обсягах це може збільшувати кількість операцій із індексом і фрагментацію.
Тому за однакової схеми вставок послідовний bigint часто ефективніший за випадковий uuid.
Однак це компроміс, а не абсолютне правило. UUID може бути правильним вибором, якщо переваги розподіленої генерації важливіші за додатковий розмір індексу.
Для числових ключів у сучасному PostgreSQL варто використовувати identity-колонки:
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);PostgreSQL сам генерує значення:
INSERT INTO products (name)
VALUES ('Keyboard'), ('Monitor');
SELECT * FROM products;GENERATED ALWAYS AS IDENTITY не дозволяє випадково передати власне значення без явної вказівки. Для більшості таблиць це безпечна поведінка.
Можна використовувати і GENERATED BY DEFAULT AS IDENTITY, якщо застосунку іноді потрібно передавати власні значення:
CREATE TABLE imported_products (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text NOT NULL
);Послідовність чисел не гарантує:
відсутності пропусків;
відповідності часу створення;
відсутності повторного використання значень між різними базами.
Пропуски з'являються, наприклад, після відкату транзакції або видалення запису. Це нормальна властивість sequence і не повинно бути проблемою для технічного ID.
UUID можна генерувати в застосунку до виконання SQL або на рівні бази даних. Наприклад, нижче UUID передається під час вставки:
CREATE TABLE api_tokens (
id uuid PRIMARY KEY,
token_hash text NOT NULL
);
INSERT INTO api_tokens (id, token_hash)
VALUES (
'550e8400-e29b-41d4-a716-446655440000',
'hash-of-token'
);У робочому застосунку UUID часто генерується бібліотекою мови програмування або функцією PostgreSQL. Конкретний спосіб залежить від версії PostgreSQL і підключених розширень, тому його потрібно узгодити з інфраструктурою проєкту.
Головна властивість UUID — його можна генерувати незалежно в різних процесах, сервісах і базах даних без спільної sequence.
Наведений приклад створює тимчасові таблиці, заповнює їх і запускає пошук за первинними ключами:
CREATE TEMP TABLE integer_items (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
CREATE TEMP TABLE uuid_items (
id uuid PRIMARY KEY,
name text NOT NULL
);
INSERT INTO integer_items (name)
SELECT 'item-' || value
FROM generate_series(1, 10000) AS values(value);
INSERT INTO uuid_items (id, name)
SELECT md5('item-' || value)::uuid, 'item-' || value
FROM generate_series(1, 10000) AS values(value);
-- План і фактичне виконання пошуку за bigint.
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM integer_items
WHERE id = 5000;
-- План і фактичне виконання пошуку за uuid.
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM uuid_items
WHERE id = md5('item-5000')::uuid;У реальному навантажувальному тесті варто порівнювати:
однаковий обсяг даних;
однакові запити;
однаковий тип диска і налаштування PostgreSQL;
вставки, читання та оновлення;
розмір таблиць та індексів.
На маленькій таблиці PostgreSQL може вибрати послідовне сканування замість індексу. Це нормально: оптимізатор оцінює, що для маленького обсягу даних прочитати таблицю цілком дешевше.
Числові ID легко перебирати:
/api/users/100
/api/users/101
/api/users/102Це може розкрити:
приблизну кількість записів;
порядок створення ресурсів;
факт існування конкретних ID.
UUID, особливо випадковий, важче вгадати:
/api/users/550e8400-e29b-41d4-a716-446655440000Тому UUID зручніший як зовнішній ідентифікатор у публічних URL або API.
Але UUID не є механізмом авторизації. Якщо користувач знає UUID ресурсу, це не повинно автоматично давати йому доступ. Сервер усе одно має перевіряти права:
користувач може читати замовлення?а не лише:
замовлення має правильний UUID?Навіть числовий ID можна безпечно використовувати у відкритому API, якщо правильно реалізовано контроль доступу. UUID лише зменшує передбачуваність ідентифікаторів.
Одна PostgreSQL-база легко генерує послідовні числа через sequence. Це простий і продуктивний варіант для централізованої системи.
Проблеми можуть виникнути, якщо кілька незалежних баз генерують ID окремо:
у кожній базі можуть з'явитися однакові значення;
потрібно розподіляти діапазони;
потрібна окрема система генерації;
об'єднання даних стає складнішим.
Для цього можна використовувати bigint із заздалегідь розподіленими діапазонами, але така схема потребує додаткової координації.
Різні сервіси можуть генерувати UUID незалежно:
сервіс замовлень створює ID без запиту до центральної бази;
кілька екземплярів застосунку не конкурують за одну sequence;
дані з різних баз легше об'єднувати;
попередньо створені об'єкти можна ідентифікувати до вставки в базу.
Це особливо корисно для розподілених систем, асинхронних процесів та імпорту даних.
Ціна такої зручності:
більші ключі та індекси;
складніші для читання значення;
потенційно менш ефективні вставки випадкових UUID;
більше місця у зовнішніх API.
UUID зручний, коли ідентифікатор проходить через кілька систем. Наприклад, подія може містити ID замовлення:
{
"event": "order.created",
"orderId": "550e8400-e29b-41d4-a716-446655440000"
}Якщо окремі середовища, регіони або сервіси створюють записи незалежно, UUID зменшує ризик конфліктів.
Ціле число зручніше, коли:
ID використовується переважно всередині однієї бази;
важливі компактні URL;
таблиці дуже великі;
потрібні максимально прості SQL-запити;
записи створюються в одному централізованому сховищі.
Іноді застосунок використовує два ідентифікатори:
внутрішній bigint як первинний ключ і зовнішні ключі;
окремий UUID для публічних URL та API.
Наприклад:
CREATE TABLE documents (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
public_id uuid NOT NULL UNIQUE,
title text NOT NULL
);Це дає компактні внутрішні зв'язки та непрогнозований зовнішній ідентифікатор. Але схема стає складнішою: потрібно підтримувати два ID і правильно обирати, який із них використовувати в кожному контексті.
Тип зовнішнього ключа має відповідати типу первинного ключа:
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE invoices (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers(id),
amount numeric(12, 2) NOT NULL
);Для UUID:
CREATE TABLE uuid_customers (
id uuid PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE uuid_invoices (
id uuid PRIMARY KEY,
customer_id uuid NOT NULL REFERENCES uuid_customers(id),
amount numeric(12, 2) NOT NULL
);Не варто змішувати integer, bigint і uuid для одного логічного ідентифікатора. Невідповідність типів ускладнює запити, міграції та підтримку зовнішніх ключів.
Обирайте integer, якщо:
кількість записів гарантовано вкладається в його діапазон;
таблиця невелика або середня;
потрібен мінімальний розмір ключа;
ID генерується в одній базі.
Обирайте bigint, якщо:
таблиця може стати дуже великою;
потрібні послідовні та компактні ключі;
кілька мільярдів значень недостатньо;
важлива продуктивність індексів і зв'язків.
Обирайте uuid, якщо:
ID генерують кілька сервісів або баз;
записи потрібно об'єднувати з різних джерел;
ідентифікатор відкривається через API;
небажано розкривати послідовність створення записів;
розмір індексу менш важливий за незалежну генерацію ID.
Не варто обирати UUID лише через припущення, що він автоматично «безпечніший». Безпека залежить від авторизації, перевірки доступу та захисту секретів.
integer без оцінки майбутнього обсягуЯкщо таблиця може перевищити приблизно два мільярди значень, одразу використовуйте bigint. Міграція первинного ключа та всіх зовнішніх ключів після досягнення ліміту може бути складною і довгою.
Невгадуваний ID не замінює перевірку прав. Сервер має перевіряти, чи має поточний користувач доступ до ресурсу.
Послідовний ID не є номером документа або бухгалтерським номером. Відкати транзакцій і видалення можуть створювати пропуски.
Випадковий UUID не містить корисного хронологічного порядку. Для сортування за часом зберігайте окрему колонку:
CREATE TABLE messages (
id uuid PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
body text NOT NULL
);textЯкщо значення має бути UUID, використовуйте тип uuid, а не text. PostgreSQL перевірятиме формат, а значення та індекси зберігатимуться ефективніше.
У команді потрібно визначити:
де генерується UUID;
яка версія UUID використовується;
чи має колонка значення за замовчуванням;
як ID передається між сервісами.
Непослідовний підхід ускладнює міграції та тестування.
integer займає 4 байти, bigint — 8, uuid — 16.
Числові ключі компактніші та зазвичай ефективніші для індексів.
bigint — практичний вибір для великих централізованих систем.
UUID можна генерувати незалежно в різних сервісах і базах.
Випадкові UUID можуть гірше використовувати локальність B-tree-індексу.
UUID складніше перебирати у відкритому API, але він не замінює авторизацію.
Для зовнішніх ключів потрібно використовувати той самий тип, що й для первинного ключа.
Вибір між UUID і цілим числом — це компроміс між компактністю, продуктивністю, незалежною генерацією та вимогами інтеграцій.