TGViewer
Cross Join - канал о разработке Cross Join - канал о разработке @crossjoin · 3.82K subscribers
Post #276 4.29K
Узнал сегодня небольшой хак из мира Postgres. Например, у вас есть таблица с купонами, и у купонов есть некая дата устаревания valid_until. Допустим, вам надо обеспечить такой констрейнт, чтобы у одного человека не было одновременно больше одного неустаревшего купона.

Например, таблица изначально выглядит так:

CREATE TABLE coupons (
id bigint primary key generated by default as identity,
user_id bigint not null,
created_at timestamp not null,
valid_until timestamp not null
)

Думаю, всем известно, что есть ключевое слово UNIQUE, с помощью которого можно обеспечить уникальность какого-то поля/полей. Но напрямую оно нам не подойдет, так как в базе могут остаться устаревшие купоны, которые будут конфликтовать со свежими.

Также многим известно, что UNIQUE может идти с условием. Казалось бы, добавить просто к констрейнту WHERE (valid_until > NOW()) и дело с концом. Однако так нельзя: невозможно сделать констрейт по функции, которую заранее не вычислить. Т.е. WHERE (is_valid) бы прокатило, но не сравнение с NOW().

Можно выкрутиться с помощью триггера, но есть способ поэлегантнее.

В посгресе есть тип данных tsrange (диапазон времени), а также ключевое слово EXCLUDE, с помощью которого можно запретить вставку строк, где диапазоны пересекаются:

EXCLUDE USING GIST (tsrange(created_at, valid_until) WITH &&)

Но это условие действует на всю таблицу, а не в рамках юзера. Поэтому надо добавить еще одно:

EXCLUDE USING GIST (user_id WITH =, tsrange(created_at, valid_until) WITH &&)

(чтобы это работало, нужно сначала включить расширение  btree_gist: CREATE EXTENSION IF NOT EXISTS btree_gist;)

Итого, проверяем:


CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE coupons (
id bigint primary key generated by default as identity,
user_id bigint not null,
created_at timestamp not null,
valid_until timestamp not null,
EXCLUDE USING GIST (
user_id WITH =,
tsrange(created_at, valid_until) WITH &&
)
);

INSERT INTO coupons(user_id, created_at, valid_until)
VALUES
(100500, now(), now() + interval '1 day');

INSERT INTO coupons(user_id, created_at, valid_until)
VALUES
(100501, now(), now() + interval '10 day');

INSERT INTO coupons(user_id, created_at, valid_until)
VALUES
(100500, now(), now() + interval '10 day');


последний insert не пройдёт: "ERROR: conflicting key value violates exclusion constraint"
  • 👍 28
  • 🔥 10
  • 🌚 1
More from @crossjoin
  1. Sep 29, 2026😱 Отправили свое резюме на 129 вакансий на хх, а в ответ тишина .. Думаете, что дело в ры…
  2. Sep 28, 2026Слышал недавно в каком-то подкасте мысль, что Haskell плохо подходит для вайбкодинга прост…
  3. Sep 26, 2026Антон Жиянов написал мини-книгу по Go-concurrency. Это что-то вроде плотного конспекта с и…
  4. Sep 22, 2026photo post
  5. Sep 14, 2026Поможем Руслану собрать фидбек. Проект некоммерческий 👆
  6. Sep 14, 2026AGBX (Agent Box) — небольшой open-source CLI для запуска Claude Code и Codex в изолированн…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →