Как распределить скидку заказа по товарам в SQL без потерянной копейки

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

Как разобрать ситуацию

Работаем в целых копейках. Для каждой позиции вычисляем точную долю скидки, берём целую нижнюю часть и сортируем дробные остатки. Нераспределённые копейки выдаём позициям с наибольшими остатками, при равенстве используем id. Важно заранее определить, какие товары участвуют в скидке: исключённая категория не должна случайно попасть в знаменатель.

Пример и ожидаемый результат

Стоимость трёх позиций равна 33, 33 и 34 копейкам. Общая скидка — 10 копеек. Нижние части дают 3, 3 и 3, оставляя одну копейку. Она достаётся третьей позиции, поэтому итоговое распределение равно 3, 3 и 4.

WITH items(id,cents) AS(VALUES(1,33),(2,33),(3,34)), base AS(
SELECT id,10.0*cents/SUM(cents) OVER() exact 
FROM items), ranked AS(
SELECT id,CAST(exact AS INT) floor_part,ROW_NUMBER() OVER(
ORDER BY exact-CAST(exact AS INT) DESC,id) rn,SUM(CAST(exact AS INT)) OVER() assigned 
FROM base) 
SELECT id,floor_part+CASE WHEN rn<=10-assigned THEN 1 ELSE 0 END 
FROM ranked 
ORDER BY id;

Запрос возвращает (1,3), (2,3), (3,4). Сумма равна исходным десяти копейкам, а правило выбора получателя остатка воспроизводимо.

Проверка перед применением

  • Сверьте сумму распределённых скидок с общей скидкой заказа.
  • Проверьте равные остатки: ответ не должен зависеть от физического порядка строк.
  • Проверьте нулевую корзину и скидку больше допустимой суммы до запуска формулы.

Если заказ содержит одинаковый товар несколькими строками, решите, распределяется скидка по строкам или по объединённой позиции. Это влияет на последующий частичный возврат. В тестовой фикстуре полезно разделить одну позицию на две и проверить сохранение общей скидки при выбранной детализации.

Границы решения

В реальной денежной модели учитывайте переполнение целых чисел и точность выбранной СУБД. Налоги и возврат отдельной позиции должны использовать сохранённое распределение, иначе повторный расчёт по изменившейся корзине даст другие деньги.

Если запросы уже пишутся, а уверенности в кардинальности, оконных рамках и проверке результата не хватает, продолжить практику можно в нашем платном курсе

Продвинутый SQL: сложные запросы и задачи для собеседований. Перед записью сравните программу со своей задачей и подготовкой.

Техническая справка: www.sqlite.org: документация, www.sqlite.org: документация.

Следующий шаг: Сложные SQL-запросы: разборы расчётов и ошибок. По смежным вопросам: Как разбить события на сессии в SQL по паузе между действиями, Конверсия в SQL без деления на ноль и целочисленной ошибки.