On Mon, Feb 27, 2023 at 12:24 AM Heikki Linnakangas <hlinn...@iki.fi> wrote: > On 22/02/2023 15:03, Aleksander Alekseev wrote: > > If memory serves I noticed that WHERE ... IS NULL queries don't even > > hit HeapKeyTest() and I was curious where the check for NULLs is > > actually made. As I understand, SeqNext() in nodeSeqscan.c simply > > iterates over all the tuples it can find and pushes them to the parent > > node. We could get a slightly better performance for certain queries > > if SeqNext() did the check internally. > > Right, it might be faster to perform the NULL-checks before checking > visibility, for example. Arbitrary quals cannot be evaluated before > checking visibility, but NULL checks could be.
Hi Heikki, There's quite a bit of work left to do, but I wanted to check if the attached patch (0002, based on top of Aleks' 0001 from upthread) was going in the direction you were thinking. This patch pushes down any forced-null and not-null Vars as ScanKeys. It doesn't remove the redundant quals after turning them into ScanKeys, so it's needlessly inefficient, but there's still a decent speedup for some of the basic benchmarks in 0003. Plans look something like this: # EXPLAIN SELECT * FROM t WHERE i IS NULL; QUERY PLAN ------------------------------------------------------------ Seq Scan on t (cost=0.00..1393.00 rows=49530 width=4) Scan Cond: (i IS NULL) Filter: (i IS NULL) (3 rows) # EXPLAIN SELECT * FROM t WHERE i = 3; QUERY PLAN -------------------------------------------------------- Seq Scan on t (cost=0.00..1643.00 rows=1 width=4) Scan Cond: (i IS NOT NULL) Filter: (i = 3) (3 rows) The non-nullable case worries me a bit because so many things imply IS NOT NULL. I think I need to do some sort of cost analysis using the null_frac statistics -- it probably only makes sense to push an implicit SK_SEARCHNOTNULL down to the AM layer if some fraction of rows would actually be filtered out -- but I'm not really sure how to choose a threshold. It would also be neat if `COUNT(col)` could push down SK_SEARCHNOTNULL, but I think that would require a new support function to rewrite the plan for an aggregate. Am I on the right track? Thanks, --Jacob
From 45ed1948b6aac4fc8a268a77211327786fd4315f Mon Sep 17 00:00:00 2001 From: Jacob Champion <jchamp...@timescale.com> Date: Tue, 6 Jun 2023 15:57:19 -0700 Subject: [PATCH 3/3] WIP: naive benchmarks Three benchmarks (bench1-3) for varying degrees of selectivity depending on the table used. Two benchmarks (bench4-5) for 1.0 selectivity, on both a single column and a larger number of columns. $ psql -f ./init postgres $ for i in 1 2 3 4; do for t in zero twenty fifty eighty hundred; do echo "= $i: $t =" pgbench -n -f ./bench$i -T5 -c1 -j1 -Dtable=$t postgres done; done $ pgbench -n -f ./bench5 -T5 -c1 -j1 postgres --- bench1 | 1 + bench2 | 1 + bench3 | 1 + bench4 | 1 + bench5 | 4 ++++ init | 37 +++++++++++++++++++++++++++++++++++++ 6 files changed, 45 insertions(+) create mode 100644 bench1 create mode 100644 bench2 create mode 100644 bench3 create mode 100644 bench4 create mode 100644 bench5 create mode 100755 init diff --git a/bench1 b/bench1 new file mode 100644 index 0000000000..9cb32d4fcb --- /dev/null +++ b/bench1 @@ -0,0 +1 @@ +SELECT COUNT(i) FROM :table; diff --git a/bench2 b/bench2 new file mode 100644 index 0000000000..1377bae0f5 --- /dev/null +++ b/bench2 @@ -0,0 +1 @@ +SELECT COUNT(*) FROM :table WHERE i IS NOT NULL; diff --git a/bench3 b/bench3 new file mode 100644 index 0000000000..524a140c0a --- /dev/null +++ b/bench3 @@ -0,0 +1 @@ +SELECT COUNT(*) FROM :table WHERE i IS NULL; diff --git a/bench4 b/bench4 new file mode 100644 index 0000000000..c8216537db --- /dev/null +++ b/bench4 @@ -0,0 +1 @@ +SELECT COUNT(*) FROM :table WHERE i > 0; diff --git a/bench5 b/bench5 new file mode 100644 index 0000000000..90a77741f6 --- /dev/null +++ b/bench5 @@ -0,0 +1,4 @@ +SELECT COUNT(*) FROM wide + WHERE i <> 0 AND j <> 0 AND k <> 0 AND l <> 0 AND m <> 0 AND n <> 0 AND o <> 0 + AND p <> 0 AND q <> 0 AND r <> 0 AND s <> 0 AND t <> 0 AND u <> 0 AND v <> 0 + AND w <> 0 AND x <> 0 AND y <> 0 AND z <> 0; diff --git a/init b/init new file mode 100755 index 0000000000..98c4a0acdb --- /dev/null +++ b/init @@ -0,0 +1,37 @@ +-- Tables are named after the percentage of values that are non-NULL. +DROP TABLE IF EXISTS zero; +DROP TABLE IF EXISTS twenty; +DROP TABLE IF EXISTS fifty; +DROP TABLE IF EXISTS eighty; +DROP TABLE IF EXISTS hundred; + +DROP TABLE IF EXISTS wide; + +\if :{?scale} +\else + \set scale 1 +\endif + +CREATE TABLE zero AS + SELECT NULL::int AS i FROM generate_series(1, 100000 * :scale); + +CREATE TABLE twenty AS + SELECT CASE WHEN random() < 0.2 THEN i ELSE NULL END AS i + FROM generate_series(1, 100000 * :scale) i; + +CREATE TABLE fifty AS + SELECT CASE WHEN random() < 0.5 THEN i ELSE NULL END AS i + FROM generate_series(1, 100000 * :scale) i; + +CREATE TABLE eighty AS + SELECT CASE WHEN random() < 0.8 THEN i ELSE NULL END AS i + FROM generate_series(1, 100000 * :scale) i; + +CREATE TABLE hundred AS + SELECT i FROM generate_series(1, 100000 * :scale) i; + +CREATE TABLE wide AS + SELECT i, 2 AS j, 3 AS k, 4 AS l, 5 AS m, 6 AS n, 7 AS o, 8 AS p, 9 AS q, + 10 AS r, 11 AS s, 12 AS t, 13 AS u, 14 AS v, 15 AS w, 16 AS x, 17 AS y, + 18 AS z + FROM generate_series(1, 100000 * :scale) i; -- 2.25.1
From 4d904883a68dc2025716948b816e43cb847afe72 Mon Sep 17 00:00:00 2001 From: Aleksander Alekseev <aleksan...@timescale.com> Date: Mon, 13 Feb 2023 16:15:45 +0300 Subject: [PATCH 1/3] Support SK_SEARCHNULL / SK_SEARCHNOTNULL for heap-only scans Previously it was not supported which could be of inconvenience for the extension authors. Author: Aleksander Alekseev Reviewed-by: Andres Freund Discussion: https://postgr.es/m/caj7c6tpkeh7uwen9orqp_dmr8uxifhxt8pecq01zw1hkptb...@mail.gmail.com --- src/include/access/skey.h | 7 +-- src/include/access/valid.h | 41 ++++++++++----- src/test/regress/expected/heapscan.out | 33 ++++++++++++ src/test/regress/expected/test_setup.out | 13 +++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/regress.c | 67 ++++++++++++++++++++++++ src/test/regress/sql/heapscan.sql | 12 +++++ src/test/regress/sql/test_setup.sql | 16 ++++++ 8 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 src/test/regress/expected/heapscan.out create mode 100644 src/test/regress/sql/heapscan.sql diff --git a/src/include/access/skey.h b/src/include/access/skey.h index fbdb23c5c7..81b0530277 100644 --- a/src/include/access/skey.h +++ b/src/include/access/skey.h @@ -46,9 +46,10 @@ * and the sk_strategy, sk_subtype, sk_collation, and sk_func fields are * not used (unless set by the index AM). * - * SK_SEARCHARRAY, SK_SEARCHNULL and SK_SEARCHNOTNULL are supported only - * for index scans, not heap scans; and not all index AMs support them, - * only those that set amsearcharray or amsearchnulls respectively. + * SK_SEARCHARRAY is supported only for index scans, not heap scans; and not all + * index AMs support it, only those that set amsearcharray. SK_SEARCHNULL and + * SK_SEARCHNOTNULL are supported for heap and index scans but similarly not all + * index AMs support them, only those that set amsearchnulls. * * A ScanKey can also represent an ordering operator invocation, that is * an ordering requirement "ORDER BY indexedcol op constant". This looks diff --git a/src/include/access/valid.h b/src/include/access/valid.h index 85d476aab5..477ac75242 100644 --- a/src/include/access/valid.h +++ b/src/include/access/valid.h @@ -32,24 +32,41 @@ HeapKeyTest(HeapTuple tuple, TupleDesc tupdesc, int nkeys, ScanKey keys) for (; cur_nkeys--; cur_key++) { - Datum atp; bool isnull; - Datum test; - if (cur_key->sk_flags & SK_ISNULL) - return false; + if (cur_key->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL)) + { + /* special case: looking for NULL / NOT NULL values */ + Assert(cur_key->sk_flags & SK_ISNULL); - atp = heap_getattr(tuple, cur_key->sk_attno, tupdesc, &isnull); + isnull = heap_attisnull(tuple, cur_key->sk_attno, tupdesc); - if (isnull) - return false; + if (isnull && (cur_key->sk_flags & SK_SEARCHNOTNULL)) + return false; - test = FunctionCall2Coll(&cur_key->sk_func, - cur_key->sk_collation, - atp, cur_key->sk_argument); + if (!isnull && (cur_key->sk_flags & SK_SEARCHNULL)) + return false; + } + else + { + Datum atp; + Datum test; - if (!DatumGetBool(test)) - return false; + if (cur_key->sk_flags & SK_ISNULL) + return false; + + atp = heap_getattr(tuple, cur_key->sk_attno, tupdesc, &isnull); + + if (isnull) + return false; + + test = FunctionCall2Coll(&cur_key->sk_func, + cur_key->sk_collation, + atp, cur_key->sk_argument); + + if (!DatumGetBool(test)) + return false; + } } return true; diff --git a/src/test/regress/expected/heapscan.out b/src/test/regress/expected/heapscan.out new file mode 100644 index 0000000000..055bf9bc62 --- /dev/null +++ b/src/test/regress/expected/heapscan.out @@ -0,0 +1,33 @@ +-- make sure that initially the table is empty +SELECT * FROM phonebook; + id | name | phone +----+------+------- +(0 rows) + +SELECT phonebook_find_first_phone(isnull => false); + phonebook_find_first_phone +---------------------------- + -1 +(1 row) + +SELECT phonebook_find_first_phone(isnull => true); + phonebook_find_first_phone +---------------------------- + -1 +(1 row) + +INSERT INTO phonebook (id, name, phone) VALUES +(1, 'Alice', 123456), +(2, 'Bob', NULL); +SELECT phonebook_find_first_phone(isnull => false); + phonebook_find_first_phone +---------------------------- + 1 +(1 row) + +SELECT phonebook_find_first_phone(isnull => true); + phonebook_find_first_phone +---------------------------- + 2 +(1 row) + diff --git a/src/test/regress/expected/test_setup.out b/src/test/regress/expected/test_setup.out index 5d9e6bf12b..70db759697 100644 --- a/src/test/regress/expected/test_setup.out +++ b/src/test/regress/expected/test_setup.out @@ -213,6 +213,19 @@ CREATE FUNCTION get_columns_length(oid[]) RETURNS int AS :'regresslib' LANGUAGE C STRICT STABLE PARALLEL SAFE; +-- +-- These table and function are used for testing the support of SK_SEARCHNULL +-- and SK_SEARCHNOTNULL flags for heap-only scans. +-- +CREATE TABLE phonebook( + id INT PRIMARY KEY NOT NULL, + name NAME NOT NULL, + phone INT /* nullable */ +); +CREATE FUNCTION phonebook_find_first_phone(isnull bool) + RETURNS int + AS :'regresslib', 'phonebook_find_first_phone' + LANGUAGE C; -- Use hand-rolled hash functions and operator classes to get predictable -- result on different machines. The hash function for int4 simply returns -- the sum of the values passed to it and the one for text returns the length diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..d0eb5433eb 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr # The stats test resets stats, so nothing else needing stats access can be in # this group. # ---------- -test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats +test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats heapscan # event_trigger depends on create_am and cannot run concurrently with # any test that runs DDL diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index bcbc6d910f..c7f2eed699 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -20,6 +20,7 @@ #include <signal.h> #include "access/detoast.h" +#include "access/heapam.h" #include "access/htup_details.h" #include "access/transam.h" #include "access/xact.h" @@ -45,6 +46,7 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/snapmgr.h" #include "utils/typcache.h" #define EXPECT_TRUE(expr) \ @@ -1262,3 +1264,68 @@ get_columns_length(PG_FUNCTION_ARGS) PG_RETURN_INT32(column_offset); } + +/* + * Used for testing SK_SEARCHNULL/SK_SEARCHNOTNULL support for heap-only scans. + * + * Returns the primary key of the first row found in the "phonebook" table for + * which "phone" is NULL or NOT NULL, depending on the "isnull" argument. + * + * Returns -1 if nothing was found. + */ +PG_FUNCTION_INFO_V1(phonebook_find_first_phone); + +typedef enum Anum_phonebook +{ + Anum_phonebook_id = 1, + Anum_phonebook_name, + Anum_phonebook_phone, + _Anum_phonebook_max, +} Anum_phonebook; + +#define Natts_phonebook (_Anum_phonebook_max - 1) + +Datum +phonebook_find_first_phone(PG_FUNCTION_ARGS) +{ + bool isnull = PG_GETARG_BOOL(0); + int flags = SK_ISNULL; + int32 found_id = -1; + Relation rel; + HeapTuple tup; + TableScanDesc scan; + Datum tbl_oid_datum; + ScanKeyData scanKey; + + flags |= isnull ? SK_SEARCHNULL : SK_SEARCHNOTNULL; + ScanKeyEntryInitialize( + &scanKey, + flags, + Anum_phonebook_phone, + InvalidStrategy, /* no strategy */ + InvalidOid, /* no strategy subtype */ + InvalidOid, /* no collation */ + InvalidOid, /* no reg proc for this */ + (Datum) 0); /* constant */ + + tbl_oid_datum = DirectFunctionCall1(to_regclass, + CStringGetTextDatum("phonebook")); + + rel = table_open(DatumGetObjectId(tbl_oid_datum), AccessShareLock); + scan = table_beginscan(rel, GetTransactionSnapshot(), 1, &scanKey); + + while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Datum values[Natts_phonebook]; + bool isnull[Natts_phonebook]; + + heap_deform_tuple(tup, RelationGetDescr(rel), values, isnull); + found_id = DatumGetInt32(values[Anum_phonebook_id - 1]); + break; + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + + PG_RETURN_INT32(found_id); +} diff --git a/src/test/regress/sql/heapscan.sql b/src/test/regress/sql/heapscan.sql new file mode 100644 index 0000000000..de6528f3dd --- /dev/null +++ b/src/test/regress/sql/heapscan.sql @@ -0,0 +1,12 @@ +-- make sure that initially the table is empty +SELECT * FROM phonebook; + +SELECT phonebook_find_first_phone(isnull => false); +SELECT phonebook_find_first_phone(isnull => true); + +INSERT INTO phonebook (id, name, phone) VALUES +(1, 'Alice', 123456), +(2, 'Bob', NULL); + +SELECT phonebook_find_first_phone(isnull => false); +SELECT phonebook_find_first_phone(isnull => true); \ No newline at end of file diff --git a/src/test/regress/sql/test_setup.sql b/src/test/regress/sql/test_setup.sql index 1b2d434683..25b16e75d6 100644 --- a/src/test/regress/sql/test_setup.sql +++ b/src/test/regress/sql/test_setup.sql @@ -262,6 +262,22 @@ CREATE FUNCTION get_columns_length(oid[]) AS :'regresslib' LANGUAGE C STRICT STABLE PARALLEL SAFE; +-- +-- These table and function are used for testing the support of SK_SEARCHNULL +-- and SK_SEARCHNOTNULL flags for heap-only scans. +-- + +CREATE TABLE phonebook( + id INT PRIMARY KEY NOT NULL, + name NAME NOT NULL, + phone INT /* nullable */ +); + +CREATE FUNCTION phonebook_find_first_phone(isnull bool) + RETURNS int + AS :'regresslib', 'phonebook_find_first_phone' + LANGUAGE C; + -- Use hand-rolled hash functions and operator classes to get predictable -- result on different machines. The hash function for int4 simply returns -- the sum of the values passed to it and the one for text returns the length -- 2.25.1
From 49e93bda7ddc25d23b3b2d66bb65c9b3db256ee0 Mon Sep 17 00:00:00 2001 From: Jacob Champion <jchamp...@timescale.com> Date: Wed, 7 Jun 2023 16:42:17 -0700 Subject: [PATCH 2/3] WIP: create ScanKeys from derived null tests ...for pushdown to the TableAM layer. During the scan fixup, we have to also adjust the new splan->keyexprs list. Otherwise our Vars won't reference the correct RTEs. TODO: - quals made redundant with the ScanKeys need to be removed - costing --- src/backend/commands/explain.c | 12 +++++- src/backend/executor/nodeSeqscan.c | 53 +++++++++++++++++++++++- src/backend/optimizer/plan/createplan.c | 55 ++++++++++++++++++++++++- src/backend/optimizer/plan/setrefs.c | 3 ++ src/include/nodes/execnodes.h | 2 + src/include/nodes/plannodes.h | 1 + 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..622d0615bc 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -1821,12 +1821,20 @@ ExplainNode(PlanState *planstate, List *ancestors, if (es->analyze) show_tidbitmap_info((BitmapHeapScanState *) planstate, es); break; + case T_SeqScan: + if (((SeqScan *) plan)->keyexprs) + show_scan_qual(((SeqScan *) plan)->keyexprs, "Scan Cond", + planstate, ancestors, es); + show_scan_qual(plan->qual, "Filter", planstate, ancestors, es); + if (plan->qual) + show_instrumentation_count("Rows Removed by Filter", 1, + planstate, es); + break; case T_SampleScan: show_tablesample(((SampleScan *) plan)->tablesample, planstate, ancestors, es); - /* fall through to print additional fields the same as SeqScan */ + /* fall through to print additional fields the same as basic scans */ /* FALLTHROUGH */ - case T_SeqScan: case T_ValuesScan: case T_CteScan: case T_NamedTuplestoreScan: diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c index 4da0f28f7b..67ff9d1482 100644 --- a/src/backend/executor/nodeSeqscan.c +++ b/src/backend/executor/nodeSeqscan.c @@ -28,6 +28,7 @@ #include "postgres.h" #include "access/relscan.h" +#include "access/skey.h" #include "access/tableam.h" #include "executor/execdebug.h" #include "executor/nodeSeqscan.h" @@ -70,7 +71,7 @@ SeqNext(SeqScanState *node) */ scandesc = table_beginscan(node->ss.ss_currentRelation, estate->es_snapshot, - 0, NULL); + node->nkeys, node->scankeys); node->ss.ss_currentScanDesc = scandesc; } @@ -91,6 +92,8 @@ SeqRecheck(SeqScanState *node, TupleTableSlot *slot) /* * Note that unlike IndexScan, SeqScan never use keys in heap_beginscan * (and this is very bad) - so, here we do not check are keys ok or not. + * + * TODO: so I guess this isn't true anymore. */ return true; } @@ -114,6 +117,48 @@ ExecSeqScan(PlanState *pstate) (ExecScanRecheckMtd) SeqRecheck); } +static void +create_scankeys(SeqScanState *scanstate, List *keyexprs) +{ + struct ScanKeyData *keys; + int nkeys; + ListCell *l; + + nkeys = list_length(keyexprs); + keys = palloc(sizeof(struct ScanKeyData) * nkeys); + + foreach(l, keyexprs) + { + NullTest *n; + Var *var; + int typeflag; + int i = foreach_current_index(l); + + /* + * create_seqscan_plan() only puts NullTests of Vars into the SeqScan's + * key clauses, so that's all we handle here. + */ + n = lfirst_node(NullTest, l); + var = (Var *) n->arg; + Assert(IsA(var, Var)); + + typeflag = (n->nulltesttype == IS_NULL) ? SK_SEARCHNULL + : SK_SEARCHNOTNULL; + + Assert(i < nkeys); + ScanKeyEntryInitialize(&keys[i], + SK_ISNULL | typeflag, + var->varattno, + InvalidStrategy, + InvalidOid, + InvalidOid, + InvalidOid, + (Datum) 0); + } + + scanstate->nkeys = nkeys; + scanstate->scankeys = keys; +} /* ---------------------------------------------------------------- * ExecInitSeqScan @@ -171,6 +216,12 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, (PlanState *) scanstate); + /* + * Populate scankeys, if necessary. + */ + if (node->keyexprs) + create_scankeys(scanstate, node->keyexprs); + return scanstate; } diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index af48109058..c7cfe59ea9 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -177,7 +177,8 @@ static void copy_generic_path_info(Plan *dest, Path *src); static void copy_plan_costsize(Plan *dest, Plan *src); static void label_sort_with_costsize(PlannerInfo *root, Sort *plan, double limit_tuples); -static SeqScan *make_seqscan(List *qptlist, List *qpqual, Index scanrelid); +static SeqScan *make_seqscan(List *qptlist, List *qpqual, List *scankeys, + Index scanrelid); static SampleScan *make_samplescan(List *qptlist, List *qpqual, Index scanrelid, TableSampleClause *tsc); static IndexScan *make_indexscan(List *qptlist, List *qpqual, Index scanrelid, @@ -2885,6 +2886,47 @@ create_limit_plan(PlannerInfo *root, LimitPath *best_path, int flags) *****************************************************************************/ +/* + * reconstruct_null_tests + * Creates a list of NullTests, one for each Var in varset, which is a + * multibitmapset of varno/varattno. Whole-row Vars are omitted. + */ +static List * +reconstruct_null_tests(List *tests, NullTestType type, List *varset) +{ + ListCell *lc; + + foreach(lc, varset) + { + Bitmapset *varattnos = lfirst_node(Bitmapset, lc); + int i = -1; + + while ((i = bms_next_member(varattnos, i)) >= 0) + { + AttrNumber varattno = i + FirstLowInvalidHeapAttributeNumber; + Var *var; + NullTest *n; + + if (varattno == 0) + continue; /* skip whole-row vars */ + + var = makeNode(Var); + var->varno = foreach_current_index(lc); + var->varattno = varattno; + + n = makeNode(NullTest); + n->arg = (Expr *) var; + n->nulltesttype = type; + n->location = -1; + + tests = lappend(tests, n); + } + } + + return tests; +} + + /* * create_seqscan_plan * Returns a seqscan plan for the base relation scanned by 'best_path' @@ -2896,6 +2938,7 @@ create_seqscan_plan(PlannerInfo *root, Path *best_path, { SeqScan *scan_plan; Index scan_relid = best_path->parent->relid; + List *keyexprs = NIL; /* it should be a base rel... */ Assert(scan_relid > 0); @@ -2914,8 +2957,16 @@ create_seqscan_plan(PlannerInfo *root, Path *best_path, replace_nestloop_params(root, (Node *) scan_clauses); } + keyexprs = + reconstruct_null_tests(keyexprs, IS_NULL, + find_forced_null_vars((Node *) scan_clauses)); + keyexprs = + reconstruct_null_tests(keyexprs, IS_NOT_NULL, + find_nonnullable_vars((Node *) scan_clauses)); + scan_plan = make_seqscan(tlist, scan_clauses, + keyexprs, scan_relid); copy_generic_path_info(&scan_plan->scan.plan, best_path); @@ -5460,6 +5511,7 @@ bitmap_subplan_mark_shared(Plan *plan) static SeqScan * make_seqscan(List *qptlist, List *qpqual, + List *keyexprs, Index scanrelid) { SeqScan *node = makeNode(SeqScan); @@ -5470,6 +5522,7 @@ make_seqscan(List *qptlist, plan->lefttree = NULL; plan->righttree = NULL; node->scan.scanrelid = scanrelid; + node->keyexprs = keyexprs; return node; } diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 97fa561e4e..2b58b6c2b3 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -632,6 +632,9 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) splan->scan.plan.qual = fix_scan_list(root, splan->scan.plan.qual, rtoffset, NUM_EXEC_QUAL(plan)); + splan->keyexprs = + fix_scan_list(root, splan->keyexprs, + rtoffset, NUM_EXEC_QUAL(plan)); } break; case T_SampleScan: diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..d015a3a127 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1485,6 +1485,8 @@ typedef struct SeqScanState { ScanState ss; /* its first field is NodeTag */ Size pscan_len; /* size of parallel heap scan descriptor */ + struct ScanKeyData *scankeys; + int nkeys; } SeqScanState; /* ---------------- diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 1b787fe031..b6dbf64d1e 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -394,6 +394,7 @@ typedef struct Scan typedef struct SeqScan { Scan scan; + List *keyexprs; /* expressions to push down as ScanKeys */ } SeqScan; /* ---------------- -- 2.25.1