【问题标题】:Use postgres_fdw to speed up view containing multiple self-join on group-by使用 postgres_fdw 加速包含多个自加入分组的视图
【发布时间】:2023-04-02 07:34:01
【问题描述】:

(警告道歉和黑客入侵...)

背景:

我有一个遗留应用程序,我希望避免重写它的大量 SQL 代码。我正在尝试加快它执行的特定类型的非常昂贵的查询(即:低悬的果实)。

它有一个由transactions 表表示的金融交易分类帐。当插入新行时,触发函数(此处未显示)会为给定实体结转新余额。

某些类型的交易模型外部性(如飞行中支付)通过使用“相关”交易标记新交易,以便应用程序可以将相关交易组合在一起。

\d transactions

                  Table "public.transactions"
       Column        |   Type    | Modifiers 
---------------------+-----------+-----------
 entityid            | bigint    | not null
 transactionid       | bigint    | not null default nextval('tid_seq')
 type                | smallint  | not null
 status              | smallint  | not null
 related             | bigint    | 
 amount              | bigint    | not null
 abs_amount          | bigint    | not null
 is_credit           | boolean   | not null
 inserted            | timestamp | not null default now()
 description         | text      | not null
 balance             | bigint    | not null

Indexes:
    "transactions_pkey" PRIMARY KEY, btree (transactionid)
    "transactions by entityid" btree (entityid)
    "transactions by initial trans" btree ((COALESCE(related, transactionid)))

Foreign-key constraints:
    "invalid related transaction!" FOREIGN KEY (related) 
                                   REFERENCES transactions(transactionid)

在我的测试数据集中,我有:

  • 总共大约 550 万行
  • 大约 370 万行没有“相关”事务
  • 大约 180 万行包含“相关”事务
  • 大约有 55k 个不同的 entityid(客户)。

因此,大约 1/3 的事务行是与某个早期事务“相关”的更新。生产数据大约是 transactionid-wise 的 25 倍,不同 entityid-wise 的大约 8 倍,并且 1/3 的比率用于事务更新。

代码查询了一个特别是无效的视图,它被定义为:

CREATE VIEW collapsed_transactions AS
SELECT t.entityid,
    g.initial,
    g.latest,
    i.inserted AS created,
    t.inserted AS updated,
    t.type,
    t.status,
    t.amount,
    t.abs_amount,
    t.is_credit,
    t.balance,
    t.description
FROM ( SELECT 
          COALESCE(x.related, x.transactionid) AS initial,
          max(x.transactionid) AS latest
       FROM transactions x
       GROUP BY COALESCE(x.related, x.transactionid)
     ) g
INNER JOIN transactions t ON t.transactionid = g.latest
INNER JOIN transactions i ON i.transactionid = g.initial;

典型的查询采用以下形式:

SELECT * FROM collapsed_transactions WHERE entityid = 204425;

如您所见,where entityid = 204425 子句不会用于约束 GROUP BY 子查询,因此 所有 entitids 的事务将被分组,从而产生 55,000 个更大的子查询结果集和愚蠢地更长的查询时间......在撰写本文时平均达到 40 行(在本例中为 71 行)。

如果不重写代码库的数百个 SQL 查询,我就无法进一步规范化 transactions 表(比如让 initial_transactionsupdated_transactions 表加入 related),其中许多查询使用自连接语义以不同的方式。

洞察力:

我最初尝试使用 WINDOW 函数重写查询,但遇到了各种各样的问题(另一个 SO 问题再次出现),当我看到 www_fdw 将其 WHERE 子句作为 GET/POST 参数传递给 HTTP 时,我对在不进行大量重组的情况下优化非常幼稚的查询的可能性非常感兴趣。

Postgresql 9.3 manual 说:

F.31.4。远程查询优化

postgres_fdw 尝试优化远程查询以减少数量 从国外服务器传输的数据。这是通过发送 查询 WHERE 子句到远程服务器执行,而不是 检索当前查询不需要的表列。到 减少错误执行查询的风险,不发送 WHERE 子句 到远程服务器,除非他们只使用内置数据类型, 运算符和函数。子句中的运算符和函数必须 也是不可变的。

实际发送到远程服务器执行的查询可以 使用 EXPLAIN VERBOSE 进行检查。

尝试:

所以我认为也许我可以将 GROUP-BY 放入一个视图中,将该视图视为一个外部表,并且优化器将通过 WHERE 子句传递到该外部表,从而产生更有效的查询...... .

CREATE VIEW foreign_transactions_grouped_by_initial_transaction AS 
  SELECT
    entityid,
    COALESCE(t.related, t.transactionid) AS initial,
    MAX(t.transactionid) AS latest
  FROM transactions t
  GROUP BY
    t.entityid,
    COALESCE(t.related, t.transactionid);

