Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.doris.nereids.datasets.tpch.TPCHTestBase;
import org.apache.doris.nereids.properties.SelectHint;
import org.apache.doris.nereids.properties.SelectHintLeading;
import org.apache.doris.nereids.trees.plans.JoinType;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalSelectHint;
Expand All @@ -30,124 +31,130 @@

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;

/**
* A hint only tells the optimizer which plan to prefer; it must never change the result of the
* query. Both tests below build a random join graph, evaluate it with an in-memory join evaluator,
* optimize it, and assert the evaluated result is unchanged.
*/
public class DistributeHintTest extends TPCHTestBase {

private int used = 0;
private int unused = 0;

private int syntaxError = 0;

private int successCases = 0;

private int unsuccessCases = 0;

private List<String> failCases = new ArrayList<>();
// Leading order is only unconditionally valid for inner joins, so the permutations are drawn
// from a fixed seed to keep a failure reproducible from the test output.
private static final long LEADING_SEED = 20260729L;

/**
* A distribute hint (broadcast / shuffle) picks a physical strategy, so it can never change the
* result. This is the only place that covers distribute hints against random join graphs.
*/
@Test
public void testLeading() {
public void testHintJoin() {
for (int t = 3; t < 10; t++) {
for (int e = t - 1; e <= (t * (t - 1)) / 2; e++) {
for (int i = 0; i < 10; i++) {
System.out.println("TableNumber: " + String.valueOf(t) + " EdgeNumber: " + e + " Iteration: " + i);
randomTest(t, e, false, true);
HyperGraphBuilderOld builder = new HyperGraphBuilderOld();
Plan plan = builder.buildJoinPlanWithJoinHint(t, e);
plan = new LogicalProject(plan.getOutput(), plan);
assertOptimizeKeepsResult(builder, plan, plan,
String.format("distribute hint changed the result (tables=%d, edges=%d, iter=%d)",
t, e, i));
}
}
}
int totalCases = successCases + unsuccessCases;
System.out.println("TotalCases: " + totalCases + "\tSuccessCases: " + successCases + unsuccessCases + "\tUnSuccessCases: " + 0);
for (String treePlan : failCases) {
System.out.println(treePlan);
}
}

/**
* Reordering inner joins is always semantics-preserving, so any leading order must produce the
* same result as the un-hinted plan.
*/
@Test
public void testHintJoin() {
for (int t = 3; t < 10; t++) {
public void testLeading() {
Random random = new Random(LEADING_SEED);
for (int t = 3; t < 7; t++) {
for (int e = t - 1; e <= (t * (t - 1)) / 2; e++) {
for (int i = 0; i < 10; i++) {
System.out.println("TableNumber: " + String.valueOf(t) + " EdgeNumber: " + e + " Iteration: " + i);
randomTest(t, e, true, false);
for (int i = 0; i < 3; i++) {
HyperGraphBuilderOld builder =
new HyperGraphBuilderOld(ImmutableSet.of(JoinType.INNER_JOIN));
Plan plan = builder.randomBuildPlanWith(t, e);
plan = new LogicalProject(plan.getOutput(), plan);
for (int p = 0; p < Math.min(t, 4); p++) {
List<String> order = leadingOrder(t, random);
Plan leadingPlan = withLeadingHint(order, plan);
assertOptimizeKeepsResult(builder, plan, leadingPlan,
String.format("leading %s changed the result (tables=%d, edges=%d, iter=%d)",
order, t, e, i));
}
}
}
}
int totalCases = successCases + unsuccessCases;
System.out.println("TotalCases: " + totalCases + "\tSuccessCases: " + successCases + "\tUnSuccessCases: " + unsuccessCases);
for (String treePlan : failCases) {
System.out.println(treePlan);
}

/**
* Same property as {@link #testLeading}, but the graph also contains outer / semi / anti joins,
* for which an arbitrary leading order is generally not a valid join order. Today Doris applies
* such a hint anyway and the result changes -- extra null-padded rows appear. Whether the hint
* should be rejected and ignored instead is an open question for the Nereids owners, so this
* test records the case rather than asserting a behaviour nobody has signed off on.
*
* <p>Reproduces at 3 tables. Re-enable once the intended behaviour is decided.
*/
@Disabled("leading hint over outer joins is not result-preserving today; intended behaviour undecided")
@Test
public void testLeadingWithOuterJoin() {
Random random = new Random(LEADING_SEED);
for (int t = 3; t < 6; t++) {
for (int e = t - 1; e <= (t * (t - 1)) / 2; e++) {
HyperGraphBuilderOld builder = new HyperGraphBuilderOld();
Plan plan = builder.randomBuildPlanWith(t, e);
plan = new LogicalProject(plan.getOutput(), plan);
List<String> order = leadingOrder(t, random);
assertOptimizeKeepsResult(builder, plan, withLeadingHint(order, plan),
String.format("leading %s over outer joins changed the result (tables=%d, edges=%d)",
order, t, e));
}
}
}

private Plan generateLeadingHintPlan(int tableNum, Plan childPlan) {
ImmutableList.Builder<SelectHint> hints = ImmutableList.builder();
List<String> leadingParameters = new ArrayList<String>();
private List<String> leadingOrder(int tableNum, Random random) {
List<String> order = new ArrayList<>();
for (int i = 0; i < tableNum; i++) {
leadingParameters.add(String.valueOf(i));
order.add(String.valueOf(i));
}
Collections.shuffle(leadingParameters);
System.out.println("LeadingHint: " + leadingParameters.toString());
hints.add(new SelectHintLeading("Leading", leadingParameters, ImmutableMap.of()));
return new LogicalSelectHint<>(hints.build(), childPlan);
Collections.shuffle(order, random);
return order;
}

private void randomTest(int tableNum, int edgeNum, boolean withJoinHint, boolean withLeading) {
HyperGraphBuilderOld hyperGraphBuilder = new HyperGraphBuilderOld();
Plan plan = withJoinHint ? hyperGraphBuilder.buildJoinPlanWithJoinHint(tableNum, edgeNum) :
hyperGraphBuilder.randomBuildPlanWith(tableNum, edgeNum);
plan = new LogicalProject(plan.getOutput(), plan);
Set<List<String>> res1 = hyperGraphBuilder.evaluate(plan);
if (!withLeading) {
CascadesContext cascadesContext = MemoTestUtils.createCascadesContext(connectContext, plan);
hyperGraphBuilder.initStats("tpch", cascadesContext);
Plan optimizedPlan = PlanChecker.from(cascadesContext)
.analyze()
.optimize()
.getBestPlanTree();

Set<List<String>> res2 = hyperGraphBuilder.evaluate(optimizedPlan);
if (!res1.equals(res2)) {
System.out.println(plan.treeString());
System.out.println(optimizedPlan.treeString());
cascadesContext = MemoTestUtils.createCascadesContext(connectContext, plan);
PlanChecker.from(cascadesContext).dpHypOptimize().getBestPlanTree();
System.out.println(res1);
System.out.println(res2);
unsuccessCases++;
failCases.add(plan.treeString());
failCases.add(optimizedPlan.treeString());
}
successCases++;
} else {
// generate select hint
for (int i = 0; i < (tableNum * tableNum - 1); i++) {
Plan leadingPlan = generateLeadingHintPlan(tableNum, plan);
CascadesContext cascadesContext = MemoTestUtils.createCascadesContext(connectContext, leadingPlan);
hyperGraphBuilder.initStats("tpch", cascadesContext);
Plan optimizedPlan = PlanChecker.from(cascadesContext)
.analyze()
.optimize()
.getBestPlanTree();
private Plan withLeadingHint(List<String> order, Plan childPlan) {
ImmutableList.Builder<SelectHint> hints = ImmutableList.builder();
hints.add(new SelectHintLeading("Leading", order, ImmutableMap.of()));
return new LogicalSelectHint<>(hints.build(), childPlan);
}

Set<List<String>> res2 = hyperGraphBuilder.evaluate(optimizedPlan);
if (!res1.equals(res2)) {
System.out.println(leadingPlan.treeString());
System.out.println(optimizedPlan.treeString());
cascadesContext = MemoTestUtils.createCascadesContext(connectContext, plan);
PlanChecker.from(cascadesContext).dpHypOptimize().getBestPlanTree();
System.out.println(res1);
System.out.println(res2);
unsuccessCases++;
failCases.add(leadingPlan.treeString());
failCases.add(optimizedPlan.treeString());
}
successCases++;
}
}
/**
* Evaluates {@code originalPlan}, optimizes {@code planToOptimize}, and asserts both produce the
* same tuples.
*/
private void assertOptimizeKeepsResult(HyperGraphBuilderOld builder, Plan originalPlan,
Plan planToOptimize, String message) {
Set<List<String>> expected = builder.evaluate(originalPlan);
CascadesContext cascadesContext = MemoTestUtils.createCascadesContext(connectContext, planToOptimize);
builder.initStats("tpch", cascadesContext);
Plan optimizedPlan = PlanChecker.from(cascadesContext)
.analyze()
.optimize()
.getBestPlanTree();
Assertions.assertEquals(expected, builder.evaluate(optimizedPlan),
message + "\noriginal plan:\n" + originalPlan.treeString()
+ "\noptimized plan:\n" + optimizedPlan.treeString());
}
}
Loading
Loading