Spaces:
Running
Running
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 <strong>158Γ</strong></td>
<td class="yes">recommend</td></tr>
<tr><td class="k">users(country)</td>
<td class="k">4,772 β 3,303 1.4Γ</td>
<td class="no">decline</td></tr>
<tr><td class="k">events(event_type)</td>
<td class="k">35,602 β 27,179 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 & 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>
|