CREATE FOREIGN TABLE transactions_grouped_by_initial_transaction 
  (entityid bigint, initial bigint, latest bigint) 
  SERVER local_pg_server 
  OPTIONS (table_name 'foreign_transactions_grouped_by_initial_transaction');

EXPLAIN ANALYSE VERBOSE
  SELECT 
    t.entityid, 
    g.initial, 
    g.latest, 
    i.inserted AS created, 
    t.inserted AS updated, 
    t.type, 
    t.status,
    t.amount,
    t.abs_amount,
    t.is_credit,
    t.balance,
    t.description
  FROM transactions_grouped_by_initial_transaction g 
  INNER JOIN transactions t on t.transactionid = g.latest
  INNER JOIN transactions i on i.transactionid = g.initial 
  WHERE g.entityid = 204425;

而且效果很好!

 Nested Loop  (cost=100.87..305.05 rows=10 width=116) 
              (actual time=4.113..16.646 rows=71 loops=1)
   Output: t.entityid, g.initial, g.latest, i.inserted, 
           t.inserted, t.type, t.status, t.amount, t.abs_amount, 
           t.balance, t.description
   ->  Nested Loop  (cost=100.43..220.42 rows=10 width=108) 
                    (actual time=4.017..10.725 rows=71 loops=1)
         Output: g.initial, g.latest, t.entityid, t.inserted, 
                 t.type, t.status, t.amount, t.abs_amount, t.is_credit,
                 t.balance, t.description
     ->  Foreign Scan on public.transactions_grouped_by_initial_transaction g
                 (cost=100.00..135.80 rows=10 width=16) 
                 (actual time=3.914..4.694 rows=71 loops=1)
            Output: g.entityid, g.initial, g.latest
            Remote SQL: 
              SELECT initial, latest
              FROM public.foreign_transactions_grouped_by_initial_transaction
              WHERE ((entityid = 204425))
         ->  Index Scan using transactions_pkey on public.transactions t  
                  (cost=0.43..8.45 rows=1 width=100) 
                  (actual time=0.023..0.035 rows=1 loops=71)
               Output: t.entityid, t.transactionid, t.type, t.status, 
                       t.related, t.amount, t.abs_amount, t.is_credit, 
                       t.inserted, t.description, t.balance
               Index Cond: (t.transactionid = g.latest)
   ->  Index Scan using transactions_pkey on public.transactions i  
            (cost=0.43..8.45 rows=1 width=16) 
            (actual time=0.021..0.033 rows=1 loops=71)
         Output: i.entityid, i.transactionid, i.type, i.status, 
                 i.related, i.amount, i.abs_amount, i.is_credit, 
                 i.inserted, i.description, i.balance
         Index Cond: (i.transactionid = g.initial)
 Total runtime: 20.363 ms

问题:

但是,当我尝试将其烘焙到 VIEW 中(有或没有另一层 postgres_fdw)时,查询优化器似乎没有通过 WHERE 子句:-(

CREATE view collapsed_transactions_fast AS
  SELECT 
    t.entityid, 
    g.initial, 
    g.latest, 
    i.inserted AS created, 
    t.inserted AS updated, 
    t.type, 
    t.status,
    t.amount,
    t.abs_amount,
    t.is_credit,
    t.balance,
    t.description
  FROM transactions_grouped_by_initial_transaction g 
  INNER JOIN transactions t on t.transactionid = g.latest
  INNER JOIN transactions i on i.transactionid = g.initial;

EXPLAIN ANALYSE VERBOSE
  SELECT * FROM collapsed_transactions_fast WHERE entityid = 204425; 

结果:

Nested Loop  (cost=534.97..621.88 rows=1 width=117) 
             (actual time=104720.383..139307.940 rows=71 loops=1)
  Output: t.entityid, g.initial, g.latest, i.inserted, t.inserted, t.type, 
          t.status, t.amount, t.abs_amount, t.is_credit, t.balance, 
          t.description
  ->  Hash Join  (cost=534.53..613.66 rows=1 width=109) 
                 (actual time=104720.308..139305.522 rows=71 loops=1)
        Output: g.initial, g.latest, t.entityid, t.inserted, t.type, 
                t.status, t.amount, t.abs_amount, t.is_credit, t.balance, 
                t.description
        Hash Cond: (g.latest = t.transactionid)
    ->  Foreign Scan on public.transactions_grouped_by_initial_transaction g
         (cost=100.00..171.44 rows=2048 width=16) 
         (actual time=23288.569..108916.051 rows=3705600 loops=1)
           Output: g.entityid, g.initial, g.latest
           Remote SQL: 
            SELECT initial, latest 
            FROM public.foreign_transactions_grouped_by_initial_transaction
        ->  Hash  (cost=432.76..432.76 rows=142 width=101) 
                  (actual time=2.103..2.103 rows=106 loops=1)
              Output: 
                t.entityid, t.inserted, t.type, t.status, t.amount, 
                t.abs_amount, t.is_credit, t.balance, t.description, 
                t.transactionid
              Buckets: 1024  Batches: 1  Memory Usage: 14kB
              ->  Index Scan using "transactions by entityid" 
                  on public.transactions t  
                     (cost=0.43..432.76 rows=142 width=101) 
                     (actual time=0.049..1.241 rows=106 loops=1)
                    Output: t.entityid, t.inserted, t.type, t.status, 
                            t.amount, t.abs_amount, t.is_credit, 
                            t.balance, t.description, t.transactionid
                    Index Cond: (t.entityid = 204425)
  ->  Index Scan using transactions_pkey on public.transactions i  
        (cost=0.43..8.20 rows=1 width=16) 
        (actual time=0.013..0.018 rows=1 loops=71)
        Output: i.entityid, i.transactionid, i.type, i.status, i.related, 
                i.amount, i.abs_amount, i.is_credit, i.inserted, i.description, 
                 i.balance
        Index Cond: (i.transactionid = g.initial)
Total runtime: 139575.140 ms

如果我可以将该行为烘焙到 VIEW 或 FDW 中,那么我可以在非常少的查询中替换 VIEW 的 name 以提高效率。我不在乎它对于其他用例(更复杂的 WHERE 子句)是否超级慢,我将命名 VIEW 以反映其预期用途。

use_remote_estimate 的默认值为FALSE,但无论哪种方式都没有区别。

问题:

我可以使用一些技巧来使这个公认的黑客工作起作用吗?

【问题讨论】:

  • 从这里看起来,FDW 本身是无关紧要的。由于您将条件从t.entityid 切换到g.entityid,因此计划得到了改进。当你把它放在视图后面时它会倒退,因为视图的entityid 仍然指向t。计划者甚至允许改变这个条件,因为没有任何迹象表明相关交易的entityids 是相关的。
  • @NickBarnes 会试试看! (我认为这基本上可以归结为@RichardHuxton 在下面所说的。)

标签: postgresql group-by postgresql-9.3 sql-view foreign-data-wrapper


【解决方案1】:

如果我正确理解了您的问题,答案是“否”。没有任何“技巧”可以通过 fdw 包装器获得额外的 where 子句。

但是,我认为您可能优化了错误的东西。

我会替换整个 collapsed_transactions 视图。除非我遗漏了什么,否则它只取决于交易表。创建一个表,使用触发器对其进行更新,并且只向普通用户授予 SELECT 权限。如果您还没有,请从pgtap 获取一些测试工具,并且一切顺利。


编辑:优化视图。

如果您只想针对视图优化该查询,并且可以调整视图的定义,请尝试以下操作:

CREATE VIEW collapsed_transactions AS
SELECT
    g.entityid,  -- THIS HERE
    g.initial,
    g.latest,
    i.inserted AS created,
    t.inserted AS updated,
    t.type,
    t.status,
    t.amount,
    t.abs_amount,
    t.is_credit,
    t.balance,
    t.description
FROM (
    SELECT 
    entityid, -- THIS HERE
    COALESCE(x.related, x.transactionid) AS initial,
    max(x.transactionid) AS latest
    FROM transactions x
    GROUP BY entityid, COALESCE(x.related, x.transactionid)
) g
INNER JOIN transactions t ON t.transactionid = g.latest
INNER JOIN transactions i ON i.transactionid = g.initial;

请注意,子查询公开 entityid 并允许我们对其进行过滤。我假设 entityid 对于主要和相关项目是恒定的,否则我看不到查询如何工作。这应该让规划者充分掌握问题,以便首先使用 entityid 上的索引并将查询时间缩短到毫秒。

【讨论】:

  • 不幸的是,通过触发器保持折叠事务更新的成本也相对较高(系统一次在transactions 上执行数万次插入)......不是我的东西d想在每次插入或删除时都做。单个实体的 collapsed_transactions 查询仅按需运行(由人工操作员),所以这就是我要优化的目标。
  • 它太诱人了,它可以很好地作为涉及 FDW 的 SELECT 查询,但查询优化器不会对等效视图做同样的事情。
  • 查看编辑以获取有关优化该单实体查询的视图定义的建议。
  • 我现在觉得很傻。生产系统中真正的查询确实是按entityid&初始事务分组,但是group by并没有选择entityid,所以显然查询规划器不能约束group by。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-04-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-25
  • 2017-07-01
相关资源
最近更新 更多