Commit 8c91019b authored by Radu Berinde's avatar Radu Berinde

opt: elide FK checks when setting NULL values

When we set NULL values on the FK columns, we can elide foreign key checks. This
is common when we are inserting/upserting on a subset of columns. It will also
be the case for the mutation generated by `ON CASCADE SET NULL` (and to some
extent `SET DEFAULT`).

To determine whether we are inserting NULL values, we check the input if it's a
Project or Values; these will be the common cases for mutations.

Release note: None
parent 39d5ea1b
......@@ -163,7 +163,7 @@ CREATE TABLE multi_ref_child (
)
query TTT
EXPLAIN INSERT INTO multi_ref_child VALUES (1, NULL, NULL, NULL)
EXPLAIN INSERT INTO multi_ref_child VALUES (1, NULL, NULL, NULL), (2, 3, 4, 5)
----
· distributed false
· vectorized false
......@@ -175,7 +175,7 @@ root · ·
│ └── buffer node · ·
│ │ label buffer 1
│ └── values · ·
│ size 4 columns, 1 row
│ size 4 columns, 2 rows
├── fk-check · ·
│ └── error if rows · ·
│ └── lookup-join · ·
......@@ -203,6 +203,20 @@ root · ·
└── scan buffer node · ·
· label buffer 1
# FK check can be omitted when we are inserting only NULLs.
query TTT
EXPLAIN INSERT INTO multi_ref_child VALUES (1, NULL, NULL, NULL)
----
· distributed false
· vectorized false
count · ·
└── insert · ·
│ into multi_ref_child(k, a, b, c)
│ strategy inserter
│ auto commit ·
└── values · ·
· size 4 columns, 1 row
# -- Tests with DELETE --
query TTT
......
......@@ -681,6 +681,41 @@ func ExprIsNeverNull(e opt.ScalarExpr, notNullCols opt.ColSet) bool {
}
}
// OutputColumnIsAlwaysNull returns true if the expression produces only NULL
// values for the given column. Used to elide foreign key checks.
//
// This could be a logical property but we only care about simple cases (NULLs
// in Projections and Values).
func OutputColumnIsAlwaysNull(e RelExpr, col opt.ColumnID) bool {
switch e.Op() {
case opt.ProjectOp:
p := e.(*ProjectExpr)
if p.Passthrough.Contains(col) {
return OutputColumnIsAlwaysNull(p.Input, col)
}
for i := range p.Projections {
if p.Projections[i].Col == col {
return p.Projections[i].Element.Op() == opt.NullOp
}
}
case opt.ValuesOp:
v := e.(*ValuesExpr)
colOrdinal, ok := v.Cols.Find(col)
if !ok {
return false
}
for i := range v.Rows {
if v.Rows[i].(*TupleExpr).Elems[colOrdinal].Op() != opt.NullOp {
return false
}
}
return true
}
return false
}
// FKCascades stores metadata necessary for building cascading queries.
type FKCascades []FKCascade
......
......@@ -81,8 +81,9 @@ func (mb *mutationBuilder) buildFKChecksForInsert() {
h := &mb.fkCheckHelper
for i, n := 0, mb.tab.OutboundForeignKeyCount(); i < n; i++ {
h.initWithOutboundFK(mb, i)
mb.checks = append(mb.checks, h.buildInsertionCheck())
if h.initWithOutboundFK(mb, i) {
mb.checks = append(mb.checks, h.buildInsertionCheck())
}
}
telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter)
}
......@@ -265,8 +266,9 @@ func (mb *mutationBuilder) buildFKChecksForUpdate() {
for i, n := 0, mb.tab.OutboundForeignKeyCount(); i < n; i++ {
// Verify that at least one FK column is actually updated.
if mb.outboundFKColsUpdated(i) {
h.initWithOutboundFK(mb, i)
mb.checks = append(mb.checks, h.buildInsertionCheck())
if h.initWithOutboundFK(mb, i) {
mb.checks = append(mb.checks, h.buildInsertionCheck())
}
}
}
......@@ -368,8 +370,9 @@ func (mb *mutationBuilder) buildFKChecksForUpsert() {
h := &mb.fkCheckHelper
for i := 0; i < numOutbound; i++ {
h.initWithOutboundFK(mb, i)
mb.checks = append(mb.checks, h.buildInsertionCheck())
if h.initWithOutboundFK(mb, i) {
mb.checks = append(mb.checks, h.buildInsertionCheck())
}
}
for i := 0; i < numInbound; i++ {
......@@ -465,6 +468,9 @@ type fkCheckHelper struct {
}
// initWithOutboundFK initializes the helper with an outbound FK constraint.
//
// Returns false if the FK relation should be ignored (e.g. because the new
// values for the FK columns are known to be always NULL).
func (h *fkCheckHelper) initWithOutboundFK(mb *mutationBuilder, fkOrdinal int) bool {
*h = fkCheckHelper{
mb: mb,
......@@ -492,6 +498,26 @@ func (h *fkCheckHelper) initWithOutboundFK(mb *mutationBuilder, fkOrdinal int) b
h.tabOrdinals[i] = h.fk.OriginColumnOrdinal(mb.tab, i)
h.otherTabOrdinals[i] = h.fk.ReferencedColumnOrdinal(h.otherTab, i)
}
// Check if we are setting NULL values for the FK columns, like when this
// mutation is the result of a SET NULL cascade action.
numNullCols := 0
for _, tabOrd := range h.tabOrdinals {
col := mb.scopeOrdToColID(mb.mapToReturnScopeOrd(tabOrd))
if memo.OutputColumnIsAlwaysNull(mb.outScope.expr, col) {
numNullCols++
}
}
if numNullCols == numCols {
// All FK columns are getting NULL values; FK check not needed.
return false
}
if numNullCols > 0 && h.fk.MatchMethod() == tree.MatchSimple {
// At least one FK column is getting a NULL value and we are using MATCH
// SIMPLE; FK check not needed.
return false
}
return true
}
......
......@@ -171,6 +171,40 @@ insert child_nullable
└── filters
└── column2:5 = parent.p:6
# In this case, we know that we are inserting *only* NULL values, so we don't
# need to check any FKs. We need to use norm because internally the values
# become NULL::INT and the normalization rules are needed to fold the cast.
norm
INSERT INTO child_nullable VALUES (100, NULL), (200, NULL)
----
insert child_nullable
├── columns: <none>
├── insert-mapping:
│ ├── column1:3 => c:1
│ └── column2:4 => p:2
└── values
├── columns: column1:3!null column2:4
├── (100, NULL)
└── (200, NULL)
# Same as above.
norm
INSERT INTO child_nullable (c) VALUES (100), (200)
----
insert child_nullable
├── columns: <none>
├── insert-mapping:
│ ├── column1:3 => c:1
│ └── column4:4 => p:2
└── project
├── columns: column4:4 column1:3!null
├── values
│ ├── columns: column1:3!null
│ ├── (100,)
│ └── (200,)
└── projections
└── CAST(NULL AS INT8) [as=column4:4]
# Check planning of filter with FULL match (which should be the same on a
# single column).
exec-ddl
......@@ -207,6 +241,24 @@ insert child_nullable_full
└── filters
└── column2:5 = parent.p:6
# No FK check needed.
norm
INSERT INTO child_nullable_full (c) VALUES (100), (200)
----
insert child_nullable_full
├── columns: <none>
├── insert-mapping:
│ ├── column1:3 => c:1
│ └── column4:4 => p:2
└── project
├── columns: column4:4 column1:3!null
├── values
│ ├── columns: column1:3!null
│ ├── (100,)
│ └── (200,)
└── projections
└── CAST(NULL AS INT8) [as=column4:4]
# Tests with multicolumn FKs.
exec-ddl
CREATE TABLE multi_col_parent (p INT, q INT, r INT, other INT, PRIMARY KEY (p, q, r))
......@@ -222,7 +274,7 @@ CREATE TABLE multi_col_child (
# All columns are nullable and must be part of the filter.
build
INSERT INTO multi_col_child VALUES (4, NULL, NULL, NULL)
INSERT INTO multi_col_child VALUES (4, NULL, NULL, NULL), (5, 1, 2, 3)
----
insert multi_col_child
├── columns: <none>
......@@ -234,7 +286,8 @@ insert multi_col_child
├── input binding: &1
├── values
│ ├── columns: column1:5!null column2:6 column3:7 column4:8
│ └── (4, NULL::INT8, NULL::INT8, NULL::INT8)
│ ├── (4, NULL::INT8, NULL::INT8, NULL::INT8)
│ └── (5, 1, 2, 3)
└── f-k-checks
└── f-k-checks-item: multi_col_child(p,q,r) -> multi_col_parent(p,q,r)
└── anti-join (hash)
......@@ -328,6 +381,21 @@ insert multi_col_child
├── column3:10 = multi_col_parent.q:13
└── column4:11 = multi_col_parent.r:14
# No FK check needed - we have only NULL values for a FK column.
norm
INSERT INTO multi_col_child VALUES (1, 10, NULL, 10)
----
insert multi_col_child
├── columns: <none>
├── insert-mapping:
│ ├── column1:5 => c:1
│ ├── column2:6 => p:2
│ ├── column3:7 => q:3
│ └── column4:8 => r:4
└── values
├── columns: column1:5!null column2:6!null column3:7 column4:8!null
└── (1, 10, NULL, 10)
exec-ddl
CREATE TABLE multi_col_child_full (
c INT PRIMARY KEY,
......@@ -338,7 +406,7 @@ CREATE TABLE multi_col_child_full (
# All columns are nullable and must be part of the filter.
build
INSERT INTO multi_col_child_full VALUES (4, NULL, NULL, NULL)
INSERT INTO multi_col_child_full VALUES (4, NULL, NULL, NULL), (5, 1, 2, 3)
----
insert multi_col_child_full
├── columns: <none>
......@@ -350,7 +418,8 @@ insert multi_col_child_full
├── input binding: &1
├── values
│ ├── columns: column1:5!null column2:6 column3:7 column4:8
│ └── (4, NULL::INT8, NULL::INT8, NULL::INT8)
│ ├── (4, NULL::INT8, NULL::INT8, NULL::INT8)
│ └── (5, 1, 2, 3)
└── f-k-checks
└── f-k-checks-item: multi_col_child_full(p,q,r) -> multi_col_parent(p,q,r)
└── anti-join (hash)
......@@ -437,6 +506,54 @@ insert multi_col_child_full
├── column3:10 = multi_col_parent.q:13
└── column4:11 = multi_col_parent.r:14
# No FK check needed when all FK columns only have NULL values.
norm
INSERT INTO multi_col_child_full VALUES (1, NULL, NULL, NULL)
----
insert multi_col_child_full
├── columns: <none>
├── insert-mapping:
│ ├── column1:5 => c:1
│ ├── column2:6 => p:2
│ ├── column3:7 => q:3
│ └── column4:8 => r:4
└── values
├── columns: column1:5!null column2:6 column3:7 column4:8
└── (1, NULL, NULL, NULL)
# But with MATCH FULL, the FK check is needed when only a subset of the columns
# only have NULL values.
norm
INSERT INTO multi_col_child_full VALUES (1, NULL, 2, NULL)
----
insert multi_col_child_full
├── columns: <none>
├── insert-mapping:
│ ├── column1:5 => c:1
│ ├── column2:6 => multi_col_child_full.p:2
│ ├── column3:7 => multi_col_child_full.q:3
│ └── column4:8 => multi_col_child_full.r:4
├── input binding: &1
├── values
│ ├── columns: column1:5!null column2:6 column3:7!null column4:8
│ └── (1, NULL, 2, NULL)
└── f-k-checks
└── f-k-checks-item: multi_col_child_full(p,q,r) -> multi_col_parent(p,q,r)
└── anti-join (hash)
├── columns: column2:9 column3:10!null column4:11
├── with-scan &1
│ ├── columns: column2:9 column3:10!null column4:11
│ └── mapping:
│ ├── column2:6 => column2:9
│ ├── column3:7 => column3:10
│ └── column4:8 => column4:11
├── scan multi_col_parent
│ └── columns: multi_col_parent.p:12!null multi_col_parent.q:13!null multi_col_parent.r:14!null
└── filters
├── column2:9 = multi_col_parent.p:12
├── column3:10 = multi_col_parent.q:13
└── column4:11 = multi_col_parent.r:14
exec-ddl
CREATE TABLE multi_ref_parent_a (a INT PRIMARY KEY, other INT)
----
......
......@@ -212,6 +212,28 @@ update child
└── filters
└── c:11 = grandchild.c:14
exec-ddl
CREATE TABLE child_nullable (c INT PRIMARY KEY, p INT REFERENCES parent(p))
----
# We don't need the FK check in this case because we are only setting NULL
# values. We need to use norm because internally the value becomes NULL::INT
# and the normalization rules are needed to fold the cast.
norm
UPDATE child_nullable SET p = NULL
----
update child_nullable
├── columns: <none>
├── fetch columns: c:3 p:4
├── update-mapping:
│ └── column5:5 => p:2
└── project
├── columns: column5:5 c:3!null p:4
├── scan child_nullable
│ └── columns: c:3!null p:4
└── projections
└── CAST(NULL AS INT8) [as=column5:5]
# Multiple grandchild tables
exec-ddl
CREATE TABLE grandchild2 (g INT PRIMARY KEY, c INT NOT NULL REFERENCES child(c))
......@@ -313,6 +335,110 @@ update self
└── filters
└── x:6 = y:9
exec-ddl
CREATE TABLE parent_multicol (a INT, b INT, c INT, PRIMARY KEY (a,b,c))
----
exec-ddl
CREATE TABLE child_multicol_simple (
k INT PRIMARY KEY,
a INT, b INT, c INT,
CONSTRAINT fk FOREIGN KEY(a,b,c) REFERENCES parent_multicol(a,b,c) MATCH SIMPLE
)
----
# With MATCH SIMPLE, we can elide the FK check if any FK column is NULL.
norm
UPDATE child_multicol_simple SET a = 1, b = NULL, c = 1 WHERE k = 1
----
update child_multicol_simple
├── columns: <none>
├── fetch columns: k:5 a:6 b:7 c:8
├── update-mapping:
│ ├── column9:9 => a:2
│ ├── column10:10 => b:3
│ └── column9:9 => c:4
└── project
├── columns: column9:9!null column10:10 k:5!null a:6 b:7 c:8
├── select
│ ├── columns: k:5!null a:6 b:7 c:8
│ ├── scan child_multicol_simple
│ │ └── columns: k:5!null a:6 b:7 c:8
│ └── filters
│ └── k:5 = 1
└── projections
├── 1 [as=column9:9]
└── CAST(NULL AS INT8) [as=column10:10]
exec-ddl
CREATE TABLE child_multicol_full (
k INT PRIMARY KEY,
a INT, b INT, c INT,
CONSTRAINT fk FOREIGN KEY(a,b,c) REFERENCES parent_multicol(a,b,c) MATCH FULL
)
----
# With MATCH FULL, we can elide the FK check only if all FK columns are NULL.
norm
UPDATE child_multicol_full SET a = 1, b = NULL, c = 1 WHERE k = 1
----
update child_multicol_full
├── columns: <none>
├── fetch columns: k:5 child_multicol_full.a:6 child_multicol_full.b:7 child_multicol_full.c:8
├── update-mapping:
│ ├── column9:9 => child_multicol_full.a:2
│ ├── column10:10 => child_multicol_full.b:3
│ └── column9:9 => child_multicol_full.c:4
├── input binding: &1
├── project
│ ├── columns: column9:9!null column10:10 k:5!null child_multicol_full.a:6 child_multicol_full.b:7 child_multicol_full.c:8
│ ├── select
│ │ ├── columns: k:5!null child_multicol_full.a:6 child_multicol_full.b:7 child_multicol_full.c:8
│ │ ├── scan child_multicol_full
│ │ │ └── columns: k:5!null child_multicol_full.a:6 child_multicol_full.b:7 child_multicol_full.c:8
│ │ └── filters
│ │ └── k:5 = 1
│ └── projections
│ ├── 1 [as=column9:9]
│ └── CAST(NULL AS INT8) [as=column10:10]
└── f-k-checks
└── f-k-checks-item: child_multicol_full(a,b,c) -> parent_multicol(a,b,c)
└── anti-join (hash)
├── columns: column9:11!null column10:12 column9:13!null
├── with-scan &1
│ ├── columns: column9:11!null column10:12 column9:13!null
│ └── mapping:
│ ├── column9:9 => column9:11
│ ├── column10:10 => column10:12
│ └── column9:9 => column9:13
├── scan parent_multicol
│ └── columns: parent_multicol.a:14!null parent_multicol.b:15!null parent_multicol.c:16!null
└── filters
├── column9:11 = parent_multicol.a:14
├── column10:12 = parent_multicol.b:15
└── column9:13 = parent_multicol.c:16
norm
UPDATE child_multicol_full SET a = NULL, b = NULL, c = NULL WHERE k = 1
----
update child_multicol_full
├── columns: <none>
├── fetch columns: k:5 a:6 b:7 c:8
├── update-mapping:
│ ├── column9:9 => a:2
│ ├── column9:9 => b:3
│ └── column9:9 => c:4
└── project
├── columns: column9:9 k:5!null a:6 b:7 c:8
├── select
│ ├── columns: k:5!null a:6 b:7 c:8
│ ├── scan child_multicol_full
│ │ └── columns: k:5!null a:6 b:7 c:8
│ └── filters
│ └── k:5 = 1
└── projections
└── CAST(NULL AS INT8) [as=column9:9]
exec-ddl
CREATE TABLE two (a int, b int, primary key (a, b))
----
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment