首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >postgres_fdw优化案例--GUC控制join pushdown

postgres_fdw优化案例--GUC控制join pushdown

作者头像
NickYoung
发布2026-07-09 16:25:27
发布2026-07-09 16:25:27
20
举报

前阵子同事小帅找到老杨,说一个跨库访问的sql运行很慢,慢得就像找女朋友的节奏一样。经过诊断发现是joinclause没有按预期pushdown到remote,导致性能差。

本篇我们透过现象看本质,从案例出发,深入剖析postgres_fdw中join及clause pushdown的原理。并且老杨对这里进行了优化,当join pushdown性能糟糕时,我们能够直接禁用join pushdown。

顺便提一下,分析fdw sql性能问题,就看执行计划, 使用explain verbose确认remote sql是否符合预期。

本文依然很长,还是希望感兴趣的老铁能耐心看完。

有朋友“吐槽”公众号的文章比较长,基本不想看。 这里解释下,老杨之前就讲过做公众号的目的:一是给自己做案例记录,二是给大家分享技术原理。因此我得“记得清楚”,“讲得明白”。原理加实验确实篇幅就过长了,可能也是老杨目前的“抽象”能力不够,还望大家多多担待。


一、 问题现象

两个外表关联查询203490ms。

代码语言:javascript
复制
postgres=> explain (analyze,verbose) select distinct a.sid,a.ip,a.port,a.type from tbl a join inf b on a.sid::text=b.id::text where a.ext=1;
                                                                QUERY PLAN                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=452.77..452.86 rows=1 width=328) (actual time=203472.078..203472.100 rows=28 loops=1)
   Output: a.sid, a.ip, a.port, a.type
   ->  Sort  (cost=452.77..452.79 rows=7 width=328) (actual time=203472.076..203472.080 rows=28 loops=1)
         Output: a.sid, a.ip, a.port, a.type
         Sort Key: a.sid, a.ip, a.port, a.type
         Sort Method: quicksort  Memory: 26kB
         ->  Foreign Scan  (cost=100.00..452.68 rows=7 width=328) (actual time=25657.993..203471.879 rows=28 loops=1)
               Output: a.sid, a.ip, a.port, a.type
               Filter: ((a.sid)::text = (b.id)::text)
               Rows Removed by Filter: 50474621
               Relations: (public.tbl a) INNER JOIN (public.inf b)
               Remote SQL: SELECT r1.sid, r1.ip, r1.port, r1.type, r2.id FROM (public.tbl r1 INNERJOIN public.inf r2 ON (((r1.ext = 1))))
 Planning Time: 0.256 ms
 Execution Time: 203489.909 ms
(14rows)

postgres=> 

这两个外表映射对应的实际表在同一remote,在remote库本地执行sql仅17ms。

代码语言:javascript
复制
testdb=> explain analyze select distinct a.sid,a.ip,a.port,a.type from tbl a join inf b on a.sid::text=b.id::text where a.ext=1;
                                                                   QUERY PLAN                                                                    
-------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=24036.30..24198.05 rows=1294 width=34) (actual time=16.465..17.169 rows=28 loops=1)
   ->  Gather Merge  (cost=24036.30..24185.11rows=1294 width=34) (actual time=16.464..17.152rows=28 loops=1)
         Workers Planned: 1
         Workers Launched: 1
         ->  Sort  (cost=23036.29..23039.52rows=1294 width=34) (actual time=13.913..13.917rows=14 loops=2)
               SortKey: a.sid, a.ip, a.port, a.type
               Sort Method: quicksort  Memory: 25kB
               Worker 0:  Sort Method: quicksort  Memory: 25kB
               ->  HashAggregate  (cost=22956.46..22969.40rows=1294 width=34) (actual time=13.835..13.853rows=14 loops=2)
                     GroupKey: a.sid, a.ip, a.port, a.type
                     Batches: 1MemoryUsage: 73kB
                     Worker 0:  Batches: 1MemoryUsage: 73kB
                     ->  HashJoin  (cost=94.34..21471.91rows=148455 width=34) (actual time=5.451..13.809rows=14 loops=2)
                           Hash Cond: ((b.id)::text = (a.sid)::text)
                           ->  Parallel Seq Scanon inf b  (cost=0.00..1260.92rows=17292 width=4) (actual time=0.007..2.763rows=14698 loops=2)
                           ->  Hash  (cost=72.88..72.88rows=1717 width=34) (actual time=2.448..2.448rows=1717 loops=2)
                                 Buckets: 2048  Batches: 1MemoryUsage: 133kB
                                 ->  Seq Scanon tbl a  (cost=0.00..72.88rows=1717 width=34) (actual time=0.016..1.169rows=1717 loops=2)
                                       Filter: (ext = 1)
                                       Rows Removed by Filter: 1633
 Planning Time: 0.242 ms
 Execution Time: 17.311 ms
