【问题标题】:How to only select rows closest to a given series of values?如何只选择最接近给定值系列的行?
【发布时间】:2021-06-28 17:36:07
【问题描述】:

我有下表:

create table foobar (
    account_id  bigint    not null,
    timestamp   timestamp not null,
    balance_one numeric   not null,
    balance_two numeric   not null,
    primary key (timestamp, account_id)
);

我有一个给定的时间范围,BEGINEND。在该范围内,以给定步长生成一系列时间戳,从下限开始。我们将这些时间戳称为“间隔边界”。

我想选择给定时间范围内的所有行,其时间戳比任何其他都更接近系列中生成的时间戳之一。

查询的一般部分非常简单,选择给定时间范围内的行:

select * from foobar
where account_id = ?
and timestamp between {BEGIN} and {END}
order by timestamp asc;

我不明白的部分是如何将我的结果集修剪为仅最接近区间边界的行。

示例数据:

account_id timestamp balance_one balance_two
1 28 June 2021 17:00:00 0.0 0.0
1 28 June 2021 17:00:05 1.0 0.0
1 28 June 2021 17:00:10 0.5 0.0
1 28 June 2021 17:00:15 0.0 1.0
1 28 June 2021 17:00:20 0.5 0.0
1 28 June 2021 17:00:25 1.0 1.0
2 28 June 2021 17:00:00 0.0 0.0
2 28 June 2021 17:00:05 1.0 7.0
2 28 June 2021 17:00:07 2.0 6.0
2 28 June 2021 17:00:15 3.0 5.0
2 28 June 2021 17:00:20 4.0 4.0
2 28 June 2021 17:00:25 5.0 3.0
2 28 June 2021 17:00:30 6.0 2.0
2 28 June 2021 17:00:35 7.0 1.0

带参数的示例查询:

ID = 1,BEGIN = 2021 年 6 月 28 日 17:00:00,END = 2021 年 6 月 28 日 17:00:30,INTERVAL = 10 秒

结果:

account_id timestamp balance_one balance_two
1 28 June 2021 17:00:00 0.0 0.0
1 28 June 2021 17:00:10 0.5 0.0
1 28 June 2021 17:00:20 0.5 0.0
1 28 June 2021 17:00:25 1.0 1.0

ID = 2,BEGIN = 2021 年 6 月 28 日 17:00:00,END = 2021 年 6 月 28 日 17:00:30,INTERVAL = 10 秒 结果:

account_id timestamp balance_one balance_two
2 28 June 2021 17:00:00 0.0 0.0
2 28 June 2021 17:00:10 2.0 6.0
2 28 June 2021 17:00:20 4.0 4.0
2 28 June 2021 17:00:30 6.0 2.0

【问题讨论】:

  • “最接近区间边界”是什么意思?样本数据、期望的结果会有所帮助——清晰的解释也会有所帮助。
  • 从任何起始时间戳说0,以5 的间隔递增,我想要时间戳最接近间隔时间戳的记录
  • 我冒昧地改写您的问题描述。如果我没有做对,请纠正。

标签: sql postgresql


【解决方案1】:
SELECT DISTINCT (COALESCE(p.t, n.t)).*
FROM   generate_series(timestamp '2021-06-28 17:00:00'  -- lower bound of time frame
                     , timestamp '2021-06-28 17:00:30'  -- upper bound of time frame
                     , interval '10 sec') AS g(ts)      -- step size
LEFT   JOIN LATERAL (
   SELECT t.timestamp, t
   FROM   tbl t
   WHERE  account_id = 2
   AND    t.timestamp >= g.ts
   AND    t.timestamp BETWEEN timestamp '2021-06-28 17:00:00'
                      AND     timestamp '2021-06-28 17:00:30'  -- enforce time frame
   ORDER  BY t.timestamp
   LIMIT  1
   ) n ON true
