Skip to content

Commit 52e877c

Browse files
author
Piotr Kołaczkowski
committed
CNDB-15508: Query planner metrics
This commit adds new metrics related to the operation of SAI query planner. The metrics should help checking if the query planner makes proper decisions by correlating them with the other metrics, e.g. the metrics of the actual query execution. Per-query metrics (histograms): - `RowsEstimated`: the estimated number of rows to be returned by the query - `CostEstimated`: the abstract cost of query execution - `LogSelectivityEstimated`: minus decimal logarithm of query selectivity, before applying the query LIMIT (0 means the query selects all rows, 5 means it selects 10^(-5) = 0.00001 subset of rows) - `IndexReferencesInQuery`: the number of index references in the unoptimized query execution plan (the same index may be referenced multiple times and counts separately) - `IndexReferencesInPlan`: the number of index references in the optimized query execution plan (the same index may be referenced multiple times and counts separately) Per-table: - `TotalRowsEstimated`: counts the sum of all row estimates from all completed queries - `TotalCostEstimated`: counts the sum of all cost estimates from all completed queries
1 parent 95724c0 commit 52e877c

File tree

8 files changed

+311
-58
lines changed

8 files changed

+311
-58
lines changed

src/java/org/apache/cassandra/index/sai/QueryContext.java

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020

2121
import java.util.concurrent.TimeUnit;
2222
import java.util.concurrent.atomic.LongAdder;
23+
import javax.annotation.Nonnull;
24+
import javax.annotation.Nullable;
2325
import javax.annotation.concurrent.NotThreadSafe;
2426

2527
import com.google.common.annotations.VisibleForTesting;
2628

2729
import org.apache.cassandra.config.DatabaseDescriptor;
30+
import org.apache.cassandra.index.sai.plan.Plan;
2831
import org.apache.cassandra.index.sai.utils.AbortedOperationException;
2932
import org.apache.cassandra.utils.MonotonicClock;
3033

@@ -66,9 +69,10 @@ public class QueryContext
6669

6770
private final LongAdder shadowedPrimaryKeyCount = new LongAdder();
6871

69-
// Determines the order of using indexes for filtering and sorting.
70-
// Null means the query execution order hasn't been decided yet.
71-
private FilterSortOrder filterSortOrder = null;
72+
private Plan originalPlan = null;
73+
74+
75+
private Plan optimizedPlan = null;
7276

7377
@VisibleForTesting
7478
public QueryContext()
@@ -145,11 +149,6 @@ public void addAnnGraphSearchLatency(long val)
145149
annGraphSearchLatency.add(val);
146150
}
147151

148-
public void setFilterSortOrder(FilterSortOrder filterSortOrder)
149-
{
150-
this.filterSortOrder = filterSortOrder;
151-
}
152-
153152
// getters
154153

155154
public long sstablesHit()
@@ -208,9 +207,18 @@ public long annGraphSearchLatency()
208207
return annGraphSearchLatency.longValue();
209208
}
210209

211-
public FilterSortOrder filterSortOrder()
210+
/** Can return null when query plan hasn't been prepared and optimized yet */
211+
@Nullable
212+
public Plan optimizedPlan()
212213
{
213-
return filterSortOrder;
214+
return optimizedPlan;
215+
}
216+
217+
/** Can return null when query plan hasn't been prepared yet */
218+
@Nullable
219+
public Plan originalPlan()
220+
{
221+
return originalPlan;
214222
}
215223

216224
public void checkpoint()
@@ -246,17 +254,14 @@ public void updateAnnRerankFloor(float observedFloor)
246254
annRerankFloor = max(annRerankFloor, observedFloor);
247255
}
248256

249-
/**
250-
* Determines the order of filtering and sorting operations.
251-
* Currently used only by vector search.
252-
*/
253-
public enum FilterSortOrder
257+
public void setOriginalPlan(Plan originalPlan)
254258
{
255-
/** First get the matching keys from the non-vector indexes, then use vector index to return the top K by similarity order */
256-
SEARCH_THEN_ORDER,
259+
this.originalPlan = originalPlan;
260+
}
257261

258-
/** First get the candidates in ANN order from the vector index, then fetch the rows and filter them until we find K matching the predicates */
259-
SCAN_THEN_FILTER
262+
public void setOptimizedPlan(Plan optimizedPlan)
263+
{
264+
this.optimizedPlan = optimizedPlan;
260265
}
261266

