【问题标题】:Joining series of dates and counting continous days加入一系列日期并计算连续天数
【发布时间】:2015-08-18 14:18:29
【问题描述】:

假设我有一张如下表

date        add_days
2015-01-01  5
2015-01-04  2
2015-01-11  7
2015-01-20  10
2015-01-30  1

我想要做的是检查days_balance,即如果date大于或小于前一个日期+ N天(add_days),如果它们是连续系列,则取累计天数总和.

所以算法应该像这样工作

for i in 2:N_rows {
   days_balance[i] := date[i-1] + add_days[i-1] - date[i]
   if days_balance[i] >= 0 then
      date[i] := date[i] + days_balance[i]
}

预期的结果应该如下

date        days_balance
2015-01-01  0
2015-01-04  2
2015-01-11  -3
2015-01-20  -2
2015-01-30  0

纯SQL可以吗?我想它应该带有一些条件连接,但看不到它是如何实现的。

【问题讨论】:

  • 如果您使用的是 PostgreSQL 9.1.13 或更高版本,则需要查看 PostgresSQL 中的 laglead 窗口函数。另外,您如何计算第一条记录的 previous_date 和 previous_add_days?
  • @FutbolFan 按当前和以前的日期我是指后续行 - 为了清楚起见,我将进行编辑。
  • 是的,我知道了。但是,对于 date = 2015-01-01 的行,我要问的是该记录的 previous_date 和 previous_add_days 是多少?
  • 预期结果是否正确?第四行不应该是-5吗?这是我的数学:第 01 天到第 4 天 = 0 余额减去 3 天变化 + 5 add_days = 2;第 04 天到第 11 天 = 2 余额减去 7 天变化 + 2 add_days = -3;第 11 天到第 20 天 = -3 余额减去 9 天变化 + 7 add_days = -5;第 20 天到第 30 天 = -5 余额减去 10 天变化 + 10 add_days = -5;
  • @FutbolFan 我更新了我的问题和伪代码 - 现在清楚了吗?

标签: sql postgresql


【解决方案1】:

我发布了另一个答案,因为比较它们可能会很好,因为它们使用不同的方法(这个只是进行 n^2 样式连接,另一个使用递归 CTE)。这一个利用了这样一个事实,即您不必在为特定行计算之前为每一行计算 days_balance ,您只需要将前几天的内容相加......

drop table junk
create table junk(date DATETIME, add_days int)

insert into junk values
('2015-01-01',5  ),       
('2015-01-04',2  ),      
('2015-01-11',7  ),      
('2015-01-20',10 ),      
('2015-01-30',1  )    

;WITH cte as
(
    select ROW_NUMBER() OVER (ORDER BY date) i, date, add_days, ISNULL(DATEDIFF(DAY, LAG(date) OVER (ORDER BY date), date), 0) days_since_prev
    FROM Junk
)
, combinedWithAllPreviousDaysCte as
(
    select i [curr_i], date [curr_date], add_days [curr_add_days], days_since_prev [curr_days_since_prev], 0 [prev_add_days], 0 [prev_days_since_prev] from cte where i = 1 --get first row explicitly since it has no preceding rows
    UNION ALL
    select curr.i [curr_i], curr.date [curr_date], curr.add_days [curr_add_days], curr.days_since_prev [curr_days_since_prev], prev.add_days [prev_add_days], prev.days_since_prev [prev_days_since_prev]
    from cte curr 
    join cte prev on curr.i > prev.i --join to all previous days
)
select curr_i, curr_date, SUM(prev_add_days) - curr_days_since_prev - SUM(prev_days_since_prev) [days_balance]
from combinedWithAllPreviousDaysCte
group by curr_i, curr_date, curr_days_since_prev
order by curr_i

输出:

+--------+-------------------------+--------------+
| curr_i |        curr_date        | days_balance |
+--------+-------------------------+--------------+
|      1 | 2015-01-01 00:00:00.000 |            0 |
|      2 | 2015-01-04 00:00:00.000 |            2 |
|      3 | 2015-01-11 00:00:00.000 |           -3 |
|      4 | 2015-01-20 00:00:00.000 |           -5 |
|      5 | 2015-01-30 00:00:00.000 |           -5 |
+--------+-------------------------+--------------+

