|
2 | 2 |
|
3 | 3 | Exact SQL for interrogating optimizer statistics and actual cardinalities against the DSQL cluster. |
4 | 4 |
|
| 5 | +**Placeholder substitution:** All queries in this file use `{...}` placeholders. MUST substitute via `safe_query.build()` — see input-validation.md. Use the correct helper per position: |
| 6 | + |
| 7 | +- **Identifier positions** (FROM clause, GROUP BY, column aliases): `ident()` → emits `"value"` |
| 8 | +- **String-literal positions** (WHERE `= {schema}`, `IN ({table})`, equality comparisons against catalog columns): `allow()` or `regex()` → emits `'value'` |
| 9 | + |
| 10 | +Worked example: |
| 11 | + |
| 12 | +```python |
| 13 | +safe_query.build( |
| 14 | + "SELECT reltuples FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace " |
| 15 | + "WHERE n.nspname = {schema} AND c.relname IN ({t1}, {t2})", |
| 16 | + schema=regex(r"^[a-z_]+$", user_schema), |
| 17 | + t1=regex(r"^[a-z_]+$", table1), |
| 18 | + t2=regex(r"^[a-z_]+$", table2), |
| 19 | +) |
| 20 | +``` |
| 21 | + |
5 | 22 | ## Table of Contents |
6 | 23 |
|
7 | 24 | 1. [Table-Level Statistics (pg_class)](#table-level-statistics) |
8 | 25 | 2. [Column Statistics (pg_stats)](#column-statistics) |
9 | 26 | 3. [Index Definitions](#index-definitions) |
10 | 27 | 4. [Actual Row Counts](#actual-row-counts) |
11 | 28 | 5. [Actual Distinct Counts](#actual-distinct-counts) |
12 | | -6. [Value Distribution Analysis](#value-distribution-analysis) |
| 29 | +6. [Column Types for Predicate Columns](#column-types-for-predicate-columns) |
| 30 | +7. [B-Tree Cross-Type Operator Support](#b-tree-cross-type-operator-support) |
| 31 | +8. [Indexed Column Types](#indexed-column-types) |
| 32 | +9. [Value Distribution Analysis](#value-distribution-analysis) |
13 | 33 |
|
14 | 34 | --- |
15 | 35 |
|
|
25 | 45 | relpages |
26 | 46 | FROM pg_class c |
27 | 47 | JOIN pg_namespace n ON n.oid = c.relnamespace |
28 | | -WHERE n.nspname = '{schema}' |
29 | | - AND c.relname IN ('{table1}', '{table2}', '{table3}'); |
| 48 | +WHERE n.nspname = {schema} |
| 49 | + AND c.relname IN ({table1}, {table2}, {table3}); |
30 | 50 | ``` |
31 | 51 |
|
32 | 52 | Compare `reltuples` against actual `COUNT(*)`. A divergence >20% on the table-stats snapshot indicates stale `reltuples` requiring `ANALYZE`. This is distinct from the row-estimate-vs-actual error thresholds used for plan findings (see plan-interpretation.md: 2x–5x minor, 5x–50x significant, 50x+ severe). |
|
46 | 66 | histogram_bounds, |
47 | 67 | correlation |
48 | 68 | FROM pg_stats |
49 | | -WHERE schemaname = '{schema}' |
50 | | - AND tablename = '{table}' |
51 | | - AND attname IN ('{col1}', '{col2}'); |
| 69 | +WHERE schemaname = {schema} |
| 70 | + AND tablename = {table} |
| 71 | + AND attname IN ({col1}, {col2}); |
52 | 72 | ``` |
53 | 73 |
|
54 | 74 | **Key fields:** |
|
71 | 91 | indexname, |
72 | 92 | indexdef |
73 | 93 | FROM pg_indexes |
74 | | -WHERE schemaname = '{schema}' |
75 | | - AND tablename IN ('{table1}', '{table2}', '{table3}') |
| 94 | +WHERE schemaname = {schema} |
| 95 | + AND tablename IN ({table1}, {table2}, {table3}) |
76 | 96 | ORDER BY tablename, indexname; |
77 | 97 | ``` |
78 | 98 |
|
@@ -103,6 +123,85 @@ Compare against `pg_stats.n_distinct`: |
103 | 123 | - If `n_distinct` is positive: compare directly |
104 | 124 | - If `n_distinct` is negative: multiply absolute value by actual row count to get estimated distinct count |
105 | 125 |
|
| 126 | +## Column Types for Predicate Columns |
| 127 | + |
| 128 | +Retrieve the declared types for columns used in WHERE predicates and JOIN conditions, to detect type coercion index bypass (see plan-interpretation.md): |
| 129 | + |
| 130 | +```sql |
| 131 | +SELECT |
| 132 | + c.table_name, |
| 133 | + c.column_name, |
| 134 | + c.data_type, |
| 135 | + c.udt_name, |
| 136 | + c.is_nullable |
| 137 | +FROM information_schema.columns c |
| 138 | +WHERE c.table_schema = {schema} |
| 139 | + AND c.table_name IN ({table1}, {table2}) |
| 140 | + AND c.column_name IN ({col1}, {col2}); |
| 141 | +``` |
| 142 | + |
| 143 | +Cross-reference the column type against predicate literals visible in the EXPLAIN output. When the types differ, use the B-Tree Cross-Type Operator Support query below to determine whether the mismatch prevents index usage. |
| 144 | + |
| 145 | +## B-Tree Cross-Type Operator Support |
| 146 | + |
| 147 | +Determine which type pairs the DSQL B-Tree access method supports for index scans. If a (predicate-type, column-type) pair has no registered operator, the index cannot be used for that comparison: |
| 148 | + |
| 149 | +```sql |
| 150 | +SELECT DISTINCT |
| 151 | + lt.typname AS left_type, |
| 152 | + rt.typname AS right_type |
| 153 | +FROM pg_amop ao |
| 154 | +JOIN pg_type lt ON lt.oid = ao.amoplefttype |
| 155 | +JOIN pg_type rt ON rt.oid = ao.amoprighttype |
| 156 | +-- 10003 is DSQL's B-Tree OID (PG mainline is 403). |
| 157 | +-- Verify with: SELECT oid FROM pg_am WHERE amname = 'btree_index' |
| 158 | +WHERE ao.amopmethod = 10003 |
| 159 | + AND ao.amoplefttype != ao.amoprighttype |
| 160 | +ORDER BY lt.typname, rt.typname; |
| 161 | +``` |
| 162 | + |
| 163 | +This returns only the cross-type pairs (where left and right types differ). Same-type pairs are always supported. Use this to confirm whether a suspected type mismatch actually prevents index usage — if the pair appears in the result, the index CAN be used and the issue lies elsewhere. |
| 164 | + |
| 165 | +To check a specific pair: |
| 166 | + |
| 167 | +```sql |
| 168 | +SELECT EXISTS ( |
| 169 | + SELECT 1 |
| 170 | + FROM pg_amop ao |
| 171 | + JOIN pg_type lt ON lt.oid = ao.amoplefttype |
| 172 | + JOIN pg_type rt ON rt.oid = ao.amoprighttype |
| 173 | + -- 10003 = DSQL B-Tree OID; verify with: SELECT oid FROM pg_am WHERE amname = 'btree_index' |
| 174 | + WHERE ao.amopmethod = 10003 |
| 175 | + AND lt.typname = {predicate_type} |
| 176 | + AND rt.typname = {column_type} |
| 177 | +) AS index_usable; |
| 178 | +``` |
| 179 | + |
| 180 | +## Indexed Column Types |
| 181 | + |
| 182 | +Retrieve index definitions together with their column types to identify type coercion bypass candidates: |
| 183 | + |
| 184 | +```sql |
| 185 | +SELECT |
| 186 | + i.indexname, |
| 187 | + i.tablename, |
| 188 | + a.attname AS column_name, |
| 189 | + t.typname AS column_type, |
| 190 | + i.indexdef |
| 191 | +FROM pg_indexes i |
| 192 | +JOIN pg_class ic ON ic.relname = i.indexname |
| 193 | +JOIN pg_index ix ON ix.indexrelid = ic.oid |
| 194 | +JOIN pg_attribute a ON a.attrelid = ix.indrelid |
| 195 | + AND a.attnum = ANY(ix.indkey) |
| 196 | +JOIN pg_type t ON t.oid = a.atttypid |
| 197 | +JOIN pg_namespace n ON n.oid = ic.relnamespace |
| 198 | +WHERE n.nspname = {schema} |
| 199 | + AND i.tablename IN ({table1}, {table2}) |
| 200 | +ORDER BY i.tablename, i.indexname, a.attnum; |
| 201 | +``` |
| 202 | + |
| 203 | +Use this when a Full Scan appears despite an apparently usable index — compare the index column's `column_type` against the predicate literal's inferred type. |
| 204 | + |
106 | 205 | ## Value Distribution Analysis |
107 | 206 |
|
108 | 207 | For columns with suspected data skew, retrieve the actual top-N value frequencies: |
|
0 commit comments