Пропуски в последовательности и пользователи без заказов

Найти пропущенные значения в последовательности id и пользователей без единого заказа.

Поиск «дыр» в последовательности и «строк без связанных» — две задачи на отсутствие данных, обе решаются через сравнение или анти-джойн.

Пропуски в последовательности

Есть таблица с id 1,2,4,5,7 — каких чисел не хватает? Идея: число n — начало пропуска, если n+1 в таблице нет, но дальше значения ещё есть. Удобный приём — LEAD: сравнить значение со следующим. Если разрыв больше 1 — между ними дыра.

CREATE TABLE seq (id INTEGER PRIMARY KEY);
INSERT INTO seq (id) VALUES (1), (2), (4), (5), (7), (8);

WITH pairs AS (
    SELECT id AS cur,
           LEAD(id) OVER (ORDER BY id) AS nxt
    FROM seq
)
SELECT cur + 1 AS propusk_ot,
       nxt - 1 AS propusk_do
FROM pairs
WHERE nxt - cur > 1            -- между cur и nxt есть разрыв
ORDER BY cur;

Вывод:

3|3
6|6

Разрывы: между 2 и 4 пропущена 3 (диапазон 3..3), между 5 и 7 — 6 (диапазон 6..6). LEAD заглядывает в следующее значение, а условие nxt - cur > 1 ловит дыру. Так находят пропущенные номера счетов, билетов, заказов.

Пользователи без заказов

Это анти-джойн из раздела про JOIN, и его очень любят спрашивать. «Кто не сделал ни одного заказа» — через NOT EXISTS:

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users (name) VALUES ('Аня'), ('Борис'), ('Вера'), ('Гена');

CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER);
INSERT INTO orders (user_id) VALUES (1), (1), (3);

SELECT u.name
FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.id
)
ORDER BY u.name;

Вывод:

Борис
Гена

Заказы есть у id 1 (Аня) и 3 (Вера). Без заказов остались Борис и Гена — их и вернул NOT EXISTS. Эквивалентно работает LEFT JOIN ... WHERE o.id IS NULL.

Связанный вопрос: кто заказывал в этом месяце, но не в прошлом

Развитие темы — «ушедшие» или «новые» пользователи. Это два анти-джойна между подмножествами. Логика та же: NOT EXISTS по соответствующему срезу. Понимание анти-джойна закрывает целый класс таких вопросов.

Итог

  • Пропуски в последовательности: LEAD + условие следующее - текущее > 1.
  • Строки без связанных (пользователи без заказов): NOT EXISTS или LEFT JOIN ... IS NULL.
  • Оба — задачи «на отсутствие»; ключ к ним — анти-джойн и сравнение соседей.
Проверьте себя
1. Как найти пропуски в последовательности чисел?
AGROUP BY id
BСравнить значение со следующим через LEAD: если разрыв > 1, есть дыра
CORDER BY id DESC
DCOUNT(DISTINCT id)
2. Как найти пользователей без единого заказа?
AINNER JOIN с orders
BNOT EXISTS подзапрос по orders (или LEFT JOIN ... IS NULL)
CGROUP BY user_id
DCOUNT(*) > 0
3. К какому классу относятся обе задачи этого урока?
AЗадачи на агрегацию
BЗадачи «на отсутствие» данных — анти-джойн и сравнение соседей
CЗадачи на сортировку
DЗадачи на транзакции
Поддержать проект