У меня есть таблица сообщений, таблица хэштегов и таблица, связывающая сообщения с хэштегами, например:
CREATE TABLE posts(
id SERIAL PRIMARY KEY,
post_data VARCHAR(128) NOT NULL
);
CREATE TABLE hashtags (
id SERIAL PRIMARY KEY,
value VARCHAR(128) NOT NULL
);
CREATE TABLE post_hashtags(
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL REFERENCES posts(id),
hashtag_id INTEGER NOT NULL REFERENCES hashtag(id)
);
INSERT INTO posts(post_data) VALUES ('post1');
INSERT INTO hashtags (value) VALUES ('hashtag1'), ('hashtag2'), ('hashtag3');
-- At this point I might want to add links between post1 and existing hashtags as well as possibly new ones
Всякий раз, когда пользователь публикует сообщение, используя несколько хэш-тегов, я хочу:
posts
и получите идентификатор.hashtags
для несуществующих хэштегов и получите их идентификаторы.post_hashtags
с идентификаторами публикации и хэштега.Прямо сейчас я могу справиться с этим на стороне сервера, но это, очевидно, ужасная производительность: вставить сообщение и получить идентификатор; для каждого хештега вставьте в hashtags
, если его нет, получите идентификатор; затем вставьте в post_hashtags
. Множество вызовов к БД, которые, я думаю, можно было бы упростить, но на данный момент мне не хватает навыков SQL.
Хорошо, я добавил код SQL, который можно запустить для настройки состояния. Меня будет интересовать любой запрос, независимо от версии Postgres. Как я уже упоминал в посте, у меня есть только серверный код, но пока нет SQL.
Столбец id
в post_hashtags
не обязателен; однако для (post_id, hashtag_id)
должно быть уникальное ограничение, чтобы исключить избыточные строки.
Вы можете сделать это безопасно и эффективно с помощью одного запроса, используя изменяющие данные CTE и UPSERT для тегов.
Конечно, вам нужно ограничение UNIQUE
или PRIMARY KEY
для hashtags (value)
. (И у вас должен быть один на post_hashtags (post_id, hashtag_id)
.)
WITH input(post_data, tags) AS ( -- provide single data row with array of tags
VALUES ('post2', '{hashtag1, hashtag2, hashtag4}'::text[]) -- single post!
)
, tag_set AS ( -- unnest tags - may be empty/missing (?)
SELECT unnest(i.tags) AS value
FROM input i
)
, ins_p AS (
INSERT INTO posts (post_data)
SELECT i.post_data
FROM input i
RETURNING id AS post_id
)
, ins_h AS (
INSERT INTO hashtags (value)
SELECT t.value
FROM tag_set t
WHERE NOT EXISTS (SELECT FROM hashtags h WHERE h.value = t.value) -- optional to avoid burning lots of serial IDs
ON CONFLICT (value) DO NOTHING
RETURNING id AS hashtag_id
)
INSERT INTO post_hashtags
( post_id, hashtag_id)
SELECT p.post_id, t.hashtag_id
FROM ins_p p
CROSS JOIN ( -- only if actual tags were entered
TABLE ins_h -- new tags
UNION ALL
SELECT h.id AS hastag_id -- pre-existing tags
FROM tag_set t
JOIN hashtags h USING (value)
) t
RETURNING *;
Это безопасно даже при большой нагрузке на одновременную запись.
Остаются два возможных крайних случая:
Параллельная транзакция может создать тот же новый хэштег, но затем откатиться, что приведет к отсутствию записи в таблице hashtags
для этого запроса, что приведет к возникновению исключения.
Ранее существовавший хэштег может быть удален в ходе параллельной транзакции между его поиском здесь и вставкой в post_hashtags
. Опять же, вызывая исключение. Крайне маловероятно, но возможно.
Вы можете просто повторно запустить этот запрос, если возникнет одна из этих двух маловероятных проблем.
Или вы охватываете оба крайних случая в одном запросе. Я не стал заходить так далеко. Инструкции и подробное объяснение смотрите здесь:
Связанный:
По крайней мере, предоставьте правильные определения таблиц, вашу версию Postgres и образец ввода. Кроме того, все, что было до сих пор, даже если это не работает.