Milestone 23: uncorrelated subqueries, folded to a constant - #34
Merged
Conversation
SELECT id FROM orders WHERE total > (SELECT AVG(total) FROM orders); The parser has refused `(SELECT …)` for twenty-two milestones. It no longer does, for the shape where the answer is simple and the simplicity is not a shortcut. An uncorrelated subquery names nothing outside itself, so it depends on no row of the query around it and has one value for the whole statement however many rows that statement scans. It is run once and substituted BEFORE binding, which is what makes everything downstream work unchanged: the planner, the index matcher and the cost model were all written against `column <op> literal`, and after folding that is exactly what this is. `total = (SELECT MAX(total) FROM orders)` uses the index on total, which it could not if the subquery survived into the plan. Four refusals, each a decision rather than a gap: - more than one row, because no answer is defensible and taking the first makes the query depend on physical order - more than one column, with SELECT * counting as more even when the table has one today, so an ALTER TABLE cannot turn a working query into a wrong one - zero rows is NULL rather than an error, because `x = NULL` is UNKNOWN for every row and that is the answer - a correlated subquery is refused BY NAME. It would otherwise have been refused by accident, as "no column named city" for a column that plainly exists, and that is an hour of somebody's life Folding happens in both _execute_select and _execute_explain. EXPLAIN does not go through the first, and must show the plan the query really gets, which means EXPLAIN runs the subquery: you cannot plan around a constant you have not computed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The parser has refused
(SELECT …)for twenty-two milestones. It no longer does,for the shape where the answer is simple and the simplicity is not a shortcut.
An uncorrelated subquery is a constant
It names nothing outside itself, so it depends on no row of the query around it
and has one value for the whole statement however many rows that statement
scans. It is run once and substituted before binding:
That is not an optimisation of a more general mechanism; for this shape it is the
entire semantics. Doing it before binding is what makes everything downstream
work unchanged: the planner, the index matcher and the cost model were all
written against
column <op> literal, and after folding that is exactly whatthis is.
total = (SELECT MAX(total) FROM orders)uses the index ontotal,which it could not if the subquery survived into the plan — checked by planning
it, not by asserting it.
Four refusals, each a decision
first would make the query depend on physical order — the kind of wrong that
looks right until a
VACUUMmoves a page.SELECT *counts as more even whenthe table has one column today, so an
ALTER TABLEcannot turn a workingquery into a wrong one.
x = NULLis UNKNOWN for every row, whichis the answer. An error here would mean a reasonable query failing because a
table happened to be empty.
refused by accident:
o.citydoes not resolve inside a subquery overorders i, so the binder would have said "no column named 'city'" about acolumn that plainly exists. Correlation is detected first, by comparing the
subquery's qualifiers against the outer aliases, and
MAX(orders.total)insidea query over
ordersis correctly not correlated.Folding happens in both
_execute_selectand_execute_explain.EXPLAINdoesnot go through the first and must show the plan the query really gets, which
means
EXPLAINruns the subquery: you cannot plan around a constant you have notcomputed.
Verification
CI-seed queries, using
COUNT(*)andMIN/MAXover an INTEGER column —the forms where both engines agree on the result's type as well as its value.
make cigreen. 1,714 Python tests, 160 frontend.🤖 Generated with Claude Code