如何链接postgres规则?

Mil*_*vic 4 postgresql

我已经使用 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_hitsproject_hits(等等)的转换是由后台工作进程完成的(因为它涉及联系 3rd 方服务以获取更多信息)。它足够聪明,可以重新计算丢失日期的所有内容。所以我唯一需要的是一种以优化的方式级联删除记录的方法。


更新:stats表格每天都会填满。唯一可能的情况是无条件删除一整天的数据,然后用新值替换它。


更新:我注意到,受影响的行数(从提取的explain语句)是正好等于在受影响的行的产品user_hitsproject_hitsclient_hits,和application_hits桌子(数亿行的)。

事实证明它是这样工作的:

  1. 我跑 DELETE FROM user_hits WHERE day = current_date;
  2. 对于user_hits表中的每一行,都会触发 RULE 删除每一行project_hits
  3. 对于 的每一行project_hits,都会触发 RULE 删除其中的每一行client_hits
  4. 对于 的每一行client_hits,都会触发 RULE 删除其中的每一行application_hits

因此,操作数等于这些表中受影响行数的乘积。

Chr*_*ers 7

下次,请包括 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 毫秒。

关于规则的难点之一是记住它们是什么,并注意到计算机实际上无法读取您的思想。这就是为什么我不会将它们视为初学者工具的原因。