PostgreSQL表连接 nestloop/hash/merge join详解

PostgreSQL中如果查询需要连接两个或更多表,在所有扫描单个表的可能计划都被找到后,连接计划将会被考虑。和很多数据库一样,可供选择的三种表连接方式为:nested loop join、merge join、hash join。

nested loop join:
对左表中找到的每一行都要扫描右表一次。这种策略最容易实现但是可能非常耗时(不过,如果右表可以通过索引扫描,这将是一个不错的策略。因为可以用左表当前行的值来作为右表上索引扫描的键)。
在这里插入图片描述
merge join:
在连接开始之前,每一个表都按照连接的列排好序。然后两个表会被并行扫描,匹配的行被整合成连接行。由于这种连接中每个表只被扫描一次。它所要求的排序可以通过一个显式的排序步骤得到,或使用一个连接键上的索引按适当顺序扫描关系得到。
在这里插入图片描述
hash join:
右表会先被扫描并且被载入到一个哈希表,使用连接列作为哈希键。接下来左表被扫描,扫描中找到的每一行的连接属性值被用作哈希键在哈希表中查找匹配的行。
在这里插入图片描述

当查询涉及两个以上的表时,最终结果必须由一个连接步骤树构成,每个连接步骤有两个输入。规划器会检查不同可能的连接序列来找到代价最小的那一个。

例子:

建表:

bill@bill=>create table t1(id int,info text);
CREATE TABLE
bill@bill=>create table t2(id int,info text);
CREATE TABLE

bill@bill=>insert into t2 select generate_series(1,100000),'bill'||generate_series(1,100000);  
INSERT 0 100000
bill@bill=>insert into t1 select generate_series(1,100000),'bill'||generate_series(1,100000);  
INSERT 0 100000

–nested loop join:
nl连接大致过程为:1、t2表进行扫描, 过滤条件info = ‘bill’;2、t2表根据过滤条件输出的中间结果, 每一条中间结果, t1表都根据索引idx_t1_id扫一遍, 过滤条件id= t2.id。

bill@bill=>explain select * from t2 join t1 on (t1.id=t2.id) where t2.info='bill';
                                QUERY PLAN                                 
---------------------------------------------------------------------------
 Nested Loop  (cost=0.29..1793.92 rows=1 width=26)
   ->  Seq Scan on t2  (cost=0.00..1791.00 rows=1 width=13)
         Filter: (info = 'bill'::text)
   ->  Index Scan using idx_t1_id on t1  (cost=0.29..2.91 rows=1 width=13)
         Index Cond: (id = t2.id)
(5 rows)

–merge join:
merge join的两张表需要按照连接列排序,因为这里用到了索引,所以没有排序过程;
相较于nl连接,merge join不需要多次扫描,两个表都只扫描一次。

bill@bill=>explain select * from t2 join t1 on (t1.id=t2.id) ;  
                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Merge Join  (cost=0.69..6300.71 rows=100000 width=26)
   Merge Cond: (t2.id = t1.id)
   ->  Index Scan using idx_t2_id on t2  (cost=0.29..2400.39 rows=100000 width=13)
   ->  Index Scan using idx_t1_id on t1  (cost=0.29..2400.39 rows=100000 width=13)
(4 rows)

如果没有索引,则需要先进行排序,则代价会很大。

bill@bill=>explain select * from t2 join t1 on (t1.id=t2.id);
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Merge Join  (cost=19691.64..21691.64 rows=100000 width=26)
   Merge Cond: (t2.id = t1.id)
   ->  Sort  (cost=9845.82..10095.82 rows=100000 width=13)
         Sort Key: t2.id
         ->  Seq Scan on t2  (cost=0.00..1541.00 rows=100000 width=13)
   ->  Sort  (cost=9845.82..10095.82 rows=100000 width=13)
         Sort Key: t1.id
         ->  Seq Scan on t1  (cost=0.00..1541.00 rows=100000 width=13)
(8 rows)

