Key joins

Started by Joel Jacobsonabout 2 months ago28 messageshackers
Jump to latest
#1Joel Jacobson
joel@compiler.org

Hi hackers,

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

Key Joins
---------

Mathematically, CROSS JOINs are a cartesian product, INNER JOINs a
subset of it, and OUTER JOINs potentially null-extend that subset. In
practice, however, most joins look up additional information along a
foreign key rather than resembling a cartesian product.

Today a written query does not convey whether a particular JOIN simply
enriches the referencing side, filters it, or fans it out:

-- both sums silently inflated by the fan-out
SELECT o.id, SUM(oi.amount), SUM(p.amount)
FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.id
LEFT JOIN payments p ON p.order_id = o.id
GROUP BY o.id;

The above example is based on a pgsql-generals thread [1][Avoiding double-counting in aggregates with more than one join?] (/messages/by-id/86b9ec78-925c-1935-bc9d-6bad4ceb1f40@illuminatedcomputing.com).

We propose a new JOIN syntax that makes it easy to determine locally
that the immediate join result, before any further steps, just enriches
the referencing side with information from the referenced side, with
null-extension for OUTER JOINs. It conveys the author's intent, makes
the referencing side visually clear, and is enforced at compile time
against the schema. If we can't prove it, the user gets a compile-time
error.

Under FOR KEY the same query will not compile:

SELECT o.id, SUM(oi.amount), SUM(p.amount)
FROM orders o
LEFT JOIN order_items oi FOR KEY (order_id) -> o (id)
LEFT JOIN payments p FOR KEY (order_id) -> o (id)
GROUP BY o.id;

ERROR: key join from referencing relation p to referenced relation o cannot be proven
LINE 7: LEFT JOIN payments AS p FOR KEY (order_id) -> o (id)
^
DETAIL: Referenced columns o (id) are not proven unique. A preceding join may duplicate rows from referenced relation o.

Web demo
--------

The attached Discussion paper has also been published at https://keyjoin.org
with all examples in the paper runnable in the browser using a patched PGLite.

Patches
-------

0001
The first patch is an attempt to fix a problem discussed in thread [2][RE: Parallel INSERT SELECT take 2] (/messages/by-id/TY4PR01MB17718A4DE63020A9EA5E9CB6594382@TY4PR01MB17718.jpnprd01.prod.outlook.com), where
DROP FUNCTION can cause issues with concurrent dependency lookups. For key
joins, this issue also extends to ALTER FUNCTION.

The patch prevents stored expressions from depending on stale function
OIDs by locking referenced procedures before recording dependencies, and
by making CREATE OR REPLACE FUNCTION and ALTER FUNCTION take conflicting
locks before changing pg_proc.

0002
The second patch implements the FOR KEY join feature. As this is a
first prototype, there are definitively things that needs to be
improved. For example, we would love feedback on our the revalidation
logic and our dependency tracking approach, that adds a new deptype for
purpose of tracking the proof facts. Another problem we didn't find a
perfect solution to, was our need to expand views during parse for proof
checking and finding constraints.

The logics to compute the facts needed by the proof checker are kind of
complex, which is partially due to the ambition to not introduce
overhead to queries not using the new feature.

The patch comes with a massive test suite, that we understand will need
to be trimmed down to have a chance to be committable.

0003
The third patch adds information_schema.view_constraint_usage, that
shows what constraints a view depend on, due to usage of key joins in
the view's query. This is also part of the proposal.

Joel Jacobson
Vik Fearing
Andreas Karlsson
Arne Roland
Anders Granlund

[1]: [Avoiding double-counting in aggregates with more than one join?] (/messages/by-id/86b9ec78-925c-1935-bc9d-6bad4ceb1f40@illuminatedcomputing.com)
(/messages/by-id/86b9ec78-925c-1935-bc9d-6bad4ceb1f40@illuminatedcomputing.com)
[2]: [RE: Parallel INSERT SELECT take 2] (/messages/by-id/TY4PR01MB17718A4DE63020A9EA5E9CB6594382@TY4PR01MB17718.jpnprd01.prod.outlook.com)
(/messages/by-id/TY4PR01MB17718A4DE63020A9EA5E9CB6594382@TY4PR01MB17718.jpnprd01.prod.outlook.com)

Attachments:

v1-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v1-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
v1-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v1-0002-Implement-FOR-KEY-join-support.patch.gz?="Download+0-4
v1-0001-Lock-procedures-before-recording-dependencies.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v1-0001-Lock-procedures-before-recording-dependencies.patch.gz?="Download
key_joins.pdfapplication/pdf; name=key_joins.pdfDownload+1-6
#2Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#1)
Re: Key joins

On Thu, May 28, 2026, at 20:47, Joel Jacobson wrote:

Hi hackers,

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

Key Joins
---------

Mathematically, CROSS JOINs are a cartesian product, INNER JOINs a
subset of it, and OUTER JOINs potentially null-extend that subset. In
practice, however, most joins look up additional information along a
foreign key rather than resembling a cartesian product.

Today a written query does not convey whether a particular JOIN simply
enriches the referencing side, filters it, or fans it out:

-- both sums silently inflated by the fan-out
SELECT o.id, SUM(oi.amount), SUM(p.amount)
FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.id
LEFT JOIN payments p ON p.order_id = o.id
GROUP BY o.id;

The above example is based on a pgsql-generals thread [1].

We propose a new JOIN syntax that makes it easy to determine locally
that the immediate join result, before any further steps, just enriches
the referencing side with information from the referenced side, with
null-extension for OUTER JOINs. It conveys the author's intent, makes
the referencing side visually clear, and is enforced at compile time
against the schema. If we can't prove it, the user gets a compile-time
error.

Under FOR KEY the same query will not compile:

SELECT o.id, SUM(oi.amount), SUM(p.amount)
FROM orders o
LEFT JOIN order_items oi FOR KEY (order_id) -> o (id)
LEFT JOIN payments p FOR KEY (order_id) -> o (id)
GROUP BY o.id;

ERROR: key join from referencing relation p to referenced relation o
cannot be proven
LINE 7: LEFT JOIN payments AS p FOR KEY (order_id) -> o (id)
^
DETAIL: Referenced columns o (id) are not proven unique. A preceding
join may duplicate rows from referenced relation o.

Web demo
--------

The attached Discussion paper has also been published at https://keyjoin.org
with all examples in the paper runnable in the browser using a patched PGLite.

Patches
-------

0001
The first patch is an attempt to fix a problem discussed in thread [2], where
DROP FUNCTION can cause issues with concurrent dependency lookups. For key
joins, this issue also extends to ALTER FUNCTION.

The patch prevents stored expressions from depending on stale function
OIDs by locking referenced procedures before recording dependencies, and
by making CREATE OR REPLACE FUNCTION and ALTER FUNCTION take conflicting
locks before changing pg_proc.

0002
The second patch implements the FOR KEY join feature. As this is a
first prototype, there are definitively things that needs to be
improved. For example, we would love feedback on our the revalidation
logic and our dependency tracking approach, that adds a new deptype for
purpose of tracking the proof facts. Another problem we didn't find a
perfect solution to, was our need to expand views during parse for proof
checking and finding constraints.

The logics to compute the facts needed by the proof checker are kind of
complex, which is partially due to the ambition to not introduce
overhead to queries not using the new feature.

The patch comes with a massive test suite, that we understand will need
to be trimmed down to have a chance to be committable.

0003
The third patch adds information_schema.view_constraint_usage, that
shows what constraints a view depend on, due to usage of key joins in
the view's query. This is also part of the proposal.

Joel Jacobson
Vik Fearing
Andreas Karlsson
Arne Roland
Anders Granlund

[1] [Avoiding double-counting in aggregates with more than one join?]
(/messages/by-id/86b9ec78-925c-1935-bc9d-6bad4ceb1f40@illuminatedcomputing.com)
[2] [RE: Parallel INSERT SELECT take 2]
(/messages/by-id/TY4PR01MB17718A4DE63020A9EA5E9CB6594382@TY4PR01MB17718.jpnprd01.prod.outlook.com)

I noticed cfbot was red; I see I forgot to include these files in patch 0002:
src/test/modules/injection_points/expected/key_join_proof_race_record_proc_dep.out
src/test/modules/injection_points/specs/key_join_proof_race_record_proc_dep.spec

Fixed in new version attached.

/Joel

Attachments:

v2-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v2-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
v2-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v2-0002-Implement-FOR-KEY-join-support.patch.gz?="Download+4-6
v2-0001-Lock-procedures-before-recording-dependencies.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v2-0001-Lock-procedures-before-recording-dependencies.patch.gz?="Download
#3Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#2)
Re: Key joins

On Fri, May 29, 2026, at 00:13, Joel Jacobson wrote:

On Thu, May 28, 2026, at 20:47, Joel Jacobson wrote:

Hi hackers,

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

...

The attached Discussion paper has also been published at https://keyjoin.org
with all examples in the paper runnable in the browser using a patched PGLite.

v3 is mostly a rebase over recent master changes.

