{t('table_overview.metric.relative_size')}
- {maxCombinedSize > 0 ? `${Math.round(sizeRatio * 100)}%` : '—'}
+ {maxCombinedSize > 0 && hasKnownTableSize(table) ? `${Math.round(sizeRatio * 100)}%` : '—'}
diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go
index a98a174a..668ba174 100644
--- a/internal/app/methods_db.go
+++ b/internal/app/methods_db.go
@@ -1964,6 +1964,14 @@ func (a *App) DBGetTables(config connection.ConnectionConfig, dbName string) con
logger.Warnf("DBGetTables 获取表行数失败(保留已获取的表列表):%s err=%v", formatConnSummary(runConfig), countErr)
}
}
+ tableStorageStats := map[string]db.TableStorageStats{}
+ if storageProvider, ok := dbInst.(db.TableStorageStatsProvider); ok {
+ var storageErr error
+ tableStorageStats, storageErr = storageProvider.GetTableStorageStats(dbName, tables)
+ if storageErr != nil {
+ logger.Warnf("DBGetTables 获取表存储大小失败(保留已获取的表列表):%s err=%v", formatConnSummary(runConfig), storageErr)
+ }
+ }
resData := make([]map[string]string, 0, len(tables))
for _, name := range tables {
@@ -1971,6 +1979,10 @@ func (a *App) DBGetTables(config connection.ConnectionConfig, dbName string) con
if rowCount, ok := tableRowCounts[name]; ok {
item["Rows"] = strconv.FormatInt(rowCount, 10)
}
+ if storageStats, ok := tableStorageStats[name]; ok {
+ item["Data_length"] = strconv.FormatInt(storageStats.DataLength, 10)
+ item["Index_length"] = strconv.FormatInt(storageStats.IndexLength, 10)
+ }
resData = append(resData, item)
}
diff --git a/internal/db/database.go b/internal/db/database.go
index 0e08f885..c6f51d97 100644
--- a/internal/db/database.go
+++ b/internal/db/database.go
@@ -50,6 +50,17 @@ type TableRowCounter interface {
GetTableRowCounts(dbName string, tables []string) (map[string]int64, error)
}
+// TableStorageStatsProvider is an optional metadata interface for drivers that
+// can report per-table data and index storage usage in bytes.
+type TableStorageStatsProvider interface {
+ GetTableStorageStats(dbName string, tables []string) (map[string]TableStorageStats, error)
+}
+
+type TableStorageStats struct {
+ DataLength int64
+ IndexLength int64
+}
+
func getSQLiteTableRowCounts(query func(string) ([]map[string]interface{}, []string, error), tables []string) (map[string]int64, error) {
counts := make(map[string]int64, len(tables))
var firstErr error
@@ -88,6 +99,80 @@ func getSQLiteTableRowCounts(query func(string) ([]map[string]interface{}, []str
return counts, firstErr
}
+func getSQLiteTableStorageStats(query func(string) ([]map[string]interface{}, []string, error), tables []string) (map[string]TableStorageStats, error) {
+ requestedTables := make(map[string]struct{}, len(tables))
+ for _, rawTableName := range tables {
+ tableName := strings.TrimSpace(rawTableName)
+ if tableName != "" {
+ requestedTables[tableName] = struct{}{}
+ }
+ }
+ if len(requestedTables) == 0 {
+ return map[string]TableStorageStats{}, nil
+ }
+
+ data, _, err := query(`
+WITH object_sizes AS (
+ SELECT name, SUM(pgsize) AS bytes
+ FROM dbstat
+ GROUP BY name
+), index_sizes AS (
+ SELECT idx.tbl_name AS table_name, SUM(object_sizes.bytes) AS bytes
+ FROM sqlite_master AS idx
+ JOIN object_sizes ON object_sizes.name = idx.name
+ WHERE idx.type = 'index'
+ GROUP BY idx.tbl_name
+)
+SELECT
+ tbl.name AS table_name,
+ COALESCE(table_sizes.bytes, 0) AS data_length,
+ COALESCE(index_sizes.bytes, 0) AS index_length
+FROM sqlite_master AS tbl
+LEFT JOIN object_sizes AS table_sizes ON table_sizes.name = tbl.name
+LEFT JOIN index_sizes ON index_sizes.table_name = tbl.name
+WHERE tbl.type = 'table'`)
+ if err != nil {
+ return map[string]TableStorageStats{}, fmt.Errorf("读取 SQLite 表存储大小失败: %w", err)
+ }
+
+ stats := make(map[string]TableStorageStats, len(data))
+ for _, row := range data {
+ tableName := strings.TrimSpace(fmt.Sprint(metadataRowValue(row, "table_name")))
+ if tableName == "" {
+ continue
+ }
+ if len(requestedTables) > 0 {
+ if _, ok := requestedTables[tableName]; !ok {
+ continue
+ }
+ }
+ dataLength, dataErr := metadataInt64(row, "data_length")
+ indexLength, indexErr := metadataInt64(row, "index_length")
+ if dataErr != nil || indexErr != nil || dataLength < 0 || indexLength < 0 {
+ return map[string]TableStorageStats{}, fmt.Errorf("读取 SQLite 表 %q 存储大小失败: data_length=%v index_length=%v", tableName, metadataRowValue(row, "data_length"), metadataRowValue(row, "index_length"))
+ }
+ stats[tableName] = TableStorageStats{DataLength: dataLength, IndexLength: indexLength}
+ }
+ return stats, nil
+}
+
+func metadataRowValue(row map[string]interface{}, key string) interface{} {
+ for rowKey, value := range row {
+ if strings.EqualFold(strings.TrimSpace(rowKey), key) {
+ return value
+ }
+ }
+ return nil
+}
+
+func metadataInt64(row map[string]interface{}, key string) (int64, error) {
+ value := metadataRowValue(row, key)
+ if value == nil {
+ return 0, fmt.Errorf("查询结果缺少 %s", key)
+ }
+ return strconv.ParseInt(strings.TrimSpace(fmt.Sprint(value)), 10, 64)
+}
+
// MultiResultQuerier 是可选接口,支持多结果集的驱动实现此接口。
// 执行可能包含多条 SQL 语句的查询,返回所有结果集。
type MultiResultQuerier interface {
diff --git a/internal/db/optional_driver_agent_impl.go b/internal/db/optional_driver_agent_impl.go
index 07b07628..a95873f0 100644
--- a/internal/db/optional_driver_agent_impl.go
+++ b/internal/db/optional_driver_agent_impl.go
@@ -861,6 +861,20 @@ func (d *OptionalDriverAgentDB) GetTables(dbName string) ([]string, error) {
return tables, nil
}
+func (d *OptionalDriverAgentDB) GetTableRowCounts(_ string, tables []string) (map[string]int64, error) {
+ if normalizeRuntimeDriverType(d.driverType) != "sqlite" {
+ return map[string]int64{}, nil
+ }
+ return getSQLiteTableRowCounts(d.Query, tables)
+}
+
+func (d *OptionalDriverAgentDB) GetTableStorageStats(_ string, tables []string) (map[string]TableStorageStats, error) {
+ if normalizeRuntimeDriverType(d.driverType) != "sqlite" {
+ return map[string]TableStorageStats{}, nil
+ }
+ return getSQLiteTableStorageStats(d.Query, tables)
+}
+
func (d *OptionalDriverAgentDB) GetCreateStatement(dbName, tableName string) (string, error) {
client, err := d.requireClient()
if err != nil {
diff --git a/internal/db/optional_driver_agent_impl_test.go b/internal/db/optional_driver_agent_impl_test.go
index e1612ac5..d4a5b04c 100644
--- a/internal/db/optional_driver_agent_impl_test.go
+++ b/internal/db/optional_driver_agent_impl_test.go
@@ -168,6 +168,47 @@ func TestOptionalDriverAgentDBQueryWithMessagesParsesAgentMessages(t *testing.T)
}
}
+func TestOptionalDriverAgentDBProvidesSQLiteTableStats(t *testing.T) {
+ var stdin optionalAgentTestWriteCloser
+ stdout := strings.Join([]string{
+ `{"id":1,"success":true,"data":[{"table_rows":2}],"fields":["table_rows"]}`,
+ `{"id":2,"success":true,"data":[{"table_name":"orders","data_length":4096,"index_length":8192}],"fields":["table_name","data_length","index_length"]}`,
+ }, "\n") + "\n"
+
+ dbInst := &OptionalDriverAgentDB{
+ driverType: "sqlite",
+ client: &optionalDriverAgentClient{
+ stdin: &stdin,
+ reader: bufio.NewReader(strings.NewReader(stdout)),
+ driver: "sqlite",
+ },
+ }
+
+ rowCounts, err := dbInst.GetTableRowCounts("main", []string{"orders"})
+ if err != nil {
+ t.Fatalf("GetTableRowCounts 返回错误: %v", err)
+ }
+ if rowCounts["orders"] != 2 {
+ t.Fatalf("SQLite driver-agent 行数异常: %#v", rowCounts)
+ }
+
+ storageStats, err := dbInst.GetTableStorageStats("main", []string{"orders"})
+ if err != nil {
+ t.Fatalf("GetTableStorageStats 返回错误: %v", err)
+ }
+ if storageStats["orders"].DataLength != 4096 || storageStats["orders"].IndexLength != 8192 {
+ t.Fatalf("SQLite driver-agent 存储统计异常: %#v", storageStats)
+ }
+
+ requests := stdin.String()
+ if !strings.Contains(requests, `SELECT COUNT(*) AS table_rows FROM \"orders\"`) {
+ t.Fatalf("driver-agent 未执行 SQLite 行数查询: %s", requests)
+ }
+ if !strings.Contains(requests, "FROM dbstat") {
+ t.Fatalf("driver-agent 未执行 SQLite dbstat 查询: %s", requests)
+ }
+}
+
func TestOptionalDriverAgentDBQueryMultiWithMessagesParsesResultSets(t *testing.T) {
var stdin optionalAgentTestWriteCloser
stdout := `{"id":1,"success":true,"data":[{"statementIndex":1,"rows":[{"name":"master"}],"columns":["name"]},{"statementIndex":1,"rows":[],"columns":[],"messages":["PRINT generated sql"]}],"messages":["batch top-level message"]}` + "\n"