【讨论】:

    【解决方案2】:

    好吧,我想我有一个递归 CTE(抱歉,我目前只有 Microsoft SQL Server 可用,所以它可能不符合 PostgreSQL)。

    我也认为你的预期结果是错误的(见上面的评论)。如果没有,这可能会被修改以符合您的数学。

    drop table junk
    create table junk(date DATETIME, add_days int)
    
    insert into junk values
    ('2015-01-01',5  ),       
    ('2015-01-04',2  ),      
    ('2015-01-11',7  ),      
    ('2015-01-20',10 ),      
    ('2015-01-30',1  )    
    
    ;WITH cte as
    (
        select ROW_NUMBER() OVER (ORDER BY date) i, date, add_days, ISNULL(DATEDIFF(DAY, LAG(date) OVER (ORDER BY date), date), 0) days_since_prev
        FROM Junk
    )
    ,recursiveCte (i, date, add_days, days_since_prev, days_balance, math)  as
    (
        select top 1 
            i, 
            date, 
            add_days, 
            days_since_prev, 
            0 [days_balance], 
            CAST('no math for initial one, just has zero balance' as varchar(max)) [math]
        from cte where i = 1
        UNION ALL --recursive step now
        select 
            curr.i, 
            curr.date, 
            curr.add_days, 
            curr.days_since_prev, 
            prev.days_balance - curr.days_since_prev + prev.add_days [days_balance],
            CAST(prev.days_balance as varchar(max)) + ' - ' + CAST(curr.days_since_prev as varchar(max)) + ' + ' + CAST(prev.add_days as varchar(max)) [math]
        from cte curr
        JOIN recursiveCte prev ON curr.i = prev.i + 1       
    )
    select i,  DATEPART(day,date) [day], add_days, days_since_prev, days_balance, math
    from recursiveCTE
    order by date
    

    结果是这样的:

    +---+-----+----------+-----------------+--------------+------------------------------------------------+
    | i | day | add_days | days_since_prev | days_balance | math                                           |
    +---+-----+----------+-----------------+--------------+------------------------------------------------+
    | 1 | 1   | 5        | 0               | 0            | no math for initial one, just has zero balance |
    | 2 | 4   | 2        | 3               | 2            | 0 - 3 + 5                                      |
    | 3 | 11  | 7        | 7               | -3           | 2 - 7 + 2                                      |
    | 4 | 20  | 10       | 9               | -5           | -3 - 9 + 7                                     |
    | 5 | 30  | 1        | 10              | -5           | -5 - 10 + 10                                   |
    +---+-----+----------+-----------------+--------------+------------------------------------------------+
    

    【讨论】:

    • 另外,我觉得这不是一个很好的解决方案,因为递归级别随着每一行而增加,所以只使用一些东西来迭代处理结果可能会更好/更有效(但是最初的问题不是要求那个)。
    【解决方案3】:

    我不太明白您的算法如何返回您的预期结果?但让我分享一个我想出的可能会有所帮助的技术。

    这仅在您的数据的最终结果要导出到 Excel 时才有效,即使这样,它也不会在所有情况下都有效,具体取决于您导出数据集的格式,但在这里......

    如果您熟悉 Excel 公式,我发现如果您在 SQL 中将 Excel 公式作为另一个字段编写,它会在您导出到 Excel 后立即为您执行该公式(适用于我只是将其复制并粘贴到 Excel 中,因此它不会将其格式化为文本)

    因此,对于您的示例,您可以执行以下操作(再次注意我不理解您的算法,所以这可能是错误的,但这只是为了给您一个概念)

    SELECT
        date
      , add_days
      , '=INDEX($1:$65536,ROW()-1,COLUMN()-2)'
      ||'+INDEX($1:$65536,ROW()-1,COLUMN()-1)'
      ||'-INDEX($1:$65536,ROW(),COLUMN()-2)'
        AS "days_balance[i]"
      ,'=IF(INDEX($1:$65536,ROW(),COLUMN()-1)>=0'
      ||',INDEX($1:$65536,ROW(),COLUMN()-3)'
      ||'+INDEX($1:$65536,ROW(),COLUMN()-1))'
        AS "date[i]"
    FROM
      myTable
    ORDER BY /*Ensure to order by whatever you need for your formula to work*/
    

    完成这项工作的关键部分是使用INDEX 公式函数根据当前单元格的位置选择一个单元格。所以ROW()-1 告诉它获取上一条记录的结果,COLUMN()-2 表示从当前记录左侧的两列中获取值。因为您不能使用像A2+B2-A3 这样的单元格引用,因为行号在导出时不会改变,并且它假定列的位置。

    我将 SQL 字符串连接与 || 一起使用,以便在屏幕上更容易阅读。

    我在excel中试过这个;它与您的预期结果不符。但是,如果这种技术对您有用,那么只需更正 excel 公式以适应。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-03-13
      • 2021-02-03
      • 1970-01-01
      • 2014-10-31
      • 1970-01-01
      • 1970-01-01
      • 2015-02-03
      相关资源
      最近更新 更多