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)]
|
||||
pub struct TableInfo {
|
||||
pub name: String,
|
||||
/// 行数估算(`pg_class.reltuples`,非 COUNT(*),UI 标注"~估算")。
|
||||
pub row_estimate: i64,
|
||||
/// 行数:total_size 小于阈值时为真实 COUNT(*),否则回退 reltuples 估算(见 row_count_estimated)。
|
||||
pub row_count: i64,
|
||||
/// true 表示 row_count 是 reltuples 估算值(大表回退,未 ANALYZE 时 reltuples=-1 会显示为估算)。
|
||||
pub row_count_estimated: bool,
|
||||
pub table_size_bytes: i64,
|
||||
pub index_size_bytes: i64,
|
||||
pub total_size_bytes: i64,
|
||||
@ -118,7 +120,9 @@ pub async fn get_db_status() -> Result<DbStatus, ServerFnError> {
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
// 表清单:行数用 reltuples 估算,避免大表 COUNT(*) 拖垮库
|
||||
// 表清单:大小/统计走 catalog;行数对小表用真实 COUNT(*),大表回退 reltuples 估算。
|
||||
// 阈值 100MB:超过则 COUNT(*) 成本过高(全表扫描),降级为估算值并标注。
|
||||
const COUNT_SIZE_THRESHOLD: i64 = 100 * 1024 * 1024;
|
||||
let table_rows = client
|
||||
.query(
|
||||
"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
|
||||
.map_err(AppError::query)?;
|
||||
let tables = table_rows
|
||||
.into_iter()
|
||||
.map(|r| TableInfo {
|
||||
name: r.get(0),
|
||||
row_estimate: r.get(1),
|
||||
table_size_bytes: r.get(2),
|
||||
index_size_bytes: r.get(3),
|
||||
total_size_bytes: r.get(4),
|
||||
last_vacuum: r.get(5),
|
||||
last_analyze: r.get(6),
|
||||
dead_tuples: r.get(7),
|
||||
live_tuples: r.get(8),
|
||||
})
|
||||
.collect();
|
||||
let tables = {
|
||||
let mut out = Vec::with_capacity(table_rows.len());
|
||||
for r in table_rows {
|
||||
let name: String = r.get(0);
|
||||
let reltuples: i64 = r.get(1);
|
||||
let total_size: i64 = r.get(4);
|
||||
// 小表:真实 COUNT(*);大表:回退 reltuples 估算。
|
||||
// relname 来自 pg_class 可信,但表名可能含大写/特殊字符,需 PG 标识符转义(双引号包裹,内部双引号双写)。
|
||||
let (row_count, estimated) = if total_size < COUNT_SIZE_THRESHOLD {
|
||||
let ident = format!("\"{}\"", name.replace('"', "\"\""));
|
||||
let cnt: i64 = client
|
||||
.query_one(&format!("SELECT count(*)::bigint FROM {}", ident), &[])
|
||||
.await
|
||||
.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
|
||||
let index_rows = client
|
||||
|
||||
@ -273,17 +273,17 @@ fn DbStatusTab() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
// 表清单
|
||||
// 表清单(小表显示真实行数;大表回退估算,行数前标 ~)
|
||||
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: "overflow-x-auto",
|
||||
table { class: "w-full text-sm",
|
||||
thead {
|
||||
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 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() {
|
||||
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 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.index_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