From bb1647247df3155c2b3cf9bd73906eb9614ce98e Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <Masahiro.Ikeda@nttdata.com>
Date: Fri, 11 Oct 2024 14:37:03 +0900
Subject: [PATCH v3] Support "Non Key Filter" for multicolumn B-Tree Index in
 the EXPLAIN output

This patch changes following.
* add a new index AM function "amexplain_function()" and it's called in ExplainNode()
* add "amexplain_function" for B-Tree index and show "Non Key Filter"

-- Example dataset
CREATE TABLE test (id1 int, id2 int, id3 int, value varchar(32));
CREATE INDEX test_idx ON test(id1, id2, id3);  -- multicolumn B-Tree index
INSERT INTO test (SELECT i % 2, i, i, 'hello' FROM generate_series(1,1000000) s(i));
ANALYZE;

-- The output is same as without this patch if it can search efficiently.
=# EXPLAIN (VERBOSE, ANALYZE, BUFFERS, MEMORY, SERIALIZE) SELECT id3 FROM test WHERE id1 = 1 AND id2 = 101;
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using test_idx on public.test  (cost=0.42..4.44 rows=1 width=4) (actual time=0.058..0.060 rows=1 loops=1)
   Output: id3
   Index Cond: ((test.id1 = 1) AND (test.id2 = 101))
   Heap Fetches: 0
   Buffers: shared hit=4
 Planning:
   Memory: used=14kB  allocated=16kB
 Planning Time: 0.166 ms
 Serialization: time=0.009 ms  output=1kB  format=text
 Execution Time: 0.095 ms
(10 rows)

-- "Non Key Filter" will be displayed if it will scan index tuples and filter them.
=# EXPLAIN (VERBOSE, ANALYZE, BUFFERS, MEMORY, SERIALIZE) SELECT id3 FROM test WHERE id1 = 1 AND id3 = 101;
                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using test_idx on public.test  (cost=0.42..12724.10 rows=1 width=4) (actual time=0.055..69.446 rows=1 loops=1)
   Output: id3
   Index Cond: ((test.id1 = 1) AND (test.id3 = 101))
   Heap Fetches: 0
   Non Key Filter: (test.id3 = 101)
   Buffers: shared hit=1920
 Planning:
   Memory: used=14kB  allocated=16kB
 Planning Time: 0.113 ms
 Serialization: time=0.004 ms  output=1kB  format=text
 Execution Time: 69.491 ms
(11 rows)
---
 src/backend/access/nbtree/nbtree.c | 266 +++++++++++++++++++++++++++++
 src/backend/commands/explain.c     |  56 +++++-
 src/include/access/amapi.h         |  11 ++
 src/include/access/nbtree.h        |   3 +
 src/include/commands/explain.h     |   3 +
 5 files changed, 335 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 56e502c4fc9..2e3c6d6e564 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -21,10 +21,13 @@
 #include "access/nbtree.h"
 #include "access/relscan.h"
 #include "access/xloginsert.h"
+#include "commands/explain.h"
 #include "commands/progress.h"
 #include "commands/vacuum.h"
 #include "miscadmin.h"
 #include "nodes/execnodes.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
 #include "pgstat.h"
 #include "storage/bulk_write.h"
 #include "storage/condition_variable.h"
@@ -34,6 +37,7 @@
 #include "storage/smgr.h"
 #include "utils/fmgrprotos.h"
 #include "utils/index_selfuncs.h"
+#include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
 
@@ -146,6 +150,7 @@ bthandler(PG_FUNCTION_ARGS)
 	amroutine->amendscan = btendscan;
 	amroutine->ammarkpos = btmarkpos;
 	amroutine->amrestrpos = btrestrpos;
+	amroutine->amexplain = btexplain;
 	amroutine->amestimateparallelscan = btestimateparallelscan;
 	amroutine->aminitparallelscan = btinitparallelscan;
 	amroutine->amparallelrescan = btparallelrescan;
@@ -529,6 +534,267 @@ btrestrpos(IndexScanDesc scan)
 	}
 }
 
