我已经在stackoverflow上发布了这个问题,但我认为我可能会在这里得到更好的答案。
我有一个表存储发生在用户身上的数百万个事件:
Table "public.events"
Column | Type | Modifiers
------------+--------------------------+-----------------------------------------------------------
event_id | integer | not null default nextval('events_event_id_seq'::regclass)
user_id | bigint |
event_type | integer |
ts | timestamp with time zone |
event_type 有 5 个不同的值,即数百万用户,每个 event_type 的每个用户的事件数量不同,通常在 1 到 50 之间。
数据样本:
+-----------+----------+-------------+----------------------------+
| event_id | user_id | event_type | timestamp |
+-----------+----------+-------------+----------------------------+
| 1 | 1 | 1 | January, 01 2015 00:00:00 |
| 2 | 1 | 1 | January, 10 2015 00:00:00 |
| 3 | 1 | 1 | January, 20 2015 00:00:00 |
| 4 | 1 | 1 | January, 30 2015 00:00:00 |
| 5 | 1 | 1 | February, 10 2015 00:00:00 |
| 6 | 1 | 1 | February, 21 2015 00:00:00 |
| 7 | 1 | 1 | February, 22 2015 00:00:00 |
+-----------+----------+-------------+----------------------------+
对于每个事件,我想获取同一用户的事件数以及事件event_type
发生前 30 天内发生的事件数。
它应该如下所示:
+-----------+----------+-------------+-----------------------------+-------+
| event_id | user_id | event_type | timestamp | count |
+-----------+----------+-------------+-----------------------------+-------+
| 1 | 1 | 1 | January, 01 2015 00:00:00 | 1 |
| 2 | 1 | 1 | January, 10 2015 00:00:00 | 2 |
| 3 | 1 | 1 | January, 20 2015 00:00:00 | 3 |
| 4 | 1 | 1 | January, 30 2015 00:00:00 | 4 |
| 5 | 1 | 1 | February, 10 2015 00:00:00 | 3 |
| 6 | 1 | 1 | February, 21 2015 00:00:00 | 3 |
| 7 | 1 | 1 | February, 22 2015 00:00:00 | 4 |
+-----------+----------+-------------+-----------------------------+-------+
到目前为止,我成功地使用了两个不同的查询(在 PostgreSQL 9.4.1 上对 1000 行生成的样本进行测试):
SELECT
event_id, user_id,event_type,"timestamp",
(
SELECT count(*)
FROM events
WHERE timestamp >= e.timestamp - interval '30 days'
AND timestamp <= e.timestamp
AND user_id = e.user_id
AND event_type = e.event_type
GROUP BY event_type, user_id
) as "count"
FROM events e;
它工作得很好,特别是因为我有一个关于时间戳的索引:
Index Scan using pk_event_id on events e (cost=0.28..12018.74 rows=1000 width=24)
SubPlan 1
-> GroupAggregate (cost=4.33..11.97 rows=1 width=20)
Group Key: events.event_type, events.user_id
-> Bitmap Heap Scan on events (cost=4.33..11.95 rows=1 width=20)
Recheck Cond: ((""timestamp"" >= (e."timestamp" - '30 days'::interval)) AND ("timestamp" <= e."timestamp"))
Filter: ((user_id = e.user_id) AND (event_type = e.event_type))
-> Bitmap Index Scan on idx_events_timestamp (cost=0.00..4.33 rows=5 width=0)
Index Cond: ((""timestamp"" >= (e."timestamp" - '30 days'::interval)) AND ("timestamp" <= e."timestamp"))
尽管如此,它还是不能很好地扩展,我认为使用窗口函数可能会提高性能:
SELECT toto.event_id,toto.user_id,toto.event_type,toto.lv as time,COUNT(*)
FROM(
SELECT e.event_id, e.user_id,e.event_type,"timestamp",
last_value("timestamp") OVER w as lv,
unnest(array_agg(e."timestamp") OVER w) as agg
FROM events e
WINDOW w AS (PARTITION BY e.user_id,e.event_type ORDER BY e."timestamp"
ROWS UNBOUNDED PRECEDING)) AS toto
WHERE toto.agg >= toto.lv - interval '30 days'
GROUP by event_id,user_id,event_type,lv;
由于我必须使用 unnest 和子查询,因此性能实际上变得更糟:
Sort (cost=5344.41..5427.74 rows=33333 width=24)
Sort Key: toto.event_id
-> HashAggregate (cost=2506.99..2840.32 rows=33333 width=24)
Group Key: toto.event_id, toto.user_id, toto.event_type, toto.lv
-> Subquery Scan on toto (cost=67.83..2090.33 rows=33333 width=24)
Filter: (toto.agg >= (toto.lv - '30 days'::interval))
-> WindowAgg (cost=67.83..590.33 rows=100000 width=24)
-> Sort (cost=67.83..70.33 rows=1000 width=24)
Sort Key: e.user_id, e.event_type, e."timestamp"
-> Seq Scan on events e (cost=0.00..18.00 rows=1000 width=24)
我想知道是否可以修改是否只能保留子查询并以某种方式修改窗口框架以仅保留行时间戳之前 30 天或更短的时间戳。您是否认为可以在不切换到 MapReduce 框架的情况下针对非常大的表扩展此查询?
第二次,我想排除重复的事件,即event_type
相同的时间戳。
假设这个清理过的表定义
您的比较似乎不公平,第一个查询有
ORDER BY event_id
,但第二个没有。EXPLAIN
输出不适合第一个查询(无排序步骤)。确保使用相同的ORDER BY
子句运行所有测试以获得有效结果。最好运行几次并比较最好的 5 次以消除缓存效果。指数
性能的关键是这个多列索引:
列的顺序很重要!为什么?
查询
您的每个查询都可以改进:
查询 1
删除
group by event_type, user_id
而不替换:等效于更现代的
LATERAL
连接(Postgres 9.3+):这也可能是结合上述索引最快的查询。
相关答案和更多解释:
查询 2
last_value(ts) OVER w as lv
只是一个昂贵的副本ts
。ROWS UNBOUNDED PRECEDING
是默认值,因此只是噪音。但这是不必要的复杂。使用连接而不是使用窗口函数的子查询可以使相同的逻辑便宜得多:
这是我最喜欢的另一个顶级性能。再次使用上述索引。
其他查询
这是另一个想法,但我怀疑它是否可以竞争。不过,试一试:
SQL Fiddle在 Postgres 9.3 中演示了所有内容。
我接受了@Erwin 的回答,但这里是使用更正查询生成的数据(10000 行,最佳 5 次执行)的基准。我使用多列索引运行它。
正如预期的那样,查询 1 (26.324 ms) 和 2 (23.264 ms) 在性能方面非常相似,而查询 3 最慢 (32.775 ms)。
查询 1
解释(缓冲,分析)
查询 2
解释(缓冲,分析)
查询 3
解释(缓冲,分析)
我不认为这会比已经提供的替代方案更好,但它可能值得测试并添加到选项中: