【问题标题】:in sql, calculating date parts versus date lookup table in group queries在 sql 中,计算日期部分与组查询中的日期查找表
【发布时间】:2012-06-04 00:05:50
【问题描述】:

当基表日期为 datetimestamp 时,许多查询是按周、月或季度进行的。

一般来说,在group by 查询中,是否使用 - 日期功能 - 已预先计算提取的day

注意:与DATE lookup table (1990/01/01:2041/12/31)类似的问题

例如,postgresql

create table sale(
  tran_id   serial       primary key,
  tran_dt   date         not null default current_date,
  sale_amt  decimal(8,2) not null,
  ...
);

create table days(
  day       date      primary key,
  week      date      not null,
  month     date      not null,
  quarter   date      non null
);

-- week query 1: group using funcs
select
  date_trunc('week',tran_dt)::date - 1 as week,
  count(1) as sale_ct,
  sum(sale_amt) as sale_amt
from sale
where date_trunc('week',tran_dt)::date - 1 between '2012-1-1' and '2011-12-31'
group by date_trunc('week',tran_dt)::date - 1
order by 1;

-- query 2: group using days
select
  days.week,
  count(1) as sale_ct,
  sum(sale_amt) as sale_amt
from sale
join days on( days.day = sale.tran_dt )
where week between '2011-1-1'::date and '2011-12-31'::date
group by week
order by week;

对我来说,date_trunc() 函数似乎更有机,days 表更易于使用。

这里有什么比口味更重要的吗?

【问题讨论】:

  • 您可以在表达式date_trunc('week',tran_dt)::date 上创建索引以加快处理速度。

标签: sql postgresql datetime


【解决方案1】:
-- query 3: group using instant "immediate" calendar table
WITH calender AS (
        SELECT  ser::date AS dd
        , date_trunc('week', ser)::date AS wk
        -- , date_trunc('month', ser)::date AS mon
        -- , date_trunc('quarter', ser)::date AS qq
        FROM generate_series( '2012-1-1' , '2012-12-31', '1 day'::interval) ser
        )
SELECT
  cal.wk
  , count(1) as sale_ct
  , sum(sa.sale_amt) as sale_amt
FROM sale sa
JOIN calender cal ON cal.dd = sa.tran_dt
-- WHERE week between '2012-1-1' and '2011-12-31'
GROUP BY cal.wk
ORDER BY cal.wk
        ;

注意:我修正了 BETWEEN 范围内的一个明显错字。

更新:我使用 Erwin 的递归 CTE 来挤出重复的 date_trunc()。大量嵌套 CTE:

WITH calendar AS (
        WITH RECURSIVE montag AS (
        SELECT '2011-01-01'::date  AS dd
        UNION ALL
        SELECT dd + 1 AS dd
        FROM   montag
        WHERE  dd < '2012-1-1'::date
        )
    SELECT mo.dd, date_trunc('week', mo.dd + 1)::date AS wk
    FROM montag mo
    )
SELECT
  cal.wk
  , count(1) as sale_ct
  , sum(sa.sale_amt) as sale_amt
FROM sale sa
JOIN calendar cal ON cal.dd = sa.tran_dt
-- WHERE week between '2012-1-1' and '2011-12-31'
GROUP BY cal.wk
ORDER BY cal.wk
        ;

【讨论】:

  • 几乎是我将在回答中用作示例的查询。 :)
  • Is 也是表达查询的 restrict 部分的简洁方式。也许你这次可以做递归 CTE ? ;-)
  • 您不能徒劳地请求递归 CTE! ;) Look! BTW,简化 date_trunc('day', ser)::date -> ser::date.
  • 哈哈。额外的 date_trunc 确实是懒惰(ddkpppp+ search&& replace)。我会编辑下来。
  • 非常好!不断忘记这些 CTE 有多甜。真的很想找到大约 20-30 个 CTE 的集合来阅读,这样它们就会在我的思想中变得更加根深蒂固。
【解决方案2】:

是的,这不仅仅是品味问题。查询的性能取决于方法。

作为第一个近似值,函数应该更快。它们不需要连接,在单个表扫描中进行读取。

但是,一个好的优化器可以有效地利用查找表。它会知道目标值的分布。而且,内存中的连接可能会非常快。