0001: Serialize routine definition changes with dependency recording
0002: Implement FOR KEY join support
0003: Add information_schema.view_constraint_usage

Changes from v2:

* 0001 was reworked after 2fbb211 added generic dependency locking to
master. The patch now only keeps CREATE OR REPLACE FUNCTION /
ALTER FUNCTION serialization with dependency recording. This also
matches the wording change from e2b3573.

* 0002 race tests now expect the generic dependency-locking error path,
handle stale dependency lookups during proof revalidation, and avoid
timing-dependent deadlock/injection-point output in the function and
operator prelock tests.

* cfbot showed the ICU-dependent nondeterministic-collation tests in v2
failed when such collations were unavailable. Moved to a separate guarded
key_join_icu test.

* 0003 is unchanged.

/Joel

Attachments:

v3-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v3-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
v3-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v3-0002-Implement-FOR-KEY-join-support.patch.gz?="Download+3-5
v3-0001-Serialize-routine-definition-changes-with-depende.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v3-0001-Serialize-routine-definition-changes-with-depende.patc?= =?UTF-8?Q?h.gz?="Download
#4Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#3)
Re: Key joins

On Fri, May 29, 2026, at 07:08, Joel Jacobson wrote:

On Fri, May 29, 2026, at 00:13, Joel Jacobson wrote:

On Thu, May 28, 2026, at 20:47, Joel Jacobson wrote:

Hi hackers,

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

...

The attached Discussion paper has also been published at https://keyjoin.org
with all examples in the paper runnable in the browser using a patched PGLite.

v4 is a small follow-up to fix a non-cassert build failure,
reported by cfbot on NetBSD/FreeBSD animals:

* 0002 removes assertion-only local variables in parse_key_join.c
that triggered -Werror=unused-but-set-variable.

* 0001 and 0003 are unchanged from v3.

/Joel

Attachments:

v4-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v4-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
v4-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v4-0002-Implement-FOR-KEY-join-support.patch.gz?="Download
v4-0001-Serialize-routine-definition-changes-with-depende.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v4-0001-Serialize-routine-definition-changes-with-depende.patc?= =?UTF-8?Q?h.gz?="Download
#5Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#4)
Re: Key joins

On Fri, May 29, 2026, at 09:45, Joel Jacobson wrote:

On Fri, May 29, 2026, at 07:08, Joel Jacobson wrote:

On Fri, May 29, 2026, at 00:13, Joel Jacobson wrote:

On Thu, May 28, 2026, at 20:47, Joel Jacobson wrote:

Hi hackers,

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

...

The attached Discussion paper has also been published at https://keyjoin.org
with all examples in the paper runnable in the browser using a patched PGLite.

v5 is another small follow-up to fix a cfbot regression-test warning:

* 0002 renames the roles created by key_join.sql to use the required
regress_ prefix, so builds with ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS
do not emit warnings.

* 0001 and 0003 are unchanged from v4.

/Joel

Attachments:

v5-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v5-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
v5-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v5-0002-Implement-FOR-KEY-join-support.patch.gz?="Download+2-8
v5-0001-Serialize-routine-definition-changes-with-depende.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v5-0001-Serialize-routine-definition-changes-with-depende.patc?= =?UTF-8?Q?h.gz?="Download
#6Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#5)
Re: Key joins

On Fri, May 29, 2026, at 10:54, Joel Jacobson wrote:

On Fri, May 29, 2026, at 09:45, Joel Jacobson wrote:

On Fri, May 29, 2026, at 07:08, Joel Jacobson wrote:

On Fri, May 29, 2026, at 00:13, Joel Jacobson wrote:

On Thu, May 28, 2026, at 20:47, Joel Jacobson wrote:

Hi hackers,

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

...

The attached Discussion paper has also been published at https://keyjoin.org
with all examples in the paper runnable in the browser using a patched PGLite.

v6 fixes a cfbot failure in an injection_points isolation test:

* 0002 stabilizes the expected completion order in two key-join deadlock
tests using isolationtester permutation markers, instead of relying on
scheduler-dependent output order.

* 0001 and 0003 are unchanged from v5.

/Joel

Attachments:

v6-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v6-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
v6-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v6-0002-Implement-FOR-KEY-join-support.patch.gz?="Download+1-3
v6-0001-Serialize-routine-definition-changes-with-depende.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v6-0001-Serialize-routine-definition-changes-with-depende.patc?= =?UTF-8?Q?h.gz?="Download
#7Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Joel Jacobson (#1)
Re: Key joins

On Thu, 28 May 2026 at 20:48, Joel Jacobson <joel@compiler.org> wrote:

Hi hackers,

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

Web demo
--------

The attached Discussion paper has also been published at https://keyjoin.org
with all examples in the paper runnable in the browser using a patched PGLite.

Re: "8.4 Why Column Lists Instead of Constraint Names" [0]https://keyjoin.org/#sec8.4

It's mentioned that the use of named foreign key constraints as key
column list definitions is not part of the proposal because they are
not universally applicable. While I do understand that for some cases
(multiple mentions of the same target table, CTEs, subqueries, ...)
there won't be a (uniquely) named constraint to reference, in many
(possibly most) cases the FK constraint name _will_ uniquely identify
the base table pair to join, and I think that using the FK name should
be supported as a major QoL addition in this proposal.

Note that the FOR KEY (cols) -> alias (cols) is still useful for the
reasons why constraint names can't always be used, but it's probably
not something I'd ever try to use unless I really, really needed the
specific guarantees granted by the new processing while I was writing
the query. `FOR KEY (something) -> something (something)` just doesn't
feel natural to me.

Kind regards,

