On Mon, Feb 27, 2023 at 12:24 AM Heikki Linnakangas <[email protected]> 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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