+/*
+ * btexplain -- add some additional details for EXPLAIN
+ */
+void
+btexplain(Plan *plan, PlanState *planstate,
+			List *ancestors, ExplainState *es)
+{
+	if (es->verbose)
+	{
+		Oid indexoid;
+		List *fixed_indexquals = NIL;
+		List *stripped_indexquals = NIL;
+		List		*indexBoundQuals = NIL;
+		List		*indexFilterQuals = NIL;
+		ListCell	*qual_cell;
+		Relation	index;
+		LOCKMODE	lockmode;
+		int		indexcol;
+		int		qualno;
+		int		indnkeyatts;
+		bool	eqQualHere;
+		AttrNumber	varattno_pre;
+
+		/* Fetch the indexoid and qual */
+		switch (nodeTag(plan))
+		{
+			case T_IndexScan:
+				indexoid = ((IndexScan *) plan)->indexid;
+				fixed_indexquals = ((IndexScan *) plan)->indexqual;
+				stripped_indexquals = ((IndexScan *) plan)->indexqualorig;
+				break;
+			case T_IndexOnlyScan:
+				indexoid = ((IndexOnlyScan *) plan)->indexid;
+				fixed_indexquals = ((IndexOnlyScan *) plan)->indexqual;
+				break;
+			case T_BitmapIndexScan:
+				indexoid = ((BitmapIndexScan *) plan)->indexid;
+				fixed_indexquals = ((BitmapIndexScan *) plan)->indexqual;
+				stripped_indexquals = ((BitmapIndexScan *) plan)->indexqualorig;
+				break;
+			default:
+				elog(ERROR, "unsupported expression type: %d", (int) nodeTag(plan));
+		}
+
+		/* Open the target index relations to fetch op families */
+		lockmode = AccessShareLock;
+		index = index_open(indexoid, lockmode);
+
+		/* Determine boundary quals (see btcostestimate()) */
+		indexBoundQuals = NIL;
+		indexcol = 0;
+		qualno = 0;
+		eqQualHere = false;
+		varattno_pre = 0;
+
+		indnkeyatts = IndexRelationGetNumberOfKeyAttributes(index);
+		foreach(qual_cell, fixed_indexquals)
+		{
+			AttrNumber	varattno;
+			Expr	   *clause = (Expr *) lfirst(qual_cell);
+			Oid			clause_op = InvalidOid;
+
+			/* Examine varattno */
+			if (IsA(clause, OpExpr))
+			{
+				Expr	   *leftop;
+
+				/*
+				 * leftop should be the index key Var, possibly relabeled
+				 */
+				leftop = (Expr *) get_leftop(clause);
+
+				if (leftop && IsA(leftop, RelabelType))
+					leftop = ((RelabelType *) leftop)->arg;
+
+				Assert(leftop != NULL);
+
+				if (!(IsA(leftop, Var) &&
+					  ((Var *) leftop)->varno == INDEX_VAR))
+					elog(ERROR, "indexqual doesn't have key on left side");
+
+				varattno = ((Var *) leftop)->varattno;
+			}
+			else if (IsA(clause, RowCompareExpr))
+			{
+				Expr	   *leftop;
+				RowCompareExpr *rc = (RowCompareExpr *) clause;
+
+				/*
+				 * leftop should be the index key Var, possibly relabeled
+				 */
+				leftop = (Expr *) linitial(rc->largs);
+
+				if (leftop && IsA(leftop, RelabelType))
+					leftop = ((RelabelType *) leftop)->arg;
+
+				Assert(leftop != NULL);
+
+				if (!(IsA(leftop, Var) &&
+					  ((Var *) leftop)->varno == INDEX_VAR))
+					elog(ERROR, "indexqual doesn't have key on left side");
+
+				varattno = ((Var *) leftop)->varattno;
+			}
+			else if (IsA(clause, ScalarArrayOpExpr))
+			{
+				Expr	   *leftop;
+				ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
+
+				/*
+				 * leftop should be the index key Var, possibly relabeled
+				 */
+				leftop = (Expr *) linitial(saop->args);
+
+				if (leftop && IsA(leftop, RelabelType))
+					leftop = ((RelabelType *) leftop)->arg;
+
+				Assert(leftop != NULL);
+
+				if (!(IsA(leftop, Var) &&
+					  ((Var *) leftop)->varno == INDEX_VAR))
+					elog(ERROR, "indexqual doesn't have key on left side");
+
+				varattno = ((Var *) leftop)->varattno;
+			}
+			else if (IsA(clause, NullTest))
+			{
+				Expr	   *leftop;
+				NullTest   *ntest = (NullTest *) clause;
+
+				/*
+				 * argument should be the index key Var, possibly relabeled
+				 */
+				leftop = ntest->arg;
+
+				if (leftop && IsA(leftop, RelabelType))
+					leftop = ((RelabelType *) leftop)->arg;
+
+				Assert(leftop != NULL);
+
+				if (!(IsA(leftop, Var) &&
+					  ((Var *) leftop)->varno == INDEX_VAR))
+					elog(ERROR, "NullTest indexqual has wrong key");
+
+				varattno = ((Var *) leftop)->varattno;
+			}
+			else
+				elog(ERROR, "unsupported indexqual type: %d",
+					 (int) nodeTag(clause));
+
+			if (varattno < 1 || varattno > indnkeyatts)
+				elog(ERROR, "bogus index qualification");
+
+			/* Check for the boundary qual */
+			if ((varattno != varattno_pre) && (indexcol != varattno - 1))
+			{
+				/* Beginning of a new column's quals */
+				if (!eqQualHere)
+					break;			/* done if no '=' qual for indexcol */
+				eqQualHere = false;
+				indexcol++;
+				if (indexcol != varattno - 1)
+					break;
+			}
+			varattno_pre = varattno;
+
+			/* Check for equaliy operator */
+			if (IsA(clause, OpExpr))
+			{
+				OpExpr	   *op = (OpExpr *) clause;
+
+				clause_op = op->opno;
+			}
+			else if (IsA(clause, RowCompareExpr))
+			{
+				RowCompareExpr *rc = (RowCompareExpr *) clause;
+
+				clause_op = linitial_oid(rc->opnos);
+			}
+			else if (IsA(clause, ScalarArrayOpExpr))
+			{
+				ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
+
+				clause_op = saop->opno;
+			}
+			else if (IsA(clause, NullTest))
+			{
+				NullTest   *nt = (NullTest *) clause;
+
+				if (nt->nulltesttype == IS_NULL)
+				{
+					/* IS NULL is like = for selectivity purposes */
+					eqQualHere = true;
+				}
+			}
+			else
+				elog(ERROR, "unsupported indexqual type: %d",
+					 (int) nodeTag(clause));
+
+
+			if (OidIsValid(clause_op))
+			{
+				int	op_strategy;
+
+				op_strategy = get_op_opfamily_strategy(clause_op,
+													   index->rd_opfamily[indexcol]);
+				Assert(op_strategy != 0);	/* not a member of opfamily?? */
+				if (op_strategy == BTEqualStrategyNumber)
+					eqQualHere = true;
+			}
+
+			/*
+			 * Add as boundary qual
+			 *
+			 * To reuse show_scan_qual(), decide whether to store stripped_indexquals or
+			 * fixed_indexquals depending on each Node type. It might be better to deparse
+			 * it without using show_scan_qual().
+			 *
+			 * In the case of T_IndexScan and T_BitmapIndexScan, even if it passes
+			 * fixed_indexquals to show_scan_qual(), it will cause an error because it does
+			 * not hold indextlist and does not save deparse_namespace->index_tlist in
+			 * set_deparse_plan(). They only store information equivalent to index_tlist
+			 * in fixed_indexquals.
+			 */
+			switch (nodeTag(plan))
+			{
+				case T_IndexScan:
+				case T_BitmapIndexScan:
+					indexBoundQuals = lappend(indexBoundQuals, list_nth(stripped_indexquals, qualno));
+					break;
+				case T_IndexOnlyScan:
+					indexBoundQuals = lappend(indexBoundQuals, list_nth(fixed_indexquals, qualno));
+					break;
+				default:
+					elog(ERROR, "unsupported expression type: %d", (int) nodeTag(plan));
+			}
+			qualno++;
+		}
+
+		index_close(index, lockmode);
+
+		/*
+		 * Maybe, it's better to change "Skip Scan Cond" after it's supported.
+		 * https://www.postgresql.org/message-id/flat/CAH2-Wzmn1YsLzOGgjAQZdn1STSG_y8qP__vggTaPAYXJP+G4bw@mail.gmail.com
+		 */
+		switch (nodeTag(plan))
+		{
+			case T_IndexScan:
+			case T_BitmapIndexScan:
+				indexFilterQuals = list_difference_ptr(stripped_indexquals, indexBoundQuals);
+				break;
+			case T_IndexOnlyScan:
+				indexFilterQuals = list_difference_ptr(fixed_indexquals, indexBoundQuals);
+				break;
+			default:
+				elog(ERROR, "unsupported expression type: %d", (int) nodeTag(plan));
+		}
+		show_scan_qual(indexFilterQuals, "Non Key Filter", planstate, ancestors, es);
+	}
+}
+
 /*
  * btestimateparallelscan -- estimate storage for BTParallelScanDescData
  */
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 18a5af6b919..e882dd09426 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -37,6 +37,7 @@
 #include "utils/rel.h"
 #include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/tuplesort.h"
 #include "utils/typcache.h"
 #include "utils/xml.h"