Matthias van de Meent
Databricks (https://www.databricks.com)

[0]: https://keyjoin.org/#sec8.4

#8Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Joel Jacobson (#1)
Re: Key joins

On Thu, 2026-05-28 at 20:47 +0200, Joel Jacobson wrote:

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

Your presentation at the pgconf.dev really convinced me that this is
a useful feature.

I had only one consideration:

FROM orders o
LEFT JOIN order_items oi FOR KEY (order_id) -> o (id)

In the spirit of looking more like SQL, how about replacing the
arrows with FROM and TO?

Either

a JOIN b FOR KEY (col1) TO (col2)

or, slightly more verbose and natural language-like:

a JOIN b FOR KEY FROM (col1) TO (col2)

And if the arrow points the other way,

a JOIN b FOR KEY (col1) FROM (col2)

or

a JOIN b FOR KEY TO (col1) FROM (col2)

Yours,
Laurenz Albe

#9Joel Jacobson
joel@compiler.org
In reply to: Laurenz Albe (#8)
Re: Key joins

On Fri, May 29, 2026, at 14:51, Laurenz Albe wrote:

On Thu, 2026-05-28 at 20:47 +0200, Joel Jacobson wrote:

This patch implements a new SQL language feature, that we intent to
submit as a Change Proposal to the WG 3 SQL committee for the next
meeting in Stockholm in June. We would greatly appreciate any feedback
from the community.

Your presentation at the pgconf.dev really convinced me that this is
a useful feature.

Thanks for the opportunity of presenting.

I had only one consideration:

FROM orders o
LEFT JOIN order_items oi FOR KEY (order_id) -> o (id)

In the spirit of looking more like SQL, how about replacing the
arrows with FROM and TO?

Either

a JOIN b FOR KEY (col1) TO (col2)

or, slightly more verbose and natural language-like:

a JOIN b FOR KEY FROM (col1) TO (col2)

And if the arrow points the other way,

a JOIN b FOR KEY (col1) FROM (col2)

or

a JOIN b FOR KEY TO (col1) FROM (col2)

We actually originally considered TO and FROM as keywords for indicating
direction, but FROM in a join clause causes confusion with the FROM
clause itself. Our user discussions over the last three years indicates
that arrows are clearer and less ambiguous.

It's also worth to mention that SQL/PGQ also uses ASCII arrows to
indicate direction for its graph pattern syntax [1]https://peter.eisentraut.org/blog/2023/04/04/sql-2023-is-finished-here-is-whats-new.

[1]: https://peter.eisentraut.org/blog/2023/04/04/sql-2023-is-finished-here-is-whats-new

/Joel

#10Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Joel Jacobson (#9)
Re: Key joins

On Fri, 2026-05-29 at 15:21 +0200, Joel Jacobson wrote:

In the spirit of looking more like SQL, how about replacing the

arrows with FROM and TO?

We actually originally considered TO and FROM as keywords for indicating
direction, but FROM in a join clause causes confusion with the FROM
clause itself.  Our user discussions over the last three years indicates
that arrows are clearer and less ambiguous.

It's also worth to mention that SQL/PGQ also uses ASCII arrows to
indicate direction for its graph pattern syntax.

I understand the problem with FROM, and I have no objection to the
arrows.

Yours,
Laurenz Albe

#11Joel Jacobson
joel@compiler.org
In reply to: Laurenz Albe (#10)
Re: Key joins

On Fri, May 29, 2026, at 18:20, Laurenz Albe wrote:

On Fri, 2026-05-29 at 15:21 +0200, Joel Jacobson wrote:

We actually originally considered TO and FROM as keywords for indicating
direction, but FROM in a join clause causes confusion with the FROM
clause itself.  Our user discussions over the last three years indicates
that arrows are clearer and less ambiguous.

It's also worth to mention that SQL/PGQ also uses ASCII arrows to
indicate direction for its graph pattern syntax.

I understand the problem with FROM, and I have no objection to the
arrows.

Thanks for reviewing.

v7 updates 0002 to match the revised Change Proposal wording for
GROUPING SETS/ROLLUP/CUBE:

* Row-coverage facts can now pass through GROUPING SETS, ROLLUP, and
CUBE when one expanded grouping set contains all key columns under the
same key identity. This is less conservative than v6: subtotal rows
with NULLs in omitted grouping columns do not invalidate row coverage,
since row coverage is about containment of all-non-null key values.

* Uniqueness and not-null handling for grouping sets remain conservative.
GROUPING SETS/ROLLUP/CUBE do not by themselves prove referenced
uniqueness or not-nullness; a following DISTINCT or simple GROUP BY can
still provide uniqueness where needed.

* 0001 and 0003 are unchanged from v6.

/Joel

Attachments:

v7-0001-Serialize-routine-definition-changes-with-depende.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v7-0001-Serialize-routine-definition-changes-with-depende.patc?= =?UTF-8?Q?h.gz?="Download
v7-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v7-0002-Implement-FOR-KEY-join-support.patch.gz?="Download
v7-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v7-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
#12Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#11)
Re: Key joins

On Sun, May 31, 2026, at 10:05, Joel Jacobson wrote:

On Fri, May 29, 2026, at 18:20, Laurenz Albe wrote:

On Fri, 2026-05-29 at 15:21 +0200, Joel Jacobson wrote:

We actually originally considered TO and FROM as keywords for indicating
direction, but FROM in a join clause causes confusion with the FROM
clause itself.  Our user discussions over the last three years indicates
that arrows are clearer and less ambiguous.

It's also worth to mention that SQL/PGQ also uses ASCII arrows to
indicate direction for its graph pattern syntax.

I understand the problem with FROM, and I have no objection to the
arrows.

Thanks for reviewing.

v7 updates 0002 to match the revised Change Proposal wording for
GROUPING SETS/ROLLUP/CUBE:

v8 fixes another nondeterministic isolation-test ordering issue seen by
cfbot on the FreeBSD machine.

0001 and 0003 are unchanged from v7.

/Joel

Attachments:

v8-0001-Serialize-routine-definition-changes-with-depende.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v8-0001-Serialize-routine-definition-changes-with-depende.patc?= =?UTF-8?Q?h.gz?="Download
v8-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v8-0002-Implement-FOR-KEY-join-support.patch.gz?="Download+1-4
v8-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v8-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
#13Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#12)
Re: Key joins

On Sun, May 31, 2026, at 16:40, Joel Jacobson wrote:

On Sun, May 31, 2026, at 10:05, Joel Jacobson wrote:

On Fri, May 29, 2026, at 18:20, Laurenz Albe wrote:

On Fri, 2026-05-29 at 15:21 +0200, Joel Jacobson wrote:

We actually originally considered TO and FROM as keywords for indicating
direction, but FROM in a join clause causes confusion with the FROM
clause itself.  Our user discussions over the last three years indicates
that arrows are clearer and less ambiguous.

It's also worth to mention that SQL/PGQ also uses ASCII arrows to
indicate direction for its graph pattern syntax.

I understand the problem with FROM, and I have no objection to the
arrows.

Thanks for reviewing.

v7 updates 0002 to match the revised Change Proposal wording for
GROUPING SETS/ROLLUP/CUBE:

v8 fixes another nondeterministic isolation-test ordering issue seen by
cfbot on the FreeBSD machine.

0001 and 0003 are unchanged from v7.

I noted a small error in 7.4.13 in the paper. The examples incorrectly
used "LEFT JOIN" instead of "JOIN", which made the claim "Both queries,
if accepted, would produce the same result rows" to not hold true.

Fixed subsection below:

===

7.4.13 Filtered Views and the PK Side

A key join through a filtered view on the PK side is rejected unless the
FK side supplies matching direct key-equality filter evidence; see
Section 7.3.2. This differs from a query that filters the base table
with a WHERE clause after the join:

For the examples below, assume orders has a NOT NULL customer_id that
references customers (id).

-- Accepted: join the full base table, then filter the result.
SELECT *
FROM orders AS o
JOIN customers AS c FOR KEY (id) <- o (customer_id)
WHERE c.active;

CREATE VIEW active_customers AS
SELECT * FROM customers WHERE active;

-- Rejected: the view is the PK side and lacks row coverage.
SELECT *
FROM orders AS o
JOIN active_customers AS ac FOR KEY (id) <- o (customer_id);
ERROR: key join from referencing relation o to referenced relation ac cannot be proven
ERROR: key join from referencing relation o to referenced relation ac cannot be proven
LINE 3: JOIN active_customers AS ac FOR KEY (id) <- o (customer_id);
^
DETAIL: Not every o (customer_id) value can be proven to have a matching ac row. Referenced relation ac is filtered before this key join. The relevant operation occurs inside view public.active_customers.

Both queries, if accepted, would produce the same result rows, but would
differ from the key join's perspective. In the first query, the join is
against the full customers table: every order is guaranteed to find its
matching customer, and the WHERE clause filters the result after the
join has been evaluated. In the second query, active_customers is the PK
side, and it is missing inactive customers. An order that references an
inactive customer will not find a match.

This asymmetry is inherent in row coverage. A key join guarantees that
the join will behave as a proper foreign key traversal, producing
exactly one match for each FK row. A filtered PK side breaks this
guarantee unless the missing PK values are matched by filters that
remove the corresponding FK rows before the join.

===

/Joel

#14Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#13)
Re: Key joins

On Mon, Jun 1, 2026, at 22:06, Joel Jacobson wrote:

-- Rejected: the view is the PK side and lacks row coverage.
SELECT *
FROM orders AS o
JOIN active_customers AS ac FOR KEY (id) <- o (customer_id);
ERROR: key join from referencing relation o to referenced relation ac
cannot be proven
ERROR: key join from referencing relation o to referenced relation ac
cannot be proven
LINE 3: JOIN active_customers AS ac FOR KEY (id) <- o (customer_id);
^
DETAIL: Not every o (customer_id) value can be proven to have a
matching ac row. Referenced relation ac is filtered before this key
join. The relevant operation occurs inside view public.active_customers.

Ops, that extra ERROR: line was a mistake, sorry about that.

The corresponding subsection has now also been fixed in the web version:
https://keyjoin.org/#sec7.4.13

/Joel

#15Joel Jacobson
joel@compiler.org
In reply to: Joel Jacobson (#14)
Re: Key joins

On Mon, Jun 1, 2026, at 22:36, Joel Jacobson wrote:

Ops, that extra ERROR: line was a mistake, sorry about that.

The corresponding subsection has now also been fixed in the web version:
https://keyjoin.org/#sec7.4.13

v9 rebases over Stamp 19beta1 and improves one type of error message in 0002.

The affected queries were already rejected correctly. The change is
only that DETAIL now first reports if there is not even a matching
referential constraint.

0001 and 0003 are unchanged from v8.

/Joel

Attachments:

v9-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v9-0003-Add-information=5Fschema.view=5Fconstraint=5Fusage.pat?= =?UTF-8?Q?ch.gz?="Download
v9-0002-Implement-FOR-KEY-join-support.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v9-0002-Implement-FOR-KEY-join-support.patch.gz?="Download+4-3
v9-0001-Serialize-routine-definition-changes-with-depende.patch.gzapplication/x-gzip; name="=?UTF-8?Q?v9-0001-Serialize-routine-definition-changes-with-depende.patc?= =?UTF-8?Q?h.gz?="Download
#16Arne Roland
arne.roland@malkut.net
In reply to: Joel Jacobson (#15)
Re: Key joins

Greetings,

the change with the new patch is, that the locking is less excessive.
Foreign key paths unnecessarily blocked concurrent DML. This version of
the patch relaxes the locking to a more balanced degree. In particular
every plainly reading SELECT now takes only an ACCESS SHARE.

Regards
Arne

Show quoted text

On 2026-06-04 6:02 PM, Joel Jacobson wrote:

On Mon, Jun 1, 2026, at 22:36, Joel Jacobson wrote:

Ops, that extra ERROR: line was a mistake, sorry about that.

The corresponding subsection has now also been fixed in the web version:
https://keyjoin.org/#sec7.4.13

v9 rebases over Stamp 19beta1 and improves one type of error message in 0002.

The affected queries were already rejected correctly. The change is
only that DETAIL now first reports if there is not even a matching
referential constraint.

0001 and 0003 are unchanged from v8.

/Joel

Attachments:

v10-0003-Add-information_schema.view_constraint_usage.patch.gzapplication/gzip; name=v10-0003-Add-information_schema.view_constraint_usage.patch.gzDownload
v10-0002-Implement-FOR-KEY-join-support.patch.gzapplication/gzip; name=v10-0002-Implement-FOR-KEY-join-support.patch.gzDownload+0-3
v10-0001-Serialize-routine-definition-changes-with-depend.patch.gzapplication/gzip; name=v10-0001-Serialize-routine-definition-changes-with-depend.patch.gzDownload
#17Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Arne Roland (#16)
Re: Key joins

Hi,

I took a quick look at the patch over the past couple days. I don't have
a perfect understanding of how it works, but let me share what I have so
far, before I get distracted by other stuff.

I'm interested in this patch because there seems to be a possible
overlap with the overlap with the starjoin planning (in that maybe we
could try reusing some of the derived information for that).

It's a mix of random thoughts, high/low level, important/superficial in
no particular order.

1) It does not compile, ATExecSetRowSecurity seems to be missing
prev_rls or something like that. I simply commented this out, to get it
to compile.

2) fk_referenced_selected in find_key_join_match should be marked as
PG_USED_FOR_ASSERTS_ONLY, to fix compiler warning

3) It'd be helpful if the commit message for 0001 explained why this
change is needed. More clearly than now, I mean. The initial message in
this thread points at another thread as the source of this, but that
thread is huge so how are reviewers expected to find the explanation?

Don't explain just what the commit does, but why it's needed. Say, give
an example of how the old code fails.

4) I think there needs to be a README explaining how the feature works,
i.e. the design and trade offs. Alternatively, it could be explained in
a comment at the beginning of some .c file, but a README seems easier to
find. Without this, reviewers have to piece it from the sgml "user"
docs, and random comments all over the place.

5) It might be helpful if the README defined a couple terms introduced
by the patch, but not really defined anywhere. I mean terms like: join
point, proof, proof graph, fact, surface, "relation visible from". I can
guess what each of these means, but maybe I got it wrong.

Although, I now see "join point" was used in the docs before, so maybe
it's a well-known term?

6) Does it always have to be a "foreign key traversal"? Consider an
example like this:

create table dim (id serial primary key);
create table f (id serial primary key, did int);
select * from f left join dim for key (id) <- f(did);

Currently this fails because of no matching foreign key constraint, but
isn't that pretty much the same thing as if the table actually had the
foreign key (at least considering the cardinality of the join - it won't
change it by adding/removing rows).

I realize that'd contradict the "FOR KEY" part of this patch, but it's
also one of the things that might be beneficial for the starjoin
planning (in that we could maybe extend it to more cases). Although,
maybe we could check those constraints during planning, just like we
check the fkey_list.

7) The patch invents a new "FILTER" clause for joins:

[ FILTER (WHERE join_filter) ]

I understand why it's done - there may be additional join conditions, on
top of the FK equality. But I think it'll be rather confusing. We
already have a "Join Filter", which is used for join clauses that happen
to not be used as "proper" join conditions (e.g. Hash/Merge Cond), and
has to be evaluated "after" the join itself. And now we'd have another
kind of "join filter" ...

Is there a different that'd make this work without the FILTER? For
example, we might encode the "key join" information in the existing ON
(...) clause:

ON (KEY (oi.order_id) -> (o.id) AND ... other clauses ...)

but it seems not as readable. Or maybe just use something else than
"FILTER"? Not sure.

8) I don't understand this change in functioncmds:

/* Routine kind cannot change for an existing pg_proc OID. */
Assert(procForm->prokind != PROKIND_AGGREGATE);

The comment says we can't change OID, but the assert checks it's not an
aggregate functions. Isn't that misleading?

9) DefineView now adjusts the query ID. Why is that needed? Isn't that a
bit weird?

10) checkWellFormedRecursionWalker now recurses, but why is that needed?

11) It's not clear to me why we need p_creating_stored_object, and it's
not explained anywhere. Maybe it's obvious, but not to me.

12) I'm very skeptical anyone can meaningfully review parse_key_join.c.
It's 150KB with ~5200 lines (very dense, with pretty minimal comments).
That's a massive file. The chance of me declaring this committable is
about 0%, simply because I wouldn't believe I understand it.

I think it needs to be broken up into smaller pieces, somehow. I don't
know how, but perhaps it's possible to extract a "minimal feature"
handling some limited subset of cases, and then gradually expand it?

Furthermore, it seems a lot of that file is duplicate with code we
already have elsewhere. For example, couldn't it use a bunch of list
functions instead of writing a local version?

list_contains_equal_node -> list_member
append_dependencies_unique -> list_concat_unique
append_dependency_unique -> list_append_unique
...

There may be more such cases, I haven't looked for them.

13) I think the main question is whether parse-analyze is the right
place to handle this. I don't know. I assume one of the reasons for
doing that is to get an error when defining a view, or when altering an
object - e.g. like when dropping a function used by a view. Which seems
reasonable, people would not like views silently broken by DDL.

But it seems to be this also leads to a lot of code duplication, because
the parse analysis now has to "reimplement" a lot of the stuff already
done in later stages, after parse analyze. For example, we have the
root->fkey_list thing, but that's not available yet.

There's probably more such information - like innerrel_is_unique,
rel_is_distinct_for, relation_has_unique_index_for etc.

Maybe it's not worth it? What if we did this in the planner instead? How
much simpler would it get? My intuition is it'd get much smaller, but
maybe I'm wrong.

It'd probably mean it's not necessary to expand views during parse,
which was one of the problems mentioned at the beginning of this thread.

I suppose we'd need to keep additional information in the plan to know
which joins to check, etc.

14) If we wanted to use some of this for the starjoin planning stuff,
we'd need to propagate more information too, I think. But I'm not sure
about this - we already have the information about foreign keys, so if
key joins are tied to foreign keys, there would not be no new useful
information I suppose.

Plus, I don't want to make that patch dependent on people using new
syntax. If that can give us *additional* information, that would be a
different thing.

15) I'm not sure about using dependencies to store "proofs". Maybe it's
fine, but the existing deptypes are only about DROP behavior - whether
it's OK to drop either side automatically, etc.). But the new deptype
DEPENDENCY_KEYJOIN also carries additional meaning (i.e. it means this
is a proof). I don't see an immediate issue with it, though.

16) I was wondering what performance impact this has, roughly. So I
created a simple 4-way join with fact + 3 dimensions:

create table dim1 (id serial primary key, val text);
create table dim2 (id serial primary key, val text);
create table dim3 (id serial primary key, val text);
create table f (id serial primary key,
d1 int not null references dim1(id),
d2 int not null references dim2(id),
d3 int not null references dim3(id));

and then measured throughput with 2 queries

select * from f
join dim1 on (dim1.id = d1)
join dim2 on (dim2.id = d2)
join dim3 on (dim3.id = d3);

select * from f
join dim1 for key (id) <- f (d1)
join dim2 for key (id) <- f (d2)
join dim3 for key (id) <- f (d3);

which is the same query, except for the FOR KEY syntax. And I got this
(under explain, to only do the planning):

patched master
-----------------------------
join 25251 24906
keyjoin 17159

So that's ~30% regression compared to master / regular joins. I also
tried with join_collapse_limit=1 to eliminate the join order planning:

patched master
-----------------------------
join 31476 31252
keyjoin 19353

That's 40% regression. Of course, this is just explain - once there is
data in the table, and actual execution, the differences will be much
smaller. Still, it's not great.

I haven't looked very closely, but based on some quick profiling it
seems ensure_key_join_surface_facts / compute_key_join_relation_facts is
doing expensive stuff like findNotNullConstraintAttnum or
get_index_constraint, both of which do systable scans. That should
probably go through syscache or something like that.

regards

--
Tomas Vondra

#18Arne Roland
arne.roland@malkut.net
In reply to: Tomas Vondra (#17)
Re: Key joins

Hi Tomas,

thank you for checking it out!

On 2026-06-11 11:02 PM, Tomas Vondra wrote:

Hi,

I took a quick look at the patch over the past couple days. I don't have
a perfect understanding of how it works, but let me share what I have so
far, before I get distracted by other stuff.

I'm interested in this patch because there seems to be a possible
overlap with the overlap with the starjoin planning (in that maybe we
could try reusing some of the derived information for that).

It's a mix of random thoughts, high/low level, important/superficial in
no particular order.

1) It does not compile, ATExecSetRowSecurity seems to be missing
prev_rls or something like that. I simply commented this out, to get it
to compile.

Sorry, I am confused. There is the self contained block of

@@ -18879,6 +18898,7 @@ ATExecSetRowSecurity(Relation rel, bool rls)
Relation pg_class;
Oid relid;
HeapTuple tuple;
+ bool prev_rls;

relid = RelationGetRelid(rel);

@@ -18890,6 +18910,7 @@ ATExecSetRowSecurity(Relation rel, bool rls)
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", relid);

+ prev_rls = ((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity;
((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity = rls;
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);

@@ -18898,6 +18919,21 @@ ATExecSetRowSecurity(Relation rel, bool rls)

table_close(pg_class, RowExclusiveLock);
heap_freetuple(tuple);
+
+	/*
+	 * Revalidate stored key-join proofs that depend on this relation.  The
+	 * key-join base-fact computation refuses to expose facts for a relation
+	 * with row-level security enabled (parse_key_join.c), so flipping
+	 * relrowsecurity can leave a stored proof unprovable.  Revalidate
+	 * whenever the flag actually changes; for the off-to-on transition the
+	 * revalidation raises an error and aborts this DDL, while the on-to-off
+	 * transition is a no-op for proofs that were already valid.
+	 */
+	if (rls != prev_rls)
+	{
+		CommandCounterIncrement();
+		RevalidateDependentKeyJoinObjectsOnRelation(relid);
+	}
}

Apart from that there shouldn't be any references to that. What
definition was missing?

2) fk_referenced_selected in find_key_join_match should be marked as
PG_USED_FOR_ASSERTS_ONLY, to fix compiler warning

Thank you for spotting that.

3) It'd be helpful if the commit message for 0001 explained why this
change is needed. More clearly than now, I mean. The initial message in
this thread points at another thread as the source of this, but that
thread is huge so how are reviewers expected to find the explanation?

Don't explain just what the commit does, but why it's needed. Say, give
an example of how the old code fails.

In short we need that to prevent objects our proof relies on to be
dropped or altered below us. I'll try to work that into the commit message.

4) I think there needs to be a README explaining how the feature works,
i.e. the design and trade offs. Alternatively, it could be explained in
a comment at the beginning of some .c file, but a README seems easier to
find. Without this, reviewers have to piece it from the sgml "user"
docs, and random comments all over the place.

5) It might be helpful if the README defined a couple terms introduced
by the patch, but not really defined anywhere. I mean terms like: join
point, proof, proof graph, fact, surface, "relation visible from". I can
guess what each of these means, but maybe I got it wrong.

Although, I now see "join point" was used in the docs before, so maybe
it's a well-known term?

Thank you, I will try to come up with some better document explaining
the concept. For now I can mainly point you only to
https://keyjoin.org/#sec7.1, which gives a birds eye overview over how
the implementation and it's artifacts work.

Seeing your confusion, I think we should definitely reduce the amount of
terminology used. For instance "proof graph" is shorthand for the slice
of the catalog dependency graph contributed by key-join proofs
(depending object → proof object), i.e. the deptype='k' subgraph of the
dependency graph. I don't think we need to introduce this word as a new
concept. Thank you, this input is very valuable to improve this.

6) Does it always have to be a "foreign key traversal"? Consider an
example like this:

create table dim (id serial primary key);
create table f (id serial primary key, did int);
select * from f left join dim for key (id) <- f(did);

Currently this fails because of no matching foreign key constraint, but
isn't that pretty much the same thing as if the table actually had the
foreign key (at least considering the cardinality of the join - it won't
change it by adding/removing rows).

I realize that'd contradict the "FOR KEY" part of this patch, but it's
also one of the things that might be beneficial for the starjoin
planning (in that we could maybe extend it to more cases). Although,
maybe we could check those constraints during planning, just like we
check the fkey_list.

This is a very different feature avoiding far less bugs than the
original key join. To give just once, consider

SELECT *
FROM customers cu
LEFT JOIN FOR KEY orders (id) <- cu (id)
WHERE customers.id = 122354;

We decided, this (among other) error classes, was important enough, we
pushed for stronger error guarantees, than just the uniqueness of the
referenced side. I still am convinced, this more save way of writing
queries is better for it's additional safety. I do think caring about
the foreign key gives the better syntax.

Do you think such constructions are very common for your customers?

Our proof framework with proof facts could potentially be leveraged to
proof such constructions too. We opted to keep this patch as minimal as
possible, since it's already huge. In the end our conquest is to get to
something, which is eventually commitable. This patch has a lot things
to get wrong. But I think as a second step adding support of other
schemas and handing this information of to the planer, should be a
fairly straight forward thing to do. Although it would add a few lines
on it's own, it's step, that I seems easier than landing a minimal
version of this patch.

7) The patch invents a new "FILTER" clause for joins:

[ FILTER (WHERE join_filter) ]

I understand why it's done - there may be additional join conditions, on
top of the FK equality. But I think it'll be rather confusing. We
already have a "Join Filter", which is used for join clauses that happen
to not be used as "proper" join conditions (e.g. Hash/Merge Cond), and
has to be evaluated "after" the join itself. And now we'd have another
kind of "join filter" ...

Is there a different that'd make this work without the FILTER? For
example, we might encode the "key join" information in the existing ON
(...) clause:

ON (KEY (oi.order_id) -> (o.id) AND ... other clauses ...)

but it seems not as readable. Or maybe just use something else than
"FILTER"? Not sure.

This word has the benefit of being already a keyword and it solves the
problem rather clear cut. Do you think, there is something we could do
to make this better?

8) I don't understand this change in functioncmds:

/* Routine kind cannot change for an existing pg_proc OID. */
Assert(procForm->prokind != PROKIND_AGGREGATE);

The comment says we can't change OID, but the assert checks it's not an
aggregate functions. Isn't that misleading?

Maybe it would be cleaner to mention additionally, that "aggregates were
already rejected on the pre-lock tuple"? I actually think, that comment
is indeed stating a helpful invariant to explain why we are concurrency
save here.

9) DefineView now adjusts the query ID. Why is that needed? Isn't that a
bit weird?

Sorry, does it? Could you point out where exactly? I might be too tired
to see it right now.

10) checkWellFormedRecursionWalker now recurses, but why is that needed?

Didn't in already recurse before? Didn't check the blame, but I do think
it's recursing for a long time.

I just see an added line to check the new filter clause for subqueries
or something to recurse into.

11) It's not clear to me why we need p_creating_stored_object, and it's
not explained anywhere. Maybe it's obvious, but not to me.

Sorry, that's my oversight. We need to take stronger locks, if we store
something materially, to prevent concurrent changes, while we store it.

Where do you recon a better comment would be helpful? A longer comment
directly in the struct definition?

12) I'm very skeptical anyone can meaningfully review parse_key_join.c.
It's 150KB with ~5200 lines (very dense, with pretty minimal comments).
That's a massive file. The chance of me declaring this committable is
about 0%, simply because I wouldn't believe I understand it.

I think it needs to be broken up into smaller pieces, somehow. I don't
know how, but perhaps it's possible to extract a "minimal feature"
handling some limited subset of cases, and then gradually expand it?

I think there is serious complexity involved in just getting the basic
the architecture. That being said, I am very open to any idea of a
stepping stone to this.

It's something I thought a lot about during the last weeks. I am toying
with the thought of ripping out all changes related to anything stored
like views and sql functions as a minimal, minimal version. It's so bare
bone, I'm not terribly exited about doing that, but maybe it's a
necessary evil. Having thought about a lot of edge cases related to that
storing, As I see it, we wouldn't save a lot of lines of code with this,
but it makes reasoning about correctness easier. I can testament to the
complexity in reasoning and correctness it adds. Do you have any other
ideas for a stepping stone, that could reasonably land?

I think this one of the most fundamental questions to be answered,
before we optimize one of the paths. What is the first consumer of the
surface fact framework and how can we stage a minimal commit around it.
We need a committer to feel save to commit something with this
framework. If you have something notably smaller than this, I am all ears.

Furthermore, it seems a lot of that file is duplicate with code we
already have elsewhere. For example, couldn't it use a bunch of list
functions instead of writing a local version?

list_contains_equal_node -> list_member
append_dependencies_unique -> list_concat_unique
append_dependency_unique -> list_append_unique
...

There may be more such cases, I haven't looked for them.

Thanks for bringing it up. I will look into that, once I've properly
gone through the other suggestions. I suspect there won't be many line
to save, but I'll have a look.

13) I think the main question is whether parse-analyze is the right
place to handle this. I don't know. I assume one of the reasons for
doing that is to get an error when defining a view, or when altering an
object - e.g. like when dropping a function used by a view. Which seems
reasonable, people would not like views silently broken by DDL.

But it seems to be this also leads to a lot of code duplication, because
the parse analysis now has to "reimplement" a lot of the stuff already
done in later stages, after parse analyze. For example, we have the
root->fkey_list thing, but that's not available yet.

There's probably more such information - like innerrel_is_unique,
rel_is_distinct_for, relation_has_unique_index_for etc.

Maybe it's not worth it? What if we did this in the planner instead? How
much simpler would it get? My intuition is it'd get much smaller, but
maybe I'm wrong.

It'd probably mean it's not necessary to expand views during parse,
which was one of the problems mentioned at the beginning of this thread.

I suppose we'd need to keep additional information in the plan to know
which joins to check, etc.

If we want to use this for correctness and for proofs, we can't do it
plan time. We have DDL-writes to store proofs. In it's nature this is a
compile time feature.

Even if we don't include all of that in the first patch, this places
very severe limitations like the DDL issue, which would be just tooo
limiting to get to as an end place. I do think it's more reasonable how
to cut this up into more digestible pieces.