(22rows)

跨库访问和本地执行性能相差10000倍,网络问题?

莫慌,从执行计划来看有所不同。重点关注fdw跨库访问的执行计划,我使用了explain verbose打印出了remote sql。 发现两表inner join是直接pushdown到了remote,但是joinclause不符合预期,a.sid::text=b.id::text没有pushdown,而变为了local filter,原本whereclause(r1.ext = 1)变为了joinclause。

remote sql: SELECT r1.sid, r1.ip, r1.port, r1.type, r2.id FROM (public.tbl r1 INNER JOIN public.inf r2 ON (((r1.ext = 1))))

joinclause都变了,join执行操作的结果集和过程肯定有差别了。从执行计划也能看出来,扫描了更多数据所以这个remote sql执行的慢。因为a.sid::text=b.id::text作为了本地过滤条件,所以最终结果是相同的。

为什么原本的joinclause没有pushdown?

二、原理分析

简述下postgres_fdw的原理

在local实例中,创建好postgres_fdw和foreign table,以及user mapping。执行query查询foreign table访问remote端,这里并不是简单粗暴地将sql下推到remote执行。

1、local stmt要经过fdw的deparse处理,主要是判断joinclause和whereclause,limit ,sort等是否可以pushdown到remote

2、根据deparse的结果,生成Foreign scan的路径,结合本地计划节点,生成整体的执行计划;

3、执行器,按照Foreign scan在remote执行,fetch结果返回给local,local端继续执行剩余计划节点,返回最终结果。

再回到这个case,joinclause a.sid::text=b.id::text为什么没有pushdown到remote?

whereclause a.ext=1为什么被pushdown到remote作为了joinclause?

1、首先我们看下join pushdown的原理

并不是任意join都可以pushdown到remote,当不能pushdown到remote时,一般是每个外表生成一个Foreign scan,获取数据到本地再做join。

什么情况下join可以pushdown到remote?主要是foreign_join_ok[1]函数的逻辑控制,当函数return false时不可pushdown。

可pushdown

1)支持的类型有INNER, LEFT, RIGHT, FULL OUTER and SEMI joins

2)对于semi join在reltarget安全的情况下才可下推

3)并且inner和outer都要是标记为safe的情况下

4)并且inner和outer的local_conds为NUll

5)并且joinclause经过is_foreign_expr函数验证is_remote_clause为true时可作为remote_conds;

对于outer join,rinfo->is_push_down为true且rinfo->required_relids是joinrel->relids的子集,并且is_remote_clause为true,则可下推。

6)对于PlaceHolder,如果phinfo->ph_eval_at不是relids的子集,且relids的set不大于phinfo->ph_eval_at,也就是两者相同时则可下推。

代码语言:javascript
复制
static bool
foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
    RelOptInfo *outerrel, RelOptInfo *innerrel,
    JoinPathExtraData *extra)
{
 PgFdwRelationInfo *fpinfo;
 PgFdwRelationInfo *fpinfo_o;
 PgFdwRelationInfo *fpinfo_i;
 ListCell   *lc;
 List    *joinclauses;

/* 1)
  * We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
  * Constructing queries representing ANTI joins is hard, hence not
  * considered right now.
  */
if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
  jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
  jointype != JOIN_SEMI)
return false;

