【问题标题】:Extrapolate daily historical values from a table that only records when a value changes (Postgresql 9.3)从仅记录值更改时的表中推断每日历史值(Postgresql 9.3)
【发布时间】:2019-11-14 08:44:23
【问题描述】:

我有一个表格,每次更改某个位置的分数时都会记录一行。

score_history:

  • id int PK(uuid 自动递增 int)
  • happened_at 时间戳(分数发生变化时)
  • location_id int FK(值所针对的位置)
  • 分数浮动(新分数)

这样做是着眼于效率,并且能够简单地检索给定位置的更改列表并很好地服务于该目的。

我正在尝试以非常冗余的格式输出数据,以帮助将其加载到严格的外部系统中。外部系统期望每个位置 * 每个日期都有一行。目标是表示每个日期每个位置的最后一个分数值。因此,如果分数在给定日期更改了 3 次,则只有最接近午夜的分数才会被视为当天的收盘分数。我想这类似于创建关闭业务库存水平事实表的挑战。

我有一个方便的星型样式日期维度表,其中每个日期都有一行,完全涵盖了这个样本期间和未来。

那张桌子看起来像

dw_dim_date:

  • 日期日期PK
  • 一堆其他列,例如周数、is_us_holiday 等。

所以,如果我在 score_history 表中只有 3 条记录...

1, 2019-01-01:10:13:01, 100, 5.0
2, 2019-01-05:20:00:01, 100, 5.8
3, 2019-01-05:23:01:22, 100, 6.2

期望的输出是:

2019-01-01, 100, 5.0 
2019-01-02, 100, 5.0 
2019-01-03, 100, 5.0
2019-01-04, 100, 5.0 
2019-01-05, 100, 6.2

3 要求:

  1. 每个位置每天一行,即使没有得分记录 那天。
  2. 如果有最后一天的得分记录 午夜前的一个应该是该行的分数值。如果出现平局,则两者中的较大者应该“获胜”。
  3. 如果当天的分数记录为零,则分数应为最近的先前分数。

我一直在通过子查询和窗口函数追逐我的尾巴。

因为我不愿意发布没有我尝试过的东西我会分享这个产生输出但没有意义的火车失事......

SELECT dw_dim_date.date,
       (SELECT score 
        FROM score_history 
        WHERE score_history.happened_at::DATE < dw_dim_date.date 
           OR score_history.happened_at::DATE = dw_dim_date.date 
        ORDER BY score_history.id desc limit 1) as last_score
FROM dw_dim_date
WHERE dw_dim_date.date > '2019-06-01'

感谢您提供其他阅读问题的指导或指针。

【问题讨论】:

  • 我不是在一天之内寻找 MAX 值。我正在寻找一天的最后一个值。而且很多天根本没有价值。
  • 1) 每个位置每天一行,即使当天没有得分记录。 2)如果有当天的得分记录,则午夜前的最后一个应该是该行的得分值 3)如果当天的得分记录为零,那么得分应该是最近的上一个得分。

标签: sql postgresql data-warehouse postgresql-9.3


【解决方案1】:

您可以通过使用相关子查询和LATERAL 来实现它:

SELECT sub.date, sub.location_id, score
FROM (SELECT * FROM dw_dim_date
      CROSS JOIN (SELECT DISTINCT location_id FROM score_history) s
      WHERE date >= '2019-01-01'::date) sub
,LATERAL(SELECT score FROM score_history sc 
         WHERE sc.happened_at::date <= sub.date
           AND sc.location_id = sub.location_id
         ORDER BY happened_at DESC LIMIT 1) l
,LATERAL(SELECT MIN(happened_at::date) m1, MAX(happened_at::date) m2 
         FROM score_history sc
         WHERE sc.location_id = sub.location_id) lm
WHERE sub.date BETWEEN lm.m1 AND lm.m2
ORDER BY location_id, date;

db<>fiddle demo

它是如何工作的:

1) s(它是每个 location_id 的所有日期的交叉连接)

2) l(选择每个位置的分数)

3) lm(选择每个位置的最小/最大日期进行过滤)

4) WHERE 在可用范围内过滤日期,如果需要可以放宽

