During NestLoop execution we have bad corner case: if outer subtree contains tuples the join node will scan inner subtree even if it does not return any tuples.

To reproduce the problem see 'problem.sql' in attachment:
Out of explain analyze see in 'problem_explain.txt'

As you can see, executor scan each of 1e5 outer tuples despite the fact that inner can't return any tuples.

Teodor Sigaev and I developed a patch to solve this problem. Result of explain analyze procedure can be found in the 'optimized_execution.txt'.

--
Andrey Lepikhov
Postgres Professional
https://postgrespro.com
The Russian Postgres Company

Attachment: problem.sql
Description: application/sql

                                                                    QUERY PLAN  
                                                                  
--------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.29..1515.83 rows=1 width=80) (actual time=25.194..25.194 rows=0 
loops=1)
   ->  Nested Loop  (cost=0.29..504674.12 rows=333 width=80) (actual 
time=25.193..25.193 rows=0 loops=1)
         Join Filter: (big.id = small.id)
         ->  Index Scan Backward using big_pkey on big  (cost=0.29..5152.29 
rows=100000 width=44) (actual time=0.005..12.933 rows=100000 loops=1)
         ->  Materialize  (cost=0.00..22.66 rows=333 width=36) (actual 
time=0.000..0.000 rows=0 loops=100000)
               ->  Seq Scan on small  (cost=0.00..21.00 rows=333 width=36) 
(actual time=0.145..0.145 rows=0 loops=1)
                     Filter: ((id)::numeric > '1000'::numeric)
                     Rows Removed by Filter: 1000
 Planning Time: 0.215 ms
 Execution Time: 25.214 ms
(10 rows)

>From 40ffbd2d9ee5954e5ae09f1e5a929ae2f2b17b8c Mon Sep 17 00:00:00 2001
From: "Andrey V. Lepikhov" <a.lepik...@postgrespro.ru>
Date: Mon, 9 Dec 2019 18:25:04 +0500
Subject: [PATCH] Skip scan of outer subtree if inner of the NestedLoop node is
 guaranteed empty.

---
 src/backend/executor/nodeMaterial.c           | 11 +++++++++++
 src/backend/executor/nodeNestloop.c           |  5 +++++
 src/include/nodes/execnodes.h                 |  4 ++++
 src/test/regress/expected/partition_prune.out |  8 ++++----
 4 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/nodeMaterial.c b/src/backend/executor/nodeMaterial.c
index cc93bbe45b..98771647f6 100644
--- a/src/backend/executor/nodeMaterial.c
+++ b/src/backend/executor/nodeMaterial.c
@@ -135,6 +135,8 @@ ExecMaterial(PlanState *pstate)
 		if (TupIsNull(outerslot))
 		{
 			node->eof_underlying = true;
+			if (tuplestorestate && tuplestore_tuple_count(tuplestorestate) == 0)
+				node->ss.ps.guaranteed_empty = true;
 			return NULL;
 		}
 
@@ -348,6 +350,9 @@ ExecReScanMaterial(MaterialState *node)
 			node->tuplestorestate = NULL;
 			if (outerPlan->chgParam == NULL)
 				ExecReScan(outerPlan);
+			else
+				/* At first look no cases can lead to this code. But still. */
+				node->ss.ps.guaranteed_empty = false;
 			node->eof_underlying = false;
 		}
 		else
@@ -363,6 +368,12 @@ ExecReScanMaterial(MaterialState *node)
 		 */
 		if (outerPlan->chgParam == NULL)
 			ExecReScan(outerPlan);
+
+		/*
+		 * The node suppress tuplestore and guaranteed_empty trick isn't work.
+		 */
+		Assert(node->ss.ps.guaranteed_empty == false);
+
 		node->eof_underlying = false;
 	}
 }
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index fc6667ef82..ea26a7d9d1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -164,6 +164,11 @@ ExecNestLoop(PlanState *pstate)
 		{
 			ENL1_printf("no inner tuple, need new outer tuple");
 
+			if (innerPlan->guaranteed_empty &&
+				(node->js.jointype == JOIN_INNER ||
+				 node->js.jointype == JOIN_SEMI))
+				return NULL;
+
 			node->nl_NeedNewOuter = true;
 
 			if (!node->nl_MatchedOuter &&
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 692438d6df..c7f1a14526 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1007,6 +1007,9 @@ typedef struct PlanState
 	 * ExecConditionalAssignProjectionInfo()).  If no projection is necessary
 	 * ExecConditionalAssignProjectionInfo() defaults those fields to the scan
 	 * operations.
+	 *
+	 * If guaranteed_empty field is true, this means that no tuple will be
+	 * returned by the node.
 	 */
 	const TupleTableSlotOps *scanops;
 	const TupleTableSlotOps *outerops;
@@ -1020,6 +1023,7 @@ typedef struct PlanState
 	bool		outeropsset;
 	bool		inneropsset;
 	bool		resultopsset;
+	bool		guaranteed_empty;
 } PlanState;
 
 /* ----------------
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index f9eeda60e6..04cfe0944e 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2455,9 +2455,9 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_2 (actual rows=0 loops=1)
+               ->  Bitmap Heap Scan on ab_a1_b3 ab_2 (never executed)
                      Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=0 loops=1)
+                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                            Index Cond: (a = 1)
          ->  Materialize (actual rows=0 loops=1)
                ->  Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
@@ -2496,9 +2496,9 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_2 (actual rows=0 loops=1)
+               ->  Bitmap Heap Scan on ab_a1_b3 ab_2 (never executed)
                      Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
+                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                            Index Cond: (a = 1)
          ->  Materialize (actual rows=0 loops=1)
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
-- 
2.17.1

Limit  (cost=0.29..1515.83 rows=1 width=80) (actual time=0.151..0.151 rows=0 
loops=1)
   ->  Nested Loop  (cost=0.29..504674.12 rows=333 width=80) (actual 
time=0.150..0.150 rows=0 loops=1)
         Join Filter: (big.id = small.id)
         ->  Index Scan Backward using big_pkey on big  (cost=0.29..5152.29 
rows=100000 width=44) (actual time=0.004..0.004 rows=1 loops=1)
         ->  Materialize  (cost=0.00..22.66 rows=333 width=36) (actual 
time=0.145..0.145 rows=0 loops=1)
               ->  Seq Scan on small  (cost=0.00..21.00 rows=333 width=36) 
(actual time=0.144..0.144 rows=0 loops=1)
                     Filter: ((id)::numeric > '1000'::numeric)
                     Rows Removed by Filter: 1000
 Planning Time: 0.182 ms
 Execution Time: 0.166 ms
(10 rows)

Reply via email to