@@ -92,9 +93,6 @@ static void show_expression(Node *node, const char *qlabel,
 static void show_qual(List *qual, const char *qlabel,
 					  PlanState *planstate, List *ancestors,
 					  bool useprefix, ExplainState *es);
-static void show_scan_qual(List *qual, const char *qlabel,
-						   PlanState *planstate, List *ancestors,
-						   ExplainState *es);
 static void show_upper_qual(List *qual, const char *qlabel,
 							PlanState *planstate, List *ancestors,
 							ExplainState *es);
@@ -156,6 +154,8 @@ static void ExplainModifyTarget(ModifyTable *plan, ExplainState *es);
 static void ExplainTargetRel(Plan *plan, Index rti, ExplainState *es);
 static void show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 								  ExplainState *es);
+static void show_amindex_info(Plan *plan, PlanState *planstate,
+								List *ancestors, ExplainState *es);
 static void ExplainMemberNodes(PlanState **planstates, int nplans,
 							   List *ancestors, ExplainState *es);
 static void ExplainMissingMembers(int nplans, int nchildren, ExplainState *es);
@@ -2096,6 +2096,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			show_amindex_info(plan, planstate, ancestors, es);
+			// TODO: add ANALYZE. filterd tuples
 			break;
 		case T_IndexOnlyScan:
 			show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
