Skip to content

Commit 13cc038

Browse files
committed
[ASTERIXDB-3702][RT][STO] LSM Sampling
- user model changes: yes - storage format changes: yes - interface changes: yes Details: Introduced sample cursor to collect random objects from the leaf, in an attempt to improve the sampling performance by ditching the full scan. Ext-ref: MB-68399 Change-Id: If5c5b7eac5199b85fc40b722c87ae0bbb73eecbd Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/20959 Reviewed-by: Murtadha Hubail <mhubail@apache.org> Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu> Reviewed-by: Ritik Raj <ritik.raj@couchbase.com> Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
1 parent 62a6aa1 commit 13cc038

292 files changed

Lines changed: 9494 additions & 2693 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/InvertedIndexAccessMethod.java

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.HashMap;
2525
import java.util.Iterator;
2626
import java.util.LinkedHashMap;
27+
import java.util.LinkedHashSet;
2728
import java.util.List;
2829
import java.util.Map;
2930

@@ -588,23 +589,42 @@ public boolean applyJoinPlanTransformation(List<Mutable<ILogicalOperator>> after
588589
context.computeAndSetTypeEnvironmentForOperator(topSelect);
589590
ILogicalOperator topOp = topSelect;
590591

592+
// Maps each index-subtree variable consumed by the panic UnionAll to the fresh variable
593+
// the UnionAll produces in its place. Used below the equi-join to rewire operators above
594+
// the join (afterJoinRefs) off the now-consumed input variables. Empty when no panic plan.
595+
Map<LogicalVariable, LogicalVariable> indexVarToOutVar = new LinkedHashMap<>();
591596
// Hook up the indexed-nested loop join path with the "panic" (non indexed) nested-loop join path by putting a union all on top.
592597
if (panicJoinRef != null) {
593598
LogicalVariable inputSearchVar = getInputSearchVar(optFuncExpr, indexSubTree);
594599
indexSubTreeLiveVars.addAll(originalSubTreePKs);
595600
indexSubTreeLiveVars.add(inputSearchVar);
596601
List<LogicalVariable> panicPlanLiveVars = new ArrayList<>();
597602
VariableUtilities.getLiveVariables(panicJoinRef.getValue(), panicPlanLiveVars);
598-
// Create variable mapping for union all operator.
603+
// Create variable mapping for the union all operator. Each output variable must be a
604+
// FRESH variable so produced variables are disjoint from used (input) variables — the
605+
// SSA invariant checked by PlanStructureVerifier, which reusing the input var as the
606+
// output violates. indexSubTreeLiveVars can contain duplicates (getLiveVariables above
607+
// already returns the PKs, which are then added again, and inputSearchVar may already
608+
// be live), so dedupe first: exactly one fresh output per distinct input. Otherwise the
609+
// same input maps to two outputs and downstream references to the un-chosen one dangle.
599610
List<Triple<LogicalVariable, LogicalVariable, LogicalVariable>> varMap = new ArrayList<>();
600-
for (int i = 0; i < indexSubTreeLiveVars.size(); i++) {
601-
LogicalVariable indexSubTreeVar = indexSubTreeLiveVars.get(i);
611+
for (LogicalVariable indexSubTreeVar : new LinkedHashSet<>(indexSubTreeLiveVars)) {
602612
LogicalVariable panicPlanVar = panicVarMap.get(indexSubTreeVar);
603613
if (panicPlanVar == null) {
604614
panicPlanVar = indexSubTreeVar;
605615
}
616+
LogicalVariable outVar = context.newVar();
617+
indexVarToOutVar.put(indexSubTreeVar, outVar);
606618
varMap.add(new Triple<LogicalVariable, LogicalVariable, LogicalVariable>(indexSubTreeVar, panicPlanVar,
607-
indexSubTreeVar));
619+
outVar));
620+
}
621+
// Remap originalSubTreePKs to the UnionAll output vars so the downstream equi-join
622+
// condition references the post-UnionAll variables.
623+
for (int i = 0; i < originalSubTreePKs.size(); i++) {
624+
LogicalVariable outVar = indexVarToOutVar.get(originalSubTreePKs.get(i));
625+
if (outVar != null) {
626+
originalSubTreePKs.set(i, outVar);
627+
}
608628
}
609629
UnionAllOperator unionAllOp = new UnionAllOperator(varMap);
610630
unionAllOp.setSourceLocation(topOp.getSourceLocation());
@@ -627,6 +647,18 @@ public boolean applyJoinPlanTransformation(List<Mutable<ILogicalOperator>> after
627647
joinRef.setValue(topEqJoin);
628648
context.computeAndSetTypeEnvironmentForOperator(topEqJoin);
629649

650+
// When a panic UnionAll was inserted, the index-subtree variables it consumed are no longer
651+
// live above the join — the UnionAll now produces fresh variables in their place. Rewire the
652+
// operators above the join (afterJoinRefs) from the old variables to the fresh UnionAll
653+
// outputs so their references don't dangle (undefined-used-variable). Substitute in each
654+
// operator (self only); afterJoinRefs already enumerates the full above-join chain.
655+
if (!indexVarToOutVar.isEmpty()) {
656+
for (Mutable<ILogicalOperator> aboveJoinRef : afterJoinRefs) {
657+
VariableUtilities.substituteVariables(aboveJoinRef.getValue(), indexVarToOutVar, context);
658+
context.computeAndSetTypeEnvironmentForOperator(aboveJoinRef.getValue());
659+
}
660+
}
661+
630662
return true;
631663
}
632664

@@ -839,9 +871,11 @@ private void createIsFilterableSelectOps(ILogicalOperator inputOp, LogicalVariab
839871
isFilterableSelectOp.setExecutionMode(ExecutionMode.LOCAL);
840872
context.computeAndSetTypeEnvironmentForOperator(isFilterableSelectOp);
841873

842-
// Select operator for removing tuples that are filterable.
874+
// Select operator for removing tuples that are filterable. Clone the expression: the same
875+
// isFilterableExpr instance is already owned by isFilterableSelectOp above, and an operator
876+
// graph must not share expression object instances across operators (PlanStructureVerifier).
843877
List<Mutable<ILogicalExpression>> isNotFilterableArgs = new ArrayList<Mutable<ILogicalExpression>>();
844-
isNotFilterableArgs.add(new MutableObject<ILogicalExpression>(isFilterableExpr));
878+
isNotFilterableArgs.add(new MutableObject<ILogicalExpression>(isFilterableExpr.cloneExpression()));
845879
ScalarFunctionCallExpression isNotFilterableExpr = new ScalarFunctionCallExpression(
846880
FunctionUtil.getFunctionInfo(BuiltinFunctions.NOT), isNotFilterableArgs);
847881
isNotFilterableExpr.setSourceLocation(sourceLoc);

asterixdb/asterix-app/src/main/java/org/apache/asterix/app/function/DumpIndexReader.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ private void printField(StringBuilder sb, IAObject field) {
154154
case STRING:
155155
JSONUtil.quoteAndEscape(recordBuilder, ((AString) field).getStringValue());
156156
break;
157+
case NULL:
158+
sb.append("null");
159+
break;
157160
case MISSING:
161+
// Should not reach here - MISSING fields filtered in printObject/buildJsonRecord
158162
break;
159163
default:
160164
sb.append(field);
@@ -165,14 +169,20 @@ private void printObject(StringBuilder sb, ARecord record) {
165169
sb.append("{ ");
166170
int num = record.numberOfFields();
167171
ARecordType type = record.getType();
172+
boolean firstField = true;
168173
for (int i = 0; i < num; i++) {
169-
if (i > 0) {
174+
IAObject value = record.getValueByPos(i);
175+
// Skip MISSING fields to avoid malformed JSON like "field":,
176+
if (value.getType().getTypeTag() == ATypeTag.MISSING) {
177+
continue;
178+
}
179+
if (!firstField) {
170180
sb.append(", ");
171181
}
172-
IAObject value = record.getValueByPos(i);
173182
JSONUtil.quoteAndEscape(sb, type.getFieldNames()[i]);
174183
sb.append(": ");
175184
printField(sb, value);
185+
firstField = false;
176186
}
177187
sb.append(" }");
178188
}

asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5235,10 +5235,34 @@ protected void doAnalyzeDataset(MetadataProvider metadataProvider, AnalyzeStatem
52355235
InternalDatasetDetails dsDetails = (InternalDatasetDetails) ds.getDatasetDetails();
52365236
int sampleCardinalityTarget = stmtAnalyze.getSampleSize();
52375237
long sampleSeed = stmtAnalyze.getOrCreateSampleSeed();
5238+
Index.SampleIndexDetails.SampleMethod sampleMethod = stmtAnalyze.getSampleMethod();
5239+
5240+
// Random sampling requires every primary-index disk component to carry the sampling metadata (theta
5241+
// sketch + max leaf tuple count). Components predating the feature acquire it only once merged; a freshly
5242+
// flushed component always carries it, so probing the existing disk components is sufficient. If any
5243+
// component is missing the metadata, an unbiased random sample is impossible, so fall back to full scan.
5244+
if (sampleMethod != Index.SampleIndexDetails.SampleMethod.FULL_SCAN) {
5245+
JobSpecification probeSpec = DatasetUtil.buildSamplingMetadataProbeJobSpec(ds, metadataProvider);
5246+
MetadataManager.INSTANCE.commitTransaction(mdTxnCtx);
5247+
bActiveTxn = false;
5248+
List<IOperatorStats> probeStats = runJob(hcc, probeSpec, jobFlags,
5249+
Collections.singletonList(DatasetUtil.SAMPLING_METADATA_PROBE_OPERATOR_NAME));
5250+
// No stats => could not determine; be conservative and force a full scan.
5251+
long componentsMissingMetadata =
5252+
probeStats == null || probeStats.isEmpty() ? -1 : probeStats.get(0).getTupleCounter().get();
5253+
if (componentsMissingMetadata != 0) {
5254+
LOGGER.warn("Dataset '{}' has {} primary-index disk component(s) missing random-sampling "
5255+
+ "metadata; forcing full scan.", datasetName, componentsMissingMetadata);
5256+
sampleMethod = Index.SampleIndexDetails.SampleMethod.FULL_SCAN;
5257+
}
5258+
mdTxnCtx = MetadataManager.INSTANCE.beginTransaction();
5259+
bActiveTxn = true;
5260+
metadataProvider.setMetadataTxnContext(mdTxnCtx);
5261+
}
52385262

52395263
Index.SampleIndexDetails newIndexDetailsPendingAdd = new Index.SampleIndexDetails(dsDetails.getPrimaryKey(),
52405264
dsDetails.getKeySourceIndicator(), dsDetails.getPrimaryKeyType(), sampleCardinalityTarget, 0, 0,
5241-
sampleSeed, Collections.emptyMap());
5265+
sampleSeed, sampleMethod, Collections.emptyMap());
52425266
newIndexPendingAdd = new Index(databaseName, dataverseName, datasetName, newIndexName, sampleIndexType,
52435267
newIndexDetailsPendingAdd, false, false, MetadataUtil.PENDING_ADD_OP, Creator.DEFAULT_CREATOR);
52445268

@@ -5278,9 +5302,12 @@ protected void doAnalyzeDataset(MetadataProvider metadataProvider, AnalyzeStatem
52785302
}
52795303
DatasetStreamStats stats = new DatasetStreamStats(opStats.get(0));
52805304

5305+
LOGGER.info("ANALYZED statement stats: coldReads: {} -- pinnedPages: {} -- cloudPageReads:{}",
5306+
stats.getColdReads(), stats.getPinnedPages(), stats.getCloudPageReads());
5307+
52815308
Index.SampleIndexDetails newIndexDetailsFinal = new Index.SampleIndexDetails(dsDetails.getPrimaryKey(),
52825309
dsDetails.getKeySourceIndicator(), dsDetails.getPrimaryKeyType(), sampleCardinalityTarget,
5283-
stats.getCardinality(), stats.getAvgTupleSize(), sampleSeed, stats.getIndexesStats());
5310+
stats.getCardinality(), stats.getAvgTupleSize(), sampleSeed, sampleMethod, stats.getIndexesStats());
52845311
Index newIndexFinal = new Index(databaseName, dataverseName, datasetName, newIndexName, sampleIndexType,
52855312
newIndexDetailsFinal, false, false, MetadataUtil.PENDING_NO_OP, Creator.DEFAULT_CREATOR);
52865313

asterixdb/asterix-app/src/test/java/org/apache/asterix/test/cloud_storage/CloudStorageUnstableTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static org.apache.asterix.test.cloud_storage.CloudStorageTest.MOCK_SERVER_HOSTNAME_FRAGMENT;
2424

2525
import java.util.ArrayList;
26+
import java.util.Arrays;
2627
import java.util.Collection;
2728
import java.util.List;
2829
import java.util.Random;
@@ -64,6 +65,9 @@ public class CloudStorageUnstableTest {
6465
private static final String CONFIG_FILE_NAME = "src/test/resources/cc-cloud-storage.conf";
6566
private static final String DELTA_RESULT_PATH = "results_cloud";
6667
private static final String EXCLUDED_TESTS = "MP";
68+
// Tests excluded from unstable cloud runs because simulated I/O failures (UnstableCloudClient)
69+
// cause non-deterministic LSM disk component layouts, making exact sample counts unpredictable.
70+
private static final String[] UNSTABLE_DENY_LIST = { "ddl: analyze-dataset-1" };
6771

6872
public CloudStorageUnstableTest(TestCaseContext tcCtx) {
6973
this.tcCtx = tcCtx;
@@ -108,7 +112,8 @@ public static Collection<Object[]> tests() throws Exception {
108112
@Test
109113
public void test() throws Exception {
110114
List<TestCase.CompilationUnit> cu = tcCtx.getTestCase().getCompilationUnit();
111-
Assume.assumeTrue(cu.size() > 1 || !EXCLUDED_TESTS.equals(getText(cu.get(0).getDescription())));
115+
Assume.assumeTrue(cu.size() > 1 || (!EXCLUDED_TESTS.equals(getText(cu.get(0).getDescription()))
116+
&& !Arrays.stream(UNSTABLE_DENY_LIST).anyMatch(s -> tcCtx.toString().contains(s))));
112117
LangExecutionUtil.test(tcCtx);
113118
}
114119

asterixdb/asterix-app/src/test/java/org/apache/asterix/test/cloud_storage/GCSCloudStorageUnstableTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static org.apache.asterix.api.common.LocalCloudUtil.MOCK_SERVER_REGION;
2424
import static org.apache.asterix.test.cloud_storage.CloudStorageGCSTest.S3_ONLY;
2525

26+
import java.util.Arrays;
2627
import java.util.Collection;
2728
import java.util.List;
2829

@@ -74,6 +75,9 @@ public class GCSCloudStorageUnstableTest {
7475
private static final String CONFIG_FILE_NAME = "src/test/resources/cc-cloud-storage-gcs.conf";
7576
private static final String DELTA_RESULT_PATH = "results_cloud";
7677
private static final String EXCLUDED_TESTS = "MP";
78+
// Tests excluded from unstable cloud runs because simulated I/O failures (UnstableCloudClient)
79+
// cause non-deterministic LSM disk component layouts, making exact sample counts unpredictable.
80+
private static final String[] UNSTABLE_DENY_LIST = { "ddl: analyze-dataset-1" };
7781
public static final String MOCK_SERVER_HOSTNAME = "http://127.0.0.1:24443";
7882
private static final String MOCK_SERVER_PROJECT_ID = "asterixdb-gcs-test-project-id";
7983

@@ -111,7 +115,8 @@ public static Collection<Object[]> tests() throws Exception {
111115
public void test() throws Exception {
112116
List<TestCase.CompilationUnit> cu = tcCtx.getTestCase().getCompilationUnit();
113117
Assume.assumeTrue(cu.size() > 1 || (!EXCLUDED_TESTS.equals(getText(cu.get(0).getDescription()))
114-
&& !S3_ONLY.equals(getText(cu.get(0).getDescription()))));
118+
&& !S3_ONLY.equals(getText(cu.get(0).getDescription()))
119+
&& !Arrays.stream(UNSTABLE_DENY_LIST).anyMatch(s -> tcCtx.toString().contains(s))));
115120
LangExecutionUtil.test(tcCtx);
116121
for (NodeControllerService nc : ExecutionTestUtil.integrationUtil.ncs) {
117122
IDatasetLifecycleManager lifecycleManager =

asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/AnalyzingTestExecutor.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@
2020

2121
import static java.nio.charset.StandardCharsets.UTF_8;
2222

23+
import java.io.BufferedReader;
24+
import java.io.File;
2325
import java.io.InputStream;
26+
import java.util.List;
2427
import java.util.regex.Matcher;
2528
import java.util.regex.Pattern;
29+
import java.util.stream.Collectors;
2630

2731
import org.apache.asterix.testframework.context.TestCaseContext;
2832
import org.apache.asterix.testframework.xml.TestCase;
2933
import org.apache.commons.io.IOUtils;
34+
import org.apache.hyracks.util.annotations.AiProvenance;
3035

3136
import com.fasterxml.jackson.databind.JsonNode;
3237
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -76,6 +81,19 @@ public ExtractedResult executeSqlppUpdateOrDdl(String statement, TestCaseContext
7681
return res;
7782
}
7883

84+
@Override
85+
@AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_4_8, tool = AiProvenance.Tool.CLAUDE_CODE_UI, contributionKind = AiProvenance.ContributionKind.ASSISTED, notes = "Compare plans without cost: ANALYZE uses random LSM sampling, so CBO cardinality/cost estimates are not reproducible run-to-run")
86+
public void runScriptAndCompareWithResultPlan(File scriptFile, BufferedReader readerExpected,
87+
BufferedReader readerActual) throws Exception {
88+
// This suite runs ANALYZE (random leaf sampling) before planning, so the CBO
89+
// cardinality/op-cost/total-cost estimates vary run-to-run (the LSM component layout
90+
// differs, so the seeded sample draws different tuples). Compare plan structure only,
91+
// stripping the cost annotations, which are not reproducible under random sampling.
92+
List<String> expectedLines = readerExpected.lines().collect(Collectors.toList());
93+
List<String> actualLines = readerActual.lines().collect(Collectors.toList());
94+
TestHelper.comparePlansWithoutCost(expectedLines, actualLines, scriptFile);
95+
}
96+
7997
private void analyzeFromRegex(Matcher m, String dv, int pos) throws Exception {
8098
while (m.find()) {
8199
String ds = m.group(pos);

asterixdb/asterix-app/src/test/java/org/apache/asterix/test/runtime/SqlppAnalyzedExecutionTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ public class SqlppAnalyzedExecutionTest {
4646
private final String[] denyList = { "synonym: synonym-01", "ddl: analyze-dataset-1", "misc: dump_index",
4747
"array-index: composite-index-queries", "filters: upsert", "column: analyze-dataset",
4848
"column: filter/boolean", "column: filter/sql-compat", "ddl: analyze-dataset-with-indexes",
49-
"warnings: cardinality-hint-warning", "comparison: incomparable_types" };
49+
"warnings: cardinality-hint-warning", "comparison: incomparable_types",
50+
// MB-72758: under random-sample-based stats the index-intersection cost decision is not
51+
// reproducible, so this plan's structure (INTERSECT vs single-index) varies run-to-run.
52+
// Excluded from the analyzed suite; still covered as a result test in the regular suite.
53+
"index-selection: secondary-index-intersection-01" };
5054

5155
@BeforeClass
5256
public static void setUp() throws Exception {

asterixdb/asterix-app/src/test/resources/cc-analyze.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ credential.file=src/test/resources/security/passwd
5454
log.dir = logs/
5555
log.level = INFO
5656
compiler.groupmemory=64MB
57+
# ANALYZE statement needs more memory for sorting
58+
compiler.sortmemory=128MB
5759
storage.buffercache.pagesize=32KB
5860
compiler.internal.sanitycheck=true
5961
compiler.ordered.fields=false

0 commit comments

Comments
 (0)