如何使用 PostgreSQL 计算分层定价

How to calculate tiered pricing using PostgreSQL

我正在尝试计算某些住宿的分级费率。假设我们有 属性 的每周、半周和每日费率。

period_name | nights | rate
-------------------------------------
WEEK        | 7      | 100
HALFWEEK    | 3      | 50
DAY         | 1      | 25

我如何使用总晚数查询此数据并细分符合条件的时间段,从最长到最短?一些示例结果

10 晚

我们将 10 分成(7 天)+(3 天)。 7 天将按周费率 (100)。 3 天将按半周费率 (50)。在这里它符合 (1 WEEK @ 100) + (1 HALFWEEK @ 50)

period_name | nights | rate | num | subtotal
----------------------------------------------
WEEK        | 7      | 100  | 1   | 100
HALFWEEK    | 3      | 50   | 1   | 50

4 晚

我们将 4 分成(3 天)+(1 天)。 3 天将按半周费率 (50)。 1 天将按 DAY 费率 (25) 计算。在这里它符合 (1 HALFWEEK @ 50) + (1 DAY @ 25)

period_name | nights | rate | num | subtotal
----------------------------------------------
HALFWEEK    | 3      | 50   | 1   | 50
DAY         | 1      | 25   | 1   | 25

16 晚

我们把 16 分成(14 天)+(2 天)。 14 天将采用周率(乘以 2),(100 * 2)。 2 天将按天费率 (2 x 25)。在这里它符合 (2 WEEK @ 100) + (2 DAY @ 25)

period_name | nights | rate | num | subtotal
----------------------------------------------
WEEK        | 7      | 100  | 2   | 200
DAY         | 1      | 25   | 2   | 50

我考虑过使用 lag window 函数,但现在确定我将如何跟踪上一期间已经应用的天数。

这里你需要做的是首先触发一个SQL命令来检索所有条件并写下你的业务逻辑的功能。

例如

  1. 我会将下面的查询触发到数据库中。

    Select * 从 table_name 按晚顺序 desc

结果,我将按夜间降序排列数据,这意味着首先是 7,然后是 3,然后是 1。

  1. 我会写一个函数来记录我的业务逻辑,例如。

假设我需要查找 11 天。

我将获取第一条记录,即 7 并检查它是否为 11。

if(11 > 7){// execute this if in a loop till it's greater then 7, same for 3 & 1
    days = 11-7;
    price += price_from_db;
    package += package_from_db;
}else{
   // goto fetch next record and check the above condition with next record.
}

注意:我写的是算法而不是特定语言的代码。

您可以使用 CTE 递归查询来执行此操作。

http://sqlfiddle.com/#!17/0ac709/1

等级table(可动态扩展):

id  name       days  rate  
--  ---------  ----  ----  
1   WEEK       7     100   
2   DAYS       1     25    
3   HALF_WEEK  3     50    
4   MONTH      30    200    

天数数据:

id  num  
--  ---  
1   10   
2   31   
3   30   
4   19   
5   14   
6   108  
7   3    
8   5    
9   1    
10  2    
11  7

结果:

num_id  num  days                                             total_price  
------  ---  -----------------------------------------------  -----------  
1       10   {"MONTH: 0","WEEK: 1","HALF_WEEK: 1","DAYS: 0"}  150          
2       31   {"MONTH: 1","WEEK: 0","HALF_WEEK: 0","DAYS: 1"}  225          
3       30   {"MONTH: 1","WEEK: 0","HALF_WEEK: 0","DAYS: 0"}  200          
4       19   {"MONTH: 0","WEEK: 2","HALF_WEEK: 1","DAYS: 2"}  300          
5       14   {"MONTH: 0","WEEK: 2","HALF_WEEK: 0","DAYS: 0"}  200          
6       108  {"MONTH: 3","WEEK: 2","HALF_WEEK: 1","DAYS: 1"}  875          
7       3    {"MONTH: 0","WEEK: 0","HALF_WEEK: 1","DAYS: 0"}  50           
8       5    {"MONTH: 0","WEEK: 0","HALF_WEEK: 1","DAYS: 2"}  100          
9       1    {"MONTH: 0","WEEK: 0","HALF_WEEK: 0","DAYS: 1"}  25           
10      2    {"MONTH: 0","WEEK: 0","HALF_WEEK: 0","DAYS: 2"}  50           
11      7    {"MONTH: 0","WEEK: 1","HALF_WEEK: 0","DAYS: 0"}  100          

想法:

首先我用这个查询来计算一个值 (19) 的结果:

SELECT 
    days / 7 as WEEKS,
    days % 7 / 3 as HALF_WEEKS,
    days % 7 % 3 / 1 as DAYS 
FROM
    (SELECT 19 as days) s  

在这里您可以看到以整数除法终止的模块运算的递归结构。因为应该需要一个更通用的版本,所以我考虑了一个递归版本。使用 PostgreSQL WITH RECURSIVE 子句,这是可能的

https://www.postgresql.org/docs/current/static/queries-with.html

这就是最后的查询

WITH RECURSIVE days_per_tier(row_no, name, days, rate, counts, mods, num_id, num) AS (
    SELECT 
        row_no, 
        name, 
        days, 
        rate,
        num.num / days, 
        num.num % days, 
        num.id, 
        num.num
    FROM (
        SELECT 
            *, 
            row_number() over (order by days DESC) as row_no             -- C
        FROM 
            testdata.tiers) tiers,                      -- A
            (SELECT id, num FROM testdata.numbers) num  -- B
    WHERE row_no = 1

    UNION

    SELECT 
        days_per_tier.row_no + 1,
        tiers.name, 
        tiers.days,
        tiers.rate,
        mods / tiers.days,                                      -- D
        mods % tiers.days,                                      -- E
        days_per_tier.num_id,
        days_per_tier.num                               
    FROM 
        days_per_tier,  
        (SELECT 
            *, 
            row_number() over (order by days DESC) as row_no 
            FROM testdata.tiers) tiers 
    WHERE days_per_tier.row_no + 1 = tiers.row_no
 ) 
SELECT
    num_id,
    num,
    array_agg(name || ': ' || counts ORDER BY days DESC) as days, 
    sum(total_rate_per_tier) as total_price                    -- G
FROM (
    SELECT 
        *,
        rate * counts as total_rate_per_tier                   -- F
    FROM days_per_tier) s
GROUP BY num_id, num 
ORDER BY num_Id

WITH RECURSIVE包含递归的起点UNION的递归部分。起点只是获得层级 (A) 和数字 (B)。为了根据天数对层进行排序,我添加了行数(C;仅当相应的 ID 与我的示例中的顺序不正确时才需要。如果您添加另一个层,可能会发生这种情况)。

递归部分取前一个SELECT的结果(存放在days_per_tier),计算下一个余数和整数除法(D,E)。所有其他列仅用于保存原始值(负责递归本身的递增行计数器除外)。

递归后,计数和比率相乘 (F),然后按生成总和 (G) 的原始编号 id 分组

编辑: 添加了速率函数和 sqlfiddle link.