Even though the planner is messy, I feel more at home there than here in
the parser. I could see a world were we clean up some of those out of
the planner to make them available earlier some time in parsing. We
could push those variables forward to the planner to avoid redoing the
work. Each of these steps would need a careful performance check and I
am not sure how the real stepping stones would look like.

14) If we wanted to use some of this for the starjoin planning stuff,
we'd need to propagate more information too, I think. But I'm not sure
about this - we already have the information about foreign keys, so if
key joins are tied to foreign keys, there would not be no new useful
information I suppose.

Plus, I don't want to make that patch dependent on people using new
syntax. If that can give us *additional* information, that would be a
different thing.

As I said above, if we figure out what information we want to propagate
this will be helpful. We will be able to propagate this. And if we can
get in a base patch with this architecture, adding this should be
straight forward.

One thing, that sounds fairly simple to derive from our proof, is the
proof graph. This means in every node we denote, this node can be
treated as cardinality preserving joined onto a different node. For
planning we need something more global, because we need to consider
something WHERE/HAVING, but the existence of such a clause shouldn't be
complex to check for.

[...]

16) I was wondering what performance impact this has, roughly. So I
created a simple 4-way join with fact + 3 dimensions:

create table dim1 (id serial primary key, val text);
create table dim2 (id serial primary key, val text);
create table dim3 (id serial primary key, val text);
create table f (id serial primary key,
d1 int not null references dim1(id),
d2 int not null references dim2(id),
d3 int not null references dim3(id));

and then measured throughput with 2 queries

select * from f
join dim1 on (dim1.id = d1)
join dim2 on (dim2.id = d2)
join dim3 on (dim3.id = d3);

select * from f
join dim1 for key (id) <- f (d1)
join dim2 for key (id) <- f (d2)
join dim3 for key (id) <- f (d3);

which is the same query, except for the FOR KEY syntax. And I got this
(under explain, to only do the planning):

patched master
-----------------------------
join 25251 24906
keyjoin 17159

So that's ~30% regression compared to master / regular joins. I also
tried with join_collapse_limit=1 to eliminate the join order planning:

patched master
-----------------------------
join 31476 31252
keyjoin 19353

That's 40% regression. Of course, this is just explain - once there is
data in the table, and actual execution, the differences will be much
smaller. Still, it's not great.

I haven't looked very closely, but based on some quick profiling it
seems ensure_key_join_surface_facts / compute_key_join_relation_facts is
doing expensive stuff like findNotNullConstraintAttnum or
get_index_constraint, both of which do systable scans. That should
probably go through syscache or something like that.

I'm not too surprised about ensure_key_join_surface_facts holding most
of the regression, since it's doing all the work. Since I spent no time
or thought on optimizing runtime thus far, I think there a lot of long
hanging fruits left. I'd prefer to sort out the architecture first, but
I think, we should be able to improve the parsetime of this feature by a
sizeable amount. I fully agree: Before committing this, we should hit
some of the relevant functions for a quick win.

regards

I will probably work my way through this incrementally. I hope to have a
version incorporating your vast feedback next week.

Regards
Arne

#19Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Arne Roland (#18)
Re: Key joins

On 6/12/26 02:20, Arne Roland wrote:

Hi Tomas,

thank you for checking it out!

On 2026-06-11 11:02 PM, Tomas Vondra wrote:

Hi,

I took a quick look at the patch over the past couple days. I don't have
a perfect understanding of how it works, but let me share what I have so
far, before I get distracted by other stuff.

I'm interested in this patch because there seems to be a possible
overlap with the overlap with the starjoin planning (in that maybe we
could try reusing some of the derived information for that).

It's a mix of random thoughts, high/low level, important/superficial in
no particular order.

1) It does not compile, ATExecSetRowSecurity seems to be missing
prev_rls or something like that. I simply commented this out, to get it
to compile.

Sorry, I am confused. There is the self contained block of

@@ -18879,6 +18898,7 @@ ATExecSetRowSecurity(Relation rel, bool rls)
      Relation    pg_class;
      Oid            relid;
      HeapTuple    tuple;
+    bool        prev_rls;
        relid = RelationGetRelid(rel);
  @@ -18890,6 +18910,7 @@ ATExecSetRowSecurity(Relation rel, bool rls)
      if (!HeapTupleIsValid(tuple))
          elog(ERROR, "cache lookup failed for relation %u", relid);
  +    prev_rls = ((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity;
      ((Form_pg_class) GETSTRUCT(tuple))->relrowsecurity = rls;
      CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
  @@ -18898,6 +18919,21 @@ ATExecSetRowSecurity(Relation rel, bool rls)
        table_close(pg_class, RowExclusiveLock);
      heap_freetuple(tuple);
+
+    /*
+     * Revalidate stored key-join proofs that depend on this
relation.  The
+     * key-join base-fact computation refuses to expose facts for a
relation
+     * with row-level security enabled (parse_key_join.c), so flipping
+     * relrowsecurity can leave a stored proof unprovable.  Revalidate
+     * whenever the flag actually changes; for the off-to-on
transition the
+     * revalidation raises an error and aborts this DDL, while the
on-to-off
+     * transition is a no-op for proofs that were already valid.
+     */
+    if (rls != prev_rls)
+    {
+        CommandCounterIncrement();
+        RevalidateDependentKeyJoinObjectsOnRelation(relid);
+    }
  }

Apart from that there shouldn't be any references to that. What
definition was missing?

I guess git-am gets confused in some way, because cfbot has the same
issue with this:

https://github.com/postgresql-cfbot/postgresql/actions/runs/27379594748/job/80912606962

2) fk_referenced_selected in find_key_join_match should be marked as
PG_USED_FOR_ASSERTS_ONLY, to fix compiler warning

Thank you for spotting that.

3) It'd be helpful if the commit message for 0001 explained why this
change is needed. More clearly than now, I mean. The initial message in
this thread points at another thread as the source of this, but that
thread is huge so how are reviewers expected to find the explanation?

Don't explain just what the commit does, but why it's needed. Say, give
an example of how the old code fails.

In short we need that to prevent objects our proof relies on to be
dropped or altered below us. I'll try to work that into the commit message.

Yeah, understood. I guess the question is where else we should adopt
this new idiom. AFAIK this is pretty much a TOCTOU bug - don't we have a
similar issue elsewhere?

4) I think there needs to be a README explaining how the feature works,
i.e. the design and trade offs. Alternatively, it could be explained in
a comment at the beginning of some .c file, but a README seems easier to
find. Without this, reviewers have to piece it from the sgml "user"
docs, and random comments all over the place.

5) It might be helpful if the README defined a couple terms introduced
by the patch, but not really defined anywhere. I mean terms like: join
point, proof, proof graph, fact, surface, "relation visible from". I can
guess what each of these means, but maybe I got it wrong.

Although, I now see "join point" was used in the docs before, so maybe
it's a well-known term?

Thank you, I will try to come up with some better document explaining
the concept. For now I can mainly point you only to https://keyjoin.org/
#sec7.1, which gives a birds eye overview over how the implementation
and it's artifacts work.

Seeing your confusion, I think we should definitely reduce the amount of
terminology used. For instance "proof graph" is shorthand for the slice
of the catalog dependency graph contributed by key-join proofs
(depending object → proof object), i.e. the deptype='k' subgraph of the
dependency graph. I don't think we need to introduce this word as a new
concept. Thank you, this input is very valuable to improve this.

I'm not against introducing new terminology, at least not when there's
no established terminology in the code already. But it would be helpful
to explain what the new terms mean, and how they map to the structs and
catalogs (so that people don't have to deduce them).

6) Does it always have to be a "foreign key traversal"? Consider an
example like this:

     create table dim (id serial primary key);
     create table f (id serial primary key, did int);
     select * from f left join dim for key (id) <- f(did);

Currently this fails because of no matching foreign key constraint, but
isn't that pretty much the same thing as if the table actually had the
foreign key (at least considering the cardinality of the join - it won't
change it by adding/removing rows).

I realize that'd contradict the "FOR KEY" part of this patch, but it's
also one of the things that might be beneficial for the starjoin
planning (in that we could maybe extend it to more cases). Although,
maybe we could check those constraints during planning, just like we
check the fkey_list.

This is a very different feature avoiding far less bugs than the
original key join. To give just once, consider

SELECT *
FROM customers cu
LEFT JOIN FOR KEY orders (id) <- cu (id)
WHERE customers.id = 122354;

I don't follow. What does this query illustrate?

We decided, this (among other) error classes, was important enough, we
pushed for stronger error guarantees, than just the uniqueness of the
referenced side. I still am convinced, this more save way of writing
queries is better for it's additional safety. I do think caring about
the foreign key gives the better syntax.

Do you think such constructions are very common for your customers?

I don't know, really. I was merely thinking about using this stuff for
the starjoin optimization patch. And that already handles joins on
foreign keys just fine, but extending to more queries might help.