262267
public Snapshot snapshot()
@@ -290,7 +295,9 @@ public static class Snapshot
290295
public final long queryTimeouts;
291296
public final long annGraphSearchLatency;
292297
public final long shadowedPrimaryKeyCount;
293-
public final FilterSortOrder filterSortOrder;
298+
299+
@Nullable
300+
public final QueryPlanInfo queryPlanInfo;
294301

295302
/**
296303
* Creates a snapshot of all the metrics in the given {@link QueryContext}.
@@ -315,7 +322,40 @@ private Snapshot(QueryContext context)
315322
queryTimeouts = context.queryTimeouts();
316323
annGraphSearchLatency = context.annGraphSearchLatency();
317324
shadowedPrimaryKeyCount = context.getShadowedPrimaryKeyCount();
318-
filterSortOrder = context.filterSortOrder();
325+
326+
Plan originalPlan = context.originalPlan();
327+
Plan optimizedPlan = context.optimizedPlan();
328+
if (originalPlan != null && optimizedPlan != null) {
329+
queryPlanInfo = new QueryPlanInfo(originalPlan, optimizedPlan);
330+
} else {
331+
queryPlanInfo = null;
332+
}
319333
}
334+
335+
public static class QueryPlanInfo
336+
{
337+
public final boolean searchExecutedBeforeOrder;
338+
public final boolean filterExecutedAfterOrderedScan;
339+
340+
public final double rowsEstimated;
341+
public final double selectivityEstimated;
342+
public final double costEstimated;
343+
344+
public final int indexReferencesInQuery;
345+
public final int indexReferencesInPlan;
346+
347+
public QueryPlanInfo(@Nonnull Plan originalPlan, @Nonnull Plan optimizedPlan)
348+
{
349+
this.costEstimated = optimizedPlan.fullCost();
350+
this.rowsEstimated = optimizedPlan.expectedRows();
351+
this.selectivityEstimated = optimizedPlan.selectivity();
352+
this.indexReferencesInQuery = originalPlan.referencedIndexCount();
353+
this.indexReferencesInPlan = optimizedPlan.referencedIndexCount();
354+
this.searchExecutedBeforeOrder = optimizedPlan.isSearchThenOrderHybrid();
355+
this.filterExecutedAfterOrderedScan = optimizedPlan.isOrderedScanThenFilterHybrid();
356+
}
357+
}
358+
320359
}
360+
321361
}

src/java/org/apache/cassandra/index/sai/metrics/TableQueryMetrics.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public void record(QueryContext context, ReadCommand command)
115115
{
116116
final long queryLatencyMicros = TimeUnit.NANOSECONDS.toMicros(snapshot.totalQueryTimeNs);
117117

118-
if (snapshot.filterSortOrder == QueryContext.FilterSortOrder.SEARCH_THEN_ORDER)
118+
if (snapshot.queryPlanInfo != null && snapshot.queryPlanInfo.searchExecutedBeforeOrder)
119119
{
120120
Tracing.trace("Index query accessed memtable indexes, {}, and {}, selected {} before ranking, " +
121121
"post-filtered {} in {}, and took {} microseconds.",
@@ -194,6 +194,9 @@ public static class PerTable extends AbstractQueryMetrics
194194
public final Counter totalRowsFiltered;
195195
public final Counter totalQueriesCompleted;
196196

197+
public final Counter totalRowsEstimated;
198+
public final Counter totalCostEstimated;
199+
197200
public final Counter sortThenFilterQueriesCompleted;
198201
public final Counter filterThenSortQueriesCompleted;
199202

@@ -210,6 +213,8 @@ public PerTable(TableMetadata table, QueryKind queryKind, Predicate<ReadCommand>
210213
totalRowsFiltered = Metrics.counter(createMetricName("TotalRowsFiltered"));
211214
totalQueriesCompleted = Metrics.counter(createMetricName("TotalQueriesCompleted"));
212215
totalQueryTimeouts = Metrics.counter(createMetricName("TotalQueryTimeouts"));
216+
totalRowsEstimated = Metrics.counter(createMetricName("TotalRowsEstimated"));
217+
totalCostEstimated = Metrics.counter(createMetricName("TotalCostEstimated"));
213218

214219
sortThenFilterQueriesCompleted = Metrics.counter(createMetricName("SortThenFilterQueriesCompleted"));
215220
filterThenSortQueriesCompleted = Metrics.counter(createMetricName("FilterThenSortQueriesCompleted"));
@@ -228,10 +233,17 @@ public void record(QueryContext.Snapshot snapshot)
228233
totalPartitionReads.inc(snapshot.partitionsRead);
229234
totalRowsFiltered.inc(snapshot.rowsFiltered);
230235

231-
if (snapshot.filterSortOrder == QueryContext.FilterSortOrder.SCAN_THEN_FILTER)
232-
sortThenFilterQueriesCompleted.inc();
233-
else if (snapshot.filterSortOrder == QueryContext.FilterSortOrder.SEARCH_THEN_ORDER)
234-
filterThenSortQueriesCompleted.inc();
236+
QueryContext.Snapshot.QueryPlanInfo queryPlanInfo = snapshot.queryPlanInfo;
237+
if (queryPlanInfo != null)
238+
{
239+
totalCostEstimated.inc(Math.round(queryPlanInfo.costEstimated));
240+
totalRowsEstimated.inc(Math.round(queryPlanInfo.rowsEstimated));
241+
242+
if (queryPlanInfo.filterExecutedAfterOrderedScan)
243+
sortThenFilterQueriesCompleted.inc();
244+
if (queryPlanInfo.searchExecutedBeforeOrder)
245+
filterThenSortQueriesCompleted.inc();
246+
}
235247
}
236248
}
237249

@@ -276,6 +288,28 @@ public static class PerQuery extends AbstractQueryMetrics
276288
*/
277289
public final Timer annGraphSearchLatency;
278290

