feat(status): 启用 pg_stat_statements,运行状态页显示最慢 / 最频繁语句与各表死元组;PG 调低 random_page_cost

- docker-compose.yml 的 postgres 加 shared_preload_libraries=pg_stat_statements、pg_stat_statements.track=top;
  要重建 postgres 容器才生效(docs/runbook/storage.md)
- GET /system/storage 的 db.statements:平均最慢(调用 ≥ 10 次)与调用最多各 5 条;没装扩展 / 没预加载时
  enabled=false 并说明原因
- 运行状态页「存储」:体积最大的表加死元组列,新增「数据库语句统计」
- random_page_cost 4 → 1.1(PG_RANDOM_PAGE_COST):库整个在内存里,缺省值高估随机读

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 20:24:34 +08:00
1 parent 8c8b252667
commit 63bbcf3cdb
5 files changed
+163 -1

No files matched your search

@@ -200,6 +200,72 @@ function CleanupTable({ rows }: { rows: CleanupRow[] }) {
}
/** 运行状态页的「存储」节 */
type Statements = SystemStorage["db"]["statements"];
type StatementRow = Statements["slowest"][number];
const fmtMs = (ms: number) =>
ms >= 1000 ? `${(ms / 1000).toFixed(1)} 秒` : `${ms < 10 ? ms.toFixed(2) : Math.round(ms)} 毫秒`;
function StatementTable({ rows }: { rows: StatementRow[] }) {
return (
<DataTable<StatementRow>
rows={rows}
rowKey={(r) => r.query}
empty={{ title: "还没有统计" }}
columns={[
{
key: "query",
header: "语句",
cell: (r) => (
<span className="line-clamp-2 font-mono text-meta break-all" title={r.query}>
{r.query}
</span>
),
},
{
key: "calls",
header: "次数",
align: "right",
cell: (r) => <span className="tabular-nums">{r.calls.toLocaleString("zh-CN")}</span>,
},
{
key: "mean",
header: "平均",
align: "right",
cell: (r) => <span className="whitespace-nowrap tabular-nums">{fmtMs(r.meanMs)}</span>,
},
{
key: "total",
header: "合计",
align: "right",
hideBelow: "sm",
cell: (r) => <span className="whitespace-nowrap tabular-nums text-muted-foreground">{fmtMs(r.totalMs)}</span>,
},
]}
/>
);
}
/** 数据库语句统计(pg_stat_statements,见 docs/runbook/storage.md「数据库性能」) */
function StatementsInfo({ st }: { st: Statements }) {
return (
<TechInfo title="数据库语句统计" columns={1}>
{st.enabled ? (
<div className="grid gap-6 xl:grid-cols-2">
<Panel title="平均最慢" note={`调用 10 次以上${st.since ? ` · 统计自 ${formatDateTime(st.since)}` : ""}`}>
<StatementTable rows={st.slowest} />
</Panel>
<Panel title="调用最多">
<StatementTable rows={st.frequent} />
</Panel>
</div>
) : (
<p className="text-caption text-muted-foreground">语句统计未启用:{st.note}</p>
)}
</TechInfo>
);
}
export function StorageSection({ health }: { health: SystemHealth }) {
const q = useSystemStorage();
const s = q.data;
@@ -305,6 +371,17 @@ export function StorageSection({ health }: { health: SystemHealth }) {
</span>
),
},
{
key: "dead",
header: "死元组",
align: "right",
hideBelow: "md",
cell: (t) => (
<span className="tabular-nums text-muted-foreground">
{t.deadRows === null ? "—" : t.deadRows.toLocaleString("zh-CN")}
</span>
),
},
]}
/>
</Panel>
@@ -391,6 +468,8 @@ export function StorageSection({ health }: { health: SystemHealth }) {
</div>
</div>
<StatementsInfo st={s.db.statements} />
<Panel title="清理任务" note="立即清理会把对应的维护任务排入执行,结果看运行记录">
<CleanupTable rows={s.cleanup} />
</Panel>