Skip to content

Commit c0b5be1

Browse files
authored
Query Insights: compute numeric selectivity and render low non-zero values as “below 0.1%” (#763)
2 parents 60b0a89 + 48a8bb8 commit c0b5be1

9 files changed

Lines changed: 174 additions & 23 deletions

File tree

src/documentdb/queryInsights/staticAnalysisSummary.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function makeStage2Response(overrides: Partial<QueryInsightsStage2Response> = {}
2222
isCoveringQuery: false,
2323
concerns: [],
2424
efficiencyAnalysis: {
25-
selectivity: '5.0%',
25+
selectivity: 5,
2626
indexUsed: 'rating_1',
2727
fetchOverhead: 'Direct fetch',
2828
fetchOverheadKind: 'directFetch',
@@ -97,6 +97,13 @@ describe('buildStaticAnalysisSummary', () => {
9797
expect(summary).toContain('**Selectivity**: Unknown');
9898
});
9999

100+
it('should show threshold text when selectivity is non-zero and below 0.1%', () => {
101+
const stage2 = makeStage2Response();
102+
stage2.efficiencyAnalysis.selectivity = 0.008;
103+
const summary = buildStaticAnalysisSummary(stage2);
104+
expect(summary).toContain('**Selectivity**: below 0.1%');
105+
});
106+
100107
it('should show None when no index used', () => {
101108
const stage2 = makeStage2Response();
102109
stage2.efficiencyAnalysis.indexUsed = null;
@@ -176,7 +183,7 @@ describe('buildStaticAnalysisSummary', () => {
176183
hadCollectionScan: true,
177184
concerns: ['Collection scan detected', 'In-memory sort required'],
178185
efficiencyAnalysis: {
179-
selectivity: '0.008%',
186+
selectivity: 0.008,
180187
indexUsed: null,
181188
fetchOverhead: 'Collection scan',
182189
fetchOverheadKind: 'collectionScan',
@@ -228,7 +235,7 @@ describe('buildStaticAnalysisSummary', () => {
228235
documentsReturned: 50,
229236
isCoveringQuery: true,
230237
efficiencyAnalysis: {
231-
selectivity: '0.5%',
238+
selectivity: 0.5,
232239
indexUsed: 'status_1_createdAt_-1',
233240
fetchOverhead: 'Covered query',
234241
fetchOverheadKind: 'covered',

src/documentdb/queryInsights/staticAnalysisSummary.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import * as vscode from 'vscode';
7+
68
import { type QueryInsightsStage2Response } from '../../webviews/documentdb/collectionView/types/queryInsights';
79

810
/**
@@ -42,7 +44,11 @@ export function buildStaticAnalysisSummary(stage2: QueryInsightsStage2Response,
4244

4345
// Summary indicators
4446
lines.push('### Summary Indicators (4 cells shown to user)');
45-
lines.push(`- **Selectivity**: ${stage2.efficiencyAnalysis.selectivity ?? 'Unknown'}`);
47+
const formattedSelectivity =
48+
stage2.efficiencyAnalysis.selectivity !== null
49+
? formatSelectivityForSummary(stage2.efficiencyAnalysis.selectivity)
50+
: 'Unknown';
51+
lines.push(`- **Selectivity**: ${formattedSelectivity}`);
4652
lines.push(`- **Index Used**: ${stage2.efficiencyAnalysis.indexUsed ?? 'None (collection scan)'}`);
4753
lines.push(
4854
`- **Fetch Overhead**: ${stage2.efficiencyAnalysis.fetchOverhead} (${stage2.efficiencyAnalysis.fetchOverheadKind})`,
@@ -71,3 +77,22 @@ export function buildStaticAnalysisSummary(stage2: QueryInsightsStage2Response,
7177

7278
return lines.join('\n');
7379
}
80+
81+
/**
82+
* Formats numeric selectivity for static analysis summary text.
83+
* Values between 0 and 0.1 are rendered as a threshold label.
84+
*
85+
* Keep this formatting in sync with the webview selectivity formatter so Stage 3
86+
* summary text and the UI display stay consistent for the same underlying values.
87+
* We intentionally kept this duplicated for now to avoid a broader refactor.
88+
*
89+
* @param selectivity - Raw selectivity percentage value
90+
* @returns Formatted selectivity string for summary output
91+
*/
92+
function formatSelectivityForSummary(selectivity: number): string {
93+
const locale = vscode.env?.language || 'en-US';
94+
if (selectivity > 0 && selectivity < 0.1) {
95+
return `below ${(0.1).toLocaleString(locale, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`;
96+
}
97+
return `${selectivity.toLocaleString(locale, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`;
98+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type ExecutionStatsAnalysis } from './ExplainPlanAnalyzer';
7+
import { transformStage2Response } from './transformations';
8+
9+
function createExecutionStatsAnalysis(overrides: Partial<ExecutionStatsAnalysis> = {}): ExecutionStatsAnalysis {
10+
return {
11+
executionTimeMillis: 12,
12+
totalDocsExamined: 1,
13+
totalKeysExamined: 1,
14+
nReturned: 1,
15+
efficiencyRatio: 1,
16+
usedIndexes: ['idx_a'],
17+
isCollectionScan: false,
18+
isCovered: false,
19+
hasInMemorySort: false,
20+
isIndexScan: true,
21+
performanceRating: {
22+
score: 'excellent',
23+
diagnostics: [],
24+
},
25+
rawStats: {},
26+
...overrides,
27+
};
28+
}
29+
30+
describe('transformStage2Response - selectivity computation', () => {
31+
it('returns numeric precision below 0.1% for non-zero selectivity', () => {
32+
const analyzed = createExecutionStatsAnalysis({
33+
nReturned: 1,
34+
});
35+
36+
const result = transformStage2Response(analyzed, 12_000);
37+
38+
expect(result.efficiencyAnalysis.selectivity).toBeCloseTo(0.008333333333333333, 10);
39+
});
40+
41+
it('returns numeric percent at or above 0.1%', () => {
42+
const analyzed = createExecutionStatsAnalysis({
43+
nReturned: 1,
44+
});
45+
46+
const result = transformStage2Response(analyzed, 1_000);
47+
48+
expect(result.efficiencyAnalysis.selectivity).toBe(0.1);
49+
});
50+
});

src/documentdb/queryInsights/transformations.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,13 @@ export function transformStage2Response(
193193
*
194194
* @param nReturned - Number of documents returned
195195
* @param totalCollectionDocs - Estimated total documents in the collection
196-
* @returns Formatted percentage string (e.g., "33.2%") or null if unavailable
196+
* @returns Raw percentage value (e.g., 33.2375) or null if unavailable
197197
*/
198-
function computeSelectivity(nReturned: number, totalCollectionDocs: number | undefined): string | null {
198+
function computeSelectivity(nReturned: number, totalCollectionDocs: number | undefined): number | null {
199199
if (!totalCollectionDocs || totalCollectionDocs <= 0 || nReturned === undefined) {
200200
return null;
201201
}
202-
const ratio = Math.min(nReturned / totalCollectionDocs, 1);
203-
return `${(ratio * 100).toFixed(1)}%`;
202+
return Math.min((nReturned / totalCollectionDocs) * 100, 100);
204203
}
205204

206205
/**

src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
} from '../../queryInsightsReducer';
5353
import { createImprovementCardConfig } from '../../utils/createImprovementCard';
5454
import { extractErrorCode } from '../../utils/errorCodeExtractor';
55+
import { formatSelectivityForDisplay } from '../../utils/formatSelectivityForDisplay';
5556
import { CardStack, FeedbackCard, FeedbackDialog, type CardStackItem } from './components';
5657
import { CountMetric } from './components/metricsRow/CountMetric';
5758
import { MetricsRow } from './components/metricsRow/MetricsRow';
@@ -451,6 +452,11 @@ export const QueryInsightsMain = (): JSX.Element => {
451452
const docsReturned = getMetricValue(stage2Data?.documentsReturned);
452453
const keysExamined = getMetricValue(stage2Data?.totalKeysExamined);
453454
const docsExamined = getMetricValue(stage2Data?.totalDocsExamined);
455+
const lowSelectivityLabel = `below ${(0.1).toLocaleString(navigator.language, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`;
456+
const selectivity = formatSelectivityForDisplay(
457+
getCellValue((stage2) => stage2.efficiencyAnalysis.selectivity),
458+
lowSelectivityLabel,
459+
);
454460

455461
/**
456462
* Documentation URL for the AI Performance Insights feature itself
@@ -1133,30 +1139,31 @@ export const QueryInsightsMain = (): JSX.Element => {
11331139
<SummaryCard title={l10n.t('Query Efficiency Analysis')}>
11341140
<GenericCell
11351141
label={l10n.t('Selectivity')}
1136-
value={getCellValue((stage2) => stage2.efficiencyAnalysis.selectivity)}
1142+
value={selectivity}
11371143
loadingPlaceholder="skeleton"
11381144
tooltipExplanation={(() => {
11391145
const selectivity = stage2Data?.efficiencyAnalysis.selectivity;
1140-
if (!selectivity) {
1146+
if (selectivity === null || selectivity === undefined) {
11411147
return l10n.t(
11421148
'The percentage of your collection this query returns. Could not be determined for this query.',
11431149
);
11441150
}
1145-
const pct = parseFloat(selectivity);
1151+
const displayValue = formatSelectivityForDisplay(selectivity, lowSelectivityLabel);
1152+
const pct = selectivity;
11461153
if (pct < 1) {
11471154
return l10n.t(
11481155
'This query returns {0} of your collection.\n\nThis is highly selective: only a small fraction of documents pass the filter. The database does minimal work to produce results.',
1149-
selectivity,
1156+
displayValue,
11501157
);
11511158
} else if (pct < 20) {
11521159
return l10n.t(
11531160
'This query returns {0} of your collection.\n\nThis is a reasonable level of selectivity. The filter narrows results to a manageable portion of the data.',
1154-
selectivity,
1161+
displayValue,
11551162
);
11561163
} else {
11571164
return l10n.t(
11581165
'This query returns {0} of your collection.\n\nThis is a broad query that returns a large portion of the data. Consider adding more specific filters to narrow the results.',
1159-
selectivity,
1166+
displayValue,
11601167
);
11611168
}
11621169
})()}

src/webviews/documentdb/collectionView/queryInsights/queryInsightsRouter.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,9 @@ export const queryInsightsRouter = router({
349349
ctx.telemetry.measurements.totalCollectionDocs = totalCollectionDocs;
350350
}
351351

352-
// Selectivity as a number (strip the '%' if present)
353-
if (transformed.efficiencyAnalysis.selectivity) {
354-
const selectivityNum = parseFloat(transformed.efficiencyAnalysis.selectivity);
355-
if (!isNaN(selectivityNum)) {
356-
ctx.telemetry.measurements.selectivityPercent = selectivityNum;
357-
}
352+
const selectivityPercent = transformed.efficiencyAnalysis.selectivity;
353+
if (selectivityPercent !== null && selectivityPercent !== undefined) {
354+
ctx.telemetry.measurements.selectivityPercent = selectivityPercent;
358355
}
359356

360357
// Badge IDs (safe categorical data, no PII)

src/webviews/documentdb/collectionView/types/queryInsights.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export type FetchOverheadKind = 'noMatches' | 'covered' | 'collectionScan' | 'mu
8181
* - Top-level `examinedToReturnedRatio: number` - Raw ratio for calculations (e.g., 50.5)
8282
*
8383
* Efficiency analysis metrics:
84-
* - `efficiencyAnalysis.selectivity: string | null` - Percentage of collection returned
84+
* - `efficiencyAnalysis.selectivity: number | null` - Percentage of collection returned
8585
* - `efficiencyAnalysis.fetchOverhead: string` - Localized display label for fetch state
8686
* - `efficiencyAnalysis.fetchOverheadKind: FetchOverheadKind` - Stable key for UI branching
8787
*/
@@ -102,8 +102,8 @@ export interface QueryInsightsStage2Response {
102102
/** Top-level query warnings (collection scan, in-memory sort, etc.) */
103103
concerns: string[];
104104
efficiencyAnalysis: {
105-
/** Selectivity: percentage of collection returned (e.g., "33.2%"), or null if unknown */
106-
selectivity: string | null;
105+
/** Selectivity: raw percentage of collection returned (e.g., 33.2375), or null if unknown */
106+
selectivity: number | null;
107107
indexUsed: string | null;
108108
/** Fetch overhead state label (e.g., "Direct fetch", "Covered query", "Collection scan") */
109109
fetchOverhead: string;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { formatSelectivityForDisplay } from './formatSelectivityForDisplay';
7+
8+
describe('formatSelectivityForDisplay', () => {
9+
const belowThresholdText = 'below 0.1%';
10+
// Helper that mirrors the function's own formatting so expectations stay
11+
// locale-agnostic when tests run under non-en-US system locales.
12+
const fmt = (n: number): string =>
13+
`${n.toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`;
14+
15+
it('returns threshold text for non-zero selectivity below 0.1%', () => {
16+
expect(formatSelectivityForDisplay(0.02, belowThresholdText)).toBe(belowThresholdText);
17+
expect(formatSelectivityForDisplay(0.008, belowThresholdText)).toBe(belowThresholdText);
18+
});
19+
20+
it('preserves values at or above 0.1%', () => {
21+
expect(formatSelectivityForDisplay(0.1, belowThresholdText)).toBe(fmt(0.1));
22+
expect(formatSelectivityForDisplay(5, belowThresholdText)).toBe(fmt(5));
23+
});
24+
25+
it('preserves zero and unavailable values', () => {
26+
expect(formatSelectivityForDisplay(0, belowThresholdText)).toBe(fmt(0));
27+
expect(formatSelectivityForDisplay(null, belowThresholdText)).toBeNull();
28+
expect(formatSelectivityForDisplay(undefined, belowThresholdText)).toBeUndefined();
29+
});
30+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/**
7+
* Formats selectivity for UI display.
8+
*
9+
* For very low non-zero percentages (< 0.1%), we show a threshold label
10+
* instead of a rounded numeric value.
11+
*
12+
* Keep this logic in sync with the static analysis summary formatter so Stage 3
13+
* output and the webview render the same values for the same input.
14+
* We intentionally left this duplicated for now to keep the change simple.
15+
*/
16+
export function formatSelectivityForDisplay(selectivity: number, belowThresholdLabel: string): string;
17+
export function formatSelectivityForDisplay(selectivity: null, belowThresholdLabel: string): null;
18+
export function formatSelectivityForDisplay(selectivity: undefined, belowThresholdLabel: string): undefined;
19+
export function formatSelectivityForDisplay(
20+
selectivity: number | null | undefined,
21+
belowThresholdLabel: string,
22+
): string | null | undefined;
23+
export function formatSelectivityForDisplay(
24+
selectivity: number | null | undefined,
25+
belowThresholdLabel: string,
26+
): string | null | undefined {
27+
if (selectivity === null || selectivity === undefined) {
28+
return selectivity;
29+
}
30+
31+
if (selectivity > 0 && selectivity < 0.1) {
32+
return belowThresholdLabel;
33+
}
34+
35+
return `${selectivity.toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`;
36+
}

0 commit comments

Comments
 (0)