求和直到达到阈值,然后重置计数器

Posted

技术标签:

【中文标题】求和直到达到阈值,然后重置计数器【英文标题】:Sum until threshold value reached and then reset the counter 【发布时间】:2019-04-09 07:46:01 【问题描述】:
user_id | date                 | distance
1       | 2019-04-09 00:00:00  | 2
1       | 2019-04-09 00:00:30  | 5
1       | 2019-04-09 00:01:00  | 3
1       | 2019-04-09 00:01:45  | 7
1       | 2019-04-09 00:02:30  | 6
1       | 2019-04-09 00:03:00  | 1

如何对下一行的距离求和,直到达到阈值并再次重置计数器。

例如,如果阈值为 10,我试图获得以下输出:

1       | 2019-04-09 00:00:00  | 2
1       | 2019-04-09 00:00:30  | 7            (2 + 5)
1       | 2019-04-09 00:01:00  | 10           ( 7 + 3 )
1       | 2019-04-09 00:01:45  | 7            RESET
1       | 2019-04-09 00:02:30  | 13           (7 + 6 )
1       | 2019-04-09 00:03:00  | 1            RESET

但我只能通过以下查询获得累积距离:

SELECT *, sum(distance) over (order by date asc) as running_distance FROM table;

我正在使用 PostgreSQL。

【问题讨论】:

您使用的是哪个 RDBMS? mysql还是postgre?你标记了两个 我正在使用 PostgreSQL 有一个关于类似主题的讨论。看看这里:dba.stackexchange.com/questions/163557/… “累积距离”取模 10 怎么样? 【参考方案1】:

使用user-defined aggregate

现场测试:http://sqlfiddle.com/#!17/16716/2

SELECT *, sum_with_reset(distance, 10) over (order by date asc) as running_distance 
FROM tbl;

用户定义的聚合 sum_with_reset 定义:

create or replace function sum_reset_accum(
    _accumulated numeric, _current numeric, _threshold numeric
)
returns numeric as
$$
    select case when _accumulated >= _threshold then
        _current
    else
        _current + _accumulated
    end    
$$ language sql;


create aggregate sum_with_reset(numeric, numeric)
(
    sfunc = sum_reset_accum,
    stype = numeric,
    initcond = 0
);

数据

CREATE TABLE tbl
    ("user_id" int, "date" timestamp, "distance" int)
;

INSERT INTO tbl
    ("user_id", "date", "distance")
VALUES
    (1, '2019-04-09 00:00:00', 2),
    (1, '2019-04-09 00:00:30', 5),
    (1, '2019-04-09 00:01:00', 3),
    (1, '2019-04-09 00:01:45', 7),
    (1, '2019-04-09 00:02:30', 6),
    (1, '2019-04-09 00:03:00', 1)
;

输出:

| user_id |                 date | distance | running_distance |
|---------|----------------------|----------|------------------|
|       1 | 2019-04-09T00:00:00Z |        2 |                2 |
|       1 | 2019-04-09T00:00:30Z |        5 |                7 |
|       1 | 2019-04-09T00:01:00Z |        3 |               10 |
|       1 | 2019-04-09T00:01:45Z |        7 |                7 |
|       1 | 2019-04-09T00:02:30Z |        6 |               13 |
|       1 | 2019-04-09T00:03:00Z |        1 |                1 |

单线:

create or replace function sum_reset_accum(
    _accumulated numeric, _current numeric, _threshold numeric
)
returns numeric as
$$
    select _current + _accumulated * (_accumulated < _threshold)::int
$$ language 'sql';

Postgres 布尔值可以通过使用转换运算符 ::int 将 true 转换为 1,将 false 转换为 0。

您也可以使用plpgsql 语言:

create or replace function sum_reset_accum(
    _accumulated numeric, _current numeric, _threshold numeric
)
returns numeric as
$$begin
    return _current + _accumulated * (_accumulated < _threshold)::int;
end$$ language 'plpgsql';

请注意,您无法在 sqlfiddle.com 上创建 plpgsql 函数,因此您无法在 sqlfiddle.com 上测试该 plpgsql 代码。不过你可以在你的机器上。

【讨论】:

dbfiddle.uk 和 rextester.com/l/postgresql_online_compiler 允许您创建 plpgsql 函数。请注意,language sql 对于简单的“仅查询”功能比plpgsql 更有效 @a_horse_with_no_name 谢谢。下次我会尝试使用它。我习惯使用 sqlfiddle.com,因为它具有 Text to DDL 功能,我可以立即使用 OP 的示例数据。【参考方案2】:

作为变体,您可以使用RECURSIVE CTE 来获取它

测试表:

CREATE TABLE public.table_b (
    user_id int4 NULL,
    "date" timestamp NULL,
    distance int4 NULL
);

INSERT INTO public.table_b (user_id,"date",distance) VALUES 
(1,'2019-04-09 00:00:00.000',2)
,(1,'2019-04-09 00:00:30.000',5)
,(1,'2019-04-09 00:01:00.000',3)
,(1,'2019-04-09 00:01:45.000',7)
,(1,'2019-04-09 00:02:30.000',6)
,(1,'2019-04-09 00:03:00.000',1)
,(2,'2019-04-09 00:00:00.000',2)
,(2,'2019-04-09 00:00:30.000',5)
,(2,'2019-04-09 00:01:00.000',3)
,(2,'2019-04-09 00:01:45.000',7)
,(2,'2019-04-09 00:02:30.000',6)
,(2,'2019-04-09 00:03:00.000',1);

查询:

WITH RECURSIVE cte1 AS(
    SELECT *,ROW_NUMBER()OVER(PARTITION BY user_id ORDER BY date) n
    FROM table_b
),
cte2 AS(
    SELECT user_id,date,distance,distance running_distance,n
    FROM cte1
    WHERE n=1

    UNION ALL

    SELECT c1.user_id,c1.date,c1.distance,CASE WHEN c2.running_distance<10 THEN c2.running_distance ELSE 0 END+c1.distance running_distance,c1.n
    FROM cte1 c1
    JOIN cte2 c2 ON c2.user_id=c1.user_id AND c2.n+1=c1.n
)
SELECT user_id,date,distance,running_distance
FROM cte2
ORDER BY user_id,date

【讨论】:

谢谢,它有效,但比其他答案慢。我将使用此查询来创建一个视图,以使其更快地工作。仅 4K 行需要 3-4 秒。

以上是关于求和直到达到阈值,然后重置计数器的主要内容,如果未能解决你的问题,请参考以下文章

BigQuery SQL / GIS:扩展半径直到计数大于/等于“N”

如果达到特定条件,计数重置为 0

Oracle SQL - 基于分组和条件运行求和

分组值在 SQL (maria DB) 中按时间存储直到零,并与计数一起进行求和

perf-perf stat用户层代码分析

5871: 桃子的比赛