如何缩短视图的执行时间
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
调用(未经测试) 取代
评价
中查看实际效果
查询计划表明执行时间节省了 50-70%。
我有 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
调用(未经测试) 取代
评价
中查看实际效果查询计划表明执行时间节省了 50-70%。