File size: 14,719 Bytes
caab8ee
16bfc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd946f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b07205
 
 
 
 
bd946f5
16bfc0d
 
 
 
 
 
 
 
 
 
a370505
16bfc0d
a370505
 
 
16bfc0d
 
a370505
 
 
 
16bfc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a370505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd946f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16bfc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
caab8ee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Postgres Reliability Agent</title>
<style>
  :root {
    --bg: #0d1117; --panel: #161b22; --line: #262d36;
    --fg: #e6edf3; --muted: #8b949e; --dim: #6e7681;
    --accent: #58a6ff; --good: #3fb950; --warn: #d29922; --bad: #f85149;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0; background: var(--bg); color: var(--fg);
    font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    -webkit-font-smoothing: antialiased;
  }
  .wrap { max-width: 860px; margin: 0 auto; padding: 56px 20px 96px; }
  h1 { font-size: 2.1rem; line-height: 1.2; margin: 0 0 12px; letter-spacing: -.02em; }
  h2 { font-size: 1.25rem; margin: 56px 0 16px; letter-spacing: -.01em; }
  h2::before { content: "Β§ "; color: var(--dim); font-weight: 400; }
  p { margin: 0 0 16px; }
  a { color: var(--accent); }
  .lede { font-size: 1.1rem; color: var(--muted); margin-bottom: 28px; }
  code, .mono { font-family: var(--mono); font-size: .875em; }
  code { background: var(--panel); padding: .15em .4em; border-radius: 4px; }

  .scores { display: flex; flex-wrap: wrap; gap: 12px; margin: 28px 0 8px; }
  .score {
    flex: 1 1 150px; background: var(--panel); border: 1px solid var(--line);
    border-radius: 10px; padding: 16px 18px;
  }
  .score b { display: block; font-family: var(--mono); font-size: 1.6rem; color: var(--good); }
  .score span { color: var(--muted); font-size: .82rem; }

  .tablewrap { overflow-x: auto; margin: 0 0 16px; }
  table { border-collapse: collapse; width: 100%; font-size: .9rem; min-width: 560px; }
  th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
  th { color: var(--muted); font-weight: 600; font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; }
  td.k { font-family: var(--mono); font-size: .84rem; white-space: nowrap; }
  .no { color: var(--bad); } .yes { color: var(--good); }

  .finding {
    border: 1px solid var(--line); border-left: 3px solid var(--warn);
    border-radius: 8px; background: var(--panel); padding: 18px 20px; margin: 0 0 14px;
  }
  .finding.high { border-left-color: var(--bad); }
  .finding .tag {
    font-family: var(--mono); font-size: .72rem; color: var(--muted);
    text-transform: uppercase; letter-spacing: .06em;
  }
  .finding h3 { margin: 6px 0 10px; font-size: 1rem; }
  .finding p { margin: 0 0 12px; color: var(--muted); font-size: .92rem; }
  .fix {
    font-family: var(--mono); font-size: .82rem; background: #0b0f14;
    border: 1px solid var(--line); border-radius: 6px; padding: 10px 12px;
    color: var(--good); overflow-x: auto; white-space: pre;
  }
  .ev { margin-top: 12px; font-family: var(--mono); font-size: .78rem; color: var(--dim); }
  .ev span { display: inline-block; margin-right: 14px; }

  .flow {
    font-family: var(--mono); font-size: .82rem; background: var(--panel);
    border: 1px solid var(--line); border-radius: 8px; padding: 18px 20px;
    color: var(--muted); overflow-x: auto; white-space: pre; line-height: 1.7;
  }
  .note {
    border-left: 3px solid var(--line); padding: 2px 0 2px 16px;
    color: var(--muted); font-size: .92rem; margin: 20px 0;
  }
  .links { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
  .links a {
    background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
    padding: 10px 16px; text-decoration: none; font-size: .9rem;
  }
  .links a:hover { border-color: var(--accent); }
  footer { margin-top: 64px; color: var(--dim); font-size: .84rem; border-top: 1px solid var(--line); padding-top: 20px; }
  @media (max-width: 600px) { .wrap { padding: 36px 16px 64px; } h1 { font-size: 1.7rem; } }
</style>
</head>
<body>
<div class="wrap">

  <h1>Postgres Reliability Agent</h1>
  <p class="lede">
    Diagnoses Postgres incidents from the stats views. <strong>No model decides
    what's wrong</strong> β€” deterministic rules and <code>EXPLAIN (GENERIC_PLAN)</code> do.
  </p>

  <div class="scores">
    <div class="score"><b>5/5</b><span>faults detected</span></div>
    <div class="score"><b>5/5</b><span>ranked first</span></div>
    <div class="score"><b>0</b><span>findings when healthy</span></div>
  </div>
  <p class="note">
    That last number is the one that matters. A tool that invents problems on a
    working database is worse than one that misses them β€” nobody reads its
    alerts by week three.
  </p>

  <h2>Why not just ask an LLM</h2>
  <p>
    Ask a model <em>"what's wrong with this database?"</em> and it will tell you
    something, because it is trained to be helpful rather than to say
    <em>"nothing."</em> Detection has to be deterministic; the model belongs
    above that layer, explaining and fixing, not deciding.
  </p>
  <p>
    The scenario set is built to punish the alternative. On <strong>four of the
    five faults the obvious answer is wrong</strong>:
  </p>

  <div class="tablewrap">
  <table>
    <thead><tr><th>fault</th><th>pattern-matching says</th><th>actually correct</th></tr></thead>
    <tbody>
      <tr><td class="k">missing_index</td><td>add an index</td><td class="yes">βœ“ add an index</td></tr>
      <tr><td class="k">plan_regression</td><td>add an index</td><td class="no">βœ— run ANALYZE β€” the schema is fine</td></tr>
      <tr><td class="k">bloat</td><td>the table is just large</td><td class="no">βœ— dead tuples, autovacuum is off</td></tr>
      <tr><td class="k">lock_contention</td><td>kill the slow queries</td><td class="no">βœ— they're victims; one holder is at fault</td></tr>
      <tr><td class="k">n_plus_1</td><td>nothing is slow, it's healthy</td><td class="no">βœ— 1000 calls at 0.04 ms each</td></tr>
      <tr><td class="k">healthy</td><td>find something anyway</td><td class="no">βœ— the answer is no finding</td></tr>
    </tbody>
  </table>
  </div>
  <p>
    Two naive baselines, scored against the same six cases rather than asserted
    β€” <code>python -m evals.baselines</code> reproduces this:
  </p>
  <div class="tablewrap">
  <table>
    <thead><tr><th>approach</th><th>score</th></tr></thead>
    <tbody>
      <tr>
        <td>always name something β€” for a database, usually an index</td>
        <td class="k no">1 / 6</td>
      </tr>
      <tr>
        <td>slowest statement over 10 ms <em>and</em> a large table β†’ missing index</td>
        <td class="k no">2 / 6</td>
      </tr>
      <tr>
        <td>deterministic detectors</td>
        <td class="k yes">6 / 6</td>
      </tr>
    </tbody>
  </table>
  </div>
  <p class="note">
    The second baseline is right twice: it names the one genuine index problem,
    and it stays quiet on the healthy database. On the other four it returns
    <strong>nothing at all</strong> β€” none of those faults presents as a slow
    query. Stale statistics, bloat, a lock holder and an N+1 loop are invisible
    to any heuristic that ranks by duration, which is what most dashboards do.
  </p>

  <h2>The pair that proves it works</h2>
  <p>
    Both of these are a slow query against a multi-million-row table. They need
    opposite fixes, and the agent separates them from the stats alone. Real
    output, unedited:
  </p>

  <div class="finding high">
    <div class="tag">high Β· missing_index</div>
    <h3>Sequential scan on order_items β€” index verified to fix it</h3>
    <p>order_items has 2,500,000 rows and the planner has no index for this
       predicate, so it scans the whole table on every call. Creating product_id
       as a hypothetical index and re-planning drops the estimated cost from
       32,591 to 645 (51Γ—), and the planner chooses it.</p>
    <div class="fix">CREATE INDEX CONCURRENTLY ON order_items (product_id);</div>
    <div class="ev">
      <span>calls: 150</span><span>mean: 79.42 ms</span>
      <span>filter: (product_id = $1)</span>
      <span>cost_before: 32590</span><span>cost_after: 645</span>
      <span>planner_uses_index: true</span>
    </div>
  </div>

  <div class="finding high">
    <div class="tag">high Β· stale_stats</div>
    <h3>Stale planner statistics on orders</h3>
    <p>300,000 rows in orders have changed since the last ANALYZE (23% of the
       table). autovacuum is disabled on this table, so nothing will refresh
       them on its own.</p>
    <div class="fix">ANALYZE orders;
ALTER TABLE orders SET (autovacuum_enabled = true);</div>
    <div class="ev">
      <span>n_mod_since_analyze: 300000</span><span>live_tuples: 1300000</span>
      <span>autovacuum_enabled: false</span>
    </div>
  </div>

  <p class="note">
    An index on the second one would cost write throughput and fix nothing.
    <code>missing_index</code> stays silent there because it requires
    <em>slow</em> <strong>and</strong> a confirmed sequential scan on a large
    table β€” not slowness alone.
  </p>

  <h2>And the one nothing else catches</h2>
  <div class="finding">
    <div class="tag">medium Β· n_plus_1</div>
    <h3>Statement called 1,000 times returning 2.6 rows each</h3>
    <p>Called 1,000 times at 0.044 ms per call. Each call is fast; the cost is
       the round trips. This is an application loop issuing one query per parent
       row.</p>
    <div class="fix">Batch the lookup β€” WHERE fk = ANY($1), or eager-load in the ORM.
No database change needed.</div>
    <div class="ev"><span>total: 43.9 ms</span><span>rows_per_call: 2.64</span></div>
  </div>
  <p>
    The slowest query in that database is <strong>0.044 ms</strong>. Every
    dashboard sorted by duration shows a clean bill of health.
  </p>

  <h2>How it works</h2>
  <div class="flow">collect      one read-only snapshot β€” pg_stat_statements, pg_stat_user_tables,
             pg_stat_activity, the blocking graph, pg_settings
   ↓
detectors    deterministic rules. no model involved.
   ↓
explain      EXPLAIN (GENERIC_PLAN) on suspect statements
   ↓
findings     root cause + fix + the numbers each rule fired on</div>
  <p>
    <code>pg_stat_statements</code> normalizes queries to
    <code>WHERE product_id = $1</code>, so a plain <code>EXPLAIN</code> can't run
    them. <code>GENERIC_PLAN</code> (PG16+) plans them anyway β€” so the agent reads
    the planner's real choice instead of guessing from timings, and never replays
    anything against your data.
  </p>
  <p>
    Every finding carries the evidence it fired on, so it can be checked rather
    than trusted. The connection is read-only with a statement timeout, so it's
    safe to point at a production replica.
  </p>

  <h2>It proves the index before recommending it</h2>
  <p>
    Anything can print <em>β€œadd an index on product_id.”</em> Before it says
    that, this builds the candidate as a <strong>hypothetical</strong> index β€”
    no disk write, no lock, no <code>ACCESS EXCLUSIVE</code> on a 2.5M row table
    β€” re-plans the query, and reads what the planner actually decides.
  </p>
  <div class="tablewrap">
  <table>
    <thead><tr><th>candidate</th><th>estimated cost</th><th>verdict</th></tr></thead>
    <tbody>
      <tr><td class="k">order_items(product_id)</td>
          <td class="k">32,409 β†’ 205 &nbsp;<strong>158Γ—</strong></td>
          <td class="yes">recommend</td></tr>
      <tr><td class="k">users(country)</td>
          <td class="k">4,772 β†’ 3,303 &nbsp;1.4Γ—</td>
          <td class="no">decline</td></tr>
      <tr><td class="k">events(event_type)</td>
          <td class="k">35,602 β†’ 27,179 &nbsp;1.3Γ—</td>
          <td class="no">decline</td></tr>
    </tbody>
  </table>
  </div>
  <p class="note">
    The useful surprise: <em>β€œdoes the planner use it”</em> is a weak test. The
    planner adopted <strong>all three</strong> β€” it will take almost any index
    that shaves a little cost. But an index is paid for on every INSERT and
    UPDATE forever, so the margin decides, not adoption. The bar is 3Γ—.
  </p>
  <p>
    So a suggestion has four possible outcomes rather than one: verified (with
    the delta), marginal (adopted, not worth building), refused (the planner
    won't use it, so an index is the wrong answer), and unverified β€” labelled
    as such rather than quietly implied.
  </p>

  <h2>What it does on a database that isn't mine</h2>
  <p>
    Most Postgres instances don't have <code>pg_stat_statements</code> installed
    and most logins aren't superuser. Every detector declares what it needs and
    is <strong>skipped with a reason</strong> rather than silently returning
    nothing β€” because β€œno findings” and β€œcouldn't look” are different answers
    and a tool that conflates them is worse than useless.
  </p>
  <div class="flow">$ pgra --dsn postgresql://reader@replica/app capabilities

  pg_stat_statements   no    <span style="color:#8b949e">query-level detectors unavailable</span>
  generic_plan         yes
  read_all_stats       no    <span style="color:#8b949e">other users' query text is hidden</span></div>
  <p>
    Run that first against anything unfamiliar and you know which detectors are
    live before you trust a clean report. The three catalog-only detectors β€”
    stale statistics, bloat, lock contention β€” work on any Postgres with no
    extensions at all.
  </p>

  <h2>Limitations</h2>
  <p>
    Five failure modes, not a complete taxonomy β€” real Postgres also fails
    through connection exhaustion, transaction wraparound, replication lag and
    bad migrations. Detection is read-only and advisory: it proposes fixes and
    never applies them. The sandbox is a synthetic e-commerce schema on a single
    Postgres 16 instance, so absolute timings reflect the machine that produced
    them; the ratios are the signal, not the milliseconds.
  </p>

  <h2>Try it</h2>
  <p>
    This page is a static snapshot β€” Hugging Face reserves Docker Spaces for PRO
    accounts. The interactive version and the full scenario sandbox run locally
    in two commands:
  </p>
  <div class="flow">docker compose up -d --build
python -m sandbox.cli inject plan_regression
pgra --dsn postgresql://dbra:dbra@localhost:5433/shopdb diagnose</div>

  <div class="links">
    <a href="https://github.com/Yashmaini30/pg-reliability-agent">GitHub β€” code &amp; eval harness</a>
    <a href="https://huggingface.co/datasets/yashMaini/postgres-incident-diagnosis">Dataset β€” the benchmark</a>
  </div>

  <footer>
    Postgres 16 Β· deterministic detectors Β· 48 unit tests Β· CI runs the scored
    eval on every push. Built by <a href="https://github.com/Yashmaini30">Yash Maini</a>.
  </footer>

</div>
</body>
</html>