@Dimka5

Как в PostgreSQL сделать уникальный идентификатор среди всех таблиц базы (глобально)?

У меня есть n-ое количество таблиц, в каждой из них есть столбец "идентификатор".
Надо, что бы добавляя в любую из этих таблиц запись, значение поля "идентификатор" не повторялось среди других таблиц базы.
Этот идентификатор я присваиваю сам из кода, т.е. это моё значение получившееся по моим правилам (что отличается от этого вопроса).

Конечно я могу сделать в лоб из кода, сгенерировать идентификатор, получить все таблицы, во всех проверить все записи(или просто попробовать вставить запись, а так как поле уникально база сообщит о совпадении, а потом удалить), затем если идентификатор ни с чем не совпал вставить в нужную таблицу. Но это всё костыли.
Могу ли я сделать это средствами PostgreSQL, а не из кода?
Это называется глобальный индекс как тут?
Мне нужны триггеры?
Куда смотреть? Что читать? Где в документации? Есть готовое решение?

ДОПОЛНЕНИЕ: UUID и GUID не подходят (по формату нужны только цифры, должен быть виден порядок без преобразований просто посмотрев на два идентификатора)

ДОПОЛНЕНИЕ 2:SEQUENCE не подходит так как nextval прибавляет 1, а мой идентификатор это время в наносекундах(но это неважно по тому что точность может быть меньше или больше), важно что следующее значене не просто +1, а по результату работы моего алгоритма. setval так же не подходит так как не проверяет на уникальность на уровне всех таблиц, а только на уровне текущей в комбинации с PRIMARY KEY у колонки.
  • Вопрос задан
  • 142 просмотра
Решения вопроса 1
Athanor
@Athanor
Лайк + Решение: не жмись, нажми
Здравствуйте. То, о чем вы говорите, называется GUID, но какого-то унифицированного одного алгоритма его генерации для всех нет. Но в postgres есть расширения, которые могут вам помочь и вам не придется генерить его на уровне приложения.
Эта суперкороткая статья может быть вам полезна: https://postgrespro.ru/docs/postgrespro/9.5/dataty... и дать начальный стимул и понимание куда копать дальше. Удачи )

Update (29.01.20):

Сразу к делу и примерам. Сделать это можно, например, так:

-- Начнем со схемы данных. На всякий случай проиллюстрирую как может выглядеть идея генерации guid базой, с которой
-- я в самом начале и начал. Но вам, наверное, не будет смысла эту функцию использовать, т.к. сценарий использования
-- у вас другой.
--
-- Обратите также внимание, что я поменял тип на BIGINT. У меня была гипотеза, что вы используете TEXT, т.к., возможно,
-- не знали про BIGINT. Недостаток TEXT в том, что Postgres не построит по нему индекс и выборки по id будут все
-- медленнее и медленнее. Я бы порекомендовал сразу отрефакторить все так, чтобы использовался именно BIGINT.
--
-- Все поля также назвал английскими словами, т.к. это можно считать индустриальным стандартом. NOT NULL добавил
-- по наитию: логика подсказывает, что PRIMARY KEY у каждой таблице обязателен, как и данные. Просто для полноты.

-- Начнем с функции, которая будет генерировать нам ID для примера.

CREATE OR REPLACE FUNCTION guid()
    RETURNS BIGINT AS
$BODY$
BEGIN
    -- Количество секунд с начала эпохи Линукса и домножаем на какой-то множитель, чтобы увеличить точность
    -- и получить 1580307917143.431 вместо 1580307917.143431
    RETURN CAST(EXTRACT(EPOCH FROM NOW()) * 1000 AS BIGINT);
END;
$BODY$
    LANGUAGE 'plpgsql' VOLATILE;

-- Теперь перейдем к самой схеме данных и создадим ее.

CREATE TABLE users (
   id BIGINT PRIMARY KEY NOT NULL DEFAULT guid(),
   pseudonym TEXT NOT NULL
);

CREATE TABLE posts (
   id BIGINT PRIMARY KEY NOT NULL DEFAULT guid(),
   content TEXT NOT NULL
);

CREATE TABLE comments (
  id BIGINT PRIMARY KEY NOT NULL DEFAULT guid(),
  text TEXT NOT NULL
);

-- Для того чтобы повесить CONSTRAINT на id целевых таблиц, понадобится сделать VIEW, котоый будет содержать все
-- id из всех этих таблиц, а также функцию, которую мы сможем использовать для CONSTRAINT. По сути, делаем то же,
-- что делали бы на уровне приложения, но на уровне БД.

CREATE OR REPLACE VIEW all_ids AS
SELECT id FROM users UNION
SELECT id FROM posts UNION
SELECT id FROM comments;

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

CREATE OR REPLACE FUNCTION is_unique_id (BIGINT)
    RETURNS BOOLEAN AS 'SELECT CASE WHEN
                                   (SELECT 1
                                    FROM all_ids
                                    WHERE  id = $1) > 0
                        THEN FALSE ELSE TRUE END'
    LANGUAGE 'sql' WITH  (isstrict);

-- Осталось только повесить CONSTRAINT

ALTER TABLE users ADD CONSTRAINT id CHECK (is_unique_id(id));
ALTER TABLE posts ADD CONSTRAINT id CHECK (is_unique_id(id));
ALTER TABLE comments ADD CONSTRAINT id CHECK (is_unique_id(id));

-- А теперь внимание. Теперь вам придется быть очень внимательным при добавлении таблиц, в рамках которых id должен
-- быть уникален. При добавлении новой таблцы будет необходимо:
--   1. Пересоздать VIEW, дополнив запрос новыми таблицами.
--   2. Не забыть повесить аналогичный CONSTRAINT на новую таблицу.
--
-- Также обратите внимание, что при вставке новой записи в любую из таблиц будет проверяться весь созданный VIEW
-- и очень важно чтобы это был не полнотекстовый поиск, а работали индексы, поэтому так важно отрефакторить все в BIGINT.

-- Пришло время тестирования. Вставляем данные.

INSERT INTO users (pseudonym) VALUES ('Первый');
INSERT INTO users (pseudonym) VALUES ('Второй');

INSERT INTO posts (content) VALUES ('О том как надо');
INSERT INTO posts (content) VALUES ('О том как не надо');

INSERT INTO comments (text) VALUES ('Я думаю что решение...');
INSERT INTO comments (text) VALUES ('Хорошо я пропробую сделать...');

-- И глянем что получилось.

SELECT * FROM users;
-- 1580326610797	Первый
-- 1580326611809	Второй

SELECT * FROM posts;
-- 1580326613690	О том как надо
-- 1580326613712	О том как не надо

SELECT * FROM comments;
-- 1580326613779	Я думаю что решение...
-- 1580326613797	Хорошо я пропробую сделать...

-- Время X: тестируем нашу проверку, пытаясь вставить в таблицу users id из таблицы comments:

INSERT INTO users (id, pseudonym) VALUES (1580326613779, 'tiabc');
-- [23514] ERROR: new row for relation "users" violates check constraint "id" Detail: Failing row contains (1580326613779, tiabc)

-- Profit!


Что хочу сказать с точки зрения проектирования архитектуры и вообще. Использование любого нового инструмента должно быть обосновано. Как правило, сложная схема БД ведет к сложностям в поддержке и к тому, что какие-то вещи забывают обновляться, в отличие от уровня приложения.

В вашем же случае, честно говоря, схему очень сильно хочется упростить, а не усложнить. Хочется добавить поле created_at с DEFAULT CURRENT_TIMESTAMP() и хочется добавить реляционные связи. Либо же в принципе уйти на нереляционную БД и задать структуру сущностей там (что опять же должно быть обосновано).

И дальше именно на уровне приложения делать эту выборку, т.к. это упростит поддержку кода, а с точки зрения трудозатрат ваших и БД при выборках и вставках будет то же самое.

Помимо этого, поскольку вы делаете процессинг на основе id и его порядковых номеров, не могу не порекомендовать обратить внимание на одни из базовых принципов проектирования ПО. Конечно, вся картина не видна, но я тут вижу нарушение буквы S (Single Responsibility), что id у вас и за порядок отвечает, и за уникальность (да еще и между несколькими таблицами). Хочется как-то их развязать. Со временем жизни продукта это часто оказывается полезно.

Опять же, беспокойств о том, что вы хотите достичь, достаточно много, но если отвечать именно конкретно на ваш вопрос и давать дополнительно какие-то рекомендации, то как-то так )

Желаем удачи, образования и всех благ )

С уважением,
Иван Томилов
CEO of Athanor
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы
23 февр. 2020, в 21:16
30000 руб./за проект
23 февр. 2020, в 20:49
3000 руб./за проект
23 февр. 2020, в 20:19
3000 руб./за проект