joinremoval-deferred.patch
text/x-patch
Filename: joinremoval-deferred.patch
Type: text/x-patch
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Subject: Don't allow join removal for deferrable unique constraints
| File | + | − |
|---|---|---|
| src/backend/catalog/pg_constraint.c | 2 | 2 |
| src/backend/optimizer/path/indxpath.c | 99 | 2 |
| src/test/regress/expected/join.out | 39 | 0 |
| src/test/regress/sql/join.sql | 24 | 0 |
From 23f33298485ecff80f01feb0dbd349cca2b38032 Mon Sep 17 00:00:00 2001
From: Marti Raudsepp <marti@juffo.org>
Date: Tue, 18 Oct 2011 21:31:42 +0300
Subject: [PATCH] Don't allow join removal for deferrable unique constraints
This used to be allowed, but could return incorrect results if the
deferrable constraint is invalid in the middle of a transaction.
---
src/backend/catalog/pg_constraint.c | 4 +-
src/backend/optimizer/path/indxpath.c | 101 ++++++++++++++++++++++++++++++++-
src/test/regress/expected/join.out | 39 +++++++++++++
src/test/regress/sql/join.sql | 24 ++++++++
4 files changed, 164 insertions(+), 4 deletions(-)
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 6997994..7dbaca0 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -842,8 +842,8 @@ check_functional_grouping(Oid relid,
/* Only PK constraints are of interest for now, see comment above */
if (con->contype != CONSTRAINT_PRIMARY)
continue;
- /* Constraint must be non-deferrable */
- if (con->condeferrable)
+ /* Constraint must be non-deferrable and valid */
+ if (con->condeferrable || !con->convalidated)
continue;
/* Extract the conkey array, ie, attnums of PK's columns */
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 940efb3..6d46e75 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -17,10 +17,14 @@
#include <math.h>
+#include "access/genam.h"
+#include "access/heapam.h"
#include "access/skey.h"
#include "access/sysattr.h"
+#include "catalog/indexing.h"
#include "catalog/pg_am.h"
#include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_type.h"
@@ -34,9 +38,12 @@
#include "optimizer/var.h"
#include "utils/builtins.h"
#include "utils/bytea.h"
+#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/pg_locale.h"
#include "utils/selfuncs.h"
+#include "utils/tqual.h"
+#include "storage/lock.h"
#define IsBooleanOpfamily(opfamily) \
@@ -116,6 +123,8 @@ static bool matches_any_index(RestrictInfo *rinfo, RelOptInfo *rel,
Relids outer_relids);
static List *find_clauses_for_join(PlannerInfo *root, RelOptInfo *rel,
Relids outer_relids, bool isouterjoin);
+static bool unique_index_is_consistent(Oid reloid, Oid indexoid,
+ Oid *conoid);
static bool match_boolean_index_clause(Node *clause, int indexcol,
IndexOptInfo *index);
static bool match_special_index_operator(Expr *clause,
@@ -2248,6 +2257,71 @@ find_clauses_for_join(PlannerInfo *root, RelOptInfo *rel,
}
/*
+ * unique_constraint_is_consistent
+ * Make sure that a unique index's respective constraint is validated and
+ * not deferrable. Sets *conoid to the found constraint OID, or InvalidOid
+ * if not found.
+ *
+ * This is expensive; there is on index on pg_constraint.conindid, so we have
+ * to scan all constraints on the relation.
+ */
+static bool
+unique_index_is_consistent(Oid reloid, Oid indexoid,
+ Oid *conoid)
+{
+ /* If no constraint is found, then the index cannot be invalid/deferrable */
+ bool result = true;
+ Relation pg_constraint;
+ HeapTuple tuple;
+ SysScanDesc scan;
+ ScanKeyData skey[1];
+
+ *conoid = InvalidOid;
+
+ /* Scan pg_constraint for constraints of the target rel */
+ pg_constraint = heap_open(ConstraintRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(reloid));
+
+ scan = systable_beginscan(pg_constraint, ConstraintRelidIndexId, true,
+ SnapshotNow, 1, skey);
+
+ while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ {
+ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+ if (con->conindid == indexoid)
+ {
+ if (con->condeferrable)
+ result = false;
+
+ /*
+ * Currently unique constraints are always valid, but that could
+ * change in the future. This check costs us nothing.
+ */
+ if (!con->convalidated)
+ result = false;
+
+ *conoid = HeapTupleGetOid(tuple);
+
+ /*
+ * Stop searching since we found a constraint. Assumes that
+ * conindid is unique.
+ */
+ break;
+ }
+ }
+
+ systable_endscan(scan);
+ heap_close(pg_constraint, AccessShareLock);
+
+ return result;
+}
+
+/*
* relation_has_unique_index_for
* Determine whether the relation provably has at most one row satisfying
* a set of equality conditions, because the conditions constrain all
@@ -2274,6 +2348,8 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
foreach(ic, rel->indexlist)
{
IndexOptInfo *ind = (IndexOptInfo *) lfirst(ic);
+ RangeTblEntry *rte;
+ Oid conoid = InvalidOid;
int c;
/*
@@ -2326,8 +2402,29 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
}
/* Matched all columns of this index? */
- if (c == ind->ncolumns)
- return true;
+ if (c != ind->ncolumns)
+ continue;
+
+ /*
+ * Deferrable or invalid constraints don't qualify for removal. This
+ * check is last since it's the most expensive -- requires a catalog
+ * lookup.
+ *
+ * It's tempting to peek into the queue of deferred unique checks
+ * and see if it's empty, but there's no mechanism to invalidate the
+ * plan when that queue changes.
+ */
+ rte = root->simple_rte_array[rel->relid];
+ Assert(rte->rtekind == RTE_RELATION);
+
+ if (!unique_index_is_consistent(rte->relid, ind->indexoid, &conoid))
+ continue;
+
+ /* Query plan now relies on this constraint */
+ if (conoid != InvalidOid)
+ root->parse->constraintDeps = lappend_oid(root->parse->constraintDeps, conoid);
+
+ return true;
}
return false;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index a54000b..9217714 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2755,3 +2755,42 @@ SELECT * FROM
(5 rows)
rollback;
+-- and just one more bug, deferrable unique constraints can't be relied on
+create temp table uniq (i int);
+create unique index uniq_i_idx on uniq (i);
+begin;
+prepare join_removal as
+ select a.* from uniq as a left join uniq as b using (i);
+explain (costs off) execute join_removal;
+ QUERY PLAN
+--------------------
+ Seq Scan on uniq a
+(1 row)
+
+-- query must be replanned after a deferrable constraint is added
+alter table uniq add constraint uniq_i_idx
+ unique using index uniq_i_idx deferrable initially deferred;
+explain (costs off) execute join_removal;
+ QUERY PLAN
+--------------------------------
+ Hash Left Join
+ Hash Cond: (a.i = b.i)
+ -> Seq Scan on uniq a
+ -> Hash
+ -> Seq Scan on uniq b
+(5 rows)
+
+-- test actual results
+insert into uniq values(1),(1);
+execute join_removal;
+ i
+---
+ 1
+ 1
+ 1
+ 1
+(4 rows)
+
+rollback;
+deallocate join_removal;
+drop table uniq;
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 1be80b8..ca46bae 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -736,3 +736,27 @@ SELECT * FROM
ON true;
rollback;
+
+-- and just one more bug, deferrable unique constraints can't be relied on
+create temp table uniq (i int);
+create unique index uniq_i_idx on uniq (i);
+
+begin;
+prepare join_removal as
+ select a.* from uniq as a left join uniq as b using (i);
+
+explain (costs off) execute join_removal;
+
+-- query must be replanned after a deferrable constraint is added
+alter table uniq add constraint uniq_i_idx
+ unique using index uniq_i_idx deferrable initially deferred;
+
+explain (costs off) execute join_removal;
+
+-- test actual results
+insert into uniq values(1),(1);
+execute join_removal;
+
+rollback;
+deallocate join_removal;
+drop table uniq;
--
1.7.7