feat(database): use real COUNT(*) for small tables, fall back to estimate for large
row_estimate came from pg_class.reltuples, which is -1 for tables that have never been ANALYZEd (the common case on a fresh/small DB), so the admin table list showed -1 for every table — meaningless. Replace row_estimate with row_count + row_count_estimated: - total_size < 100MB: SELECT count(*) (real value, cost negligible on small tables; identifier quoted via PG escaping to stay injection-safe) - total_size >= 100MB: fall back to reltuples estimate, flagged so the UI can prefix with ~ Frontend now shows real counts (1/2/14...) for small tables and ~N for large ones; header copy updated to reflect the mixed source.
This commit is contained in:
parent
36168f26d7
commit
0b6a076d78
@ -43,8 +43,10 @@ pub struct DbStatus {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct TableInfo {
|
pub struct TableInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// 行数估算(`pg_class.reltuples`,非 COUNT(*),UI 标注"~估算")。
|
/// 行数:total_size 小于阈值时为真实 COUNT(*),否则回退 reltuples 估算(见 row_count_estimated)。
|
||||||
pub row_estimate: i64,
|
pub row_count: i64,
|
||||||
|
/// true 表示 row_count 是 reltuples 估算值(大表回退,未 ANALYZE 时 reltuples=-1 会显示为估算)。
|
||||||
|
pub row_count_estimated: bool,
|
||||||
pub table_size_bytes: i64,
|
pub table_size_bytes: i64,
|
||||||
pub index_size_bytes: i64,
|
pub index_size_bytes: i64,
|
||||||
pub total_size_bytes: i64,
|
pub total_size_bytes: i64,
|
||||||
@ -118,7 +120,9 @@ pub async fn get_db_status() -> Result<DbStatus, ServerFnError> {
|
|||||||
None => (None, None),
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 表清单:行数用 reltuples 估算,避免大表 COUNT(*) 拖垮库
|
// 表清单:大小/统计走 catalog;行数对小表用真实 COUNT(*),大表回退 reltuples 估算。
|
||||||
|
// 阈值 100MB:超过则 COUNT(*) 成本过高(全表扫描),降级为估算值并标注。
|
||||||
|
const COUNT_SIZE_THRESHOLD: i64 = 100 * 1024 * 1024;
|
||||||
let table_rows = client
|
let table_rows = client
|
||||||
.query(
|
.query(
|
||||||
"SELECT c.relname, c.reltuples::bigint, pg_relation_size(c.oid), \
|
"SELECT c.relname, c.reltuples::bigint, pg_relation_size(c.oid), \
|
||||||
@ -134,20 +138,40 @@ pub async fn get_db_status() -> Result<DbStatus, ServerFnError> {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::query)?;
|
.map_err(AppError::query)?;
|
||||||
let tables = table_rows
|
let tables = {
|
||||||
.into_iter()
|
let mut out = Vec::with_capacity(table_rows.len());
|
||||||
.map(|r| TableInfo {
|
for r in table_rows {
|
||||||
name: r.get(0),
|
let name: String = r.get(0);
|
||||||
row_estimate: r.get(1),
|
let reltuples: i64 = r.get(1);
|
||||||
table_size_bytes: r.get(2),
|
let total_size: i64 = r.get(4);
|
||||||
index_size_bytes: r.get(3),
|
// 小表:真实 COUNT(*);大表:回退 reltuples 估算。
|
||||||
total_size_bytes: r.get(4),
|
// relname 来自 pg_class 可信,但表名可能含大写/特殊字符,需 PG 标识符转义(双引号包裹,内部双引号双写)。
|
||||||
last_vacuum: r.get(5),
|
let (row_count, estimated) = if total_size < COUNT_SIZE_THRESHOLD {
|
||||||
last_analyze: r.get(6),
|
let ident = format!("\"{}\"", name.replace('"', "\"\""));
|
||||||
dead_tuples: r.get(7),
|
let cnt: i64 = client
|
||||||
live_tuples: r.get(8),
|
.query_one(&format!("SELECT count(*)::bigint FROM {}", ident), &[])
|
||||||
})
|
.await
|
||||||
.collect();
|
.map_err(AppError::query)?
|
||||||
|
.get(0);
|
||||||
|
(cnt, false)
|
||||||
|
} else {
|
||||||
|
(reltuples, true)
|
||||||
|
};
|
||||||
|
out.push(TableInfo {
|
||||||
|
name,
|
||||||
|
row_count,
|
||||||
|
row_count_estimated: estimated,
|
||||||
|
table_size_bytes: r.get(2),
|
||||||
|
index_size_bytes: r.get(3),
|
||||||
|
total_size_bytes: total_size,
|
||||||
|
last_vacuum: r.get(5),
|
||||||
|
last_analyze: r.get(6),
|
||||||
|
dead_tuples: r.get(7),
|
||||||
|
live_tuples: r.get(8),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
// 索引占用 Top 10
|
// 索引占用 Top 10
|
||||||
let index_rows = client
|
let index_rows = client
|
||||||
|
|||||||
@ -273,17 +273,17 @@ fn DbStatusTab() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表清单
|
// 表清单(小表显示真实行数;大表回退估算,行数前标 ~)
|
||||||
div { class: "{ADMIN_TABLE_CLASS}",
|
div { class: "{ADMIN_TABLE_CLASS}",
|
||||||
div { class: "px-4 py-3 border-b border-paper-border text-sm font-medium text-paper-primary",
|
div { class: "px-4 py-3 border-b border-paper-border text-sm font-medium text-paper-primary",
|
||||||
"表清单(~行数为估算)"
|
"表清单(行数:小表为真实值,大表标 ~ 为估算)"
|
||||||
}
|
}
|
||||||
div { class: "overflow-x-auto",
|
div { class: "overflow-x-auto",
|
||||||
table { class: "w-full text-sm",
|
table { class: "w-full text-sm",
|
||||||
thead {
|
thead {
|
||||||
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
||||||
th { class: "px-4 py-2 font-medium", "表名" }
|
th { class: "px-4 py-2 font-medium", "表名" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "~行数" }
|
th { class: "px-4 py-2 font-medium text-right", "行数" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "表大小" }
|
th { class: "px-4 py-2 font-medium text-right", "表大小" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "索引大小" }
|
th { class: "px-4 py-2 font-medium text-right", "索引大小" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "总大小" }
|
th { class: "px-4 py-2 font-medium text-right", "总大小" }
|
||||||
@ -294,7 +294,9 @@ fn DbStatusTab() -> Element {
|
|||||||
for t in s.tables.iter() {
|
for t in s.tables.iter() {
|
||||||
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
||||||
td { class: "px-4 py-2 font-mono text-paper-primary", "{t.name}" }
|
td { class: "px-4 py-2 font-mono text-paper-primary", "{t.name}" }
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{t.row_estimate}" }
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
if t.row_count_estimated { "~{t.row_count}" } else { "{t.row_count}" }
|
||||||
|
}
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{format_bytes(t.table_size_bytes)}" }
|
td { class: "px-4 py-2 text-right text-paper-secondary", "{format_bytes(t.table_size_bytes)}" }
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{format_bytes(t.index_size_bytes)}" }
|
td { class: "px-4 py-2 text-right text-paper-secondary", "{format_bytes(t.index_size_bytes)}" }
|
||||||
td { class: "px-4 py-2 text-right text-paper-primary font-medium", "{format_bytes(t.total_size_bytes)}" }
|
td { class: "px-4 py-2 text-right text-paper-primary font-medium", "{format_bytes(t.total_size_bytes)}" }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user