Skip to content

Commit af73a4f

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): - `RowsToReturnEstimated`: the estimated number of rows to be returned by the query - `RowsToFetchEstimated`: the estimated number of rows the query is going to fetch from storage - `KeysToIterateEstimated`: the estimated number of primary keys to read from the indexes when executing the query to completion - `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: - `TotalRowsToReturnEstimated`: the sum of all estimates of returned rows from all completed queries - `TotalRowsToFetchEstimated`: the sum of all estimates of fetched rows from all completed queries - `TotalKeysToIterateEstimated`: the sum of all estimates of iterated primary keys from all completed queries - `TotalCostEstimated`: counts the sum of all cost estimates from all completed queries # Conflicts: # src/java/org/apache/cassandra/index/sai/QueryContext.java
1 parent 1200c45 commit af73a4f

File tree

9 files changed

+497
-64
lines changed

9 files changed

+497
-64
lines changed

src/java/org/apache/cassandra/config/CassandraRelevantProperties.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,13 @@ public enum CassandraRelevantProperties
638638
*/
639639
SAI_QUERY_KIND_PER_QUERY_METRICS_ENABLED("cassandra.sai.metrics.query_kind.per_query.enabled", "false"),
640640

641+
/**
642+
* Whether to enable SAI query plan metrics such as the estimated cost, estimated number of rows,
643+
* number of indexes used in the original and optimized query plan, etc.
644+
* These metrics are counters and histograms.
645+
*/
646+
SAI_QUERY_PLAN_METRICS_ENABLED("cassandra.sai.metrics.query_plan.enabled", "true"),
647+
641648
/**
642649
* Whether to enable SAI index metrics such as memtable flush metrics, compaction metrics, and disk usage metrics.
643650
* These metrics include timers, histograms, counters, and gauges for index operations.

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

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
package org.apache.cassandra.index.sai;
2020

2121
import java.util.concurrent.TimeUnit;
22+
import javax.annotation.Nonnull;
23+
import javax.annotation.Nullable;
2224
import javax.annotation.concurrent.NotThreadSafe;
2325

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

28+
import org.apache.cassandra.config.CassandraRelevantProperties;
2629
import org.apache.cassandra.config.DatabaseDescriptor;
30+
import org.apache.cassandra.index.sai.plan.Plan;
2731
import org.apache.cassandra.index.sai.utils.AbortedOperationException;
2832
import org.apache.cassandra.utils.MonotonicClock;
2933

@@ -88,9 +92,7 @@ public class QueryContext
8892

8993
private float annRerankFloor = 0.0f; // only called from single-threaded setup code
9094

91-
// Determines the order of using indexes for filtering and sorting.
92-
// Null means the query execution order hasn't been decided yet.
93-
private FilterSortOrder filterSortOrder = null;
95+
private PlanInfo queryPlanInfo;
9496

9597
@VisibleForTesting
9698
public QueryContext()
@@ -219,12 +221,6 @@ public void addAnnGraphSearchLatency(long val)
219221
annGraphSearchLatency += val;
220222
}
221223

222-
public void setFilterSortOrder(FilterSortOrder filterSortOrder)
223-
{
224-
checkThreadOwnership();
225-
this.filterSortOrder = filterSortOrder;
226-
}
227-
228224
public void checkpoint()
229225
{
230226
checkThreadOwnership();
@@ -250,17 +246,10 @@ public void updateAnnRerankFloor(float observedFloor)
250246
annRerankFloor = max(annRerankFloor, observedFloor);
251247
}
252248

253-
/**
254-
* Determines the order of filtering and sorting operations.
255-
* Currently used only by vector search.
256-
*/
257-
public enum FilterSortOrder
249+
public void recordQueryPlan(Plan.RowsIteration originalPlan, Plan.RowsIteration optimizedPlan)
258250
{
259-
/** First get the matching keys from the non-vector indexes, then use vector index to return the top K by similarity order */
260-
SEARCH_THEN_ORDER,
261-
262-
/** 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 */
263-
SCAN_THEN_FILTER
251+
if (CassandraRelevantProperties.SAI_QUERY_PLAN_METRICS_ENABLED.getBoolean())
252+
this.queryPlanInfo = new PlanInfo(originalPlan, optimizedPlan);
264253
}
265254

266255
public Snapshot snapshot()
@@ -311,7 +300,9 @@ public static class Snapshot
311300
public final long triePostingsDecodes;
312301
public final long queryTimeouts;
313302
public final long annGraphSearchLatency;
314-
public final FilterSortOrder filterSortOrder;
303+
304+
@Nullable
305+
public final PlanInfo queryPlanInfo;
315306

316307
/**
317308
* Creates a snapshot of all the metrics in the given {@link QueryContext}.
@@ -339,7 +330,38 @@ private Snapshot(QueryContext context)
339330
triePostingsDecodes = context.triePostingsDecodes;
340331
queryTimeouts = context.queryTimeouts;
341332
annGraphSearchLatency = context.annGraphSearchLatency;
342-
filterSortOrder = context.filterSortOrder;
333+
queryPlanInfo = context.queryPlanInfo;
334+
}
335+
}
336+
337+
/**
338+
* Captures relevant information about a query plan, both original and optimized.
339+
*/
340+
public static class PlanInfo
341+
{
342+
public final boolean searchExecutedBeforeOrder;
343+
public final boolean filterExecutedAfterOrderedScan;
344+
345+
public final long costEstimated;
346+
public final long rowsToReturnEstimated;
347+
public final long rowsToFetchEstimated;
348+
public final long keysToIterateEstimated;
349+
public final int logSelectivityEstimated;
350+
351+
public final int indexReferencesInQuery;
352+
public final int indexReferencesInPlan;
353+
354+
public PlanInfo(@Nonnull Plan.RowsIteration originalPlan, @Nonnull Plan.RowsIteration optimizedPlan)
355+
{
356+
this.costEstimated = Math.round(optimizedPlan.fullCost());
357+
this.rowsToReturnEstimated = Math.round(optimizedPlan.expectedRows());
358+
this.rowsToFetchEstimated = Math.round(optimizedPlan.estimatedRowsToFetch());
359+
this.keysToIterateEstimated = Math.round(optimizedPlan.estimatedKeysToIterate());
360+
this.logSelectivityEstimated = Math.min(20, (int) Math.floor(-Math.log10(optimizedPlan.selectivity())));
361+
this.indexReferencesInQuery = originalPlan.referencedIndexCount();
362+
this.indexReferencesInPlan = optimizedPlan.referencedIndexCount();
363+
this.searchExecutedBeforeOrder = optimizedPlan.isSearchThenOrderHybrid();
364+
this.filterExecutedAfterOrderedScan = optimizedPlan.isOrderedScanThenFilterHybrid();
343365
}
344366
}
345367
}

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

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.function.Predicate;
2323
import java.util.regex.Pattern;
2424

25+
import javax.annotation.Nullable;
26+
2527
import com.codahale.metrics.Counter;
2628
import com.codahale.metrics.Histogram;
2729
import com.codahale.metrics.Timer;
@@ -115,7 +117,7 @@ public void record(QueryContext context, ReadCommand command)
115117
{
116118
final long queryLatencyMicros = TimeUnit.NANOSECONDS.toMicros(snapshot.totalQueryTimeNs);
117119

118-
if (snapshot.filterSortOrder == QueryContext.FilterSortOrder.SEARCH_THEN_ORDER)
120+
if (snapshot.queryPlanInfo != null && snapshot.queryPlanInfo.searchExecutedBeforeOrder)
119121
{
120122
Tracing.trace("Index query accessed memtable indexes, {}, and {}, selected {} before ranking, " +
121123
"post-filtered {} in {}, and took {} microseconds.",
@@ -199,8 +201,8 @@ public static class PerTable extends AbstractQueryMetrics
199201
public final Counter totalRowTombstonesFetched;
200202
public final Counter totalQueriesCompleted;
201203

202-
public final Counter sortThenFilterQueriesCompleted;
203-
public final Counter filterThenSortQueriesCompleted;
204+
@Nullable
205+
public final QueryPlanMetrics queryPlanMetrics;
204206

205207
/**
206208
* @param table the table to measure metrics for
@@ -220,9 +222,9 @@ public PerTable(TableMetadata table, QueryKind queryKind, Predicate<ReadCommand>
220222
totalRowTombstonesFetched = Metrics.counter(createMetricName("TotalRowTombstonesFetched"));
221223
totalQueriesCompleted = Metrics.counter(createMetricName("TotalQueriesCompleted"));
222224
totalQueryTimeouts = Metrics.counter(createMetricName("TotalQueryTimeouts"));
223-
224-
sortThenFilterQueriesCompleted = Metrics.counter(createMetricName("SortThenFilterQueriesCompleted"));
225-
filterThenSortQueriesCompleted = Metrics.counter(createMetricName("FilterThenSortQueriesCompleted"));
225+
queryPlanMetrics = (CassandraRelevantProperties.SAI_QUERY_PLAN_METRICS_ENABLED.getBoolean())
226+
? new QueryPlanMetrics()
227+
: null;
226228
}
227229

228230
@Override
@@ -243,10 +245,42 @@ public void record(QueryContext.Snapshot snapshot)
243245
totalRowsReturned.inc(snapshot.rowsReturned);
244246
totalRowTombstonesFetched.inc(snapshot.rowTombstonesFetched);
245247

246-
if (snapshot.filterSortOrder == QueryContext.FilterSortOrder.SCAN_THEN_FILTER)
247-
sortThenFilterQueriesCompleted.inc();
248-
else if (snapshot.filterSortOrder == QueryContext.FilterSortOrder.SEARCH_THEN_ORDER)
249-
filterThenSortQueriesCompleted.inc();
248+
QueryContext.PlanInfo queryPlanInfo = snapshot.queryPlanInfo;
249+
if (queryPlanInfo != null && queryPlanMetrics != null)
250+
{
251+
queryPlanMetrics.totalCostEstimated.inc(queryPlanInfo.costEstimated);
252+
queryPlanMetrics.totalRowsToReturnEstimated.inc(queryPlanInfo.rowsToReturnEstimated);
253+
queryPlanMetrics.totalRowsToFetchEstimated.inc(queryPlanInfo.rowsToFetchEstimated);
254+
queryPlanMetrics.totalKeysToIterateEstimated.inc(queryPlanInfo.keysToIterateEstimated);
255+
256+
if (queryPlanInfo.filterExecutedAfterOrderedScan)
257+
queryPlanMetrics.sortThenFilterQueriesCompleted.inc();
258+
if (queryPlanInfo.searchExecutedBeforeOrder)
259+
queryPlanMetrics.filterThenSortQueriesCompleted.inc();
260+
}
261+
}
262+
263+
public class QueryPlanMetrics
264+
{
265+
public final Counter totalRowsToReturnEstimated;
266+
public final Counter totalRowsToFetchEstimated;
267+
public final Counter totalKeysToIterateEstimated;
268+
public final Counter totalCostEstimated;
269+
270+
public final Counter sortThenFilterQueriesCompleted;
271+
public final Counter filterThenSortQueriesCompleted;
272+
273+
274+
public QueryPlanMetrics()
275+
{
276+
totalRowsToReturnEstimated = Metrics.counter(createMetricName("TotalRowsToReturnEstimated"));
277+
totalRowsToFetchEstimated = Metrics.counter(createMetricName("TotalRowsToFetchEstimated"));
278+
totalKeysToIterateEstimated = Metrics.counter(createMetricName("TotalKeysToIterateEstimated"));
279+
totalCostEstimated = Metrics.counter(createMetricName("TotalCostEstimated"));
280+
281+
sortThenFilterQueriesCompleted = Metrics.counter(createMetricName("SortThenFilterQueriesCompleted"));
282+
filterThenSortQueriesCompleted = Metrics.counter(createMetricName("FilterThenSortQueriesCompleted"));
283+
}
250284
}
251285
}
252286

@@ -293,6 +327,9 @@ public static class PerQuery extends AbstractQueryMetrics
293327
*/
294328
public final Timer annGraphSearchLatency;
295329

330+
@Nullable
331+
public final QueryPlanMetrics queryPlanMetrics;
332+
296333
/**
297334
* @param table the table to measure metrics for
298335
* @param queryKind an identifier for the kind of query which metrics are being recorded for
@@ -323,6 +360,10 @@ public PerQuery(TableMetadata table, QueryKind queryKind, Predicate<ReadCommand>
323360

324361
// Key vector metrics that translate to performance
325362
annGraphSearchLatency = Metrics.timer(createMetricName("ANNGraphSearchLatency"));
363+
364+
queryPlanMetrics = CassandraRelevantProperties.SAI_QUERY_PLAN_METRICS_ENABLED.getBoolean()
365+
? new QueryPlanMetrics()
366+
: null;
326367
}
327368

328369
@Override
@@ -362,6 +403,75 @@ public void record(QueryContext.Snapshot snapshot)
362403
{
363404
annGraphSearchLatency.update(snapshot.annGraphSearchLatency, TimeUnit.NANOSECONDS);
364405
}
406+
407+
QueryContext.PlanInfo queryPlanInfo = snapshot.queryPlanInfo;
408+
if (queryPlanInfo != null && queryPlanMetrics != null)
409+
{
410+
queryPlanMetrics.costEstimated.update(queryPlanInfo.costEstimated);
411+
queryPlanMetrics.rowsToReturnEstimated.update(queryPlanInfo.rowsToReturnEstimated);
412+
queryPlanMetrics.rowsToFetchEstimated.update(queryPlanInfo.rowsToFetchEstimated);
413+
queryPlanMetrics.keysToIterateEstimated.update(queryPlanInfo.keysToIterateEstimated);
414+
queryPlanMetrics.logSelectivityEstimated.update(queryPlanInfo.logSelectivityEstimated);
415+
queryPlanMetrics.indexReferencesInQuery.update(queryPlanInfo.indexReferencesInQuery);
416+
queryPlanMetrics.indexReferencesInPlan.update(queryPlanInfo.indexReferencesInPlan);
417+
}
365418
}
419+
420+
/// Metrics related to query planning.
421+
/// Moved to separate class so they can be enabled/disabled as a group.
422+
public class QueryPlanMetrics
423+
{
424+
/**
425+
* Query execution cost as estimated by the planner
426+
*/
427+
public final Histogram costEstimated;
428+
429+
/**
430+
* Number of rows to be returned from the query as estimated by the planner
431+
*/
432+
public final Histogram rowsToReturnEstimated;
433+
434+
/**
435+
* Number of rows to be fetched by the query as estimated by the planner
436+
*/
437+
public final Histogram rowsToFetchEstimated;
438+
439+
/**
440+
* Number of keys to be iterated by the query as estimated by the planner
441+
*/
442+
public final Histogram keysToIterateEstimated;
443+
444+
/**
445+
* Negative decimal logarithm of selectivity of the query, before applying the LIMIT clause.
446+
* We use logarithm because selectivity values can be very small (e.g. 10^-9).
447+
*/
448+
public final Histogram logSelectivityEstimated;
449+
450+
/**
451+
* Number of indexes referenced by the optimized query plan.
452+
* The same index referenced from unrelated query clauses,
453+
* leading to separate index searches, are counted separately.
454+
*/
455+
public final Histogram indexReferencesInPlan;
456+
457+
/**
458+
* Number of indexes referenced by the original query plan before optimization (as stated in the query text)
459+
*/
460+
public final Histogram indexReferencesInQuery;
461+
462+
QueryPlanMetrics()
463+
{
464+
costEstimated = Metrics.histogram(createMetricName("CostEstimated"), false);
465+
rowsToReturnEstimated = Metrics.histogram(createMetricName("RowsToReturnEstimated"), true);
466+
rowsToFetchEstimated = Metrics.histogram(createMetricName("RowsToFetchEstimated"), true);
467+
keysToIterateEstimated = Metrics.histogram(createMetricName("KeysToIterateEstimated"), true);
468+
logSelectivityEstimated = Metrics.histogram(createMetricName("LogSelectivityEstimated"), true);
469+
indexReferencesInPlan = Metrics.histogram(createMetricName("IndexReferencesInPlan"), true);
470+
indexReferencesInQuery = Metrics.histogram(createMetricName("IndexReferencesInQuery"), false);
471+
}
472+
}
473+
366474
}
475+
476+
367477
}

0 commit comments

Comments
 (0)