How a Database Optimizer Proves It Can Rewrite Your LEFT JOIN into an Inner Join
Why does the optimizer dare to "silently change" the LEFT JOIN you wrote? — Deconstructing the design philosophy of the Kingbase KES optimizer
I stared at one line in the EXPLAIN output for a long time:
Hash Join (cost=...)
I wrote LEFT JOIN, but what appeared in the execution plan was Hash Join — without Left. The outer join had been eliminated.
I've encountered this more than once. At first, I thought it was a KES-specific behavior, but later I discovered that Oracle, MySQL, and PostgreSQL all do it. It's a common optimization action across all SQL-standard-compliant databases, called Outer Join Elimination.
But "doing it" and "daring to do it" are two different things. The confidence to do it comes from relational algebra — the optimizer can mathematically prove that the result sets of the two writing styles are completely identical, which gives it the right to rewrite your SQL. This article doesn't discuss application-level pitfalls (I wrote a separate piece on that); here, the lens is focused specifically on the optimizer's internals: how this elimination action is determined and executed, and what noteworthy design choices Kingbase KES has made in this area.
@[toc]
1. Starting from Relational Algebra: The Theoretical Foundation of Outer Join Elimination
To understand the optimizer's behavior, you must first accept a premise — SQL is a declarative language, and the optimizer has the right to freely rewrite it under the guarantee of equivalence. And the legitimacy of "equivalent rewriting" comes from relational algebra. Let's first look at this matter in the purest algebraic language.
1.1 Algebraic Expressions of Outer and Inner Joins
Expressed in symbolic language, for two relations R and S under a join condition θ:
- Inner Join: R ⋈θ S — keeps only rows that satisfy θ;
- Left Outer Join: R ⟕θ S — keeps all rows from R, filling unmatched S-side columns with NULL.
Then, a left outer join with a WHERE filter:
σ_p ( R ⟕θ S )
means "perform the outer join first, then apply the p filter to the result."
The Outer Join Elimination Theorem can be described in one sentence: When p is a "null-rejecting predicate" and p only acts on S-side columns, the above expression is equivalent to an inner join. Written symbolically:
σ_p ( R ⟕θ S ) ≡ σ_p ( R ⋈θ S )
This "≡" sign has a rigorous mathematical proof; it is not a heuristic rule. It is a theorem-level conclusion of relational algebra. Once the optimizer recognizes this pattern, it can rewrite the left side to the right side without any hesitation. And the execution cost of the right side is usually much lower than the left side — because an inner join does not need to "pad NULLs for unmatched R rows," and thus does not need to execute the heavier pipeline of a "null-augmented tuple stream."
1.2 Key Concept: What is a "Null-Rejecting Predicate"?
The "null-rejecting predicate" mentioned above is the core of understanding outer join elimination. The definition is:
A predicate p is called a null-rejecting predicate if it returns False or Unknown (i.e., does not return True) for any input row containing a NULL value.
In other words, any filter condition that "NULL cannot pass through" is a null-rejecting predicate. Common null-rejecting predicates include:
- Strict comparisons:
col = value/col > value/col LIKE 'pattern' - Strict function result comparisons:
UPPER(col) = 'X' - IN / BETWEEN:
col IN (...)/col BETWEEN a AND b - Strict arithmetic:
col + 1 = 10
Conversely, predicates that "NULL can pass through" are not null-rejecting, for example:
col IS NULLCOALESCE(col, default_value) = default_valuecol IS NOT DISTINCT FROM valueCASE WHEN col IS NULL THEN 'X' ELSE col END = 'X'
Determining whether a predicate is null-rejecting is a statically provable process. KES's optimizer internally has a "predicate strictness inference" mechanism that traverses the expression tree, deriving each sub-expression's attitude toward NULL from the leaf nodes upward, ultimately determining whether the overall predicate is null-rejecting.
1.3 Two Prerequisites of the Theorem
It is worth emphasizing that the equivalence of outer join elimination has two prerequisites that cannot be ignored:
- Condition A: p must only reference columns from the Nullable-Side (i.e., the side being outer-joined). If p references columns from both sides, or even only from the left side, the legality of elimination changes.
- Condition B: p must be a null-rejecting predicate. Once p allows NULL to pass through, the equivalence relationship immediately collapses.
Industrial-grade optimizers (including Kingbase KES) strictly adhere to these two prerequisites. If either one is not met, the elimination action will never trigger. This is also why "IS NULL scenarios are not eliminated" — because they violate Condition B.
2. The Optimizer's Processing Flow: From SQL to Elimination Decision
The theory has been covered, so let's now look at how the actual optimizer code handles this. In Kingbase KES, from the moment a SQL statement enters until it finally decides to eliminate an outer join, it roughly goes through five stages. Here, I'll use simplified pseudocode combined with a decision flowchart to walk you through it.
2.1 Stage 1: SQL Parsing and Initial Query Tree Construction
What the optimizer actually holds in its hands is not the raw SQL string you wrote. It receives the query tree produced by the parser. For example, if you write this:
SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t2.status = 'A';
Then the structure produced by the parser looks roughly like this:
Query
├── jointree: LeftJoin(t1, t2, ON = "t1.id = t2.id")
├── qual : "t2.status = 'A'"
└── target : *
At this stage, the optimizer itself hasn't done anything. It just holds a structural tree, which merely presents the original form of how you wrote the SQL.
2.2 Stage 2: Query Normalization
Before the optimizer performs equivalence transformations, it has one thing to do first: perform a round of normalization on the query tree. What does this round of operations include? For example, constant folding, expression simplification, flattening CASE expressions, and conversions like IS TRUE / IS FALSE.
Why is this step necessary? Simply because everyone has different SQL writing habits. The purpose of this step is to standardize various messy writing styles into a few fixed forms. This makes pattern matching much easier later on.
Let's look at an example. Normalization will take this:
WHERE (t2.status = 'A') = TRUE
and directly simplify it to this:
WHERE t2.status = 'A'
And also this:
WHERE t2.amount IN (SELECT amount FROM ...) -- but the subquery is single-valued and deterministic
will be converted to an EXISTS / semi-join form.
After this series of operations, the result is a query tree that looks simpler, with shorter expressions. All subsequent rewrite rules, including outer join elimination, run based on this cleaned-up tree.
2.3 Stage 3: Pattern Matching for Outer Join Elimination
Next comes the most critical step. The optimizer traverses this query tree and performs the following judgment on each outer join node:
# Optimizer Pseudocode: Outer Join Elimination Decision
def try_eliminate_outer_join(join_node, top_qual):
if join_node.type not in ('LeftJoin', 'RightJoin', 'FullJoin'):
return join_node
nullable_side = get_nullable_side(join_node)
for pred in split_and_predicates(top_qual):
# Condition A: Does the predicate only reference Nullable-Side columns?
if not refers_only(pred, nullable_side):
continue
# Condition B: Is the predicate null-rejecting?
if is_null_rejecting(pred):
# Both conditions met, rewrite!
join_node.type = 'InnerJoin'
return join_node
return join_node
There are two situations here worth noting:
- The optimizer does not treat the AND conditions in the WHERE clause as a single block. It judges them individually, one by one. That is, as long as any one of the null-rejecting predicates matches, it qualifies to eliminate the outer join.
- Another situation is FULL OUTER JOIN. If both its left and right sides have null-rejecting predicates, the optimizer won't do it in one step. It will first downgrade it to LEFT or RIGHT, and then proceed to eliminate it into an INNER JOIN. This is essentially a cascading elimination process.
2.4 Stage 4: Predicate Pushdown and Execution Plan Generation
Immediately after the outer join elimination action is completed, the next step is predicate pushdown.
What is the situation after elimination? The filter conditions that originally resided in the WHERE clause can now be freely pushed down, directly to the scan nodes:
-- Before elimination
LeftJoin(t1, t2) ON t1.id = t2.id
└── Filter: t2.status = 'A'
-- After elimination + pushdown
InnerJoin(t1, t2) ON t1.id = t2.id
├── SeqScan(t1)
└── SeqScan(t2) Filter: t2.status = 'A' -- Filter pushed down to scan layer
After this step, the performance improvement actually comes in two parts: first, the join algorithm no longer needs to run in the complex manner of an outer join; it falls back to the simpler inner join. Second, the filter condition is moved to the scan layer, so data that doesn't meet the condition can be directly eliminated during scanning. As a result, the amount of data participating in the join is significantly reduced.
Typically, when dealing with large tables, the combination of these two actions can often reduce query time significantly, sometimes by more than tenfold.
2.5 Stage 5: Physical Operator Selection
Moving further down, we reach the physical layer. At this point, the optimizer calculates costs, i.e., looks at the Cost Model. Based on the calculated cost, it selects the specific JOIN algorithm — choosing Hash Join, Nested Loop, or Sort-Merge Join.
Since the outer join has already been eliminated earlier, there's no need to worry about the NULL-padded data stream anymore. These algorithms just run according to the most ordinary, standard logic.
At this point, the entire process of a SQL statement from entry to final execution plan determination is complete. Outer join elimination is actually just a small link in this pipeline. However, its role is quite significant. It represents the practical implementation of relational algebra theory in an enterprise-level database.
3. Design Trade-offs of the Kingbase KES Optimizer
Having covered the theory and process, let's look at the specific design trade-offs Kingbase KES has made in this area. This is the core part of this article. I've summarized four design principles that I felt most deeply during source code reading and practical testing.
3.1 Strictness First: Never "Cover" for Incorrect SQL
Some databases, when designing, pursue so-called "user-friendliness" — for example, automatically "guessing the closest legal syntax" to execute illegal syntax. Kingbase KES's choice is to strictly adhere to SQL standard semantics: outer join elimination only triggers in scenarios that are equivalent in the relational algebra sense, never making speculative inferences beyond the standard.
For example:
SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.id
WHERE t2.status = 'A' OR t1.id > 100;
In this SQL, the WHERE clause is connected by OR, and the right side t1.id > 100 is not a null-rejecting predicate. KES's optimizer will strictly determine: the outer join cannot be eliminated. Because there is a possibility that t1.id > 100 returns True for some null-augmented rows, and these rows need to be preserved.
Some other database optimizers might use "heuristic reasoning" in such scenarios, potentially leading to erroneous elimination. KES's choice is "better conservative than overstepping" — this is a very important quality in domestic replacement scenarios, as it ensures that migrated SQL does not experience semantic drift due to an overly aggressive optimizer.
3.2 Judgment Completeness: Cascading Elimination of Multi-Layer JOINs
Some optimizers only handle simple scenarios of "a single top-level outer join." When encountering nested outer joins or multi-layer outer joins exposed after View Inline, they are powerless. KES's optimizer is quite solid in this regard.
Consider a slightly more complex example:
SELECT * FROM t1
LEFT JOIN t2 ON t1.id = t2.id
LEFT JOIN t3 ON t2.id = t3.id
WHERE t3.status = 'A';
KES optimizer's judgment process is:
- Step 1: Identify that there is a null-rejecting predicate
t3.status = 'A'ont2 LEFT JOIN t3, eliminate it, changing it tot2 INNER JOIN t3; - Step 2: Because t2 and t3 are now an inner join,
t2.idbecomes a not-null join key (it must match non-null ids in t3); - Step 3: This means that in the join result of the upper layer
t1 LEFT JOIN t2, if there are null-augmented t2 rows, theirt2.idwill be NULL and cannot match the result of the lower layert2 INNER JOIN t3; - Step 4: Further deduce that
t1 LEFT JOIN t2can also be eliminated.
One judgment eliminates two layers of outer joins. This cascading capability is very common in actual business, especially for multi-layer JOINs exposed through view nesting. KES's optimizer can recognize and reasonably simplify them, which directly determines the performance after domestic migration.
3.3 View Transparency: Secondary Elimination After View Inline
In modern business code, views are an almost inevitable abstraction layer. A typical scenario is:
CREATE VIEW v_user_order AS
SELECT u.user_id, u.user_name, o.order_id, o.order_amount
FROM t_user u LEFT JOIN t_order o ON u.user_id = o.user_id;
-- Business-side call
SELECT * FROM v_user_order WHERE order_amount > 500;
KES optimizer's processing path here is: first perform View Inline, "expanding" the view definition into the outer query, and then run another round of outer join elimination judgment. The expanded equivalent SQL becomes:
SELECT u.user_id, u.user_name, o.order_id, o.order_amount
FROM t_user u LEFT JOIN t_order o ON u.user_id = o.user_id
WHERE o.order_amount > 500;
This returns to the standard outer join elimination scenario — the outer join is eliminated, and the Filter is pushed down. KES's optimizer tightly connects the View Inline and rewrite stages, ensuring that the execution plan obtained by users encapsulating logic into views is completely identical to that of directly hand-written SQL. This is very valuable from a "performance predictability" perspective — the business side will not encounter inexplicable performance differences because of "using views vs. not using views."
3.4 Compatibility with Oracle (+) Syntax: Product Refinement in Details
Legacy business code migrated from Oracle extensively uses Oracle's unique (+) outer join syntax:
SELECT * FROM t1, t2 WHERE t1.id = t2.id(+) AND t2.status(+) = 'A';
KES fully supports this syntax and has achieved semantic equivalence alignment in the judgment of outer join elimination:
- If a condition carries
(+), its semantics are equivalent to being placed in theONclause and will not trigger outer join elimination; - If a condition does not carry
(+), its semantics are equivalent to being placed in theWHEREclause, following the standard elimination judgment.
This detail is very important in domestic replacement scenarios. In a project I participated in that migrated from Oracle to KES, about 60% of the legacy SQL used the (+) syntax. The KES optimizer's ability to faithfully translate this syntax directly determines whether this code can run with "zero modification."
4. A Complete Example You Can Run Locally
So much theory has been discussed earlier. Let's finally use a complete example that can be run locally to illustrate. This example is one I often use when training my team on the KES optimizer. Running through this example basically allows you to see all the key points mentioned earlier.
4.1 Set Up the Environment First
-- Create test tables
CREATE TABLE t_user (
user_id INT PRIMARY KEY,
user_name VARCHAR(50)
);
CREATE TABLE t_order (
order_id INT PRIMARY KEY,
user_id INT,
order_amount NUMERIC(10, 2),
order_status VARCHAR(20)
);
-- Generate data: 100,000 users, only 30,000 have orders
INSERT INTO t_user
SELECT g, 'user_' || g FROM generate_series(1, 100000) g;
INSERT INTO t_order
SELECT g,
(g % 100000) + 1,
(random() * 1000)::NUMERIC(10, 2),
(ARRAY['FINISHED','PENDING','CANCELED'])[floor(random()*3)+1]
FROM generate_series(1, 300000) g;
CREATE INDEX idx_order_user ON t_order(user_id);
CREATE INDEX idx_order_status ON t_order(order_status);
ANALYZE;
4.2 Scenario A: The Join Will Be Eliminated
EXPLAIN ANALYZE
SELECT u.user_id, u.user_name, o.order_id, o.order_amount
FROM t_user u LEFT JOIN t_order o ON u.user_id = o.user_id
WHERE o.order_status = 'FINISHED';
What to look for: Look at the topmost node of the execution plan. It should display Hash Join here. Note that the word Left is not in parentheses. Then, the scan of t_order will have a Filter: (order_status = 'FINISHED'). The number of rows in the result set is much smaller than 100,000. Why is this? Because orders with FINISHED status are only a portion. This indicates that the outer join has indeed been eliminated.
4.3 Scenario B: The Join Is Preserved (IS NULL)
EXPLAIN ANALYZE
SELECT u.user_id, u.user_name
FROM t_user u LEFT JOIN t_order o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
What to look for: This time, look at the topmost node. It becomes Hash Left Join. It carries Left now. What's going on? Because KES's optimizer found that IS NULL is not a null-rejecting predicate. So it preserves the semantics of the outer join. Look at the result set produced; it's the users without orders. The row count is around 70,000. This indicates that the outer join was not eliminated and was preserved.
4.4 Scenario C: Condition Written in ON (If Business Logic Intends This)
EXPLAIN ANALYZE
SELECT u.user_id, u.user_name, o.order_id, o.order_amount
FROM t_user u
LEFT JOIN t_order o
ON u.user_id = o.user_id
AND o.order_status = 'FINISHED';
What to look for: The top layer of the execution plan is still Hash Left Join. All 100,000 users are present at this time. For users with FINISHED orders, the order information will be displayed. What about users without? The order fields will be NULL. Generally speaking, if the business requirement is "show each user their completed orders," the SQL should be written this way.
4.5 Scenario D: Cascading Elimination of Two Layers
-- Add an additional table
CREATE TABLE t_review (
review_id INT PRIMARY KEY,
order_id INT,
review VARCHAR(500)
);
INSERT INTO t_review
SELECT g, g, 'review_' || g FROM generate_series(1, 100000) g;
EXPLAIN ANALYZE
SELECT *
FROM t_user u
LEFT JOIN t_order o ON u.user_id = o.user_id
LEFT JOIN t_review r ON o.order_id = r.order_id
WHERE r.review LIKE 'review_1%';
What to look for: Carefully examine the execution plan. Both layers of outer joins are eliminated. In the final execution plan, both JOIN nodes are Hash Join. In this case, they are eliminated together.
This test environment can be run on any KES instance. Combine it with the output from EXPLAIN ANALYZE. How the optimizer makes its judgment is very clearly visible. During my internal training, I usually have everyone type it out themselves. Once you've seen this execution plan with your own eyes, when writing SQL in the future, you'll often subconsciously avoid falling into the pitfalls of outer join elimination.
5. A Deeper Look at the Optimizer: Is It Trustworthy?
So many technical details have been discussed earlier. Now, let's stop staring at the code and talk a bit about the design philosophy behind the Kingbase KES optimizer.
Everyone is currently undertaking domestic replacements. In database development, optimizer design often faces just one question: Do you want to run fast, or do you want semantic safety?
Some optimizers are more aggressive. Their benchmark scores look great, and their benchmark data is beautiful. However, they might rewrite your SQL into a mess. The final result might be completely different from what you expected. This kind is harder to control. The other kind is more stable. It runs exactly as you wrote it, with strong predictability. The cost is that, in certain specific situations, it won't reach the absolute fastest speed.
Kingbase KES's stance on this matter is actually very clear. It prioritizes ensuring strict semantics and predictable results first. Then it considers performance. Looking back at the details of outer join elimination discussed earlier, you can see:
- Its judgment of null-rejecting predicates is very rigid; it absolutely does not guess blindly;
- It handles multi-layer nesting, View Inline, and Oracle compatibility syntax scenarios. As long as the semantics are equivalent, it covers them all;
- But if even one prerequisite is not met, it will not eliminate, very resolutely.
This approach, which seems rigid but is actually very thoughtfully designed, has a very practical benefit during domestic replacements: it reduces migration risk. Think about it: if performance suddenly improves after migration, but business data occasionally shows strange problems, who can tolerate that? Engineering teams actually prefer a situation where performance after migration is consistent with before, and business operations continue as usual. Over the past six months, running several KES projects, I've noticed one thing: the greatest benefit of this database's optimizer is not how fast it can run, but that you find it trustworthy.
6. Summary
Outer join elimination seems simple at first glance. But truly implementing it deeply in code is actually very troublesome. From the theorems of relational algebra, to how the optimizer judges it internally, to how Kingbase KES specifically writes this logic — every step requires attention to detail.
So, returning to the question raised at the beginning of this article: How exactly is outer join elimination recognized? How is it judged? And how is it ultimately executed? Let me summarize:
- Where does the theory come from: It's essentially a theorem in relational algebra. The prerequisites are having a "null-rejecting predicate" and a "Nullable-Side reference." Only when these two conditions are met can the equivalence transformation be performed.
- How is it specifically judged: After the optimizer receives the SQL, it first parses, then performs query normalization, then matches patterns, then does predicate pushdown, and finally selects physical operators. It goes through these five stages to turn the theorem just mentioned into the execution plan you see.
- What choices did Kingbase KES make: In this area, it emphasizes strictness, completeness, transparency, and compatibility. That means the judgment of null-rejecting predicates must be strict. Multi-layer nesting and view scenarios must all be covered. Results inside and outside views must be consistent. And Oracle's
(+)syntax must be compatible.
All these fragmented details added together actually piece together the most fundamental part of a database. When we make technical selections, just remember one thing: as long as an optimizer achieves "strict, complete, and predictable," its compatibility with your existing legacy code will definitely be high. The effort you spend on migration and modification will also be smaller. This is also one reason why I will spend a lot of time researching Kingbase KES going forward.
The optimizer is simply too vast a topic. One article can never cover it all. In the future, I will continue writing, breaking down principles like predicate pushdown, subquery unnesting, Join Reorder, and the cost model. I hope to provide some practical help to colleagues currently working on domestic replacements, or to friends interested in database kernels.