| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- // Hash 索引专项验证 (线性哈希重写后的正确性证明)
- //
- // 覆盖点:
- // 1. 大量分裂: 30000 行、负载因子 400 → 桶数必然 >= 75 (最小 16), 即至少 59 次分裂
- // 2. 热点/重复 key: Age=25 约占 1/3 (约 10000 行落在同一个桶的溢出链上)
- // 3. 精确性: 每个不同 Age 值的行数、以及抽样主键都必须一条不差地找回来
- // 4. 大量合并: 删到只剩 500 行 → 触发收缩/合并
- // 5. RangeScan: 哈希不保序, 但必须做到键过滤 + 排序正确 (含反向)
- // 6. 持久化: 关闭重开后数量与查找结果一致
- // 7. 空间: DB 文件不得随分裂/删除无限膨胀
- package main
- import (
- "TsDB2/DB"
- "fmt"
- "os"
- "time"
- )
- type HV struct {
- DB.BaseModel
- Name string `vrtx:"maxlen:32"`
- Age int64 `vrtx:"index:idx_age,type:hash"`
- Score int64
- Dept string `vrtx:"maxlen:16"`
- }
- var passed, failed int
- func check(msg string, ok bool, detail ...string) {
- if ok {
- passed++
- fmt.Printf(" [PASS] %s\n", msg)
- } else {
- failed++
- d := ""
- if len(detail) > 0 {
- d = " — " + detail[0]
- }
- fmt.Printf(" [FAIL] %s%s\n", msg, d)
- }
- }
- func section(t string) { fmt.Printf("\n========== %s\n", t) }
- const (
- totalRows = 30000
- hotAge = int64(25)
- keepRows = 500
- )
- // ageOf 构造分布: 1/3 的行集中在 hotAge, 其余散布在 20..219
- func ageOf(i int) int64 {
- if i%3 == 0 {
- return hotAge
- }
- return int64(20 + i%200)
- }
- func main() {
- path := fmt.Sprintf("hash_verify_%d.db", time.Now().UnixNano())
- _ = os.Remove(path)
- fmt.Println("============================================================")
- fmt.Println(" Hash 索引专项验证 (Linear Hashing)")
- fmt.Println("============================================================")
- db, err := DB.Open(path)
- if err != nil {
- panic(err)
- }
- db.SetAutoCommitDelay(0)
- db.SetBufferPoolCapacity(4000)
- tbl, err := DB.UseTable[HV](db, "hv")
- if err != nil {
- panic(err)
- }
- // idx_age (Hash) 已通过 vrtx 标签自动创建
- coreTbl, _ := db.CoreDB().FindTable("hv")
- // 期望分布
- want := map[int64]int{}
- for i := 1; i <= totalRows; i++ {
- want[ageOf(i)]++
- }
- // ---------------------------------------------------------------
- section("1. 插入 30000 行 (必然触发多次桶分裂)")
- t0 := time.Now()
- for i := 1; i <= totalRows; i++ {
- if err := tbl.Insert(&HV{
- Name: fmt.Sprintf("N%06d", i),
- Age: ageOf(i),
- Score: int64(i % 500),
- Dept: fmt.Sprintf("D%d", i%20),
- }); err != nil {
- check(fmt.Sprintf("Insert %d", i), false, err.Error())
- break
- }
- }
- if err := db.Commit(); err != nil {
- panic(err)
- }
- cnt, _ := tbl.Count()
- check(fmt.Sprintf("插入 %d 行 耗时 %.2fs, Count=%d", totalRows, time.Since(t0).Seconds(), cnt), cnt == int64(totalRows))
- // ---------------------------------------------------------------
- section("2. 索引精确性 (每个 Age 值 + 抽样主键)")
- bad := 0
- for age, exp := range want {
- got, err := tbl.Where("Age", "=", age).Count()
- if err != nil || got != int64(exp) {
- bad++
- if bad <= 3 {
- fmt.Printf(" Age=%d 期望 %d 实得 %d err=%v\n", age, exp, got, err)
- }
- }
- }
- check(fmt.Sprintf("全部 %d 个不同 Age 值的行数完全一致", len(want)), bad == 0)
- // 走 SearchByIndex (handler.Search) 逐个 Age 验证。
- // 键必须交给引擎编码: int64 列现在落 FieldType_Int64, 用保序编码 (符号位翻转),
- // 不再是原始大端 —— 手拼 uint64(age) 的键在 v7 之后与盘上键不匹配, 一律 not found。
- missSearch := 0
- for age := range want {
- key, err := coreTbl.BuildIndexKey("idx_age", age)
- if err != nil {
- missSearch++
- continue
- }
- pk, e := coreTbl.SearchByIndex("idx_age", key)
- if e != nil || len(pk) == 0 {
- missSearch++
- }
- }
- check(fmt.Sprintf("SearchByIndex 覆盖全部 %d 个 Age 值", len(want)), missSearch == 0)
- // 抽样主键回表
- pkBad := 0
- for i := 1; i <= totalRows; i += 37 {
- row, e := tbl.Find(uint64(i))
- if e != nil || row == nil || row.Age != ageOf(i) {
- pkBad++
- }
- }
- totalSample := 0
- for i := 1; i <= totalRows; i += 37 {
- totalSample++
- }
- check(fmt.Sprintf("抽样 %d 个主键回表一致", totalSample), pkBad == 0)
- // ---------------------------------------------------------------
- section("3. RangeScan (键过滤 + 排序, 哈希不保序)")
- expRange := int64(0)
- for age, c := range want {
- if age >= 30 && age <= 40 {
- expRange += int64(c)
- }
- }
- gotRange, err := tbl.Where("Age", ">=", int64(30)).Where("Age", "<=", int64(40)).Count()
- check(fmt.Sprintf("范围 [30,40] Count=%d (期望 %d)", gotRange, expRange), err == nil && gotRange == expRange)
- asc, err := tbl.Where("Age", ">=", int64(30)).Where("Age", "<=", int64(40)).OrderBy("Age", false).Limit(50).Find()
- ascOK := err == nil && len(asc) > 0
- if ascOK {
- for i := 1; i < len(asc); i++ {
- if asc[i-1].Age > asc[i].Age {
- ascOK = false
- break
- }
- }
- for _, r := range asc {
- if r.Age < 30 || r.Age > 40 {
- ascOK = false
- break
- }
- }
- }
- check(fmt.Sprintf("范围 [30,40] OrderBy Age 升序, 取回 %d 行", len(asc)), ascOK)
- desc, err := tbl.Where("Age", ">=", int64(30)).Where("Age", "<=", int64(40)).OrderBy("Age", true).Limit(50).Find()
- descOK := err == nil && len(desc) > 0
- if descOK {
- for i := 1; i < len(desc); i++ {
- if desc[i-1].Age < desc[i].Age {
- descOK = false
- break
- }
- }
- }
- check(fmt.Sprintf("范围 [30,40] OrderBy Age 降序, 取回 %d 行", len(desc)), descOK)
- // ---------------------------------------------------------------
- section("4. 大量删除 (触发桶合并/收缩)")
- keep := map[uint64]bool{}
- nk := 0
- for i := 1; i <= totalRows && nk < keepRows; i++ {
- if ageOf(i) == hotAge {
- keep[uint64(i)] = true
- nk++
- }
- }
- t1 := time.Now()
- delFail := 0
- for i := 1; i <= totalRows; i++ {
- if keep[uint64(i)] {
- continue
- }
- if err := tbl.Delete(uint64(i)); err != nil {
- delFail++
- }
- }
- if err := db.Commit(); err != nil {
- panic(err)
- }
- check(fmt.Sprintf("删除 %d 行 耗时 %.2fs (0 失败)", totalRows-keepRows, time.Since(t1).Seconds()), delFail == 0)
- cnt2, _ := tbl.Count()
- check(fmt.Sprintf("删除后 Count=%d (期望 %d)", cnt2, keepRows), cnt2 == int64(keepRows))
- hotCnt, err := tbl.Where("Age", "=", hotAge).Count()
- check(fmt.Sprintf("删除后 Age=%d Count=%d (期望 %d)", hotAge, hotCnt, keepRows), err == nil && hotCnt == int64(keepRows))
- otherCnt, err := tbl.Where("Age", ">=", int64(30)).Count()
- check(fmt.Sprintf("删除后 Age>=30 Count=%d (期望 0)", otherCnt), err == nil && otherCnt == 0)
- // ---------------------------------------------------------------
- section("5. 关闭重开 (持久化)")
- if err := db.Close(); err != nil {
- panic(err)
- }
- db, err = DB.Open(path)
- if err != nil {
- panic(err)
- }
- db.SetAutoCommitDelay(0)
- db.SetBufferPoolCapacity(4000)
- tbl, err = DB.UseTable[HV](db, "hv")
- if err != nil {
- panic(err)
- }
- coreTbl, _ = db.CoreDB().FindTable("hv")
- cnt3, _ := tbl.Count()
- check(fmt.Sprintf("重开后 Count=%d (期望 %d)", cnt3, keepRows), cnt3 == int64(keepRows))
- hotCnt2, err := tbl.Where("Age", "=", hotAge).Count()
- check(fmt.Sprintf("重开后 Age=%d Count=%d (期望 %d)", hotAge, hotCnt2, keepRows), err == nil && hotCnt2 == int64(keepRows))
- reopenBad := 0
- for pk := range keep {
- row, e := tbl.Find(pk)
- if e != nil || row == nil || row.Age != hotAge {
- reopenBad++
- }
- }
- check(fmt.Sprintf("重开后 %d 个保留主键全部可回表", len(keep)), reopenBad == 0)
- allRows, err := tbl.OrderBy("Age", false).Find()
- check(fmt.Sprintf("重开后全量扫描取回 %d 行", len(allRows)), err == nil && len(allRows) == keepRows)
- db.DebugDBState()
- if err := db.Close(); err != nil {
- panic(err)
- }
- // ---------------------------------------------------------------
- section("6. 空间占用")
- fi, err := os.Stat(path)
- if err == nil {
- perRow := float64(fi.Size()) / float64(totalRows)
- // 30000 行 + 索引, 线性哈希满载时约 40~80 字节/行; 远超则说明页泄漏
- check(fmt.Sprintf("DB 文件 %.2f MB (%.0f 字节/行)", float64(fi.Size())/1024/1024, perRow),
- perRow < 400)
- }
- // [KEEP-DB] 保留数据库文件供 Analyzer 分析, 屏蔽运行结束后的清理删除
- // _ = os.Remove(path)
- fmt.Printf("\n========== 总结: 通过 %d, 失败 %d\n", passed, failed)
- if failed > 0 {
- os.Exit(1)
- }
- }
|