From daee729b4929a02e4f1c1c706050b04eb08acb92 Mon Sep 17 00:00:00 2001 From: Kirill Anisimov Date: Tue, 24 Mar 2026 21:34:07 +0700 Subject: [PATCH 1/6] IGNITE-28341: Fix incorrect WHERE condition pushdown for LEFT OUTER JOIN --- .../query/h2/sql/GridSqlQuerySplitter.java | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java index 0bc44b5935b75..bae6ce0ceddd7 100644 --- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java +++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java @@ -718,7 +718,7 @@ private void doPushDownQueryModelRange(SplitterQueryModel model, int begin, int pushDownSelectColumns(tblAliases, cols, wrapAlias, select); // Move all the related WHERE conditions to wrap query. - pushDownWhereConditions(tblAliases, cols, wrapAlias, select); + pushDownWhereConditions(tblAliases, cols, wrapAlias, select, model, begin, end); // Push down to a subquery all the JOIN elements and process ON conditions. pushDownJoins(tblAliases, cols, model, begin, end, wrapAlias); @@ -1048,6 +1048,47 @@ private static String uniquePushDownColumnAlias(String uniqueTblAlias, String co return uniqueTblAlias + "__" + colName; } + /** + * Return table aliases whose WHERE conditions are safe to push down. + * For LEFT OUTER JOIN, only aliases from the left branch are safe. + */ + private static Set tblAliasesToPushdownConditions( + SplitterQueryModel model, + int begin, + int end, + Set tblAliases + ) { + int leftBranchEnd = -1; + + for (int i = 1; i <= end; i++) { + if (model.findJoin(i).isLeftOuter()) { + leftBranchEnd = i - 1; + break; + } + } + + if (leftBranchEnd == -1) + return tblAliases; + + Set aliases = U.newIdentityHashSet(); + + int safeBegin = begin; + int safeEnd = Math.min(end, leftBranchEnd); + + if (safeBegin > safeEnd) + return aliases; + + for (int i = safeBegin; i <= safeEnd; i++) { + GridSqlAlias uniqueTblAlias = model.childModel(i).uniqueAlias(); + + assert uniqueTblAlias != null : model.ast().getSQL(); + + aliases.add(uniqueTblAlias); + } + + return aliases; + } + /** * @param tblAliases Table aliases for push down. * @param cols Columns with generated aliases. @@ -1058,11 +1099,17 @@ private void pushDownWhereConditions( Set tblAliases, Map cols, GridSqlAlias wrapAlias, - GridSqlSelect select + GridSqlSelect select, + SplitterQueryModel model, + int begin, + int end ) { if (select.where() == null) return; + Set tblAliasesToPushdownConditions = + tblAliasesToPushdownConditions(model, begin, end, tblAliases); + GridSqlSelect wrapSelect = GridSqlAlias.unwrap(wrapAlias).subquery(); List andConditions = new ArrayList<>(); @@ -1073,7 +1120,7 @@ private void pushDownWhereConditions( SplitterAndCondition c = andConditions.get(i); GridSqlAst condition = c.ast(); - if (isAllRelatedToTables(tblAliases, U.newIdentityHashSet(), condition)) { + if (isAllRelatedToTables(tblAliasesToPushdownConditions, U.newIdentityHashSet(), condition)) { if (!SplitterUtils.isTrue(condition)) { // Replace the original condition with `true` and move it to the wrap query. c.parent().child(c.childIndex(), TRUE); From 22c30efca6fa540312cbb101775c194e0297be5b Mon Sep 17 00:00:00 2001 From: Kirill Anisimov Date: Tue, 24 Mar 2026 21:34:41 +0700 Subject: [PATCH 2/6] IGNITE-28341: Add regression test for LEFT JOIN condition pushdown --- .../query/IgniteSqlSplitterSelfTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java index 3c1262d4ea616..af0a48bcef873 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java @@ -2047,6 +2047,59 @@ public void testAvgVariousDataTypes() throws Exception { } } + /** + * Verifies LEFT JOIN behavior when the left side is a subquery with DISTINCT and + * the result is filtered by a condition on the right table. + * The test checks that no rows are returned when there is no matching department name. + * + * @throws Exception If failed. + */ + @Test + public void testLeftJoinWithSubquery() throws Exception { + IgniteCache cache = grid(0).cache(DEFAULT_CACHE_NAME); + + String personTbl = "PERSON_LEFT_JOIN_SUBQUERY"; + String depTbl = "DEPARTMENT_LEFT_JOIN_SUBQUERY"; + + try { + cache.query(new SqlFieldsQuery( + "CREATE TABLE " + personTbl + " (" + + "id INT PRIMARY KEY, " + + "depId INT, " + + "name VARCHAR" + + ')' + )).getAll(); + + cache.query(new SqlFieldsQuery( + "CREATE TABLE " + depTbl + " (" + + "id INT PRIMARY KEY, " + + "name VARCHAR" + + ')' + )).getAll(); + + cache.query(new SqlFieldsQuery( + "INSERT INTO " + personTbl + "(id, depId, name) VALUES (1, 1, 'Emma')" + )).getAll(); + + cache.query(new SqlFieldsQuery( + "INSERT INTO " + depTbl + "(id, name) VALUES (2, 'TX')" + )).getAll(); + + List> res = cache.query(new SqlFieldsQuery( + "SELECT p.id AS person_id, p.name AS person_name, o.id AS department_id " + + "FROM (SELECT DISTINCT * FROM " + personTbl + ") p " + + "LEFT JOIN " + depTbl + " o ON p.depId = o.id " + + "WHERE o.name = 'SQL'" + )).getAll(); + + assertTrue(res.isEmpty()); + } + finally { + cache.query(new SqlFieldsQuery("DROP TABLE IF EXISTS " + personTbl)).getAll(); + cache.query(new SqlFieldsQuery("DROP TABLE IF EXISTS " + depTbl)).getAll(); + } + } + /** * Check avg() with various data types. * From e6a3d02b3411a61c64aff10e797e9b62c0da6025 Mon Sep 17 00:00:00 2001 From: Kirill Anisimov Date: Wed, 25 Mar 2026 22:27:20 +0700 Subject: [PATCH 3/6] IGNITE-28341: Create cache explicitly in IgniteSqlSplitterSelfTest --- .../query/h2/sql/GridSqlQuerySplitter.java | 5 ++--- .../processors/query/IgniteSqlSplitterSelfTest.java | 13 ++++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java index bae6ce0ceddd7..77e18ab2aa6a1 100644 --- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java +++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQuerySplitter.java @@ -1072,13 +1072,12 @@ private static Set tblAliasesToPushdownConditions( Set aliases = U.newIdentityHashSet(); - int safeBegin = begin; int safeEnd = Math.min(end, leftBranchEnd); - if (safeBegin > safeEnd) + if (begin > safeEnd) return aliases; - for (int i = safeBegin; i <= safeEnd; i++) { + for (int i = begin; i <= safeEnd; i++) { GridSqlAlias uniqueTblAlias = model.childModel(i).uniqueAlias(); assert uniqueTblAlias != null : model.ast().getSQL(); diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java index af0a48bcef873..429d17f3fce8d 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/IgniteSqlSplitterSelfTest.java @@ -2056,7 +2056,9 @@ public void testAvgVariousDataTypes() throws Exception { */ @Test public void testLeftJoinWithSubquery() throws Exception { - IgniteCache cache = grid(0).cache(DEFAULT_CACHE_NAME); + IgniteCache cache = ignite(0).getOrCreateCache( + cacheConfig("psLeftJoinSubquery", true, Integer.class, Person.class) + ); String personTbl = "PERSON_LEFT_JOIN_SUBQUERY"; String depTbl = "DEPARTMENT_LEFT_JOIN_SUBQUERY"; @@ -2095,8 +2097,13 @@ public void testLeftJoinWithSubquery() throws Exception { assertTrue(res.isEmpty()); } finally { - cache.query(new SqlFieldsQuery("DROP TABLE IF EXISTS " + personTbl)).getAll(); - cache.query(new SqlFieldsQuery("DROP TABLE IF EXISTS " + depTbl)).getAll(); + try { + cache.query(new SqlFieldsQuery("DROP TABLE IF EXISTS " + personTbl)).getAll(); + cache.query(new SqlFieldsQuery("DROP TABLE IF EXISTS " + depTbl)).getAll(); + } + finally { + cache.destroy(); + } } } From a86d75189ec8d409ba6602c50117d33e3d506d8b Mon Sep 17 00:00:00 2001 From: Kirill Anisimov Date: Mon, 30 Mar 2026 22:40:26 +0700 Subject: [PATCH 4/6] IGNITE-28341: add JMH benchmarks for LEFT JOIN DISTINCT regression --- .../jmh/sql/JmhSqlJoinBenchmark.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java index 611e6ff294703..f05d4383846b4 100644 --- a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java +++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java @@ -19,7 +19,9 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; @@ -34,6 +36,9 @@ public class JmhSqlJoinBenchmark extends JmhSqlAbstractBenchmark { /** Count of entries in EMP table. */ protected static final int EMP_CNT = 10_000; + /** Counter for forcing query replanning in cold benchmark. */ + private final AtomicInteger planRnd = new AtomicInteger(); + /** * Initiate new tables. */ @@ -69,6 +74,66 @@ public void colocatedDistributedJoin() { throw new AssertionError("Unexpected result size: " + res.size()); } + /** + * LEFT JOIN with DISTINCT subquery - regression test for query splitter optimization. + */ + @Benchmark + public void leftJoinDistinctRegression(Blackhole bh) { + List> res = executeSql( + "SELECT e.empid, e.name, d.deptid " + + "FROM (SELECT DISTINCT * FROM emp) e " + + "LEFT JOIN dept d ON e.deptid = d.deptid " + + "WHERE d.name = ?", + "Department 5" + ); + + if (res == null) + throw new AssertionError("Query returned null"); + + bh.consume(res); + } + + /** + * LEFT JOIN without DISTINCT subquery - baseline for comparison. + */ + @Benchmark + public void leftJoinNoDistinctBaseline(Blackhole bh) { + List> res = executeSql( + "SELECT e.empid, e.name, d.deptid " + + "FROM emp e " + + "LEFT JOIN dept d ON e.deptid = d.deptid " + + "WHERE d.name = ?", + "Department 5" + ); + + if (res == null) + throw new AssertionError("Query returned null"); + + bh.consume(res); + } + + /** + * LEFT JOIN with DISTINCT where query text is changed every run to bypass plan cache. + */ + @Benchmark + public void leftJoinDistinctRegressionCold(Blackhole bh) { + int rnd = planRnd.incrementAndGet(); + + List> res = executeSql( + "/*rnd=" + rnd + "*/ " + + "SELECT e.empid, e.name, d.deptid " + + "FROM (SELECT DISTINCT * FROM emp) e " + + "LEFT JOIN dept d ON e.deptid = d.deptid " + + "WHERE d.name = ?", + "Department 5" + ); + + if (res == null) + throw new AssertionError("Query returned null"); + + bh.consume(res); + } + /** * Run benchmarks. * From 9fb1f13b7d0065b863112723ee6fcf19aa5934eb Mon Sep 17 00:00:00 2001 From: Kirill Anisimov Date: Tue, 31 Mar 2026 18:05:38 +0700 Subject: [PATCH 5/6] IGNITE-28341: Add JMH benchmarks for LEFT JOIN with DISTINCT regression testing --- .../jmh/sql/JmhSqlJoinBenchmark.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java index f05d4383846b4..0533399295635 100644 --- a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java +++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java @@ -76,15 +76,17 @@ public void colocatedDistributedJoin() { /** * LEFT JOIN with DISTINCT subquery - regression test for query splitter optimization. + * DISTINCT is applied to the smaller table (dept), then LEFT JOINed with the larger table (emp), + * and filtered by a condition on the right table. */ @Benchmark public void leftJoinDistinctRegression(Blackhole bh) { List> res = executeSql( - "SELECT e.empid, e.name, d.deptid " + - "FROM (SELECT DISTINCT * FROM emp) e " + - "LEFT JOIN dept d ON e.deptid = d.deptid " + - "WHERE d.name = ?", - "Department 5" + "SELECT d.deptid, d.name, e.empid " + + "FROM (SELECT DISTINCT * FROM dept) d " + + "LEFT JOIN emp e ON d.deptid = e.deptid " + + "WHERE e.name = ?", + "Employee 5" ); if (res == null) @@ -114,6 +116,7 @@ public void leftJoinNoDistinctBaseline(Blackhole bh) { /** * LEFT JOIN with DISTINCT where query text is changed every run to bypass plan cache. + * This tests the cold path where query optimization and planning happens every time. */ @Benchmark public void leftJoinDistinctRegressionCold(Blackhole bh) { @@ -121,11 +124,11 @@ public void leftJoinDistinctRegressionCold(Blackhole bh) { List> res = executeSql( "/*rnd=" + rnd + "*/ " + - "SELECT e.empid, e.name, d.deptid " + - "FROM (SELECT DISTINCT * FROM emp) e " + - "LEFT JOIN dept d ON e.deptid = d.deptid " + - "WHERE d.name = ?", - "Department 5" + "SELECT d.deptid, d.name, e.empid " + + "FROM (SELECT DISTINCT * FROM dept) d " + + "LEFT JOIN emp e ON d.deptid = e.deptid " + + "WHERE e.name = ?", + "Employee 5" ); if (res == null) From a3dfb9d323871368bd67cea834edb8e18a86f8c2 Mon Sep 17 00:00:00 2001 From: Kirill Anisimov Date: Tue, 31 Mar 2026 19:14:43 +0700 Subject: [PATCH 6/6] IGNITE-28341: Remove assertions from JMH benchmarks, use Blackhole.consume() instead --- .../benchmarks/jmh/sql/JmhSqlJoinBenchmark.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java index 0533399295635..086608409fca3 100644 --- a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java +++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlJoinBenchmark.java @@ -64,14 +64,13 @@ public class JmhSqlJoinBenchmark extends JmhSqlAbstractBenchmark { * Colocated distributed join. */ @Benchmark - public void colocatedDistributedJoin() { + public void colocatedDistributedJoin(Blackhole bh) { int key = ThreadLocalRandom.current().nextInt(EMP_CNT / BATCH_SIZE); List> res = executeSql("SELECT emp.name, dept.name FROM emp JOIN dept ON emp.deptid = dept.deptid " + "WHERE emp.salary = ?", key); - if (res.size() != BATCH_SIZE) - throw new AssertionError("Unexpected result size: " + res.size()); + bh.consume(res); } /** @@ -89,8 +88,6 @@ public void leftJoinDistinctRegression(Blackhole bh) { "Employee 5" ); - if (res == null) - throw new AssertionError("Query returned null"); bh.consume(res); } @@ -108,8 +105,6 @@ public void leftJoinNoDistinctBaseline(Blackhole bh) { "Department 5" ); - if (res == null) - throw new AssertionError("Query returned null"); bh.consume(res); } @@ -131,8 +126,6 @@ public void leftJoinDistinctRegressionCold(Blackhole bh) { "Employee 5" ); - if (res == null) - throw new AssertionError("Query returned null"); bh.consume(res); }