Our proof framework with proof facts could potentially be leveraged to
proof such constructions too. We opted to keep this patch as minimal as
possible, since it's already huge. In the end our conquest is to get to
something, which is eventually commitable. This patch has a lot things
to get wrong. But I think as a second step adding support of other
schemas and handing this information of to the planer, should be a
fairly straight forward thing to do. Although it would add a few lines
on it's own, it's step, that I seems easier than landing a minimal
version of this patch.

OK. I realize this probably goes against my suggestion to make the patch
smaller. I certainly don't insist on supporting it.

7) The patch invents a new "FILTER" clause for joins:

    [ FILTER (WHERE join_filter) ]

I understand why it's done - there may be additional join conditions, on
top of the FK equality. But I think it'll be rather confusing. We
already have a "Join Filter", which is used for join clauses that happen
to not be used as "proper" join conditions (e.g. Hash/Merge Cond), and
has to be evaluated "after" the join itself. And now we'd have another
kind of "join filter" ...

Is there a different that'd make this work without the FILTER? For
example, we might encode the "key join" information in the existing ON
(...) clause:

     ON (KEY (oi.order_id) -> (o.id) AND ... other clauses ...)

but it seems not as readable. Or maybe just use something else than
"FILTER"? Not sure.

This word has the benefit of being already a keyword and it solves the
problem rather clear cut. Do you think, there is something we could do
to make this better?

Not at the moment, sorry.

8) I don't understand this change in functioncmds:

     /* Routine kind cannot change for an existing pg_proc OID. */
     Assert(procForm->prokind != PROKIND_AGGREGATE);

The comment says we can't change OID, but the assert checks it's not an
aggregate functions. Isn't that misleading?

Maybe it would be cleaner to mention additionally, that "aggregates were
already rejected on the pre-lock tuple"? I actually think, that comment
is indeed stating a helpful invariant to explain why we are concurrency
save here.

I don't follow. The comment says we can't change OID of an existing
procedure. OK, sure. But the assert checks the procedure is not an
aggregate function. How is that comment an accurate description of the
purpose of the assert?

9) DefineView now adjusts the query ID. Why is that needed? Isn't that a
bit weird?

Sorry, does it? Could you point out where exactly? I might be too tired
to see it right now.

10) checkWellFormedRecursionWalker now recurses, but why is that needed?

Didn't in already recurse before? Didn't check the blame, but I do think
it's recursing for a long time.

I just see an added line to check the new filter clause for subqueries
or something to recurse into.

Oh, I see. My mistake, sorry.

11) It's not clear to me why we need p_creating_stored_object, and it's
not explained anywhere. Maybe it's obvious, but not to me.

Sorry, that's my oversight. We need to take stronger locks, if we store
something materially, to prevent concurrent changes, while we store it.

Where do you recon a better comment would be helpful? A longer comment
directly in the struct definition?

Well, I guess it could be mentioned before the struct definition. And
then maybe also in the README explaining how this all fits together.

12) I'm very skeptical anyone can meaningfully review parse_key_join.c.
It's 150KB with ~5200 lines (very dense, with pretty minimal comments).
That's a massive file. The chance of me declaring this committable is
about 0%, simply because I wouldn't believe I understand it.

I think it needs to be broken up into smaller pieces, somehow. I don't
know how, but perhaps it's possible to extract a "minimal feature"
handling some limited subset of cases, and then gradually expand it?

I think there is serious complexity involved in just getting the basic
the architecture. That being said, I am very open to any idea of a
stepping stone to this.

It's something I thought a lot about during the last weeks. I am toying
with the thought of ripping out all changes related to anything stored
like views and sql functions as a minimal, minimal version. It's so bare
bone, I'm not terribly exited about doing that, but maybe it's a
necessary evil. Having thought about a lot of edge cases related to that
storing, As I see it, we wouldn't save a lot of lines of code with this,
but it makes reasoning about correctness easier. I can testament to the
complexity in reasoning and correctness it adds. Do you have any other
ideas for a stepping stone, that could reasonably land?

I think this one of the most fundamental questions to be answered,
before we optimize one of the paths. What is the first consumer of the
surface fact framework and how can we stage a minimal commit around it.
We need a committer to feel save to commit something with this
framework. If you have something notably smaller than this, I am all ears.

I don't know how to split the patch, but I know that unless that happens
the patch is unlikely to move forward.

FWIW, it does not mean the parts have to be committed separately. It's
possible to review small pieces, and then squash them before commit to
get something "meaningful for users".

Furthermore, it seems a lot of that file is duplicate with code we
already have elsewhere. For example, couldn't it use a bunch of list
functions instead of writing a local version?

   list_contains_equal_node -> list_member
   append_dependencies_unique -> list_concat_unique
   append_dependency_unique -> list_append_unique
   ...

There may be more such cases, I haven't looked for them.

Thanks for bringing it up. I will look into that, once I've properly
gone through the other suggestions. I suspect there won't be many line
to save, but I'll have a look.

OK, good.

13) I think the main question is whether parse-analyze is the right
place to handle this. I don't know. I assume one of the reasons for
doing that is to get an error when defining a view, or when altering an
object - e.g. like when dropping a function used by a view. Which seems
reasonable, people would not like views silently broken by DDL.

But it seems to be this also leads to a lot of code duplication, because
the parse analysis now has to "reimplement" a lot of the stuff already
done in later stages, after parse analyze. For example, we have the
root->fkey_list thing, but that's not available yet.

There's probably more such information - like innerrel_is_unique,
rel_is_distinct_for, relation_has_unique_index_for etc.

Maybe it's not worth it? What if we did this in the planner instead? How
much simpler would it get? My intuition is it'd get much smaller, but
maybe I'm wrong.

It'd probably mean it's not necessary to expand views during parse,
which was one of the problems mentioned at the beginning of this thread.

I suppose we'd need to keep additional information in the plan to know
which joins to check, etc.

If we want to use this for correctness and for proofs, we can't do it
plan time. We have DDL-writes to store proofs. In it's nature this is a
compile time feature.

Is that actually true? If we check the correctness later in the planner,
we'd still get a failure. Why couldn't that verify all the proofs, just
like the parse-analyze? We may need to retain more information, ofc.

Even if we don't include all of that in the first patch, this places
very severe limitations like the DDL issue, which would be just tooo
limiting to get to as an end place. I do think it's more reasonable how
to cut this up into more digestible pieces.

I'm not sure what "DDL issue" is, sorry :-(

I may be missing something, but the main difference in behavior seems to
be for views - the current patch verifies the proofs when creating the
view / altering objects, and rejects those. And AFAICS after moving it
to the planner, that would not be the case anymore.

But for regular ad hoc queries there's not much difference, no? You'd
get a failure, no matter what.

Even though the planner is messy, I feel more at home there than here in
the parser. I could see a world were we clean up some of those out of
the planner to make them available earlier some time in parsing. We
could push those variables forward to the planner to avoid redoing the
work. Each of these steps would need a careful performance check and I
am not sure how the real stepping stones would look like.

I'm skeptical about moving this stuff to an earlier phase (so that the
parse can see it). You can try, but it probably requires moving a lot of
other stuff too, some of which may be expensive.

14) If we wanted to use some of this for the starjoin planning stuff,
we'd need to propagate more information too, I think. But I'm not sure
about this - we already have the information about foreign keys, so if
key joins are tied to foreign keys, there would not be no new useful
information I suppose.

Plus, I don't want to make that patch dependent on people using new
syntax. If that can give us *additional* information, that would be a
different thing.

As I said above, if we figure out what information we want to propagate
this will be helpful. We will be able to propagate this. And if we can
get in a base patch with this architecture, adding this should be
straight forward.

One thing, that sounds fairly simple to derive from our proof, is the
proof graph. This means in every node we denote, this node can be
treated as cardinality preserving joined onto a different node. For
planning we need something more global, because we need to consider
something WHERE/HAVING, but the existence of such a clause shouldn't be
complex to check for.

Understood.

[...]

16) I was wondering what performance impact this has, roughly. So I
created a simple 4-way join with fact + 3 dimensions:

   create table dim1 (id serial primary key, val text);
   create table dim2 (id serial primary key, val text);
   create table dim3 (id serial primary key, val text);
   create table f (id serial primary key,
                   d1 int not null references dim1(id),
                   d2 int not null references dim2(id),
                   d3 int not null references dim3(id));

and then measured throughput with 2 queries

   select * from f
            join dim1 on (dim1.id = d1)
            join dim2 on (dim2.id = d2)
            join dim3 on (dim3.id = d3);

   select * from f
            join dim1 for key (id) <- f (d1)
            join dim2 for key (id) <- f (d2)
            join dim3 for key (id) <- f (d3);

which is the same query, except for the FOR KEY syntax. And I got this
(under explain, to only do the planning):

               patched    master
   -----------------------------
      join       25251     24906
   keyjoin       17159

So that's ~30% regression compared to master / regular joins. I also
tried with join_collapse_limit=1 to eliminate the join order planning:

               patched    master
   -----------------------------
      join       31476     31252
   keyjoin       19353

That's 40% regression. Of course, this is just explain - once there is
data in the table, and actual execution, the differences will be much
smaller. Still, it's not great.