LEFT   JOIN LATERAL (
   SELECT t.timestamp, t
   FROM   tbl t
   WHERE  account_id = 2
   AND    t.timestamp < g.ts
   AND    t.timestamp BETWEEN timestamp '2021-06-28 17:00:00'
                      AND     timestamp '2021-06-28 17:00:30'  -- enforce time frame
   ORDER  BY t.timestamp DESC
   LIMIT  1
   ) p ON g.ts - p.timestamp < n.timestamp - g.ts
       OR n.timestamp IS NULL   -- suppress NULL row for no later match
WHERE  n.timestamp IS NOT NULL
    OR p.timestamp IS NOT NULL  -- suppress NULL row for no match at all
ORDER  BY timestamp;

db小提琴here

“区间边界”系列从时间范围的下限开始。如果间隔的步骤不符合要求,则不清楚如何处理时间范围的上限。在这种情况下,此查询忽略上限。

使用说明

  1. 使用generate_series() 生成一系列“区间边界”。见:

  2. 第一个LATERAL 子查询为每个间隔边界获取具有下一个更大(或相等)时间戳的行。 undefined 在如何处理重复时间戳的问题中。此查询选择任意获胜者。相反,我会通过添加更多 ORDER BY 表达式来实现确定性。见:

  3. 第二个LATERAL 子查询获取具有下一个较小时间戳的行 - 但前提是该时间戳比第一个子查询中的行更接近区间边界。 (后排赢得平局。)

  4. 在外部SELECT 中使用COALESCE 以获得每个区间边界的获胜行。

  5. 添加DISTINCT(或其他删除重复项的方法),因为可以多次选择同一行。 (就像它最接近多个区间边界时一样。

  6. 可选外层ORDER BY

由于您想获取整行,因此我使用了表的隐式定义的行类型。从表中保存另一个读取。

如果您的表很大,则需要在(account_id, timestamp) 上建立索引才能加快速度。

CTE 的替代方案

如果您没有索引,或者您的表不大,或者在给定的时间范围内只有几行,那么带有CTE 的替代方案会更快:

WITH cte AS (
   SELECT * FROM tbl
   WHERE  account_id = 2
   AND    timestamp BETWEEN timestamp '2021-06-28 17:00:00'  -- lower bound of time frame
                    AND     timestamp '2021-06-28 17:00:30'  -- upper bound of time frame
   )
SELECT DISTINCT (COALESCE(p.t, n.t)).*
FROM   generate_series(timestamp '2021-06-28 17:00:00'  -- lower bound
                     , timestamp '2021-06-28 17:00:30'  -- upper bound
                     , interval  '10 sec') AS g(ts)     -- step size
LEFT   JOIN LATERAL (
   SELECT t.timestamp, t::tbl
   FROM   cte t
   WHERE  t.timestamp >= g.ts
   ORDER  BY t.timestamp
   LIMIT  1
   ) n ON true
LEFT   JOIN LATERAL (
   SELECT t.timestamp, t::tbl
   FROM   cte t
   WHERE  t.timestamp < g.ts
   ORDER  BY t.timestamp DESC
   LIMIT  1
   ) p ON g.ts - p.timestamp < n.timestamp - g.ts
       OR n.timestamp IS NULL   -- suppress NULL row for no later match
WHERE  n.timestamp IS NOT NULL
    OR p.timestamp IS NOT NULL  -- suppress NULL row for no match at all
ORDER  BY timestamp;

db小提琴here

很像上面的。但首先在 CTE 中过滤外部时间范围。 我们需要显式转换为基础表类型,因为派生表没有注册。

【讨论】:

  • 你有什么可以推荐的关于这个主题的阅读材料吗?
  • “本主题”涉及SQL的几个基本章节。我的主要来源是优秀的手册。我添加了更多链接。以及关于stackoverflow的答案。我在这些答案中收集了“每组最大”的最佳答案:stackoverflow.com/a/25536748/939860stackoverflow.com/a/7630564/939860
  • 我注意到当间隔非常大并且结束间隔附近没有任何数据时,此查询可以在结果集的末尾返回空行。我将如何剔除这些空行?
  • @vddox:我添加了代码来抑制 NULL 行,其中找不到以后的匹配项(或根本没有匹配项)。
猜你喜欢
  • 2016-10-03
  • 1970-01-01
  • 2012-02-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多