我已经使用 postgresql 规则实现了数据非规范化策略。出于性能原因,我选择了规则而不是触发器。
架构的结构如下:
系统的一部分是将hits
每个用户存储在stats
表中。命中是一个虚构的指标,它并不真正相关。系统可以收集许多这些指标。统计表中有很多记录(每天> 1,000,000)。
我想知道给定日期内每个用户、每个项目、每个客户端和每个应用程序的点击次数是多少。
为了使其快速运行,我按天对统计信息进行了分组,并将输出存储到 user_hits 表中。在此过程中,还添加了 application_id、client_id 和 project_id(作为列),并创建了适当的索引。
我想通过按 project_id、client_id 和最后的 application_id 对事物进行分组来进一步优化流程。数据管道是这样的:
stats -> user_hits -> project_hits -> client_hits -> application_hits
我想确保当我删除user_hits
给定日期的数据时project_hits
,同一日期的数据也被删除。这个过程应该传播到链中的最后一个表。
我定义了这些简单的规则:
CREATE RULE delete_children AS ON DELETE TO user_hits
DO ALSO
DELETE FROM project_hits WHERE day = OLD.day;
CREATE RULE delete_children AS ON DELETE TO project_hits
DO ALSO
DELETE FROM client_hits WHERE day = OLD.day;
CREATE RULE delete_children AS ON DELETE TO client_hits
DO ALSO
DELETE FROM application_hits WHERE day = OLD.day;
Run Code Online (Sandbox Code Playgroud)
但是,当我发出这样的声明时:
DELETE FROM user_hits WHERE day = current_date;
Run Code Online (Sandbox Code Playgroud)
我希望它运行这 3 个查询作为回报:
DELETE FROM project_hits WHERE day = current_date;
DELETE FROM client_hits WHERE day = current_date;
DELETE FROM application_hits WHERE day = current_date;
Run Code Online (Sandbox Code Playgroud)
然而,事实并非如此。
它完成了操作,但需要几分钟才能完成(使用测试数据)。使用真实数据需要几个小时,而手动运行这 3 个查询需要几毫秒。花费的时间似乎与组合数量(用户 x 项目 x 客户端 x 应用程序)成正比。
这里有什么问题?我错过了什么吗?这可以通过触发器以优化的方式实现吗?
包含重现问题的示例脚本:
https://gist.github.com/assembler/5151102
更新:从user_hits
到project_hits
(等等)的转换是由后台工作进程完成的(因为它涉及联系 3rd 方服务以获取更多信息)。它足够聪明,可以重新计算丢失日期的所有内容。所以我唯一需要的是一种以优化的方式级联删除记录的方法。
更新:stats
表格每天都会填满。唯一可能的情况是无条件删除一整天的数据,然后用新值替换它。
更新:我注意到,受影响的行数(从提取的explain
语句)是正好等于在受影响的行的产品user_hits
,project_hits
,client_hits
,和application_hits
桌子(数亿行的)。
事实证明它是这样工作的:
DELETE FROM user_hits WHERE day = current_date;
user_hits
表中的每一行,都会触发 RULE 删除每一行project_hits
project_hits
,都会触发 RULE 删除其中的每一行client_hits
client_hits
,都会触发 RULE 删除其中的每一行application_hits
因此,操作数等于这些表中受影响行数的乘积。
下次,请包括 EXPLAIN 输出,而不是让我们在您的脚本中挖掘它。不能保证我的系统使用与您相同的计划(尽管使用您的测试数据很可能)。
这里的规则系统运行正常。首先,我想包括我自己的诊断查询(注意我没有运行 EXPLAIN ANALYZE 因为我只是对生成的查询计划感兴趣):
rulestest=# explain DELETE FROM user_hits WHERE day = '2013-03-16';
QUERY PLAN
--------------------------------------------------------------------------------
----------------------
Delete on application_hits (cost=0.00..3953181.85 rows=316094576 width=24)
-> Nested Loop (cost=0.00..3953181.85 rows=316094576 width=24)
-> Seq Scan on user_hits (cost=0.00..1887.00 rows=49763 width=10)
Filter: (day = '2013-03-16'::date)
-> Materialize (cost=0.00..128.53 rows=6352 width=22)
-> Nested Loop (cost=0.00..96.78 rows=6352 width=22)
-> Seq Scan on project_hits (cost=0.00..14.93 rows=397 wi
dth=10)
Filter: (day = '2013-03-16'::date)
-> Materialize (cost=0.00..2.49 rows=16 width=16)
-> Nested Loop (cost=0.00..2.41 rows=16 width=16)
-> Seq Scan on application_hits (cost=0.00..1
.10 rows=4 width=10)
Filter: (day = '2013-03-16'::date)
-> Materialize (cost=0.00..1.12 rows=4 width=
10)
-> Seq Scan on client_hits (cost=0.00..
1.10 rows=4 width=10)
Filter: (day = '2013-03-16'::date)
Delete on client_hits (cost=0.00..989722.41 rows=79023644 width=18)
-> Nested Loop (cost=0.00..989722.41 rows=79023644 width=18)
-> Seq Scan on user_hits (cost=0.00..1887.00 rows=49763 width=10)
Filter: (day = '2013-03-16'::date)
-> Materialize (cost=0.00..43.83 rows=1588 width=16)
-> Nested Loop (cost=0.00..35.89 rows=1588 width=16)
-> Seq Scan on project_hits (cost=0.00..14.93 rows=397 wi
dth=10)
Filter: (day = '2013-03-16'::date)
-> Materialize (cost=0.00..1.12 rows=4 width=10)
-> Seq Scan on client_hits (cost=0.00..1.10 rows=4
width=10)
Filter: (day = '2013-03-16'::date)
Delete on project_hits (cost=0.00..248851.80 rows=19755911 width=12)
-> Nested Loop (cost=0.00..248851.80 rows=19755911 width=12)
-> Seq Scan on user_hits (cost=0.00..1887.00 rows=49763 width=10)
Filter: (day = '2013-03-16'::date)
-> Materialize (cost=0.00..16.91 rows=397 width=10)
-> Seq Scan on project_hits (cost=0.00..14.93 rows=397 width=10
)
Filter: (day = '2013-03-16'::date)
Delete on user_hits (cost=0.00..1887.00 rows=49763 width=6)
-> Seq Scan on user_hits (cost=0.00..1887.00 rows=49763 width=6)
Filter: (day = '2013-03-16'::date)
(39 rows)
rulestest=# select distinct day from application_hits;
day
------------
2013-03-15
2013-03-16
(2 rows)
rulestest=# select count(*), day from application_hits group by day;
count | day
-------+------------
4 | 2013-03-15
4 | 2013-03-16
(2 rows)
rulestest=# select count(*), day from client_hits group by day;
count | day
-------+------------
4 | 2013-03-15
4 | 2013-03-16
(2 rows)
rulestest=# select count(*), day from project_hits group by day;
count | day
-------+------------
397 | 2013-03-15
397 | 2013-03-16
(2 rows)
Run Code Online (Sandbox Code Playgroud)
如果您的数据与现有数据类似,则规则和触发器都不会很好地工作。更好的是一个存储过程,您传递一个值并删除您想要的所有内容。
首先让我们注意,这里的索引将一事无成,因为在所有情况下,您都在拉取一半的表(我确实在所有表上添加了一天的索引以帮助计划者,但这并没有真正的区别)。
你需要从你对规则所做的事情开始。规则基本上是重写查询,并且它们使用尽可能健壮的方式来实现。您的代码也与您的示例不匹配,尽管它更符合您的问题。您对表的规则级联到其他表上的规则,这些规则级联到其他表上的规则
因此,当您delete from user_hits where [criteria]
,规则将其转换为一组查询:
DELETE FROM application_hits
WHERE day IN (SELECT day FROM client_hits
WHERE day IN (SELECT day FROM user_hits WHERE [condition]));
DELETE FROM client_hits
WHERE day IN (SELECT day FROM user_hits WHERE [condition]);
DELETE FROM user_hits WHERE [condition];
Run Code Online (Sandbox Code Playgroud)
现在,您可能认为我们可以先跳过对 client_hits 的扫描,但这不是这里发生的情况。问题是您可能有几天的 user_hits 和 application_hits 不在 client_hits 中,因此您确实必须扫描所有表。
现在这里没有灵丹妙药。触发器不会更好地工作,因为虽然它可以避免扫描每个表,但它会在每一行被删除时触发,因此您基本上最终会得到相同的嵌套循环顺序扫描,这些扫描当前正在扼杀性能。这将工作一点更好,因为它会删除一路上行,而不是重写沿途的查询,但它不会表现非常好。
一个多更好的解决方法就是定义一个存储过程并让应用程序调用。就像是:
CREATE OR REPLACE FUNCTION delete_stats_at_date(in_day date) RETURNS BOOL
LANGUAGE SQL AS
$$
DELETE FROM application_hits WHERE day = $1;
DELETE FROM project_hits WHERE day = $1;
DELETE FROM client_hits WHERE day = $1;
DELETE FROM user_hits WHERE day = $1;
SELECT TRUE;
$$;
Run Code Online (Sandbox Code Playgroud)
根据测试数据,这在我的笔记本电脑上运行了 280 毫秒。
关于规则的难点之一是记住它们是什么,并注意到计算机实际上无法读取您的思想。这就是为什么我不会将它们视为初学者工具的原因。