/* 2)
  * We can't push down semi-join if its reltarget is not safe
  */
if ((jointype == JOIN_SEMI) && !semijoin_target_ok(root, joinrel, outerrel, innerrel))
return false;

/* 3)
  * If either of the joining relations is marked as unsafe to pushdown, the
  * join can not be pushed down.
  */
 fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
 fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
 fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
  !fpinfo_i || !fpinfo_i->pushdown_safe)
return false;

/* 4)
  * If joining relations have local conditions, those conditions are
  * required to be applied before joining the relations. Hence the join can
  * not be pushed down.
  */
if (fpinfo_o->local_conds || fpinfo_i->local_conds)
return false;

/*省略*/
/* 5) */
 joinclauses = NIL;
 foreach(lc, extra->restrictlist)
 {  
  RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
bool  is_remote_clause = is_foreign_expr(root, joinrel,
                rinfo->clause);

if (IS_OUTER_JOIN(jointype) &&
   !RINFO_IS_PUSHED_DOWN(rinfo, joinrel->relids))
  {
   if (!is_remote_clause)
    return false;
   joinclauses = lappend(joinclauses, rinfo);
  }
else
  {
   if (is_remote_clause)
    fpinfo->remote_conds = lappend(fpinfo->remote_conds, rinfo);
   else
    fpinfo->local_conds = lappend(fpinfo->local_conds, rinfo);
  }
 }
 /* 6) */
 /*
  * deparseExplicitTargetList() isn't smart enough to handle anything other
  * than a Var.  In particular, if there's some PlaceHolderVar that would
  * need to be evaluated within this join tree (because there's an upper
  * reference to a quantity that may go to NULL as a result of an outer
  * join), then we can't try to push the join down because we'll fail when
  * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
  * needs to be evaluated *at the top* of this join tree is OK, because we
  * can do that locally after fetching the results from the remote side.
  */
 foreach(lc, root->placeholder_list)
 {
  PlaceHolderInfo *phinfo = lfirst(lc);
  Relids  relids;

/* PlaceHolderInfo refers to parent relids, not child relids. */
  relids = IS_OTHER_REL(joinrel) ?
   joinrel->top_parent_relids : joinrel->relids;

if (bms_is_subset(phinfo->ph_eval_at, relids) &&
   bms_nonempty_difference(relids, phinfo->ph_eval_at))
   return false;
 }

/* 省略 */
}

以上是join可pushdown的一些客观条件。但最终执行计划,一定会将join pushdown到remote吗?

当然不是,为什么呢?其实很简单,我们都知道优化器选择计划的顶层逻辑就是“最小代价”预估最优代价不一定是join pushdown对应的计划,所以实际是否使用pushdown的路径还是要看cost。

从add_paths_to_joinrel[2]函数来看首先是生成Nestloop、Mergejoin、Hashjoin等,当然这里是对每个外表的Foreign scan做local join。等这里路径生成后,最后再进入hook,调用fdw函数fdwroutine->GetForeignJoinPaths,看join是否可以下推。在add_path时会进行cost比较,决定着ForeignJoinPath是否保留,set_cheapest确定最优计划。

