我已经使用 postgresql 规则实施了数据非规范化策略。出于性能原因,我选择了规则而不是触发器。
Schema 的结构如下:
- 应用程序有很多客户
- 客户有很多项目
- 项目有很多用户
hits
系统的一部分是为表中的每个用户存储stats
。Hit 是一个虚构的指标,它并不真正相关。系统可以收集许多这些指标。统计表中有很多记录(每天超过 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;
但是,当我发表这样的声明时:
DELETE FROM user_hits WHERE day = current_date;
我希望它运行这 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;
然而,事实并非如此。
它完成了操作,但需要几分钟才能完成(使用测试数据)。使用真实数据需要数小时,而手动运行这 3 个查询需要几毫秒。它花费的时间似乎与组合的数量成正比(用户 x 项目 x 客户 x 应用程序)。
这里有什么问题?我错过了什么吗?这可以用触发器以优化的方式实现吗?
包括重现问题的示例脚本:
https://gist.github.com/assembler/5151102
user_hits
更新:从到project_hits
(等等)的过渡由工作进程在后台完成(因为它涉及联系第 3 方服务以获取更多信息)。它足够聪明,可以重新计算缺失日期的所有内容。所以我唯一需要的是一种以优化方式级联删除记录的方法。
更新:stats
每天都会填写表格。唯一可能的情况是无条件删除一整天的数据,然后用新值替换它。
更新:我注意到受影响的行数(从explain
语句中提取)正好等于user_hits
、project_hits
、client_hits
和application_hits
表(数亿行)中受影响行的乘积。
事实证明它是这样工作的:
- 我跑
DELETE FROM user_hits WHERE day = current_date;
- 对于
user_hits
表中的每一行,触发规则,从中删除每一行project_hits
- 对于 的每一行
project_hits
,都会触发规则,从中删除每一行client_hits
- 对于 的每一行
client_hits
,都会触发规则,从中删除每一行application_hits
因此,操作数等于这些表中受影响行数的乘积。
下次,请包含 EXPLAIN 输出,而不是让我们在您的脚本中挖掘它。不能保证我的系统使用与您的相同的计划(尽管使用您的测试数据很可能)。
这里的规则系统运行正常。首先,我想包括我自己的诊断查询(注意我没有运行 EXPLAIN ANALYZE 因为我只是对生成的查询计划感兴趣):
如果您的数据与现有数据有任何相似之处,则规则和触发器都不会很好地工作。更好的是一个存储过程,你传递一个值并删除你想要的一切。
首先让我们注意这里的索引将无处可去,因为在所有情况下您都在拉取一半的表(我确实在所有表上添加了一天的索引以帮助计划者,但这没有真正的区别)。
您需要从使用规则开始。RULEs 基本上重写了查询,并且它们使用尽可能健壮的方式来这样做。尽管您的代码更符合您的问题,但您的代码也不符合您的示例。您在表上有规则,这些规则级联到其他表上的规则,这些规则级联到其他表上的规则
因此,当您 时
delete from user_hits where [criteria]
,规则将其转换为一组查询:现在,您可能认为我们可以首先跳过对 client_hits 的扫描,但这不是这里发生的事情。问题是您可能在 user_hits 和 application_hits 中有几天不在 client_hits 中,因此您真的必须扫描所有表。
现在这里没有灵丹妙药。触发器不会更好地工作,因为虽然它可以避免扫描每个表,但它会在被删除的每一行中被触发,因此您基本上最终会得到相同的嵌套循环顺序扫描,而这些扫描目前正在降低性能。它会工作得更好一些,因为它会沿途删除行而不是沿途重写查询,但它不会执行得很好。
一个更好的解决方案是只定义一个存储过程并让应用程序调用它。就像是:
根据测试数据,这在我的笔记本电脑上运行了 280 毫秒。
关于规则的困难之一是记住它们是什么,并注意到计算机实际上无法读懂你的想法。这就是为什么我不认为它们是初学者的工具。
接下来:为
INSERT
......制定一些规则并且不要忘记UPDATE
关键领域的令人讨厌的案例。好吧,已经很久了,但是如果你运行一个
EXPLAIN
我们可以看看我的记忆是否正确。我认为辅助查询的计划是在错误的时间创建的,在计划者可以考虑索引之前。我认为您正在进行表扫描。话虽如此,您是否对标准级联删除外键进行基准测试太慢了?那个规则会更快?
[评论后编辑]
仔细阅读文档,似乎(与触发器不同)当使用规则时,它将(在没有添加 where 子句的情况下)应用于整个原始表?!