I haven't looked very closely, but based on some quick profiling it
seems ensure_key_join_surface_facts / compute_key_join_relation_facts is
doing expensive stuff like findNotNullConstraintAttnum or
get_index_constraint, both of which do systable scans. That should
probably go through syscache or something like that.

I'm not too surprised about ensure_key_join_surface_facts holding most
of the regression, since it's doing all the work. Since I spent no time
or thought on optimizing runtime thus far, I think there a lot of long
hanging fruits left. I'd prefer to sort out the architecture first, but
I think, we should be able to improve the parsetime of this feature by a
sizeable amount. I fully agree: Before committing this, we should hit
some of the relevant functions for a quick win.

It's just a guess. I'm not sure fixing ensure_key_join_surface_facts
will make it much faster - it's possible, but I haven't tried. There are
probably more issues like this. But I'm also not surprised the patch has
this kind of issues, it's fine for an early WIP patch.

regards

--
Tomas Vondra

#20Arne Roland
arne.roland@malkut.net
In reply to: Tomas Vondra (#19)
Re: Key joins

On 2026-06-12 7:26 PM, Tomas Vondra wrote:

On 6/12/26 02:20, Arne Roland wrote:

Hi Tomas,

thank you for checking it out!
[...]

I guess git-am gets confused in some way, because cfbot has the same
issue with this:

https://github.com/postgresql-cfbot/postgresql/actions/runs/27379594748/job/80912606962

I see. It's a rebase issue. The patch shouldn't apply cleanly on master,
but somehow it does. My next version will be rebased on the current
master again, but I want to work more of the feedback into the patch
before posting a new version here. At least more detailed documentation
and glossary, at best already some more finely grained split up.

[...]

6) Does it always have to be a "foreign key traversal"? Consider an
example like this:

     create table dim (id serial primary key);
     create table f (id serial primary key, did int);
     select * from f left join dim for key (id) <- f(did);

Currently this fails because of no matching foreign key constraint, but
isn't that pretty much the same thing as if the table actually had the
foreign key (at least considering the cardinality of the join - it won't
change it by adding/removing rows).

I realize that'd contradict the "FOR KEY" part of this patch, but it's
also one of the things that might be beneficial for the starjoin
planning (in that we could maybe extend it to more cases). Although,
maybe we could check those constraints during planning, just like we
check the fkey_list.

This is a very different feature avoiding far less bugs than the
original key join. To give just once, consider

SELECT *
FROM customers cu
LEFT JOIN FOR KEY orders (id) <- cu (id)
WHERE customers.id = 122354;

I don't follow. What does this query illustrate?

Most relevantly here it illustrates, that fks instead have additional
relevant guarantees compared to unique/primary key constraints for
making writing and reading queries easier and safer.

Our proof framework with proof facts could potentially be leveraged to
proof such constructions too. We opted to keep this patch as minimal as
possible, since it's already huge. In the end our conquest is to get to
something, which is eventually commitable. This patch has a lot things
to get wrong. But I think as a second step adding support of other
schemas and handing this information of to the planer, should be a
fairly straight forward thing to do. Although it would add a few lines
on it's own, it's step, that I seems easier than landing a minimal
version of this patch.

OK. I realize this probably goes against my suggestion to make the patch
smaller. I certainly don't insist on supporting it.

I do want that though. It has advantages in using key joins in old
codebases with existing SQL without full refactoring. And it might be
ultimately helpful to the optimizer too. I just wonder about the best
order to get a simple case of the architecture in.

[...]

8) I don't understand this change in functioncmds:

     /* Routine kind cannot change for an existing pg_proc OID. */
     Assert(procForm->prokind != PROKIND_AGGREGATE);

The comment says we can't change OID, but the assert checks it's not an
aggregate functions. Isn't that misleading?

Maybe it would be cleaner to mention additionally, that "aggregates were
already rejected on the pre-lock tuple"? I actually think, that comment
is indeed stating a helpful invariant to explain why we are concurrency
save here.

I don't follow. The comment says we can't change OID of an existing
procedure. OK, sure. But the assert checks the procedure is not an
aggregate function. How is that comment an accurate description of the
purpose of the assert?

it explains the invariant, why we can rely on our earlier pre-lock tuple
check. To me that seemed to be a relevant enough assumption about the
code to make it explicit in a comment.

[...]

13) I think the main question is whether parse-analyze is the right
place to handle this. I don't know. I assume one of the reasons for
doing that is to get an error when defining a view, or when altering an
object - e.g. like when dropping a function used by a view. Which seems
reasonable, people would not like views silently broken by DDL.

But it seems to be this also leads to a lot of code duplication, because
the parse analysis now has to "reimplement" a lot of the stuff already
done in later stages, after parse analyze. For example, we have the
root->fkey_list thing, but that's not available yet.

There's probably more such information - like innerrel_is_unique,
rel_is_distinct_for, relation_has_unique_index_for etc.

Maybe it's not worth it? What if we did this in the planner instead? How
much simpler would it get? My intuition is it'd get much smaller, but
maybe I'm wrong.

It'd probably mean it's not necessary to expand views during parse,
which was one of the problems mentioned at the beginning of this thread.

I suppose we'd need to keep additional information in the plan to know
which joins to check, etc.

If we want to use this for correctness and for proofs, we can't do it
plan time. We have DDL-writes to store proofs. In it's nature this is a
compile time feature.

Is that actually true? If we check the correctness later in the planner,
we'd still get a failure. Why couldn't that verify all the proofs, just
like the parse-analyze? We may need to retain more information, ofc.

Even if we don't include all of that in the first patch, this places
very severe limitations like the DDL issue, which would be just tooo
limiting to get to as an end place. I do think it's more reasonable how
to cut this up into more digestible pieces.

I'm not sure what "DDL issue" is, sorry :-(

I may be missing something, but the main difference in behavior seems to
be for views - the current patch verifies the proofs when creating the
view / altering objects, and rejects those. And AFAICS after moving it
to the planner, that would not be the case anymore.

But for regular ad hoc queries there's not much difference, no? You'd
get a failure, no matter what.

With DDL-issue I was eluding to a DDL command that attaches a query to
some object. Views, sql functions and policies all do that. I don't see
us doing that work planning time. Theoretically we could try to use a
new planner hook for that, to call the planner with an extra struct
element telling it about increased locking arrangements without the
intend to ever execute a query and abort after the proof.

However I do think this introduces more future complexity by entangling
separate concerns in the same code path, and I am not sure we are doing
ourselves a favor with this, even if we would save a hand full of lines.
I am not convinced I want to tackle that complexity on top of this
already non-trivial patch.

I intend to try to separate the DDL handling case out into it's own
patch of the patch series. While in lines of code is not that massive,
reasoning about those two individually is indeed simpler. Even just
getting to locking for the DDL-case right, isn't trivial.

I hope to get to that by the end of the now starting week.

Even though the planner is messy, I feel more at home there than here in
the parser. I could see a world were we clean up some of those out of
the planner to make them available earlier some time in parsing. We
could push those variables forward to the planner to avoid redoing the
work. Each of these steps would need a careful performance check and I
am not sure how the real stepping stones would look like.

I'm skeptical about moving this stuff to an earlier phase (so that the
parse can see it). You can try, but it probably requires moving a lot of
other stuff too, some of which may be expensive.

I have a high level general response and one tailored to this patch.
I'll start with this particular patch: For this patch I am mostly
concerned with not doing complex work twice: Once in the parser AND once
in the optimizer. And the below general thoughts are not easily actionable.

More generally:

Your skepticism is well placed. Every change there would warrant
benchmarking. I am just not completely convinced the planner is super
optimal in the way it works. I suspect a lot of code there has not been
optimized, because it's hard to wrap your head around it, that it
doesn't feel like an easy gain anymore.

While 1:1 pulling stuff out of the planner almost surely gives mostly
performance regressions, a clean architecture allowing more simple,
isolated performance improvements sounds beneficial, too. And I don't
want to open this can of worms right now.

I just recalled the time when Andres made the storage pluggable by
introducing a layer of pointers everywhere. The new storage interface
ended up being faster than the old.
This is not storage and I am not Andres. I just wanted to share
precedent, that a clear architecture with optimization disadvantages is
not necessarily always slower.

regards

Regards
Arne

#21Arne Roland
arne.roland@malkut.net
In reply to: Arne Roland (#20)
#22Henson Choi
assam258@gmail.com
In reply to: Arne Roland (#21)
#23Arne Roland
arne.roland@malkut.net
In reply to: Henson Choi (#22)
#24Henson Choi
assam258@gmail.com
In reply to: Arne Roland (#23)
#25Arne Roland
arne.roland@malkut.net
In reply to: Arne Roland (#21)
#26Arne Roland
arne.roland@malkut.net
In reply to: Arne Roland (#25)
#27Joel Jacobson
joel@compiler.org
In reply to: Arne Roland (#26)
#28Arne Roland
arne.roland@malkut.net
In reply to: Joel Jacobson (#27)