package main import ( "TsDB2/DB" "TsDB2/DB/core" "TsDB2/DB/core/page" "TsDB2/DB/core/types" "fmt" "os" "time" ) const dbPath = "vacuum_demo.db" func main() { os.Remove(dbPath) // ======================================================================== // Step 1: 创建一个测试库, 写 100,000 行 // ======================================================================== fmt.Println("=== Step 1: 建库 + 写 100,000 行 ===") db, err := DB.Open(dbPath) if err != nil { panic(err) } db.SetAutoCommitDelay(0) tbl, err := db.CoreDB().CreateTable("events", []types.FieldInfo{ {FieldType: types.FieldType_Uint64, FieldName: []byte("id")}, {FieldType: types.FieldType_Uint64, FieldName: []byte("ts")}, {FieldType: types.FieldType_Uint64, FieldName: []byte("value")}, }) if err != nil { panic(err) } // 主键 if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil { panic(err) } // 时序二级索引 (按 ts) if _, err := tbl.CreateIndex("ts_idx", page.IndexTypeBTree, []byte{1}, false); err != nil { panic(err) } start := time.Now() for i := 1; i <= 100_000; i++ { if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{ {FieldName: []byte("id"), Value: uint64(i)}, {FieldName: []byte("ts"), Value: uint64(1_700_000_000 + i)}, {FieldName: []byte("value"), Value: uint64(i % 1000)}, }); err != nil { panic(fmt.Errorf("插入第 %d 行: %w", i, err)) } if i%10_000 == 0 { fmt.Printf(" 写入进度: %d / 100,000 (%.1fs)\n", i, time.Since(start).Seconds()) } } if err := db.Commit(); err != nil { panic(err) } writeDur := time.Since(start) beforeSize, _ := os.Stat(dbPath) fmt.Printf("写完 100,000 行 + 2 索引, 耗时 %.1fs, 文件大小 %s\n\n", writeDur.Seconds(), humanSize(beforeSize.Size())) // ======================================================================== // Step 2: 删掉 70% 数据, 制造大量墓碑 // ======================================================================== fmt.Println("=== Step 2: 删除 70% 数据 (70,000 行) ===") delStart := time.Now() for i := uint64(1); i <= 70_000; i++ { if err := tbl.DeleteRowByPK(i); err != nil { panic(err) } if i%10_000 == 0 { fmt.Printf(" 删除进度: %d / 70,000 (%.1fs)\n", i, time.Since(delStart).Seconds()) } } if err := db.Commit(); err != nil { panic(err) } delDur := time.Since(delStart) afterDeleteSize, _ := os.Stat(dbPath) fmt.Printf("删完 70,000 行, 耗时 %.1fs, 文件大小 %s (只打墓碑, 不太缩)\n\n", delDur.Seconds(), humanSize(afterDeleteSize.Size())) c, _ := tbl.Query().Count() fmt.Printf("剩余 %d 行 (含活行 + 墓碑)\n\n", c) // ======================================================================== // Step 3: VACUUM 整库重写 // ======================================================================== fmt.Println("=== Step 3: VACUUM 整库重写 ===") vacStart := time.Now() var lastPhase string res, err := db.VACUUM(core.VACUUMOptions{ Progress: func(phase string, copied, total int64) { // 只在阶段切换时打, 避免刷屏 if phase != lastPhase { fmt.Printf(" [phase=%s] 已复制 %d / %d\n", phase, copied, total) lastPhase = phase } }, }) if err != nil { panic(err) } vacDur := time.Since(vacStart) fmt.Println() fmt.Println("=== VACUUM 结果 ===") fmt.Printf(" 旧文件大小: %s\n", humanSize(res.OldFileSize)) fmt.Printf(" 新文件大小: %s\n", humanSize(res.NewFileSize)) fmt.Printf(" 节省空间: %s (%.1f%%)\n", humanSize(res.SavedBytes), res.SavedPercent) fmt.Printf(" 重建表数: %d\n", res.Tables) fmt.Printf(" 复制行数: %d\n", res.RowsCopied) fmt.Printf(" VACUUM 耗时: %.1fs\n", vacDur.Seconds()) fmt.Println() // ======================================================================== // Step 4: 验证 // ======================================================================== fmt.Println("=== Step 4: 验证 VACUUM 后数据正确 ===") c2, _ := tbl.Query().Count() fmt.Printf(" VACUUM 后剩余行数: %d (应为 30,000)\n", c2) if c2 != 30000 { fmt.Printf(" ✗ 错误! 应为 30,000, 实际 %d\n", c2) } else { fmt.Println(" ✓ 行数正确") } // 抽样校验: id=70001 应在 (前 70000 已删), id=100000 应在 for _, id := range []uint64{70001, 85000, 99999, 100000} { row, err := tbl.FindByPK(id) if err != nil { fmt.Printf(" ✗ 查 id=%d 失败: %v\n", id, err) continue } fmt.Printf(" ✓ id=%d 存在: ts=%v value=%v\n", id, row[1].Value, row[2].Value) } // 验证已删的 id 不存在 for _, id := range []uint64{1, 35000, 70000} { _, err := tbl.FindByPK(id) if err != nil { fmt.Printf(" ✓ id=%d 已删: %v\n", id, err) } else { fmt.Printf(" ✗ id=%d 应已删, 但仍能查到\n", id) } } // 验证时序索引 (二级索引在 VACUUM 后应可查) row, err := tbl.SearchByIndex("ts_idx", encodeUint64BE(1_700_000_000+80000)) if err != nil { fmt.Printf(" ✗ 二级索引 ts_idx 查 id=80000 失败: %v\n", err) } else { fmt.Printf(" ✓ 二级索引 ts_idx 工作: id=%v ts=%v value=%v\n", row[0].Value, row[1].Value, row[2].Value) } // ======================================================================== // Step 5: 自动 VACUUM 演示 // ======================================================================== fmt.Println() fmt.Println("=== Step 5: SetAutoVACUUM 自动调度演示 ===") // 设 5s 间隔, demo 期间不等待触发 (timer 在后台运行, 生产可设 1h+) db.SetAutoVACUUM(5 * time.Second) fmt.Println(" 已设置自动 VACUUM 间隔 = 5s (timer 在后台运行, 生产可设 1h+)") fmt.Printf(" 当前 LastVACUUM: %v\n", db.LastVACUUM().Format("15:04:05.000")) _ = db.LastVACUUM // 占位, 防 lint 报 unused // 检查文件最终大小 finalSize, _ := os.Stat(dbPath) fmt.Printf(" 最终文件大小: %s\n\n", humanSize(finalSize.Size())) fmt.Println(" 准备 Close db...") if err := db.Close(); err != nil { fmt.Printf(" Close 报错: %v\n", err) fmt.Println("=== 完成 (Close 出错但不影响 VACUUM 演示) ===") return } fmt.Println(" Close 成功") fmt.Println("=== 完成 ===") } func humanSize(n int64) string { const ( KB = 1024 MB = 1024 * 1024 GB = 1024 * 1024 * 1024 ) switch { case n >= GB: return fmt.Sprintf("%.2f GB", float64(n)/GB) case n >= MB: return fmt.Sprintf("%.2f MB", float64(n)/MB) case n >= KB: return fmt.Sprintf("%.1f KB", float64(n)/KB) default: return fmt.Sprintf("%d B", n) } } func encodeUint64BE(v uint64) []byte { b := make([]byte, 8) for i := 7; i >= 0; i-- { b[i] = byte(v) v >>= 8 } return b }