@@ -2112,10 +2114,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (es->analyze)
 				ExplainPropertyFloat("Heap Fetches", NULL,
 									 planstate->instrument->ntuples2, 0, es);
+			show_amindex_info(plan, planstate, ancestors, es);
 			break;
 		case T_BitmapIndexScan:
 			show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
 						   "Index Cond", planstate, ancestors, es);
+			show_amindex_info(plan, planstate, ancestors, es);
 			break;
 		case T_BitmapHeapScan:
 			show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
@@ -2658,7 +2662,7 @@ show_qual(List *qual, const char *qlabel,
 /*
  * Show a qualifier expression for a scan plan node
  */
-static void
+void
 show_scan_qual(List *qual, const char *qlabel,
 			   PlanState *planstate, List *ancestors,
 			   ExplainState *es)
@@ -4682,6 +4686,50 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Show extra information for Index AM
+ */
+static void
+show_amindex_info(Plan *plan, PlanState *planstate,
+					List *ancestors, ExplainState *es)
+{
+	Oid indexoid;
+	HeapTuple	ht_idxrel;
+	Form_pg_class	idxrelrec;
+	IndexAmRoutine	*amroutine;
+
+	/* Fetch the index oid */
+	switch (nodeTag(plan))
+	{
+		case T_IndexScan:
+			indexoid = ((IndexScan *) plan)->indexid;
+			break;
+		case T_IndexOnlyScan:
+			indexoid = ((IndexOnlyScan *) plan)->indexid;
+			break;
+		case T_BitmapIndexScan:
+			indexoid = ((BitmapIndexScan *) plan)->indexid;
+			break;
+		default:
+			elog(ERROR, "unsupported expression type: %d", (int) nodeTag(plan));
+	}
+
+	/* Fetch the index AM's API struct */
+	ht_idxrel = SearchSysCache1(RELOID, ObjectIdGetDatum(indexoid));
+	if (!HeapTupleIsValid(ht_idxrel))
+			elog(ERROR, "cache lookup failed for relation %u", indexoid);
+	idxrelrec = (Form_pg_class) GETSTRUCT(ht_idxrel);
+
+	amroutine = GetIndexAmRoutineByAmId(idxrelrec->relam, true);
+
+	/* Let the AM emit whatever fields it wants */
+	if (amroutine != NULL && amroutine->amexplain != NULL)
+		amroutine->amexplain(plan, planstate, ancestors, es);
+
+	pfree(amroutine);
+	ReleaseSysCache(ht_idxrel);
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index c51de742ea0..68ae512c1c3 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -25,6 +25,10 @@ struct IndexPath;
 /* Likewise, this file shouldn't depend on execnodes.h. */
 struct IndexInfo;
 
+/* Likewise, this file shouldn't depend on execnodes.h. */
+struct Plan;
+struct PlanState;
+struct ExplainState;
 
 /*
  * Properties for amproperty API.  This list covers properties known to the
@@ -197,6 +201,12 @@ typedef void (*ammarkpos_function) (IndexScanDesc scan);
 /* restore marked scan position */
 typedef void (*amrestrpos_function) (IndexScanDesc scan);
 
+/* add some additional details for explain */
+typedef void (*amexplain_function) (struct Plan *plan,
+									struct PlanState *planstate,
+									List *ancestors,
+									struct ExplainState *es);
+
 /*
  * Callback function signatures - for parallel index scans.
  */
@@ -292,6 +302,7 @@ typedef struct IndexAmRoutine
 	amendscan_function amendscan;
 	ammarkpos_function ammarkpos;	/* can be NULL */
 	amrestrpos_function amrestrpos; /* can be NULL */
+	amexplain_function amexplain; /* can be NULL */
 
 	/* interface functions to support parallel index scans */
 	amestimateparallelscan_function amestimateparallelscan; /* can be NULL */
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index d64300fb973..7df5da5966d 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -21,6 +21,7 @@
 #include "access/xlogreader.h"
 #include "catalog/pg_am_d.h"
 #include "catalog/pg_index.h"
+#include "commands/explain.h"
 #include "lib/stringinfo.h"
 #include "storage/bufmgr.h"
 #include "storage/shm_toc.h"
@@ -1179,6 +1180,8 @@ extern void btparallelrescan(IndexScanDesc scan);
 extern void btendscan(IndexScanDesc scan);
 extern void btmarkpos(IndexScanDesc scan);
 extern void btrestrpos(IndexScanDesc scan);
+extern void btexplain(Plan *plan, PlanState *planstate,
+						List *ancestors, ExplainState *es);
 extern IndexBulkDeleteResult *btbulkdelete(IndexVacuumInfo *info,
 										   IndexBulkDeleteResult *stats,
 										   IndexBulkDeleteCallback callback,
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3ab0aae78f7..47203a2a688 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -141,6 +141,9 @@ extern void ExplainOpenGroup(const char *objtype, const char *labelname,
 							 bool labeled, ExplainState *es);
 extern void ExplainCloseGroup(const char *objtype, const char *labelname,
 							  bool labeled, ExplainState *es);
+extern void show_scan_qual(List *qual, const char *qlabel,
+							PlanState *planstate, List *ancestors,
+							ExplainState *es);
 
 extern DestReceiver *CreateExplainSerializeDestReceiver(ExplainState *es);
 
-- 
2.34.1

