UNION или UNION ALL: почему из финансовой выгрузки исчез одинаковый платёж

В отчёте стало на 500 рублей меньше, чем в платёжных системах. Запрос собирает два потока через UNION, а в результат выводит только дату и сумму. В обоих потоках есть платёж на 500 рублей за один день. Для бухгалтера это две операции, для UNION — две одинаковые строки результата, из которых он оставит одну.

Что именно сравнивает UNION

UNION удаляет повторы после того, как каждый SELECT уже выбрал свои колонки. Он не знает про идентификатор платежа, если тот не выбран. Строки с одинаковыми датой и суммой станут неразличимыми, даже если пришли от разных клиентов. UNION ALL сохраняет все строки обоих запросов. Для потока фактов это отправная точка: сначала не теряем операции, затем отдельно решаем, есть ли технические повторы.

Но заменить одно слово и сразу суммировать тоже опасно. Одна система может повторно доставить тот же платёж. Тогда UNION ALL честно сохранит и вторую доставку, а отчёт удвоит сумму. Нужен ключ операции. Если ID уникален только внутри источника, ключом служит пара source, payment_id.

Два платежа и одна повторная доставка

В примере магазин прислал платёж 42 дважды, а приложение прислало свой платёж с тем же локальным номером. Все три строки имеют одинаковые дату и сумму. delivery_no показывает порядок получения копий одного события. Объединяем их без потерь, нумеруем доставки внутри пары «источник — ID» и берём первую:

WITH shop(payment_id, paid_on, amount, delivery_no) AS (
  VALUES (42, '2026-10-20', 500, 1),
         (42, '2026-10-20', 500, 3)
), app(payment_id, paid_on, amount, delivery_no) AS (
  VALUES (42, '2026-10-20', 500, 2)
), combined AS (
  SELECT 'shop' AS source, payment_id, paid_on, amount, delivery_no FROM shop
  UNION ALL
  SELECT 'app', payment_id, paid_on, amount, delivery_no FROM app
), numbered AS (
  SELECT *, ROW_NUMBER() OVER (
    PARTITION BY source, payment_id ORDER BY delivery_no
  ) AS rn
  FROM combined
)
SELECT source, payment_id, amount,
       SUM(amount) OVER () AS total_amount
FROM numbered
WHERE rn = 1
ORDER BY source;

Результат содержит два платежа, shop/42 и app/42, а итоговая сумма в каждой результирующей строке равна 1000. Повтор shop/42 не участвует в сумме. Если бы мы выбрали для UNION только paid_on и amount, он оставил бы одну строку на 500. Разница между 500, 1000 и 1500 здесь объясняется ключом, а не случайной настройкой SQL.

Выбор первой доставки уместен, когда повторная доставка содержит то же самое событие. Если у одного payment_id встречаются разные суммы или статусы, это уже конфликт данных. Выводить последнюю запись без правила нельзя: надо выяснить, означает ли новая строка исправление, отмену или ошибку загрузки. Для сверки сохраняйте источник, ID и, при возможности, ID доставки; агрегированная сумма сама не объяснит расхождение.

Как перенести схему в рабочую выгрузку

Приведите обе ветки UNION ALL к одному набору колонок и единицам измерения: валюта и тип суммы должны означать одно и то же. После объединения сравните количество уникальных ключей и сумму с каждым источником по отдельности. Затем проверьте три случая: два разных ID с одинаковыми датой и суммой; повтор одного ID; один ID с разными значениями. Первые два пример обрабатывает, третий требует отдельного решения по контракту платежей.

UNION всё же полезен, когда нужны именно уникальные значения, например список валют, встреченных в нескольких системах. В финансовой выгрузке строка представляет операцию, поэтому её идентичность должна быть явной. Если источники могут содержать один и тот же глобальный платёж с разными локальными ID, понадобится таблица соответствий или глобальный ключ, а не только пара source, payment_id.

Следующий уровень проверки отчётов

В платном курсе «Продвинутый SQL» вы разберёте размножение строк, дедупликацию, сверку финансовых отчётов и напишете запросы для аналитической витрины интернет-магазина. Автоматическая проверка задач использует несколько наборов данных, поэтому ошибка на одинаковых суммах или повторных записях проявится в практике. Откройте урок «Дедупликация и выбор последней записи» и посмотрите программу курса.

Для контроля итогов пригодится разбор сверки двух SQL-отчётов по ключам; другие темы собраны в разборах аналитических запросов.