–hash join
hash join过程:
1、首先扫描t2表, 过滤条件是t2.id between 1 and 1000, 这里由于t2上id有索引, 所以是走的index scan, 扫描后的结果放到一个Hash表中. Hash key就是两个表关联的列, 这里也就是id列;
2、然后扫描t1表, 扫描到的ID列的值用于去匹配Hash表的KEY值, 匹配到则输出。

bill@bill=>explain select * from t1 join t2 on (t1.info=t2.info) where t2.id between 1 and 1000;
                                     QUERY PLAN                                     
------------------------------------------------------------------------------------
 Hash Join  (cost=41.95..1967.63 rows=968 width=26)
   Hash Cond: (t1.info = t2.info)
   ->  Seq Scan on t1  (cost=0.00..1541.00 rows=100000 width=13)
   ->  Hash  (cost=29.85..29.85 rows=968 width=13)
         ->  Index Scan using idx_t2_id on t2  (cost=0.29..29.85 rows=968 width=13)
               Index Cond: ((id >= 1) AND (id <= 1000))
(6 rows)

总结:

hash join和merge join被关联的两个表都只扫描一次, nested loop join则被关联的表其中一个扫描一次, (如果前一个表的扫描结果有多行输出)另一个扫描多次。

参考资料:
https://www.postgresql.org/docs/devel/planner-optimizer.html
http://www.interdb.jp/pg/pgsql03.html#_3.5.

  • 9
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
这里提供 PostgreSQL 数据库hash join 和 sort merge join 实现的相关源码,仅供参考: Hash Join 实现源码: ```c /* * ExecHashJoin * Implements the hashjoin algorithm. * * Returns the join relation. * * Parallel version: we distribute the outer relation into a number of * partitions with a hash function, and process each partition * independently of the others. The inner relation is replicated to * all workers, so that each can perform the join independently. * This works best if the inner relation is smaller than the outer. */ static TupleTableSlot * ExecHashJoin(PlanState *pstate) { HashJoinState *node = castNode(HashJoinState, pstate); ExprContext *econtext = node->js.ps.ps_ExprContext; TupleTableSlot *slot; CHECK_FOR_INTERRUPTS(); /* * If we're still building the hash table, do that, else fetch the current * batch of outer tuples to probe the existing hash table. */ if (!node->hj_JoinState) ExecBuildHashTable(node); else node->hj_OuterTupleSlot = ExecProcNode(outerPlanState(node)); /* * Now loop, returning join tuples as we find them. */ for (;;) { CHECK_FOR_INTERRUPTS(); /* * If we don't have an outer tuple, get the next one and reset our * state machine for new tuple. */ if (TupIsNull(node->hj_OuterTupleSlot)) { if (!ExecScanHashTableForUnmatched(node)) { /* no more unmatched tuples */ return NULL; } /* Found unmatched outer, so compute its hash value */ ResetExprContext(econtext); econtext->ecxt_outertuple = node->hj_OuterTupleSlot; node->hj_CurHashValue = ExecHashGetHashValue(node->hj_HashTable, econtext, node->hj_OuterHashKeys); node->hj_JoinState = HJ_NEED_NEW_OUTER; /* * Now we have an outer tuple and its hash value. */ } /* inner loop over all matching inner tuples */ while (node->hj_JoinState != HJ_NEED_NEW_OUTER) { /* Fetch next tuple from inner side */ slot = ExecScanHashTable(node); /* if there are no more inner tuples... */ if (TupIsNull(slot)) { node->hj_JoinState = HJ_NEED_NEW_OUTER; break; /* ... out of inner loop */ } /* we have a new join tuple, return it */ econtext->ecxt_innertuple = slot; return ExecProject(node->js.ps.ps_ProjInfo); } } } ``` Sort Merge Join 实现源码: ```c /* * ExecSortMergeJoin * Implements the sort/merge join algorithm. * * Returns the join relation. * * Parallel version: we distribute the outer relation into a number of * partitions with a hash function, and sort the inner relation on the * join key. We then perform the join independently for each partition, * with each worker performing the merge join of its partition with the * sorted inner relation. */ static TupleTableSlot * ExecSortMergeJoin(PlanState *pstate) { SortMergeJoinState *node = castNode(SortMergeJoinState, pstate); ExprContext *econtext = node->js.ps.ps_ExprContext; TupleTableSlot *slot; CHECK_FOR_INTERRUPTS(); /* First call? */ if (node->smj_JoinState == SMJ_STARTUP) { PlanState *outerNode; PlanState *innerNode; List *inInfo; ListCell *l; List *outInfo; AttrNumber *match; int nMatch; /* * We need to do some initialization for outer and inner nodes. Also, * we figure out which join keys are being used, and build equality * operators for them. */ outerNode = outerPlanState(node); innerNode = innerPlanState(node); inInfo = innerNode->plan->targetlist; outInfo = outerNode->plan->targetlist; nMatch = 0; match = palloc(list_length(node->smj_MergingClauses) * sizeof(AttrNumber)); foreach(l, node->smj_MergingClauses) { OpExpr *clause = (OpExpr *) lfirst(l); Var *innerVar; Var *outerVar; Oid eqop; /* * Currently, only "simple" cross-type comparisons work. See * comments in src/backend/utils/adt/genfile.c. */ if (!is_simple_eq_op(clause->opno)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("mergejoin operator must be a btree equality operator"))); innerVar = (Var *) get_leftop((Expr *) clause); outerVar = (Var *) get_rightop((Expr *) clause); /* We don't need to output these columns in the result */ innerVar->varno = INNER_VAR; outerVar->varno = OUTER_VAR; /* * We may have to look up the operator___ in the opfamily to check that * it is compatible with sorting. */ eqop = get_opfamily_member(clause->opfamily, innerVar->vartype, outerVar->vartype, BTEqualStrategyNumber); if (eqop == InvalidOid) elog(ERROR, "no operator___ matching clause"); match[nMatch] = outInfo ? ExecFindMatchingJoinVar(outerVar, outInfo) : ExecFindMatchingJoinVar(innerVar, inInfo); nMatch++; } node->js.ps.ps_ExprContext->ecxt_per_tuple_memory = node->smj_RuntimeContext; ExecAssignExprContext(node->js.ps.ps_ExprContext, outerNode->parent); /* * Initialize tuplesort state variables used in merging phase, and in * state where we're reading inner relation. */ node->smj_OuterSkipQual = ExecInitQual(node->js.ps.qual, outerNode); node->smj_InnerSkipQual = ExecInitQual(node->js.ps.qual, innerNode); node->smj_MatchedOuter = false; node->smj_MatchedInner = false; node->smj_OuterTupleSlot = ExecProcNode(outerNode); if (TupIsNull(node->smj_OuterTupleSlot)) { /* empty outer relation */ node->smj_JoinState = SMJ_NEEDS_INNER; return NULL; } node->smj_SortKeys = ExecBuildSortKey(node, inInfo, outInfo, match, nMatch); /* can't handle non-heap tuplesort methods here */ if (!node->smj_SortKeys->abbrev_converter && node->smj_PresortedKeys == NIL) node->smj_SortStates[0] = tuplesort_begin_merge(node->smj_SortKeys->sortFunction, node->smj_WorkMem, node->ssps_TempFileSpaces, node->smj_SortKeys->abbrev_full_comparator, node); else node->smj_SortStates[0] = tuplesort_begin_datum(node->smj_SortKeys->sortFunction, node->smj_SortKeys->abbrev_converter, node->smj_SortKeys->abbrev_full_comparator, node->smj_WorkMem, node->ssps_TempFileSpaces, node); /* * Begin scanning the inner relation. We'll read tuples in sorted * order, so the main loop will be able to use a simple and fast * algorithm for advancing the outer relation and resetting the inner * scan. */ node->smj_JoinState = SMJ_NEEDS_INNER; node->smj_MatchedOuter = false; node->smj_MatchedInner = false; /* * Set up tuplestore and materialize the inner relation. We only need to * materialize the inner relation if we are in a parallel plan. */ if (node->js.ps.plan->parallel_aware) { Assert(node->js.ps.ps_ExecProcNode == ExecSortMergeJoin); node->smj_InnerTupleSlot = outerNode->ps_ResultTupleSlot; /* * If we are in a parallel plan, and if the inner side of this join * was not fully gathered (because it was too large), then we must * materialize the inner tuples before proceeding with the join. */ if (outerNode->ps_Flow->flotype == FLOW_REPLICATE && innerNode->ps_Flow->flotype == FLOW_PARTITIONED && !innerNode->ps_Flow->initialized) { Assert(innerNode->ps_ResultTupleSlot->tts_tupleDescriptor != NULL); /* Create tuplestore to store the entire inner relation. */ node->ss.ps.ps_TupFromTlist = false; node->ss.ps.ps_ProjInfo = NULL; node->ss.ps.ps_ExprContext = node->js.ps.ps_ExprContext; node->ss.ps.ps_TupSlot = tuplestore_begin_heap(false, false, work_mem); node->ss.ps.ps_ResultTupleSlot = node->smj_InnerTupleSlot; node->ss.ps.ps_ProjInfo = NULL; /* Materialize all inner tuples. */ while (!TupIsNull(slot = ExecProcNode(innerNode))) { tuplestore_puttupleslot(node->ss.ps.ps_TupSlot, slot); } /* Seek back to start of the materialized inner relation. */ tuplestore_rescan(node->ss.ps.ps_TupSlot); } else { /* * If the inner side is fully gathered (i.e., if it is a * shared-nothing table), then we can simply use the existing * outer slot as the inner slot as well. */ node->smj_InnerTupleSlot = node->smj_OuterTupleSlot; } } else { node->smj_InnerTupleSlot = ExecProcNode(innerNode); /* if empty inner relation, advance to next outer tuple */ if (TupIsNull(node->smj_InnerTupleSlot)) node->smj_JoinState = SMJ_NEEDS_OUTER; } } /* * The main loop advances the outer scan, possibly reinitializing the * inner scan, and checks for matches between outer tuples and inner * tuples. */ for (;;) { CHECK_FOR_INTERRUPTS(); switch (node->smj_JoinState) { case SMJ_NEEDS_INNER: /* Reset the inner scan. */ if (node->js.ps.plan->parallel_aware) { /* * If we are in a parallel plan, and if the inner side of * this join was not fully gathered (because it was too * large), then we must read from the materialized inner * relation that was created earlier. We have to switch to * the other worker's partition if we've reached the end of * our own. Otherwise, we can simply rescan the materialized * inner relation. */ if (outerPlanState(node)->ps_Flow->flotype == FLOW_REPLICATE && innerPlanState(node)->ps_Flow->flotype == FLOW_PARTITIONED && !innerPlanState(node)->ps_Flow->initialized) { if (node->ss.ps.ps_TupSlot && !tuplestore_gettupleslot(node->ss.ps.ps_TupSlot, true, false, node->smj_InnerTupleSlot)) { /* * We've reached the end of our own partition, but * there may be more partitions. Advance to the * next partition by updating our slice table entry * and resetting the tuplestore so that we can read * from the new partition. If there are no more * partitions, we're done. */ if (!ExecParallelUpdatePartitionInfo(node, true)) { node->smj_JoinState = SMJ_NEEDS_OUTER; break; } tuplestore_clear(node->ss.ps.ps_TupSlot); tuplestore_rescan(node->ss.ps.ps_TupSlot); continue; } } else { /* * If the inner side is fully gathered (i.e., if it is * a shared-nothing table), then we can simply rescan * the existing outer slot as the inner slot as well. */ ExecClearTuple(node->smj_InnerTupleSlot); tuplestore_rescan(node->ss.ps.ps_TupSlot); } } else { /* advance inner scan */ ExecClearTuple(node->smj_InnerTupleSlot); node->smj_InnerTupleSlot = ExecProcNode(innerPlanState(node)); } if (TupIsNull(node->smj_InnerTupleSlot)) { /* end of inner scan */ node->smj_JoinState = SMJ_NEEDS_OUTER; break; } /* * We know the new inner tuple is not distinct from the last one * returned, so we update matched_inner accordingly. */ node->smj_MatchedInner = true; /* * Set up the state for matching tuples. */ ResetExprContext(econtext); econtext->ecxt_innertuple = node->smj_InnerTupleSlot; econtext->ecxt_outertuple = node->smj_OuterTupleSlot; /* Skip non-matching tuples based on previously established * skip qual */ if (node->smj_InnerSkipQual) { ExprState *qualexpr = node->smj_InnerSkipQual; if (!ExecQual(qualexpr, econtext)) { /* not matched */ continue; } } /* * Now we check the merge condition(s). */ if (ExecQualAndReset(node->smj_MergeClauses, econtext)) { /* matched */ node->smj_JoinState = SMJ_JOINEDMATCHING; return ExecProject(node->js.ps.ps_ProjInfo); } /* * Not joined, so try next tuple from inner side. */ break; case SMJ_JOINEDMATCHING: case SMJ_JOINEDNONMATCHING: /* Try to advance inner-side tuple */ ExecClearTuple(node->smj_InnerTupleSlot); node->smj_InnerTupleSlot = ExecProcNode(innerPlanState(node)); if (TupIsNull(node->smj_InnerTupleSlot)) { /* end of inner scan */ if (node->smj_JoinState == SMJ_JOINEDMATCHING) { node->smj_JoinState = SMJ_NEEDS_INNER; node->smj_MatchedInner = false; /* try to fetch next outer tuple */ ExecClearTuple(node->smj_OuterTupleSlot); node->smj_OuterTupleSlot = ExecProcNode(outerPlanState(node)); if (TupIsNull(node->smj_OuterTupleSlot)) { /* end of outer scan */ node->smj_JoinState = SMJ_NEEDS_INNER; break; } } else { node->smj_JoinState = SMJ_NEEDS_OUTER; break; } } node->smj_MatchedInner = false; /* * Set up the state for matching tuples. */ ResetExprContext(econtext); econtext->ecxt_innertuple = node->smj_InnerTupleSlot; econtext->ecxt_outertuple = node->smj_OuterTupleSlot; /* Skip non-matching tuples based on previously established * skip qual */ if (node->smj_InnerSkipQual) { ExprState *qualexpr = node->smj_InnerSkipQual; if (!ExecQual(qualexpr, econtext)) { /* not matched */ continue; } } /* * Now we check the merge condition(s). */ if (ExecQualAndReset(node->smj_MergeClauses, econtext)) { /* matched */ node->smj_MatchedInner = true; node->smj_JoinState = SMJ_JOINEDMATCHING; return ExecProject(node->js.ps.ps_ProjInfo); } /* * Not joined, so try again with next tuple from inner side. */ break; case SMJ_NEEDS_OUTER: /* Try to advance outer-side tuple */ ExecClearTuple(node->smj_OuterTupleSlot); node->smj_OuterTupleSlot = ExecProcNode(outerPlanState(node)); if (TupIsNull(node->smj_OuterTupleSlot)) { /* end of outer scan */ node->smj_JoinState = SMJ_NEEDS_INNER; break; } /* * New outer tuple; try to match it to first inner tuple. */ node->smj_JoinState = SMJ_FIRST_INNER; /* FALL THRU */ case SMJ_FIRST_INNER: /* * We know the new outer tuple is not distinct from the last one * returned, so we update matched_outer accordingly. */ node->smj_MatchedOuter = true; /* * Set up the state for matching tuples. */ ResetExprContext(econtext); econtext->ecxt_innertuple = node->smj_InnerTupleSlot; econtext->ecxt_outertuple = node->smj_OuterTupleSlot; /* Skip non-matching tuples based on previously established * skip qual */ if (node->smj_OuterSkipQual) { ExprState *qualexpr = node->smj_OuterSkipQual; if (!ExecQual(qualexpr, econtext)) { /* not

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值