如何缩短视图的执行时间

How to Shorten Execution Time for A View

我有 3 个 table、一个用户 table、一个管理员 table 和一个客户 table。 admin 和 cust tables 都是 user_account table 的外键。基本上,每个用户都有一个用户记录,他们是什么类型的用户取决于他们是否在管理员或客户中有记录 table.

user                   admin                   cust
 user_id         user_id | admin_id      user_id | cust_id
---------       ---------|----------    ---------|---------
 1               1       | a             2       | dd
 2               4       | b             3       | ff
 3            
 4            

然后我有一个 login_history table 记录用户每次登录应用程序时的 user_id 和登录时间戳

  login_history
 user_id | login_on
---------|---------------------
 1       | 2022-01-01 13:22:43
 1       | 2022-01-02 16:16:27
 3       | 2022-01-05 21:17:52
 2       | 2022-01-11 11:12:26
 3       | 2022-01-12 03:34:47

我想创建一个视图,其中包含从 1 月 1 日开始的一年中每周第一天的所有日期,以及一个包含该周登录的唯一管理员用户计数的计数列,以及那一周登录的唯一客户的计数。因此生成的视图应包含以下 53 条记录,每周一条。

login_counts_view
 week_start_date | admin_count | cust_count
-----------------|-------------|------------
 2022-01-01      | 1           | 1
 2022-01-08      | 0           | 2
 2022-01-15      | 0           | 0
 .
 .
 .
 2022-12-31      | 0           | 0

请注意,第一周 (2022-01-01) 只有 1 次计数 admin_count,即使 user_id1 的管理员在那一周登录了两次。

以下是我对视图的当前查询。但是,table 非常大,从视图中检索所有记录需要 10 多秒,这主要是因为左连接日期比较。

CREATE VIEW login_counts_view  AS
SELECT 
    week_start_dates.week_start_date::text AS week_start_date,
    count(distinct a.user_id) AS admin_count,
    count(distinct c.user_id) AS cust_count
FROM (
    SELECT 
        to_char(i::date, 'YYYY-MM-DD') AS week_start_date 
    FROM 
        generate_series(date_trunc('year', NOW()), to_char(NOW(), 'YYYY-12-31')::date, '1 week') i
) week_start_dates

LEFT JOIN login_history l ON l.login_on::date BETWEEN week_start_dates.week_start_date::date AND (week_start_dates.week_start_date::date + INTERVAL '6 day')::date
LEFT JOIN admin a ON a.user_id = l.user_id 
LEFT JOIN cust c ON c.user_id = l.user_id 
GROUP BY week_start_date;

有没有人对如何更有效地执行此查询有任何提示?

想法

计算每个登录日期的 pseudo-week:将年份分成 7 天的片段并连续编号。给定日期的 pseudo-week 将是它所属的切片的序号。

然后对表示 pseudo-week 的整数而不是日期值和比较进行联接。

实施

实现这个的视图如下:

CREATE VIEW login_counts_view_fast  AS
          WITH RECURSIVE Numbers(i) AS ( SELECT 0 UNION ALL SELECT i + 1 FROM Numbers WHERE i < 52 )     
        SELECT CAST ( date_trunc('year', NOW()) AS DATE) + 7 * n.i  week_start_date
             , count(distinct lw.admin_id)                          admin_count
             , count(distinct lw.cust_id)                           cust_count
          FROM (
                     SELECT i FROM Numbers 
               ) n
     LEFT JOIN (
                     SELECT admin_id
                          , cust_id
                          , base
                          , pit
                          , pit-base                     delta
                          , (pit-base) / (3600 * 24 * 7) week  
                       FROM (
                                  SELECT a.user_id  admin_id
                                       , c.user_id  cust_id
                                       , CAST ( EXTRACT ( EPOCH FROM l.login_on )                 AS INTEGER ) pit
                                       , CAST ( EXTRACT ( EPOCH FROM date_trunc('year', NOW())  ) AS INTEGER ) base
                                    FROM login_history l
                               LEFT JOIN admin         a ON a.user_id = l.user_id
                               LEFT JOIN cust          c ON c.user_id = l.user_id  
                            ) le
               ) lw
                        ON lw.week = n.i
      GROUP BY n.i
;     

一些备注:

  • 纪元值是自绝对基准日期时间(特别是 1/1/1970 0h00)以来经过的秒数。
  • CASTS 是将双精度数转换为整数和将时间戳转换为日期所必需的,这是 postgresql 日期函数签名所规定的,也是为了强制执行整数运算。
  • 递归子查询是连续整数的生成器。它可能会被 generate_series 调用(未经测试)
  • 取代

评价

this db fiddle

中查看实际效果

查询计划表明执行时间节省了 50-70%。