代码语言:javascript
复制
void
add_paths_to_joinrel(PlannerInfo *root,
      RelOptInfo *joinrel,
      RelOptInfo *outerrel,
      RelOptInfo *innerrel,
      JoinType jointype,
      SpecialJoinInfo *sjinfo,
      List *restrictlist)
{
 JoinPathExtraData extra;
bool  mergejoin_allowed = true;
 ListCell   *lc;
 Relids  joinrelids;
    
 /* 省略 */
 /*
  * Find potential mergejoin clauses.  We can skip this if we are not
  * interested in doing a mergejoin.  However, mergejoin may be our only
  * way of implementing a full outer join, so override enable_mergejoin if
  * it's a full join.
  */
if (enable_mergejoin || jointype == JOIN_FULL)
  extra.mergeclause_list = select_mergejoin_clauses(root,
                joinrel,
                outerrel,
                innerrel,
                restrictlist,
                jointype,
                &mergejoin_allowed);

 /*
  * If it's SEMI, ANTI, or inner_unique join, compute correction factors
  * for cost estimation.  These will be the same for all paths.
  */
if (jointype == JOIN_SEMI || jointype == JOIN_ANTI || extra.inner_unique)
  compute_semi_anti_join_factors(root, joinrel, outerrel, innerrel,
            jointype, sjinfo, restrictlist,
            &extra.semifactors);
/* 省略 */

 /*
  * 1. Consider mergejoin paths where both relations must be explicitly
  * sorted.  Skip this if we can't mergejoin.
  */
if (mergejoin_allowed)
  sort_inner_and_outer(root, joinrel, outerrel, innerrel,
        jointype, &extra);

 /*
  * 2. Consider paths where the outer relation need not be explicitly
  * sorted. This includes both nestloops and mergejoins where the outer
  * path is already ordered.  Again, skip this if we can't mergejoin.
  * (That's okay because we know that nestloop can't handle
  * right/right-anti/full joins at all, so it wouldn't work in the
  * prohibited cases either.)
  */
if (mergejoin_allowed)
  match_unsorted_outer(root, joinrel, outerrel, innerrel,
        jointype, &extra);
/* 省略 */
#ifdef NOT_USED

 /*
  * 3. Consider paths where the inner relation need not be explicitly
  * sorted.  This includes mergejoins only (nestloops were already built in
  * match_unsorted_outer).
  *
  * Diked out as redundant 2/13/2000 -- tgl.  There isn't any really
  * significant difference between the inner and outer side of a mergejoin,
  * so match_unsorted_inner creates no paths that aren't equivalent to
  * those made by match_unsorted_outer when add_paths_to_joinrel() is
  * invoked with the two rels given in the other order.
  */
if (mergejoin_allowed)
  match_unsorted_inner(root, joinrel, outerrel, innerrel,
        jointype, &extra);
#endif

 /*
  * 4. Consider paths where both outer and inner relations must be hashed
  * before being joined.  As above, disregard enable_hashjoin for full
  * joins, because there may be no other alternative.
  */
if (enable_hashjoin || jointype == JOIN_FULL)
  hash_inner_and_outer(root, joinrel, outerrel, innerrel,
        jointype, &extra);

 /*
  * 5. If inner and outer relations are foreign tables (or joins) belonging
  * to the same server and assigned to the same user to check access
  * permissions as, give the FDW a chance to push down joins.
  */
if (joinrel->fdwroutine &&
  joinrel->fdwroutine->GetForeignJoinPaths)
  joinrel->fdwroutine->GetForeignJoinPaths(root, joinrel,
             outerrel, innerrel,
             jointype, &extra);

 /*
  * 6. Finally, give extensions a chance to manipulate the path list.  They
  * could add new paths (such as CustomPaths) by calling add_path(), or
  * add_partial_path() if parallel aware.  They could also delete or modify
  * paths added by the core code.
  */
if (set_join_pathlist_hook)
  set_join_pathlist_hook(root, joinrel, outerrel, innerrel,
          jointype, &extra);
}

2、我们接着看原本的joinclause为什么没有pushdown

在之前join pushdown条件的第5)点,我们提到了joinclause经过is_foreign_expr函数验证is_remote_clause为true时可作为remote_conds那么关键就在于is_foreign_expr[3]函数。

1)foreign_expr_walker[4]函数check当前joinclause为不安全的expr时,返回false,不可下推;这里的场景相对比较复杂,对于不同的expr type有不同的安全判断逻辑

对于我们这个case来说,由于expr nodetag为T_CoerceViaIO,这是一种类型转换机制,当PostgreSQL需要在两种数据类型之间进行转换,但没有直接的转换函数时,会使用这种转换方式。

而foreign_expr_walker对T_CoerceViaIO直接走default分支,返回false,不可下推。 我们这里的a.sid::text=b.id::text是numeric强转text。

2)如果expr具有有效的排序规则,且该排序规则不是来自外部变量,则不可下推

3)如果expr包含可变函数则不可下推,比如now()函数,发送到远程端可能会因时钟偏移而造成混淆。

代码语言:javascript
复制
bool
is_foreign_expr(PlannerInfo *root,
    RelOptInfo *baserel,
    Expr *expr)
{
 foreign_glob_cxt glob_cxt;
 foreign_loc_cxt loc_cxt;
 PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) (baserel->fdw_private);

 /*
  * Check that the expression consists of nodes that are safe to execute
  * remotely.
  */
 glob_cxt.root = root;
 glob_cxt.foreignrel = baserel;

 /*
  * For an upper relation, use relids from its underneath scan relation,
  * because the upperrel's own relids currently aren't set to anything
  * meaningful by the core code.  For other relation, use their own relids.
  */
if (IS_UPPER_REL(baserel))
  glob_cxt.relids = fpinfo->outerrel->relids;
else
  glob_cxt.relids = baserel->relids;
 loc_cxt.collation = InvalidOid;
 loc_cxt.state = FDW_COLLATE_NONE;
/* 1) */
if (!foreign_expr_walker((Node *) expr, &glob_cxt, &loc_cxt, NULL))
return false;

 /*
  * If the expression has a valid collation that does not arise from a
  * foreign var, the expression can not be sent over.
  */
 /* 2) */
if (loc_cxt.state == FDW_COLLATE_UNSAFE)
return false;

 /*
  * An expression which includes any mutable functions can't be sent over
  * because its result is not stable.  For example, sending now() remote
  * side could cause confusion from clock offsets.  Future versions might
  * be able to make this choice with more granularity.  (We check this last
  * because it requires a lot of expensive catalog lookups.)
  */
/* 3) */
if (contain_mutable_functions((Node *) expr))
return false;

/* OK to evaluate on the remote server */
return true;
}

3、为什么whereclause会被作为新的joinclause pushdown到remote

这里foreign_join_ok[5]函数中做了介绍。

为了避免在每个连接步骤中构建子查询, 尽可能将参与join表中的其他远程条件添加到rel的joinclause或其他远程子句(remote_conds)中。

对于内连接,所有限制都可以同等处理。将下推的条件视为连接条件,可以解析顶层全外连接,而无需子查询。

所以本应该a.sid::text = b.id::text和a.ext=1都有机会一起作为joinclause pushdown到remote,但是只有a.ext=1可下推,因此就只保留了a.ext=1

代码语言:javascript
复制
static bool
foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
    RelOptInfo *outerrel, RelOptInfo *innerrel,
    JoinPathExtraData *extra)
{
/* 省略 */
/*
  * Pull the other remote conditions from the joining relations into join
  * clauses or other remote clauses (remote_conds) of this relation
  * wherever possible. This avoids building subqueries at every join step.
  *
  * For an inner join, clauses from both the relations are added to the
  * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
  * the outer side are added to remote_conds since those can be evaluated
  * after the join is evaluated. The clauses from inner side are added to
  * the joinclauses, since they need to be evaluated while constructing the
  * join.
  *
  * For SEMI-JOIN clauses from inner relation can not be added to
  * remote_conds, but should be treated as join clauses (as they are
  * deparsed to EXISTS subquery, where inner relation can be referred). A
  * list of relation ids, which can't be referred to from higher levels, is
  * preserved as a hidden_subquery_rels list.
  *
  * For a FULL OUTER JOIN, the other clauses from either relation can not
  * be added to the joinclauses or remote_conds, since each relation acts
  * as an outer relation for the other.
  *
  * The joining sides can not have local conditions, thus no need to test
  * shippability of the clauses being pulled up.
  */
      switch (jointype)
      {
          case JOIN_INNER:
              fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
                                                 fpinfo_i->remote_conds);
              fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
                                                 fpinfo_o->remote_conds);
              break;

          case JOIN_LEFT:

              /*
               * When semi-join is involved in the inner or outer part of the
               * left join, it's deparsed as a subquery, and we can't refer to
               * its vars on the upper level.
               */
              if (bms_is_empty(fpinfo_i->hidden_subquery_rels))
                  fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
                                                    fpinfo_i->remote_conds);
              if (bms_is_empty(fpinfo_o->hidden_subquery_rels))
                  fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
                                                     fpinfo_o->remote_conds);
              break;

          case JOIN_RIGHT:

              /*
               * When semi-join is involved in the inner or outer part of the
               * right join, it's deparsed as a subquery, and we can't refer to
               * its vars on the upper level.
               */
              if (bms_is_empty(fpinfo_o->hidden_subquery_rels))
                  fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
                                                    fpinfo_o->remote_conds);
              if (bms_is_empty(fpinfo_i->hidden_subquery_rels))
                  fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
                                                     fpinfo_i->remote_conds);
              break;

          case JOIN_SEMI:
              fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
                                                fpinfo_i->remote_conds);
              fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
                                                fpinfo->remote_conds);
              fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
              fpinfo->hidden_subquery_rels = bms_union(fpinfo->hidden_subquery_rels,
                                                       innerrel->relids);
              break;

          case JOIN_FULL:

              /*
               * In this case, if any of the input relations has conditions, we
               * need to deparse that relation as a subquery so that the
               * conditions can be evaluated before the join.  Remember it in
               * the fpinfo of this relation so that the deparser can take
               * appropriate action.  Also, save the relids of base relations
               * covered by that relation for later use by the deparser.
               */
              if (fpinfo_o->remote_conds)
              {
                  fpinfo->make_outerrel_subquery = true;
                  fpinfo->lower_subquery_rels =
                      bms_add_members(fpinfo->lower_subquery_rels,
                                      outerrel->relids);
              }
              if (fpinfo_i->remote_conds)
              {
                  fpinfo->make_innerrel_subquery = true;
                  fpinfo->lower_subquery_rels =
                      bms_add_members(fpinfo->lower_subquery_rels,
                                      innerrel->relids);
              }
              break;

          default:
              /* Should not happen, we have just checked this above */
              elog(ERROR, "unsupported join type %d", jointype);
      }

      /*
       * For an inner join, all restrictions can be treated alike. Treating the
       * pushed down conditions as join conditions allows a top level full outer
       * join to be deparsed without requiring subqueries.
       */
      if (jointype == JOIN_INNER)
      {
          Assert(!fpinfo->joinclauses);
          fpinfo->joinclauses = fpinfo->remote_conds;
          fpinfo->remote_conds = NIL;
      }
   /* 省略 */
   /*
  * Set the string describing this join relation to be used in EXPLAIN
  * output of corresponding ForeignScan.  Note that the decoration we add
  * to the base relation names mustn't include any digits, or it'll confuse
  * postgresExplainForeignScan.
  */
 fpinfo->relation_name = psprintf("(%s) %s JOIN (%s)",
          fpinfo_o->relation_name,
          get_jointype_name(fpinfo->jointype),
          fpinfo_i->relation_name);

 /*
  * Set the relation index.  This is defined as the position of this
  * joinrel in the join_rel_list list plus the length of the rtable list.
  * Note that since this joinrel is at the end of the join_rel_list list
  * when we are called, we can get the position by list_length.
  */
 Assert(fpinfo->relation_index == 0); /* shouldn't be set yet */
 fpinfo->relation_index =
  list_length(root->parse->rtable) + list_length(root->join_rel_list);

return true;
/* 省略 */
}

到这里,join和clause pushdown的原理就清楚了。

问题也清晰了,由于join条件的类型转换,导致inner join时原本join条件没有pushdown,其他的clause被下推为joinclause,检索了更多数据,导致性能差。

为什么会搞类型转换?业务之前有套PG业务用mysql_fdw去拉mysql数据进行分析,mysql表中有个timestamp字段default值为"0000-00-00 00:00:00",这个值在PG中是非法的,不转换类型查询外表就报错,所以干脆搞外表时这个字段建成text类型了。所以他们后来就经常搞类型转换。

三、老杨视角

问题比较清晰了,再简单回顾下join pushdown的原理。 postgres_fdw对于inner join,所有限制都可以同等处理。将下推的所有条件视为连接条件,可以解析顶层全外连接,而无需子查询。

看似是为了优化,但实际上如果原本joinclause没有下推,而将其他条件下推为joinclause,检索了更多数据,就会导致性能劣化。

所以当join pushdown到remote性能比较糟糕时,我们只能动业务?改sql,改外表?

不要轻易say yes,要大胆say no!!!

如下框图描述 当foreign_join_ok返回true时,才会调用create_foreign_join_path生成ForeignJoinPath。然后和MergePath,NestPath,HashPath进行cost比较,确定最优计划。

那么当join pushdown到remote性能比较糟糕时,我们不让生成ForeignJoinPath不就行了吗,也就是禁用join pushdown。

方案来了:给postgres_fdw新增GUC “postgres_fdw.is_join_pushdown”,默认为true,当为false时foreign_join_ok直接返回false,这样就不会生成ForeignJoinPath了。

四、测试验证

1、先来看问题复现过程

add_paths_to_joinrel函数中依次生成MergePath、NestPath、HashPath后调用GetForeignJoinPaths准备生成ForeignJoinPaths。

由于joinclause的expr nodetag为T_CoerceViaIO,强制类型转换,命中is_foreign_expr函数default分支,返回false。

因此is_remote_clause为false。打印当前的clause信息,tree结构看起来不太直观,可以看到对应的opno为98

从pg_operator可以得知对应就是left和right做text类型转换后进行等值比较。

代码语言:javascript
复制
postgres=> SELECT oprname, oprcode,oprleft::regtype, oprright::regtype FROM pg_operator WHERE oid =98;
 oprname | oprcode | oprleft | oprright 
---------+---------+---------+----------
 =       | texteq  | text    | text
(1 row)

postgres=> 

inner的remote_conds为NULL,将outer的remote_conds传递给fpinfo->remote_conds,可以看到opno为96。

对应是integer类型的等值匹配。即a.ext=1

代码语言:javascript
复制
postgres=> SELECT oprname, oprcode,oprleft::regtype, oprright::regtype FROM pg_operator WHERE oid = 96;
 oprname | oprcode | oprleft | oprright 
---------+---------+---------+----------
 =       | int4eq  | integer | integer
(1 row)

postgres=>

这里是inner join,所以将fpinfo->remote_conds传递给fpinfo->joinclauses

create_foreign_join_path函数创建foreign joinpath。

set_cheapest函数经过cost比较,确认join最优路径为foreign joinpath。

可以看到最终的Remote SQL为:SELECT r1.sid, r1.ip, r1.port, r1.type, r2.id FROM (public.tbl r1 INNER JOIN public.inf r2 ON (((r1.ext = 1))))

2、测试下禁用join pushdown效果

postgres_fdw.is_join_pushdown默认为on。复现场景,join成功pushdown,原joinclause转为local filter,whereclause下推为joinclause导致执行过程扫描数据多,耗时222760ms。

代码语言:javascript
复制
postgres=> show postgres_fdw.is_join_pushdown;
 postgres_fdw.is_join_pushdown 
-------------------------------
 on
(1 row)

postgres=> explain (verbose,analyze) select distinct a.sid,a.ip,a.port,a.type from tbl a join inf b on a.sid::text = b.id::text where a.ext=1;
                                                                QUERY PLAN                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=452.77..452.86 rows=1 width=328) (actual time=222742.200..222742.228 rows=28.00 loops=1)
   Output: a.sid, a.ip, a.port, a.type
   Buffers: shared hit=9
   ->  Sort  (cost=452.77..452.79 rows=7 width=328) (actual time=222742.197..222742.202 rows=28.00 loops=1)
         Output: a.sid, a.ip, a.port, a.type
         Sort Key: a.sid, a.ip, a.port, a.type
         Sort Method: quicksort  Memory: 26kB
         Buffers: shared hit=9
         ->  Foreign Scan  (cost=100.00..452.68 rows=7 width=328) (actual time=27949.156..222741.929 rows=28.00 loops=1)
               Output: a.sid, a.ip, a.port, a.type
               Filter: ((a.sid)::text = (b.id)::text)
               Rows Removed by Filter: 50474621
               Relations: (public.tbl a) INNER JOIN (public.inf b)
               Remote SQL: SELECT r1.sid, r1.ip, r1.port, r1.type, r2.id FROM (public.tbl r1 INNERJOIN public.inf r2 ON (((r1.ext = 1))))
 Planning Time: 0.241 ms
 Execution Time: 222760.342 ms
(16rows)

postgres=>

设置参数,关闭join pushdown,两个Foreign scan扫描remote两个表,最后在local进行inner join,耗时79ms,提升近3000倍。

代码语言:javascript
复制
postgres=> set postgres_fdw.is_join_pushdown to off;
SET
postgres=> explain (verbose,analyze) select distinct a.sid,a.ip,a.port,a.type from tbl a join inf b on a.sid::text = b.id::text where a.ext=1;
                                                                  QUERY PLAN                                                                  
----------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=557.72..557.81 rows=1 width=328) (actual time=79.539..79.566 rows=28.00 loops=1)
   Output: a.sid, a.ip, a.port, a.type
   ->  Sort  (cost=557.72..557.74 rows=7 width=328) (actual time=79.537..79.544 rows=28.00 loops=1)
         Output: a.sid, a.ip, a.port, a.type
         Sort Key: a.sid, a.ip, a.port, a.type
         Sort Method: quicksort  Memory: 26kB
         ->  Hash Join  (cost=213.12..557.62 rows=7 width=328) (actual time=17.859..79.490 rows=28.00 loops=1)
               Output: a.sid, a.ip, a.port, a.type
               Hash Cond: ((b.id)::text = (a.sid)::text)
               ->  Foreign Scan on public.inf b  (cost=100.00..431.64 rows=1462 width=32) (actual time=0.465..54.815 rows=29397.00 loops=1)
                     Output: b.id
                     Remote SQL: SELECTidFROM public.inf
               ->  Hash  (cost=113.11..113.11rows=1 width=328) (actual time=8.201..8.202rows=1717.00 loops=1)
                     Output: a.sid, a.ip, a.port, a.type
                     Buckets: 2048 (originally 1024)  Batches: 1 (originally 1)  MemoryUsage: 133kB
                     ->  ForeignScanon public.tbl a  (cost=100.00..113.11rows=1 width=328) (actual time=0.575..6.676rows=1717.00 loops=1)
                           Output: a.sid, a.ip, a.port, a.type
                           Remote SQL: SELECTsid, ip, port, typeFROM public.tbl WHERE ((ext = 1))
 Planning Time: 0.236 ms
 Execution Time: 79.839 ms
(20rows)

postgres=>

五、总结

join的pushdown有很多客观条件,但是最终是否采用ForeignJoinPath还是取决于cost是否最优;

对于inner Join所有限制都可以同等处理,将下推的所有条件视为连接条件。如果原本joinclause没有下推,而将其他条件下推为joinclause,检索了更多数据,就会导致性能劣化。

当Join pushdown后性能不理想时,可以考虑通过GUC参数来关闭join pushdown。

Reference

[1]

https://github.com/postgres/postgres/blob/REL_17_5/contrib/postgres_fdw/postgres_fdw.c#L5783

[2]

https://github.com/postgres/postgres/blob/REL_17_5/src/backend/optimizer/path/joinpath.c#L124

[3]

https://github.com/postgres/postgres/blob/REL_17_5/contrib/postgres_fdw/deparse.c#L244

[4]

https://github.com/postgres/postgres/blob/REL_17_5/contrib/postgres_fdw/deparse.c#L312

[5]

https://github.com/postgres/postgres/blob/REL_17_5/contrib/postgres_fdw/postgres_fdw.c#L5922

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PostgreSQL运维之道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、 问题现象
  • 二、原理分析
    • 1、首先我们看下join pushdown的原理
    • 2、我们接着看原本的joinclause为什么没有pushdown
    • 3、为什么whereclause会被作为新的joinclause pushdown到remote
  • 三、老杨视角
  • 四、测试验证
    • 1、先来看问题复现过程
    • 2、测试下禁用join pushdown效果
  • 五、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档