main.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. // Hash 索引专项验证 (线性哈希重写后的正确性证明)
  2. //
  3. // 覆盖点:
  4. // 1. 大量分裂: 30000 行、负载因子 400 → 桶数必然 >= 75 (最小 16), 即至少 59 次分裂
  5. // 2. 热点/重复 key: Age=25 约占 1/3 (约 10000 行落在同一个桶的溢出链上)
  6. // 3. 精确性: 每个不同 Age 值的行数、以及抽样主键都必须一条不差地找回来
  7. // 4. 大量合并: 删到只剩 500 行 → 触发收缩/合并
  8. // 5. RangeScan: 哈希不保序, 但必须做到键过滤 + 排序正确 (含反向)
  9. // 6. 持久化: 关闭重开后数量与查找结果一致
  10. // 7. 空间: DB 文件不得随分裂/删除无限膨胀
  11. package main
  12. import (
  13. "TsDB2/DB"
  14. "fmt"
  15. "os"
  16. "time"
  17. )
  18. type HV struct {
  19. DB.BaseModel
  20. Name string `vrtx:"maxlen:32"`
  21. Age int64 `vrtx:"index:idx_age,type:hash"`
  22. Score int64
  23. Dept string `vrtx:"maxlen:16"`
  24. }
  25. var passed, failed int
  26. func check(msg string, ok bool, detail ...string) {
  27. if ok {
  28. passed++
  29. fmt.Printf(" [PASS] %s\n", msg)
  30. } else {
  31. failed++
  32. d := ""
  33. if len(detail) > 0 {
  34. d = " — " + detail[0]
  35. }
  36. fmt.Printf(" [FAIL] %s%s\n", msg, d)
  37. }
  38. }
  39. func section(t string) { fmt.Printf("\n========== %s\n", t) }
  40. const (
  41. totalRows = 30000
  42. hotAge = int64(25)
  43. keepRows = 500
  44. )
  45. // ageOf 构造分布: 1/3 的行集中在 hotAge, 其余散布在 20..219
  46. func ageOf(i int) int64 {
  47. if i%3 == 0 {
  48. return hotAge
  49. }
  50. return int64(20 + i%200)
  51. }
  52. func main() {
  53. path := fmt.Sprintf("hash_verify_%d.db", time.Now().UnixNano())
  54. _ = os.Remove(path)
  55. fmt.Println("============================================================")
  56. fmt.Println(" Hash 索引专项验证 (Linear Hashing)")
  57. fmt.Println("============================================================")
  58. db, err := DB.Open(path)
  59. if err != nil {
  60. panic(err)
  61. }
  62. db.SetAutoCommitDelay(0)
  63. db.SetBufferPoolCapacity(4000)
  64. tbl, err := DB.UseTable[HV](db, "hv")
  65. if err != nil {
  66. panic(err)
  67. }
  68. // idx_age (Hash) 已通过 vrtx 标签自动创建
  69. coreTbl, _ := db.CoreDB().FindTable("hv")
  70. // 期望分布
  71. want := map[int64]int{}
  72. for i := 1; i <= totalRows; i++ {
  73. want[ageOf(i)]++
  74. }
  75. // ---------------------------------------------------------------
  76. section("1. 插入 30000 行 (必然触发多次桶分裂)")
  77. t0 := time.Now()
  78. for i := 1; i <= totalRows; i++ {
  79. if err := tbl.Insert(&HV{
  80. Name: fmt.Sprintf("N%06d", i),
  81. Age: ageOf(i),
  82. Score: int64(i % 500),
  83. Dept: fmt.Sprintf("D%d", i%20),
  84. }); err != nil {
  85. check(fmt.Sprintf("Insert %d", i), false, err.Error())
  86. break
  87. }
  88. }
  89. if err := db.Commit(); err != nil {
  90. panic(err)
  91. }
  92. cnt, _ := tbl.Count()
  93. check(fmt.Sprintf("插入 %d 行 耗时 %.2fs, Count=%d", totalRows, time.Since(t0).Seconds(), cnt), cnt == int64(totalRows))
  94. // ---------------------------------------------------------------
  95. section("2. 索引精确性 (每个 Age 值 + 抽样主键)")
  96. bad := 0
  97. for age, exp := range want {
  98. got, err := tbl.Where("Age", "=", age).Count()
  99. if err != nil || got != int64(exp) {
  100. bad++
  101. if bad <= 3 {
  102. fmt.Printf(" Age=%d 期望 %d 实得 %d err=%v\n", age, exp, got, err)
  103. }
  104. }
  105. }
  106. check(fmt.Sprintf("全部 %d 个不同 Age 值的行数完全一致", len(want)), bad == 0)
  107. // 走 SearchByIndex (handler.Search) 逐个 Age 验证。
  108. // 键必须交给引擎编码: int64 列现在落 FieldType_Int64, 用保序编码 (符号位翻转),
  109. // 不再是原始大端 —— 手拼 uint64(age) 的键在 v7 之后与盘上键不匹配, 一律 not found。
  110. missSearch := 0
  111. for age := range want {
  112. key, err := coreTbl.BuildIndexKey("idx_age", age)
  113. if err != nil {
  114. missSearch++
  115. continue
  116. }
  117. pk, e := coreTbl.SearchByIndex("idx_age", key)
  118. if e != nil || len(pk) == 0 {
  119. missSearch++
  120. }
  121. }
  122. check(fmt.Sprintf("SearchByIndex 覆盖全部 %d 个 Age 值", len(want)), missSearch == 0)
  123. // 抽样主键回表
  124. pkBad := 0
  125. for i := 1; i <= totalRows; i += 37 {
  126. row, e := tbl.Find(uint64(i))
  127. if e != nil || row == nil || row.Age != ageOf(i) {
  128. pkBad++
  129. }
  130. }
  131. totalSample := 0
  132. for i := 1; i <= totalRows; i += 37 {
  133. totalSample++
  134. }
  135. check(fmt.Sprintf("抽样 %d 个主键回表一致", totalSample), pkBad == 0)
  136. // ---------------------------------------------------------------
  137. section("3. RangeScan (键过滤 + 排序, 哈希不保序)")
  138. expRange := int64(0)
  139. for age, c := range want {
  140. if age >= 30 && age <= 40 {
  141. expRange += int64(c)
  142. }
  143. }
  144. gotRange, err := tbl.Where("Age", ">=", int64(30)).Where("Age", "<=", int64(40)).Count()
  145. check(fmt.Sprintf("范围 [30,40] Count=%d (期望 %d)", gotRange, expRange), err == nil && gotRange == expRange)
  146. asc, err := tbl.Where("Age", ">=", int64(30)).Where("Age", "<=", int64(40)).OrderBy("Age", false).Limit(50).Find()
  147. ascOK := err == nil && len(asc) > 0
  148. if ascOK {
  149. for i := 1; i < len(asc); i++ {
  150. if asc[i-1].Age > asc[i].Age {
  151. ascOK = false
  152. break
  153. }
  154. }
  155. for _, r := range asc {
  156. if r.Age < 30 || r.Age > 40 {
  157. ascOK = false
  158. break
  159. }
  160. }
  161. }
  162. check(fmt.Sprintf("范围 [30,40] OrderBy Age 升序, 取回 %d 行", len(asc)), ascOK)
  163. desc, err := tbl.Where("Age", ">=", int64(30)).Where("Age", "<=", int64(40)).OrderBy("Age", true).Limit(50).Find()
  164. descOK := err == nil && len(desc) > 0
  165. if descOK {
  166. for i := 1; i < len(desc); i++ {
  167. if desc[i-1].Age < desc[i].Age {
  168. descOK = false
  169. break
  170. }
  171. }
  172. }
  173. check(fmt.Sprintf("范围 [30,40] OrderBy Age 降序, 取回 %d 行", len(desc)), descOK)
  174. // ---------------------------------------------------------------
  175. section("4. 大量删除 (触发桶合并/收缩)")
  176. keep := map[uint64]bool{}
  177. nk := 0
  178. for i := 1; i <= totalRows && nk < keepRows; i++ {
  179. if ageOf(i) == hotAge {
  180. keep[uint64(i)] = true
  181. nk++
  182. }
  183. }
  184. t1 := time.Now()
  185. delFail := 0
  186. for i := 1; i <= totalRows; i++ {
  187. if keep[uint64(i)] {
  188. continue
  189. }
  190. if err := tbl.Delete(uint64(i)); err != nil {
  191. delFail++
  192. }
  193. }
  194. if err := db.Commit(); err != nil {
  195. panic(err)
  196. }
  197. check(fmt.Sprintf("删除 %d 行 耗时 %.2fs (0 失败)", totalRows-keepRows, time.Since(t1).Seconds()), delFail == 0)
  198. cnt2, _ := tbl.Count()
  199. check(fmt.Sprintf("删除后 Count=%d (期望 %d)", cnt2, keepRows), cnt2 == int64(keepRows))
  200. hotCnt, err := tbl.Where("Age", "=", hotAge).Count()
  201. check(fmt.Sprintf("删除后 Age=%d Count=%d (期望 %d)", hotAge, hotCnt, keepRows), err == nil && hotCnt == int64(keepRows))
  202. otherCnt, err := tbl.Where("Age", ">=", int64(30)).Count()
  203. check(fmt.Sprintf("删除后 Age>=30 Count=%d (期望 0)", otherCnt), err == nil && otherCnt == 0)
  204. // ---------------------------------------------------------------
  205. section("5. 关闭重开 (持久化)")
  206. if err := db.Close(); err != nil {
  207. panic(err)
  208. }
  209. db, err = DB.Open(path)
  210. if err != nil {
  211. panic(err)
  212. }
  213. db.SetAutoCommitDelay(0)
  214. db.SetBufferPoolCapacity(4000)
  215. tbl, err = DB.UseTable[HV](db, "hv")
  216. if err != nil {
  217. panic(err)
  218. }
  219. coreTbl, _ = db.CoreDB().FindTable("hv")
  220. cnt3, _ := tbl.Count()
  221. check(fmt.Sprintf("重开后 Count=%d (期望 %d)", cnt3, keepRows), cnt3 == int64(keepRows))
  222. hotCnt2, err := tbl.Where("Age", "=", hotAge).Count()
  223. check(fmt.Sprintf("重开后 Age=%d Count=%d (期望 %d)", hotAge, hotCnt2, keepRows), err == nil && hotCnt2 == int64(keepRows))
  224. reopenBad := 0
  225. for pk := range keep {
  226. row, e := tbl.Find(pk)
  227. if e != nil || row == nil || row.Age != hotAge {
  228. reopenBad++
  229. }
  230. }
  231. check(fmt.Sprintf("重开后 %d 个保留主键全部可回表", len(keep)), reopenBad == 0)
  232. allRows, err := tbl.OrderBy("Age", false).Find()
  233. check(fmt.Sprintf("重开后全量扫描取回 %d 行", len(allRows)), err == nil && len(allRows) == keepRows)
  234. db.DebugDBState()
  235. if err := db.Close(); err != nil {
  236. panic(err)
  237. }
  238. // ---------------------------------------------------------------
  239. section("6. 空间占用")
  240. fi, err := os.Stat(path)
  241. if err == nil {
  242. perRow := float64(fi.Size()) / float64(totalRows)
  243. // 30000 行 + 索引, 线性哈希满载时约 40~80 字节/行; 远超则说明页泄漏
  244. check(fmt.Sprintf("DB 文件 %.2f MB (%.0f 字节/行)", float64(fi.Size())/1024/1024, perRow),
  245. perRow < 400)
  246. }
  247. // [KEEP-DB] 保留数据库文件供 Analyzer 分析, 屏蔽运行结束后的清理删除
  248. // _ = os.Remove(path)
  249. fmt.Printf("\n========== 总结: 通过 %d, 失败 %d\n", passed, failed)
  250. if failed > 0 {
  251. os.Exit(1)
  252. }
  253. }