D
D
Dimka52020-01-27 18:46:03
PostgreSQL
Dimka5, 2020-01-27 18:46:03

How to make a unique identifier among all database tables in PostgreSQL (globally)?

I have nth number of tables, each of them has an "id" column.
It is necessary that adding a record to any of these tables, the value of the "identifier" field is not repeated among other tables of the database.
I assign this identifier myself from the code, i.e. this is my value obtained according to my rules (which is different from this question).

Of course, I can do it directly from the code, generate an identifier, get all tables, check all records in all (or just try to insert a record, and since the field is unique, the base will report a match, and then delete it), then if the identifier is nothing matched to insert into the desired table. But these are all crutches.
Can I do this with PostgreSQL instead of code?
It's called a global index like here ?
Do I need triggers?
Where to look? What to read? Where in the documentation? Is there a ready solution?

ADDITION: UUID and GUID are not suitable (only numbers are needed in the format, the order should be visible without transformations just by looking at the two identifiers)

ADDITION 2: SEQUENCE is not suitable because it nextvaladds 1, and my identifier is time in nanoseconds (but this that the accuracy can be less or more), it is important that the next value is not just +1, but according to the result of my algorithm. setvalalso not suitable because it does not check for uniqueness at the level of all tables, but only at the level of the current one in combination with PRIMARY KEY at the column.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
A
Athanor, 2020-01-27
@Dimka5

Hello. What you are talking about is called a GUID, but there is no unified single algorithm for generating it for everyone. But there are extensions in postgres that can help you and you don't have to generate it at the application level.
This super short article may be useful to you: https://postgrespro.ru/docs/postgrespro/9.5/dataty... and give you an initial incentive and understanding of where to dig further. Good luck)
Update (29.01.20) :
Straight to the point and examples. You can do this, for example, like this:

-- Начнем со схемы данных. На всякий случай проиллюстрирую как может выглядеть идея генерации 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!

What I want to say from the point of view of architecture design and in general. The use of any new tool must be justified. As a rule, a complex database schema leads to difficulties in maintaining and to the fact that some things are forgotten to be updated, in contrast to the application layer.
In your case, to be honest, I really want to simplify the scheme, not complicate it. I want to add a created_at field with DEFAULT CURRENT_TIMESTAMP() and I want to add relational relationships. Or, in principle, go to a non-relational database and set the entity structure there (which, again, should be justified).
And then it is at the application level to make this selection, because this will simplify code maintenance, and in terms of labor costs for you and the database, it will be the same during selections and insertions.
In addition, since you are processing based on the id and its serial numbers, I can not help but recommend paying attention to one of the basic principles of software design . Of course, the whole picture is not visible, but here I see a violation of the letter S (Single Responsibility), that your id is responsible for both order and uniqueness (and even between several tables). I would like to untie them somehow. Over the life of the product, this is often useful.
Again, there are a lot of worries about what you want to achieve, but if you answer specifically your question and give some additional recommendations, then something like this )
We wish you good luck, education and all the best )
Best regards,
Ivan Tomilov
CEO of Athanor

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question