【讨论】:

  • 谢谢,这是对 LATERAL 的一个非常有趣的介绍,而且对于较大的数据集似乎也很有效。
  • @Nick 很高兴听到这个消息:)
【解决方案2】:

我认为你可以尝试这样的事情。我更改的主要内容是将内容包装在 DATE() 中并为日期查找器使用另一个 SO 答案:

SELECT
  dw_dim_date.date,
  (
    SELECT
      score
    FROM
      score_history
    WHERE
      DATE(score_history.happened_at) <= dw_dim_date.date
    ORDER BY
      score_history.happened_at DESC
    LIMIT
      1
  ) as last_score
FROM
  dw_dim_date
WHERE
  dw_dim_date.date >= DATE('2019-01-01')

这使用 SQL 方法从这里找到最接近请求的过去数据:PostgreSQL return exact or closest date to queried date

【讨论】:

  • 您的最后一行可能是指&gt;= DATE('2019-01-01')
  • 我不确定。在最初的问题中,它没有 = 并且有 2019-06-01 所以我原来坚持使用它。马上更新。
  • 谢谢,但这适用于一天内所有 location_id 的最后得分,但不适用于每个 location_id。每个日期需要为每个 location_id 输入 1 个条目。同样,该场景是在每天结束时捕获诸如餐厅评​​级之类的东西。
【解决方案3】:
WITH
max_per_day_location AS (
SELECT
    SH.happened_at::DATE as day,
    SH.location_id,
    max(SH.happened_at) as happened_at
FROM
    score_history SH
GROUP BY
    SH.happened_at::DATE,
    SH.location_id
),
date_location AS (
SELECT DISTINCT
    DD."date",
    SH.location_id
FROM
    dw_dim_date DD,
    max_per_day_location SH
),
value_partition AS (
SELECT
    DD."date",
    DD.location_id,
    SH.score,
    SH.happened_at,
    MPD.happened_at as hap2,
    sum(case when score is null then 0 else 1 end) OVER
    (PARTITION BY DD.location_id ORDER BY "date", SH.happened_at desc) AS value_partition
FROM
    date_location DD
    LEFT JOIN score_history SH
    ON DD."date" = SH.happened_at::DATE
    AND DD.location_id = SH.location_id
    LEFT join max_per_day_location MPD
    ON SH.happened_at = MPD.happened_at
WHERE NOT (MPD.happened_at IS NULL
           AND
           SH.happened_at IS NOT NULL)
ORDER BY
    DD."date"
),
final AS (
SELECT
    "date",
    location_id,
    first_value(score) over w
FROM
    value_partition
WINDOW w AS (PARTITION BY location_id, value_partition
             ORDER BY happened_at rows between unbounded preceding and unbounded following)
order by "date"
)
SELECT DISTINCT * FROM final ORDER BY location_id, date
;

我确信没有那么冗长的方法可以做到这一点。

我在这里有一个带有一些测试数据的 SQLFiddle: http://sqlfiddle.com/#!17/9d122/1

使这项工作的主要内容是创建一个“值分区”来访问以前的非空值。更多内容:

date_location 子查询每天只为每个 location_id 创建一行,因为这是输出中所需的基本“行级别”。

max_per_day_location 子查询用于过滤掉具有多个分数的位置/日期组合的较早条目,并且只保留当天的最后一个。

【讨论】:

    【解决方案4】:

    最简单的解决方案可能是:

        select dw_dim_date.date, location_id, score
        from dw_dim_date, score_history S1
        where happened_at::date  <= dw_dim_date.date and 
              not exists (select * 
                          from score_history S2 
                          where S2.happened_at::date  <= dw_dim_date.date and 
                                S1.happened_at< S2.happened_at and
                                S1.location_id = S2.location_id)
    

    这会计算日期和分数历史之间的笛卡尔积,然后为每个日期和位置获取不存在后续分数的分数(在日期期间内)。我建议从这个开始,因为它可能是最容易维护的,并且只有在效率不够高时才使用更复杂的解决方案(使用适当的索引)。

    因为这方面的 SQL Fiddle 在https://dbfiddle.uk/?rdbms=postgres_9.4&fiddle=3c2e4ae49cbc43f7840b942d223be119

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-04-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多