291+
/** Query execution cost as estimated by the planner */
292+
public final Histogram costEstimated;
293+
294+
/** Number of rows returned by the query estimated by the planner */
295+
public final Histogram rowsEstimated;
296+
297+
/**
298+
* Negative deceimal logarithm of selectivity of the query, before applying the LIMIT clause.
299+
* We use logarithm because selectivity values can be very small (e.g. 10^-9).
300+
*/
301+
public final Histogram logSelectivityEstimated;
302+
303+
/**
304+
* Number of indexes referenced by the optimized query plan.
305+
* The same index referenced from unrelated query clauses,
306+
* leading to separate index searches, are counted separately.
307+
*/
308+
public final Histogram indexReferencesInPlan;
309+
310+
/** Number of indexes referenced by the original query plan before optimization (as stated in the query text) */
311+
public final Histogram indexReferencesInQuery;
312+
279313
/**
280314
* @param table the table to measure metrics for
281315
* @param queryKind an identifier for the kind of query which metrics are being recorded for
@@ -304,6 +338,12 @@ public PerQuery(TableMetadata table, QueryKind queryKind, Predicate<ReadCommand>
304338

305339
// Key vector metrics that translate to performance
306340
annGraphSearchLatency = Metrics.timer(createMetricName("ANNGraphSearchLatency"));
341+
342+
costEstimated = Metrics.histogram(createMetricName("CostEstimated"), false);
343+
rowsEstimated = Metrics.histogram(createMetricName("RowsEstimated"), true);
344+
logSelectivityEstimated = Metrics.histogram(createMetricName("LogSelectivityEstimated"), true);
345+
indexReferencesInPlan = Metrics.histogram(createMetricName("IndexReferencesInPlan"), true);
346+
indexReferencesInQuery = Metrics.histogram(createMetricName("IndexReferencesInQuery"), false);
307347
}
308348

309349
@Override
@@ -340,6 +380,17 @@ public void record(QueryContext.Snapshot snapshot)
340380
}
341381

342382
shadowedKeysScannedHistogram.update(snapshot.shadowedPrimaryKeyCount);
383+
384+
QueryContext.Snapshot.QueryPlanInfo queryPlanInfo = snapshot.queryPlanInfo;
385+
if (queryPlanInfo != null)
386+
{
387+
costEstimated.update(Math.round(queryPlanInfo.costEstimated));
388+
rowsEstimated.update(Math.round(queryPlanInfo.rowsEstimated));
389+
double logSelectivity = -Math.log10(queryPlanInfo.selectivityEstimated);
390+
logSelectivityEstimated.update((int) (Math.min(20, Math.floor(logSelectivity))));
391+
indexReferencesInQuery.update(queryPlanInfo.indexReferencesInQuery);
392+
indexReferencesInPlan.update(queryPlanInfo.indexReferencesInPlan);
393+
}
343394
}
344395
}
345396
}

0 commit comments

Comments
 (0)