【发布时间】:2018-02-05 00:09:08
【问题描述】:
问题陈述
我有以下定义的表“event_statistics”:
CREATE TABLE public.event_statistics (
id int4 NOT NULL DEFAULT nextval('event_statistics_id_seq'::regclass),
client_id int4 NULL,
session_id int4 NULL,
action_name text NULL,
value text NULL,
product_id int8 NULL,
product_options jsonb NOT NULL DEFAULT '{}'::jsonb,
url text NULL,
url_options jsonb NOT NULL DEFAULT '{}'::jsonb,
visit int4 NULL DEFAULT 0,
date_update timestamptz NULL,
CONSTRAINT event_statistics_pkey PRIMARY KEY (id),
CONSTRAINT event_statistics_client_id_session_id_sessions_client_id_id_for
FOREIGN KEY
(client_id,session_id) REFERENCES <?>() ON DELETE CASCADE ON UPDATE CASCADE
)
WITH (
OIDS=FALSE
) ;
CREATE INDEX regdate ON public.event_statistics (date_update
timestamptz_ops) ;
和表“客户”:
CREATE TABLE public.clients (
id int4 NOT NULL DEFAULT nextval('clients_id_seq'::regclass),
client_name text NULL,
client_hash text NULL,
CONSTRAINT clients_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
) ;
CREATE INDEX clients_client_name_idx ON public.clients (client_name
text_ops) ;
我需要的是获取每个“action_name”类型的“event_statistics”表中的事件计数,用于按“action_name”和特定时间步长分组的特定“date_update”范围以及特定客户端的所有这些。
我们的目标是在我们网站的仪表板上为每个客户提供所有相关事件的统计数据,并可选择报告日期,并且图表中的间隔时间步长应该不同,例如:
- 当前日期 — 每小时计数;
- 1+ 天和
- 1+ 个月和
- 6 个月以上 — 月。
我做了什么:
SELECT t.date, A.actionName, count(E.id)
FROM generate_series(current_date - interval '1 week',now(),interval '1
day') as t(date) cross join
(values
('page_open'),
('product_add'),
('product_buy'),
('product_event'),
('product_favourite'),
('product_open'),
('product_share'),
('session_start')) as A(actionName) left join
(select action_name,date_trunc('day',e.date_update) as dateTime, e.id
from event_statistics as e
where e.client_id = (select id from clients as c where c.client_name =
'client name') and
(date_update between (current_date - interval '1 week') and now())) E
on t.date = E.dateTime and A.actionName = E.action_name
group by A.actionName,t.date
order by A.actionName,t.date;
按事件类型和日期统计上周事件的时间太长,超过 10 秒。我需要它能够以不同的组间隔(当天的每一小时,每月的天数,然后是数周、数月)在更广泛的时间段(如数周、数月、数年)内更快地执行相同的操作。
查询计划:
GroupAggregate (cost=171937.16..188106.84 rows=1600 width=44)
Group Key: "*VALUES*".column1, t.date
InitPlan 1 (returns $0)
-> Seq Scan on clients c (cost=0.00..1.07 rows=1 width=4)
Filter: (client_name = 'client name'::text)
-> Merge Left Join (cost=171936.08..183784.31 rows=574060 width=44)
Merge Cond: (("*VALUES*".column1 = e.action_name) AND (t.date =(date_trunc('day'::text, e.date_update))))
-> Sort (cost=628.77..648.77 rows=8000 width=40)
Sort Key: "*VALUES*".column1, t.date
-> Nested Loop (cost=0.02..110.14 rows=8000 width=40)
-> Function Scan on generate_series t (cost=0.02..10.02 rows=1000 width=8)
-> Materialize (cost=0.00..0.14 rows=8 width=32)
-> Values Scan on "*VALUES*" (cost=0.00..0.10 rows=8 width=32)
-> Materialize (cost=171307.32..171881.38 rows=114812 width=24)
-> Sort (cost=171307.32..171594.35 rows=114812 width=24)
Sort Key: e.action_name, (date_trunc('day'::text, e.date_update))
-> Index Scan using regdate on event_statistics e (cost=0.57..159302.49 rows=114812 width=24)
Index Cond: ((date_update > (('now'::cstring)::date - '7 days'::interval)) AND (date_update <= now()))
Filter: (client_id = $0)
“event_statistics”表有超过 5000 万行,它只会随着客户的增加而增长,并且记录不会改变。
我尝试了很多不同的查询计划和索引,但在针对更广泛的日期范围进行聚合时无法达到可接受的速度。 我花了整整一周的时间在 stackoverflow 和一些博客上学习这个问题的不同方面以及解决这个问题的方法,但仍然不确定什么是最好的方法:
- 按client_id 或日期范围分区
- 预先聚合到单独的结果表,然后每天更新它(也不确定如何做到最好?在插入原始表时触发或为该视图或物化视图安排单独的应用程序或通过来自网站)
- 将数据库架构设计更改为每个客户端的架构或应用分片
- 更改服务器硬件(CPU Intel Xeon E7-4850 2.00GHz,RAM 6GB,它是 Web 应用程序和数据库的主机)
- 使用具有 OLAP 功能(如 Postgres-XL)的不同数据库进行分析 还是别的什么?
我还尝试了关于 event_statistics 的 btree 索引(client_id asc、action_name asc、date_update asc、id)。并且仅索引扫描速度更快,但仍然不够,并且在磁盘空间使用方面不是很好。
解决这个问题的最佳方法是什么?
更新
根据要求,explain (analyze, verbose) 命令的输出:
GroupAggregate (cost=860934.44..969228.46 rows=1600 width=44) (actual time=52388.678..54671.187 rows=64 loops=1)
Output: t.date, "*VALUES*".column1, count(e.id)
Group Key: "*VALUES*".column1, t.date
InitPlan 1 (returns $0)
-> Seq Scan on public.clients c (cost=0.00..1.07 rows=1 width=4) (actual time=0.058..0.059 rows=1 loops=1)
Output: c.id
Filter: (c.client_name = 'client name'::text)
Rows Removed by Filter: 5
-> Merge Left Join (cost=860933.36..940229.77 rows=3864215 width=44) (actual time=52388.649..54388.698 rows=799737 loops=1)
Output: t.date, "*VALUES*".column1, e.id
Merge Cond: (("*VALUES*".column1 = e.action_name) AND (t.date = (date_trunc('day'::text, e.date_update))))
-> Sort (cost=628.77..648.77 rows=8000 width=40) (actual time=0.190..0.244 rows=64 loops=1)
Output: t.date, "*VALUES*".column1
Sort Key: "*VALUES*".column1, t.date
Sort Method: quicksort Memory: 30kB
-> Nested Loop (cost=0.02..110.14 rows=8000 width=40) (actual time=0.059..0.080 rows=64 loops=1)
Output: t.date, "*VALUES*".column1
-> Function Scan on pg_catalog.generate_series t (cost=0.02..10.02 rows=1000 width=8) (actual time=0.043..0.043 rows=8 loops=1)
Output: t.date
Function Call: generate_series(((('now'::cstring)::date - '7 days'::interval))::timestamp with time zone, now(), '1 day'::interval)
-> Materialize (cost=0.00..0.14 rows=8 width=32) (actual time=0.002..0.003 rows=8 loops=8)
Output: "*VALUES*".column1
-> Values Scan on "*VALUES*" (cost=0.00..0.10 rows=8 width=32) (actual time=0.004..0.005 rows=8 loops=1)
Output: "*VALUES*".column1
-> Materialize (cost=860304.60..864168.81 rows=772843 width=24) (actual time=52388.441..54053.748 rows=799720 loops=1)
Output: e.id, e.date_update, e.action_name, (date_trunc('day'::text, e.date_update))
-> Sort (cost=860304.60..862236.70 rows=772843 width=24) (actual time=52388.432..53703.531 rows=799720 loops=1)
Output: e.id, e.date_update, e.action_name, (date_trunc('day'::text, e.date_update))
Sort Key: e.action_name, (date_trunc('day'::text, e.date_update))
Sort Method: external merge Disk: 39080kB
-> Index Scan using regdate on public.event_statistics e (cost=0.57..753018.26 rows=772843 width=24) (actual time=31.423..44284.363 rows=799720 loops=1)
Output: e.id, e.date_update, e.action_name, date_trunc('day'::text, e.date_update)
Index Cond: ((e.date_update >= (('now'::cstring)::date - '7 days'::interval)) AND (e.date_update <= now()))
Filter: (e.client_id = $0)
Rows Removed by Filter: 2983424
Planning time: 7.278 ms
Execution time: 54708.041 ms
【问题讨论】:
-
痛苦似乎在于对低基数文本列 action_name 的排序。 (就个人而言,我更喜欢数字 action_id )另外:(func)日历表和(values)action_name preudo-tables都没有用于优化(索引,统计)的可用钩子,我会将它们具体化为(TEMP ) 表
-
感谢您的提示。是的,问题似乎在于所有客户端的外部磁盘排序和读取数据缓慢。但是由于某种原因,即使使用我在帖子末尾写的覆盖索引,我也无法消除排序的需要。仅当我充分增加“work_mem”并使用内存排序但由于读取“event_statistics”表速度慢而仍然不够时,使用这样的索引才会快得多。
-
IMO 您可以在子查询中预先聚合。它不会产生超过 1600 个聚合。
-
Function Scan on generate_series t (cost=0.02..10.02 rows=1000 width=8)1000 是 generate_series() 的默认估计值。在这种情况下太大了......也许物化(+分析)日历文件可能会提示优化器。 -
请Edit您的问题并添加使用
explain (analyze, verbose)生成的执行计划。 (注意analyze选项!)
标签: sql postgresql query-optimization aggregate