作为数据库设计,我认为有一个日历表是非常有用的。某些信息(例如假期)将无法发挥作用。但是,对于大多数临时查询,日期函数都可以。

【讨论】:

    【解决方案3】:

    1.你的表情:

    ...在“2012-1-1”和“2011-12-31”之间

    不起作用。基本BETWEEN 要求左参数小于或等于右参数。必须是:

    ... BETWEEN SYMMETRIC '2012-1-1' and '2011-12-31'
    

    或者这只是一个错字,你的意思是:

    ... BETWEEN '2011-1-1' and '2011-12-31'
    

    我不清楚您的查询应该检索什么。我会假设你想要从 2011 年开始的所有周(周一到周日)来回答剩下的问题。在现代硬件上,这个表达式在不到一微秒的时间内准确生成(适用于任何年份):

    SELECT generate_series(
            date_trunc('week','2010-12-31'::date) + interval '7d'
           ,date_trunc('week','2011-12-31'::date) + interval '6d'
           , '1d')::date
    

    *注意“一年的第一周”的ISO 8601 definition略有不同。

    2.您的第二个查询根本不起作用。没有GROUP BY

    3. 您链接到的问题没有涉及 PostgreSQL,它具有出色的日期/时间戳支持。它有generate_series(),在大多数情况下可以消除对单独的“天”表的需要——如上所示。您的查询将如下所示:

    与此同时,@wildplasser provided an example query 应该去这里。

    按照流行*的需求,递归 CTE 版本 - 实际上距离成为一个严肃的替代方案不远了!
    * 我所说的“流行”是指@wildplasser's very serious request

    WITH RECURSIVE days AS (
        SELECT '2011-01-01'::date  AS dd
              ,date_trunc('week', '2011-01-01'::date )::date AS wk
    
        UNION ALL
        SELECT dd + 1
              ,date_trunc('week', dd + 1)::date AS wk
        FROM   days
        WHERE  dd < '2011-12-31'::date
        )
    SELECT d.wk
          ,count(*) AS sale_ct
          ,sum(s.sale_amt) AS sale_amt
    FROM days d
    JOIN sale s ON s.tran_dt = d.dd
    -- WHERE d.wk between '2011-01-01' and '2011-12-31'
    GROUP BY 1
    ORDER BY 1;
    

    也可以写成 (compare to @wildplasser's version):

    WITH RECURSIVE d AS (
        SELECT '2011-01-01'::date AS dd
        UNION ALL
        SELECT dd + 1 FROM d WHERE dd < '2011-12-31'::date
        ), days AS (
        SELECT dd, date_trunc('week', dd + 1)::date AS wk
        FROM d
        )
    SELECT ...
    

    4. 如果性能至关重要,请确保不要将函数或计算应用于表的值。这禁止使用 indexes 并且通常非常慢,因为必须处理每一行。这就是为什么你的第一个查询会用大表来吸。尽可能将计算应用于您过滤时使用的值。

    Indexes on expressions 是解决此问题的一种方法。如果你有一个类似的索引

    CREATE INDEX sale_tran_dt_week_idx ON sale (date_trunc('week', tran_dt)::date);
    

    .. 您的第一个查询可能会再次非常快 - 为索引维护而进行的写入操作需要付出一些代价。

    【讨论】:

    • 1) 谢谢。 2) 对错别字和遗漏group by 感到抱歉 - 已修复。 3)你的理由#4是我觉得天数表会更有效率的原因。 4) 对于这种不自然的写 CTE 的愿望,您是否考虑过专业帮助?
    • @ccyoung:您认为慢性 CTEtis 有治愈方法吗?嗯,总有希望。 :)
    • 你甚至可以使用嵌套的 CTE,避免重复的 date_trunc(;)
    • @wildplasser:我不会上钩的! (而且date_trunc() 无论如何每行只执行一次。)
    • 好的,我会做的。但我得借用你的代码。
    猜你喜欢
    • 2021-07-06
    • 1970-01-01
    • 2016-01-04
    • 1970-01-01
    • 2021-04-11
    • 2018-02-17
    • 1970-01-01
    • 1970-01-01
    • 2018-07-25
    相关资源
    最近更新 更多