turbine-orm 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/cli/index.d.ts +3 -1
- package/dist/cjs/cli/index.js +117 -7
- package/dist/cjs/client.d.ts +16 -1
- package/dist/cjs/client.js +4 -0
- package/dist/cjs/dialect.d.ts +9 -0
- package/dist/cjs/index-stats.d.ts +46 -0
- package/dist/cjs/index-stats.js +42 -1
- package/dist/cjs/plan-divergence.d.ts +244 -0
- package/dist/cjs/plan-divergence.js +444 -0
- package/dist/cjs/powql.d.ts +11 -0
- package/dist/cjs/powql.js +22 -0
- package/dist/cjs/query/builder.d.ts +45 -0
- package/dist/cjs/query/builder.js +90 -17
- package/dist/cjs/query/deferred.d.ts +9 -0
- package/dist/cjs/query/types.d.ts +140 -0
- package/dist/cjs/query/utils.d.ts +14 -0
- package/dist/cjs/query/utils.js +17 -3
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.js +117 -7
- package/dist/client.d.ts +16 -1
- package/dist/client.js +4 -0
- package/dist/dialect.d.ts +9 -0
- package/dist/index-stats.d.ts +46 -0
- package/dist/index-stats.js +42 -1
- package/dist/plan-divergence.d.ts +244 -0
- package/dist/plan-divergence.js +438 -0
- package/dist/powql.d.ts +11 -0
- package/dist/powql.js +22 -0
- package/dist/query/builder.d.ts +45 -0
- package/dist/query/builder.js +90 -17
- package/dist/query/deferred.d.ts +9 -0
- package/dist/query/types.d.ts +140 -0
- package/dist/query/utils.d.ts +14 -0
- package/dist/query/utils.js +17 -3
- package/package.json +1 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-divergence advisor, the third question doctor asks about a probe column.
|
|
3
|
+
*
|
|
4
|
+
* `index-advisor.ts` answers "which relation probes have no index" from pure
|
|
5
|
+
* topology. `index-stats.ts` answers "is adding that index worth it" from live
|
|
6
|
+
* statistics. This module answers the third question on the SAME columns: "this
|
|
7
|
+
* column IS indexed, and its value distribution makes a NAMED prepared
|
|
8
|
+
* statement's generic plan unsafe".
|
|
9
|
+
*
|
|
10
|
+
* THE MECHANISM. Postgres may promote a named prepared statement to a GENERIC
|
|
11
|
+
* plan from its sixth execution onward, and ONLY when the generic plan's
|
|
12
|
+
* estimated cost is not worse than the average custom cost. A generic plan
|
|
13
|
+
* cannot see any parameter value, so it substitutes a default for each one:
|
|
14
|
+
*
|
|
15
|
+
* - an unknown equality `col = $1` is estimated as reltuples / n_distinct;
|
|
16
|
+
* - an unknown `LIMIT $n` is estimated as 10% of the CHILD node's row estimate.
|
|
17
|
+
*
|
|
18
|
+
* THE ONE SHAPE THIS MODELS, stated narrowly on purpose. A read shaped
|
|
19
|
+
* `WHERE col = $1 ORDER BY <other indexed column> LIMIT $n`, where the generic
|
|
20
|
+
* estimate for `col = $1` sits ABOVE the plan boundary (so the generic plan
|
|
21
|
+
* keeps the ordered index scan and filters) while some real values sit far
|
|
22
|
+
* BELOW it (so for those values the ordered scan has to walk a large fraction
|
|
23
|
+
* of the table before it accumulates one page of matches, where the custom
|
|
24
|
+
* plan takes a bitmap scan over the value's own rows and sorts them).
|
|
25
|
+
*
|
|
26
|
+
* WHAT THIS DELIBERATELY DOES NOT MODEL, because it was tried and it did not
|
|
27
|
+
* work. An earlier revision carried a second rule for the opposite direction (a
|
|
28
|
+
* physically clustered column whose densest value is far ABOVE the generic
|
|
29
|
+
* estimate). Measured against live fixtures it was wrong more often than right,
|
|
30
|
+
* and twice it was wrong with the SIGN INVERTED: it predicted "at least 16,032x"
|
|
31
|
+
* and "at least 2,675x" on columns where the generic plan was in fact 10x and
|
|
32
|
+
* 105x BETTER than the custom one, so acting on the finding would have made
|
|
33
|
+
* those reads dramatically slower. The reason is structural, not a bad constant:
|
|
34
|
+
* whether that flip helps or hurts turns on WHERE in the heap the value's rows
|
|
35
|
+
* physically sit, and no pg_stats input carries that. `correlation` is a
|
|
36
|
+
* whole-column property and is identical whether the dominant band is at the
|
|
37
|
+
* head of the heap or its tail. The rule was removed rather than retuned.
|
|
38
|
+
*
|
|
39
|
+
* The same blind spot bounds what remains: this check can see how MANY rows a
|
|
40
|
+
* value has, not WHERE they are, so it under-reports (a rare value packed at the
|
|
41
|
+
* end of the heap is worse than modelled) and it cannot see the third-party
|
|
42
|
+
* shapes where a generic plan is the better one. A clean report is not evidence
|
|
43
|
+
* of immunity.
|
|
44
|
+
*
|
|
45
|
+
* EXPOSURE IS NOT AN INCIDENT. A finding says the DISTRIBUTION admits a
|
|
46
|
+
* damaging flip. It does NOT say the backend is choosing the bad plan today:
|
|
47
|
+
* `auto` promotes only when the generic plan's ESTIMATED cost is not worse than
|
|
48
|
+
* the average custom cost, and on a shape whose generic plan is estimated
|
|
49
|
+
* expensive it never promotes at all. That is why every finding ships a
|
|
50
|
+
* diagnostic that checks `pg_prepared_statements.generic_plans` BEFORE it
|
|
51
|
+
* compares the two plans: the counter is the only thing that establishes real
|
|
52
|
+
* exposure.
|
|
53
|
+
*
|
|
54
|
+
* TWO UNITS, and mixing them up is what made the removed rule wrong. The
|
|
55
|
+
* PLANNER costs the boundary in PAGES (an ordered index scan reads about
|
|
56
|
+
* limit / matching of the table's pages before it fills the limit), which is why
|
|
57
|
+
* {@link crossoverRows} is derived from relpages and why measured flip points
|
|
58
|
+
* track it. The DAMAGE a user feels is also read in pages here, deliberately
|
|
59
|
+
* conservatively; when the ordering index is not correlated with heap order,
|
|
60
|
+
* each row examined is a separate block access and the true buffer count is
|
|
61
|
+
* larger, sometimes by an order of magnitude.
|
|
62
|
+
*
|
|
63
|
+
* FRESHNESS GATE. The cost-tier half of doctor gates on `stats_reset` age,
|
|
64
|
+
* because it normalizes WRITE COUNTERS by it. Nothing here reads a counter: every
|
|
65
|
+
* input comes from pg_stats, which is refreshed by ANALYZE. This check therefore
|
|
66
|
+
* gates on ANALYZE freshness instead (a column with no pg_stats row, or an
|
|
67
|
+
* n_distinct of 0, is suppressed with a notice, and every finding carries the
|
|
68
|
+
* table's last_analyze). Reusing the counter gate would have silenced the check
|
|
69
|
+
* on every cluster whose `pg_stat_database.stats_reset` is NULL, which is the
|
|
70
|
+
* default state and has nothing to do with whether ANALYZE has ever run.
|
|
71
|
+
*
|
|
72
|
+
* Postgres-only (pg_stats + plan_cache_mode). Pure: no pg import, no EXPLAIN.
|
|
73
|
+
*/
|
|
74
|
+
import { type StatsSnapshot } from './index-stats.js';
|
|
75
|
+
import type { SchemaMetadata } from './schema.js';
|
|
76
|
+
/**
|
|
77
|
+
* Gates behind a plan-divergence finding. Exported and printed with every
|
|
78
|
+
* finding so the reasoning is never a black box.
|
|
79
|
+
*
|
|
80
|
+
* Each one is either derived from the cost model and then checked against
|
|
81
|
+
* measurement, or fitted to a measured flip boundary; the comments record which.
|
|
82
|
+
*/
|
|
83
|
+
export declare const PLAN_DIVERGENCE_THRESHOLDS: {
|
|
84
|
+
/**
|
|
85
|
+
* The LIMIT the advisor assumes, because it cannot see the application's.
|
|
86
|
+
*
|
|
87
|
+
* It is NOT a conservative bound in either direction, and an earlier comment
|
|
88
|
+
* here claimed it was. The crossover grows as sqrt(limit), so raising the
|
|
89
|
+
* limit moves BOTH gates: `rarestBucket < crossover` gets easier to satisfy
|
|
90
|
+
* and `genericEstimate >= crossover` gets harder, and the second one can turn
|
|
91
|
+
* a finding off. 20 is simply a common first page. Every finding also reports
|
|
92
|
+
* the crossover at {@link wideLimit} so a caller paginating in thousands can
|
|
93
|
+
* see where their own limit lands.
|
|
94
|
+
*/
|
|
95
|
+
readonly assumedLimit: 20;
|
|
96
|
+
/** A second crossover reported alongside, for callers paginating in thousands. */
|
|
97
|
+
readonly wideLimit: 1000;
|
|
98
|
+
/**
|
|
99
|
+
* How many of the table's pages the WRONG plan must walk before this is worth
|
|
100
|
+
* a user's attention: ~400 KB of heap. Below it the flip cannot cost more than
|
|
101
|
+
* a millisecond or two however bad the ratio looks.
|
|
102
|
+
*
|
|
103
|
+
* This replaced a `relpages >= 1000` table-size floor, which was wrong and
|
|
104
|
+
* measurably so: a wrong plan on a SMALL table is not cheap, because an ordered
|
|
105
|
+
* index scan's cost is driven by how much of the table it walks, not by how big
|
|
106
|
+
* the table is. On a 285-page / 18,500-row fixture the generic plan read 336
|
|
107
|
+
* buffers against the custom plan's 7 (48x), and the old floor dropped that
|
|
108
|
+
* column without even counting it as considered.
|
|
109
|
+
*/
|
|
110
|
+
readonly minWalkPages: 50;
|
|
111
|
+
/**
|
|
112
|
+
* ...and it must walk at least this FRACTION of the table. The two gates are
|
|
113
|
+
* different questions (absolute cost, and how badly the plan is mismatched to
|
|
114
|
+
* the value), and a finding needs both.
|
|
115
|
+
*/
|
|
116
|
+
readonly minWalkFraction: 0.1;
|
|
117
|
+
};
|
|
118
|
+
export type PlanDivergenceThresholds = typeof PLAN_DIVERGENCE_THRESHOLDS;
|
|
119
|
+
export interface PlanDivergenceFinding {
|
|
120
|
+
table: string;
|
|
121
|
+
column: string;
|
|
122
|
+
/** pg_class.reltuples. */
|
|
123
|
+
rows: number;
|
|
124
|
+
/** pg_class.relpages, the size input the crossover is computed from. */
|
|
125
|
+
pages: number;
|
|
126
|
+
/** n_distinct, decoded the way Postgres decodes it (negative = fraction of rows). */
|
|
127
|
+
distinctValues: number;
|
|
128
|
+
/** rows / distinctValues: what a generic plan assumes `col = $1` matches. */
|
|
129
|
+
genericEstimate: number;
|
|
130
|
+
/** Estimated rowcount of the rarest value (MCV minimum, or the residual bucket). */
|
|
131
|
+
rarestBucket: number;
|
|
132
|
+
/** Estimated rowcount of the densest value (MCV maximum). Reported, not gated on. */
|
|
133
|
+
densestBucket: number;
|
|
134
|
+
/** pg_stats.correlation, signed as reported. Reported, not gated on. */
|
|
135
|
+
correlation: number;
|
|
136
|
+
/** sqrt(assumedLimit x pages): below this many true rows, bitmap+sort wins. */
|
|
137
|
+
crossoverRows: number;
|
|
138
|
+
/** The same crossover at {@link PLAN_DIVERGENCE_THRESHOLDS.wideLimit}. */
|
|
139
|
+
crossoverRowsWide: number;
|
|
140
|
+
assumedLimit: number;
|
|
141
|
+
/** How many distinct values sit on the wrong side of the crossover. */
|
|
142
|
+
valuesBelowCrossover: number;
|
|
143
|
+
/** Pages the generic plan's ordered scan walks for the rarest value. */
|
|
144
|
+
walkPages: number;
|
|
145
|
+
/** {@link walkPages} as a fraction of the table. */
|
|
146
|
+
walkFraction: number;
|
|
147
|
+
/**
|
|
148
|
+
* walkPages / the pages the custom plan's bitmap scan reads, as a ROUGH scale
|
|
149
|
+
* only. It is an estimate from statistics, not a bound in either direction:
|
|
150
|
+
* it assumes the rare value's rows are spread uniformly (they usually are not,
|
|
151
|
+
* which makes the real number larger) and that the bitmap scan touches a
|
|
152
|
+
* separate page per row (which makes it smaller). On every fixture measured
|
|
153
|
+
* while calibrating this, the true amplification came out LARGER than the
|
|
154
|
+
* estimate, never smaller, but that is three fixtures and not a guarantee.
|
|
155
|
+
* The EXPLAIN pair shipped with the finding is what settles it.
|
|
156
|
+
*/
|
|
157
|
+
approxAmplification: number;
|
|
158
|
+
/** The other indexed column the generic plan can order by (usually the PK). */
|
|
159
|
+
orderColumn: string;
|
|
160
|
+
/**
|
|
161
|
+
* `column` / `orderColumn` as the TypeScript FIELD names the Turbine API takes.
|
|
162
|
+
* The SQL in a finding names database columns; the code suggestion next to it
|
|
163
|
+
* must name fields, or a user pastes a snake_case key into a camelCase `where`.
|
|
164
|
+
*/
|
|
165
|
+
columnField: string;
|
|
166
|
+
orderColumnField: string;
|
|
167
|
+
/**
|
|
168
|
+
* When the table's column statistics were last refreshed (the later of
|
|
169
|
+
* last_analyze / last_autoanalyze). Null when never analyzed or unreadable.
|
|
170
|
+
* Everything above describes the distribution AS OF this moment; a table that
|
|
171
|
+
* has since changed shape can make the finding wrong in either direction.
|
|
172
|
+
*/
|
|
173
|
+
lastAnalyze: Date | null;
|
|
174
|
+
/** A copy-pasteable check that CONFIRMS or refutes the finding. */
|
|
175
|
+
diagnosticSql: string;
|
|
176
|
+
thresholds: PlanDivergenceThresholds;
|
|
177
|
+
}
|
|
178
|
+
/** A candidate suppressed because its statistics could not support a verdict. */
|
|
179
|
+
export interface PlanDivergenceNotice {
|
|
180
|
+
table: string;
|
|
181
|
+
column: string;
|
|
182
|
+
reason: string;
|
|
183
|
+
}
|
|
184
|
+
export interface PlanDivergenceReport {
|
|
185
|
+
findings: PlanDivergenceFinding[];
|
|
186
|
+
notices: PlanDivergenceNotice[];
|
|
187
|
+
/** How many (table, column) candidates were examined against live statistics. */
|
|
188
|
+
candidatesConsidered: number;
|
|
189
|
+
}
|
|
190
|
+
/** A (table, column) pair whose distribution statistics the collector should read. */
|
|
191
|
+
export interface DivergenceCandidate {
|
|
192
|
+
table: string;
|
|
193
|
+
column: string;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Every column whose value distribution is worth reading: a column Turbine
|
|
197
|
+
* probes by equality (a relation FK) or that the schema already indexes as a
|
|
198
|
+
* leading key, minus the columns that are unique on their own (a unique
|
|
199
|
+
* equality matches one row, so both plans agree).
|
|
200
|
+
*
|
|
201
|
+
* Purely schema-derived, so the collector knows what to read BEFORE any stats
|
|
202
|
+
* exist. Whether the column is really served by a btree is decided later,
|
|
203
|
+
* against the live index list.
|
|
204
|
+
*/
|
|
205
|
+
export declare function collectDivergenceCandidateColumns(schema: SchemaMetadata): DivergenceCandidate[];
|
|
206
|
+
/**
|
|
207
|
+
* The plan-boundary crossover: below this many truly matching rows, running
|
|
208
|
+
* `ORDER BY <other column> LIMIT n` as bitmap-scan + sort is cheaper than
|
|
209
|
+
* walking the ordering index and filtering.
|
|
210
|
+
*
|
|
211
|
+
* Derivation: an ordered index scan expects to read about (limit / matching) x
|
|
212
|
+
* pages before it accumulates `limit` matches; a bitmap scan reads about
|
|
213
|
+
* min(matching, pages). Those are equal at matching = sqrt(limit x pages).
|
|
214
|
+
* Measured flip points tracked this within 13 to 26% across two orders of
|
|
215
|
+
* magnitude of limit, which is why the size input is relpages rather than a
|
|
216
|
+
* row count.
|
|
217
|
+
*
|
|
218
|
+
* PREMISE, and it is the same blind spot the whole check has: both halves
|
|
219
|
+
* assume the matching rows are SCATTERED through the heap. On a clustered
|
|
220
|
+
* column a bitmap scan reads far fewer than min(matching, pages), so there is
|
|
221
|
+
* no flip at any limit and this crossover does not describe the table at all.
|
|
222
|
+
* pg_stats.correlation hints at it but does not settle it (a column can be
|
|
223
|
+
* uncorrelated overall and still have one value packed in the tail, which is
|
|
224
|
+
* exactly the counterexample fixture). Treat a crossover as a reason to run
|
|
225
|
+
* the diagnostic block, never as a measurement.
|
|
226
|
+
*/
|
|
227
|
+
export declare function crossoverRows(pages: number, limit: number): number;
|
|
228
|
+
/**
|
|
229
|
+
* Score every candidate column against the snapshot and return the ones whose
|
|
230
|
+
* distribution admits a damaging generic-plan flip.
|
|
231
|
+
*
|
|
232
|
+
* ONE rule, for the direction that could be calibrated: the generic estimate
|
|
233
|
+
* sits ABOVE the plan boundary (so a promoted plan keeps the ordered index
|
|
234
|
+
* scan) while the table's rarest values sit below it, and the pages that
|
|
235
|
+
* ordered scan must walk for such a value are worth a user's attention both in
|
|
236
|
+
* absolute terms and as a fraction of the table.
|
|
237
|
+
*
|
|
238
|
+
* `correlation` is reported but no longer gates anything. It used to route
|
|
239
|
+
* clustered columns to a second rule; that rule is gone (see the module header),
|
|
240
|
+
* and routing on it also made the advisor STRUCTURALLY unable to report a real
|
|
241
|
+
* sparse-direction flip on any column that happened to be clustered, which was
|
|
242
|
+
* measured at 1,165x on one fixture.
|
|
243
|
+
*/
|
|
244
|
+
export declare function findPlanDivergence(schema: SchemaMetadata, snapshot: StatsSnapshot): PlanDivergenceReport;
|