Разделить число на несколько диапазонов

#sql #postgresql #analytics

#sql #postgresql #аналитика

Вопрос:

Я столкнулся с проблемой разделения метрики на несколько диапазонов. Чтобы дать вам некоторый контекст, давайте рассмотрим этот пример, где у нас есть определенное количество заказов на одного клиента. Теперь клиент может заказать n количество продуктов. Давайте предоставим клиенту определенную скидку в зависимости от количества заказов. Скидки предлагаются на основе многоуровневой модели. Я оставляю несколько категорий продуктов, чтобы упростить его. Вот несколько примеров таблиц.

Таблица заказов

 Customer    |   order_no
----------------------------
Customer1   |   400
Customer2   |   1200
Customer3   |   40
Customer4   |   2000
Customer5   |   700
  

Таблица многоуровневых цен

 Tier   | lower_th | higer_th | price |
--------------------------------------
Tier1  | 0        | 250      | 50    |
TIer2  | 251      | 500      | 45    |
Tier3  | 501      | 1000     | 40    |
TIer4  | 1001     | 10000    |  30   |
  

Пример1: я хочу иметь возможность взимать с Customer1 50 долларов США за 250 заказов и 45 долларов США за остальные 150 продуктов из общего количества 400.

Пример2: я хочу иметь возможность взимать с Customer5 50 долларов за 250 заказов и 45 долларов за еще 250 и 40 долларов за остальные 200 продуктов из общего количества 700.

Как мне добиться этого в PostgreSQL? Мой вывод должен быть следующим для Customer1. Каков наилучший способ разделить общее количество заказов и присоединить его к уровням ценообразования, чтобы получить соответствующую сумму?

 Customer  | order_no | charges |
--------------------------------
Customer1 | 250      | 50      |
Customer1 | 150      | 45      |
  

Комментарии:

1. Laurenz Albe спасибо за редактирование вопроса. a_horse_with_no_name спасибо за исправление таблицы.

Ответ №1:

Вы можете думать о своих уровнях как об интервалах.

Два интервала [a1, b1] и [a2, b2] пересекаются, когда

 a1 <= b2 AND b1 >= a2
  

Количество заказов — это еще один интервал, который всегда начинается с 1.

Ваши два интервала: уровни [lower_th, higer_th] и порядки [1, order_no] .

Запрос представляет собой простое соединение с использованием этого выражения пересечения:

 SELECT *
    ,CASE WHEN O.order_no > T.higer_th
    THEN T.higer_th - T.lower_th   1 -- full tier
    ELSE O.order_no - T.lower_th   1
    END AS SplitOrderNumbers
FROM
    Orders AS O
    INNER JOIN Tiers AS T
        -- ON 1 <= T.higer_th AND O.order_no >= T.lower_th
        ON O.order_no >= T.lower_th
ORDER BY
    O.Customer
    ,T.lower_th
;
  

На самом деле вам не нужна 1 <= T.higer_th часть, потому что она всегда истинна, поэтому выражение становится простым O.order_no >= T.lower_th .

Кроме того, обычно лучше хранить интервалы как [closed; open) . Обычно это упрощает арифметику, аналогично тому, почему большинство языков программирования имеют индексы массива, начинающиеся с 0, а не с 1. Ваши интервалы кажутся [closed; closed] . В этом случае вам нужно установить lower_th значение 1 , а не 0 и иметь 1 в вычислениях.

При такой корректировке выборки данных этот запрос выдает следующий результат:

  ----------- ---------- ------- ---------- ---------- ------- ------------------- 
| Customer  | order_no | Tier  | lower_th | higer_th | price | SplitOrderNumbers |
 ----------- ---------- ------- ---------- ---------- ------- ------------------- 
| Customer1 |      400 | Tier1 |        1 |      250 | 50.00 |               250 |
| Customer1 |      400 | Tier2 |      251 |      500 | 45.00 |               150 |
| Customer2 |     1200 | Tier1 |        1 |      250 | 50.00 |               250 |
| Customer2 |     1200 | Tier2 |      251 |      500 | 45.00 |               250 |
| Customer2 |     1200 | Tier3 |      501 |     1000 | 40.00 |               500 |
| Customer2 |     1200 | Tier4 |     1001 |    10000 | 30.00 |               200 |
| Customer3 |       40 | Tier1 |        1 |      250 | 50.00 |                40 |
| Customer4 |     2000 | Tier1 |        1 |      250 | 50.00 |               250 |
| Customer4 |     2000 | Tier2 |      251 |      500 | 45.00 |               250 |
| Customer4 |     2000 | Tier3 |      501 |     1000 | 40.00 |               500 |
| Customer4 |     2000 | Tier4 |     1001 |    10000 | 30.00 |              1000 |
| Customer5 |      700 | Tier1 |        1 |      250 | 50.00 |               250 |
| Customer5 |      700 | Tier2 |      251 |      500 | 45.00 |               250 |
| Customer5 |      700 | Tier3 |      501 |     1000 | 40.00 |               200 |
 ----------- ---------- ------- ---------- ---------- ------- ------------------- 
  

Комментарии:

1. Большое вам спасибо за ответ. Это прекрасно сработало для моей проблемы. Извините за поздний ответ, я отсутствовал некоторое время.

Ответ №2:

Для данных о ценах я бы использовал такую таблицу, чтобы упростить обслуживание данных

 create table pricing_data
(
   high_limit int, 
   price numeric
);
  

Представление предоставит вам интервалы, необходимые для этого, с помощью оконной функции:

 create view pricing as
  select coalesce(lag(high_limit) over (order by high_limit), 0) as last_limit,
         high_limit, price
    from pricing_data;
  

Это упрощает разбивку на уровни ценообразования:

 select o.customer, 
       least(o.order_no - p.last_limit, p.high_limit - p.last_limit) as order_no,
       p.price as charges
  from orders o
  join pricing p on p.last_limit < o.order_no
 order by o.customer, p.price desc
;
  

Результат:

 customer    order_no    charges
Customer1    250            50
Customer1    150            45
Customer2    250            50
Customer2    250            45
Customer2    500            40
Customer2    200            30
Customer3     40            50
Customer4    250            50
Customer4    250            45
Customer4    500            40
Customer4   1000            30
Customer5    250            50
Customer5    250            45
Customer5    200            40
14 rows
  

Скрипка здесь.

Комментарии:

1. … Ваш ответ в порядке … но таблица цен уже имеет верхнюю границу.

2. Большое вам спасибо за ответ. Извините за поздний ответ, я отсутствовал некоторое время.