main.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. package main
  2. import (
  3. "git.ycl.pub/yl32940/TsDB2/DB"
  4. "git.ycl.pub/yl32940/TsDB2/DB/core"
  5. "fmt"
  6. "os"
  7. "time"
  8. )
  9. // Record 测试表结构 — 通过 vrtx 标签声明索引,UseTable 自动创建
  10. type Record struct {
  11. DB.BaseModel
  12. Name string `vrtx:"maxlen:32"`
  13. Age int64 `vrtx:"index:idx_hash_age,type:hash"`
  14. Score int64 `vrtx:"index:idx_btree_score"`
  15. Dept string `vrtx:"maxlen:16"`
  16. }
  17. // PerfRecord 性能测试用 — 同一字段同时建 Hash 和 B+树 做对比
  18. type PerfRecord struct {
  19. DB.BaseModel
  20. Name string `vrtx:"maxlen:32"`
  21. Age int64 `vrtx:"index:idx_hash_age,type:hash"`
  22. Score int64 `vrtx:"index:idx_btree_age"`
  23. Dept string `vrtx:"maxlen:16"`
  24. }
  25. var dbPath string
  26. var passed, failed int
  27. func check(msg string, ok bool, detail ...string) {
  28. if ok {
  29. passed++
  30. fmt.Printf(" ✅ %s\n", msg)
  31. } else {
  32. failed++
  33. d := ""
  34. if len(detail) > 0 {
  35. d = " — " + detail[0]
  36. }
  37. fmt.Printf(" ❌ %s%s\n", msg, d)
  38. }
  39. }
  40. func section(title string) {
  41. fmt.Printf("\n%s %s\n", "==========", title)
  42. }
  43. // searchAge 用引擎的键编码查询 Age 索引, 返回查找错误 (nil 表示命中)。
  44. //
  45. // 不能手拼 uint64(age) 大端: int64 列现在落 FieldType_Int64, 用保序编码 (符号位翻转),
  46. // 手拼的键在 v7 之后与盘上键不匹配, SearchByIndex 会一律 not found。
  47. func searchAge(ct *core.Table, idxName string, age int64) error {
  48. key, err := ct.BuildIndexKey(idxName, age)
  49. if err != nil {
  50. return err
  51. }
  52. _, err = ct.SearchByIndex(idxName, key)
  53. return err
  54. }
  55. // countFindable 统计可被 Hash 索引查找到的 Age 值数量
  56. func countFindable(db *DB.DB, tableName string, idxName string, minAge, maxAge int64) int {
  57. coreTbl, _ := db.CoreDB().FindTable(tableName)
  58. count := 0
  59. for age := minAge; age < maxAge; age++ {
  60. if searchAge(coreTbl, idxName, age) == nil {
  61. count++
  62. }
  63. }
  64. return count
  65. }
  66. func main() {
  67. dbPath = fmt.Sprintf("hash_test_%d.db", time.Now().UnixNano())
  68. os.Remove(dbPath)
  69. fmt.Println("============================================================")
  70. fmt.Println(" TsDB2 Hash 桶式索引 — 完整测试")
  71. fmt.Println(" 覆盖: CRUD/分页/桶分裂(Split)/桶合并(Merge)/持久化/性能")
  72. fmt.Println("============================================================")
  73. // ============================================================
  74. // 1. 建表 (索引由 vrtx 标签自动创建)
  75. // ============================================================
  76. section("1. 建表 + 自动索引")
  77. db, err := DB.Open(dbPath)
  78. if err != nil {
  79. panic(err)
  80. }
  81. db.SetAutoCommitDelay(0)
  82. db.SetBufferPoolCapacity(4000)
  83. tbl, err := DB.UseTable[Record](db, "hash_test")
  84. check("UseTable[Record] 建表 (自动创建索引)", err == nil)
  85. coreTbl, _ := db.CoreDB().FindTable("hash_test")
  86. // 验证索引已自动创建
  87. err = searchAge(coreTbl, "idx_hash_age", 25)
  88. check("idx_hash_age (Hash) 已自动创建", err == nil || err.Error() == "key not found")
  89. // ============================================================
  90. // 2. 大批量插入 — 触发多页存储 (3000行, 每桶~187条目)
  91. // ============================================================
  92. section("2. 大批量插入 (3000 行, 触发多页)")
  93. N1 := 3000
  94. t0 := time.Now()
  95. for i := 1; i <= N1; i++ {
  96. err := tbl.Insert(&Record{
  97. Name: fmt.Sprintf("User%05d", i),
  98. Age: int64(20 + i%100),
  99. Score: int64(i % 500),
  100. Dept: fmt.Sprintf("D%d", i%20),
  101. })
  102. if err != nil {
  103. check(fmt.Sprintf("Insert ID=%d", i), false, err.Error())
  104. break
  105. }
  106. }
  107. insertTime := time.Since(t0)
  108. check(fmt.Sprintf("插入 %d 行 耗时 %.2fs (%.0f rows/s)", N1, insertTime.Seconds(), float64(N1)/insertTime.Seconds()), true)
  109. if err := db.Commit(); err != nil {
  110. panic(err)
  111. }
  112. check("Commit 持久化", true)
  113. // 验证数据可查
  114. found1 := countFindable(db, "hash_test", "idx_hash_age", 20, 120)
  115. check(fmt.Sprintf("Hash 查找: %d/100 个 Age 值可查", found1), found1 > 50)
  116. u100, err := tbl.Find(100)
  117. check("PK 查找 ID=100", err == nil && u100 != nil && u100.Name == "User00100")
  118. // 验证行数
  119. cnt, _ := tbl.Count()
  120. check(fmt.Sprintf("行数: %d (应=%d)", cnt, N1), cnt == int64(N1))
  121. // ============================================================
  122. // 3. 桶分裂测试 — 集中插入触发 Split (阈值=800)
  123. // ============================================================
  124. section("3. 桶分裂测试 (Split)")
  125. splitN := 1000
  126. fmt.Printf(" 集中插入 %d 行 Age=25 (全部 hash 到同一桶, 阈值=800)...\n", splitN)
  127. t0 = time.Now()
  128. for i := N1 + 1; i <= N1+splitN; i++ {
  129. if err := tbl.Insert(&Record{
  130. Name: fmt.Sprintf("Split%05d", i),
  131. Age: 25,
  132. Score: int64(i % 300),
  133. Dept: fmt.Sprintf("S%d", i%5),
  134. }); err != nil {
  135. check(fmt.Sprintf("Split Insert ID=%d", i), false, err.Error())
  136. break
  137. }
  138. }
  139. check(fmt.Sprintf("集中插入 %d 行 耗时 %.2fs", splitN, time.Since(t0).Seconds()), true)
  140. if err := db.Commit(); err != nil {
  141. panic(err)
  142. }
  143. // 验证分裂后数据完整性
  144. err25 := searchAge(coreTbl, "idx_hash_age", 25)
  145. check("分裂后 Hash 查找 Age=25", err25 == nil)
  146. splitFound := countFindable(db, "hash_test", "idx_hash_age", 20, 120)
  147. check(fmt.Sprintf("分裂后 Hash 查找: %d/100 个 Age 值可查", splitFound), splitFound > 50)
  148. uSplit, err := tbl.Find(uint64(N1 + 1))
  149. check("分裂后 PK 查找", err == nil && uSplit != nil)
  150. cnt2, _ := tbl.Count()
  151. check(fmt.Sprintf("分裂后行数: %d (应=%d)", cnt2, N1+splitN), cnt2 == int64(N1+splitN))
  152. // ============================================================
  153. // 4. 删除测试
  154. // ============================================================
  155. section("4. Hash 索引删除测试")
  156. delN := 500
  157. for i := 1; i <= delN; i++ {
  158. if err := tbl.Delete(uint64(i)); err != nil {
  159. check(fmt.Sprintf("Delete ID=%d", i), false, err.Error())
  160. break
  161. }
  162. }
  163. check(fmt.Sprintf("删除 %d 行 (ID 1~%d)", delN, delN), true)
  164. if err := db.Commit(); err != nil {
  165. panic(err)
  166. }
  167. _, err = tbl.Find(1)
  168. check("删除后 PK 查找 ID=1 (应不存在)", err != nil)
  169. _, err = tbl.Find(uint64(delN + 1))
  170. check(fmt.Sprintf("删除后 PK 查找 ID=%d (应存在)", delN+1), err == nil)
  171. err = searchAge(coreTbl, "idx_hash_age", 25)
  172. check("删除后 Hash 查找 Age=25", err == nil)
  173. // ============================================================
  174. // 5. 桶合并测试 — 大量删除触发 Merge
  175. // ============================================================
  176. section("5. 桶合并测试 (Merge)")
  177. deleteCount := 0
  178. for i := uint64(delN + 1); i <= uint64(N1+splitN); i++ {
  179. if err := tbl.Delete(i); err == nil {
  180. deleteCount++
  181. }
  182. }
  183. check(fmt.Sprintf("删除 %d 行", deleteCount), deleteCount > 0)
  184. if err := db.Commit(); err != nil {
  185. panic(err)
  186. }
  187. remaining, _ := tbl.Count()
  188. check(fmt.Sprintf("剩余行数: %d (已删除全部 %d 行)", remaining, N1+splitN), remaining == 0)
  189. // 验证全删后哈希索引仍可安全查询
  190. err = searchAge(coreTbl, "idx_hash_age", 25)
  191. check("合并后 Hash 索引不崩溃", true)
  192. // ============================================================
  193. // 6. 持久化测试 (关闭重开)
  194. // ============================================================
  195. section("6. 持久化 + 重开验证")
  196. countBefore := remaining
  197. db.Close()
  198. check("关闭数据库", true)
  199. db, err = DB.Open(dbPath)
  200. if err != nil {
  201. panic(err)
  202. }
  203. db.SetBufferPoolCapacity(4000)
  204. tbl, err = DB.UseTable[Record](db, "hash_test")
  205. check("重开 UseTable[Record]", err == nil)
  206. coreTbl, _ = db.CoreDB().FindTable("hash_test")
  207. countAfter, _ := tbl.Count()
  208. check(fmt.Sprintf("重开后行数: %d (应=%d)", countAfter, countBefore),
  209. countAfter == countBefore,
  210. fmt.Sprintf("expected=%d actual=%d", countBefore, countAfter))
  211. db.Close()
  212. // ============================================================
  213. // 7. 性能对比: Hash vs B+树 (用 PerfRecord 同一字段两种索引)
  214. // ============================================================
  215. section("7. 性能对比: Hash vs B+树")
  216. os.Remove(dbPath)
  217. db, _ = DB.Open(dbPath)
  218. db.SetAutoCommitDelay(0)
  219. db.SetBufferPoolCapacity(4000)
  220. perfTbl, _ := DB.UseTable[PerfRecord](db, "perf_test")
  221. perfCoreTbl, _ := db.CoreDB().FindTable("perf_test")
  222. N := 5000
  223. t0 = time.Now()
  224. for i := 1; i <= N; i++ {
  225. perfTbl.Insert(&PerfRecord{
  226. Name: fmt.Sprintf("Perf%06d", i),
  227. Age: int64(20 + i%200),
  228. Score: int64(i % 1000),
  229. Dept: fmt.Sprintf("D%d", i%30),
  230. })
  231. }
  232. db.Commit()
  233. insertT := time.Since(t0)
  234. fmt.Printf(" 插入 %d 行: %.2fs (%.0f rows/s)\n", N, insertT.Seconds(), float64(N)/insertT.Seconds())
  235. lookupN := 1000
  236. t0 = time.Now()
  237. for i := 0; i < lookupN; i++ {
  238. age := int64(20 + i%200)
  239. searchAge(perfCoreTbl, "idx_hash_age", age)
  240. }
  241. hashTime := time.Since(t0)
  242. hashQPS := float64(lookupN) / hashTime.Seconds()
  243. fmt.Printf(" Hash 查找 %d 次: %.3fs (%.0f qps)\n", lookupN, hashTime.Seconds(), hashQPS)
  244. t0 = time.Now()
  245. for i := 0; i < lookupN; i++ {
  246. age := int64(20 + i%200)
  247. searchAge(perfCoreTbl, "idx_btree_age", age)
  248. }
  249. btreeTime := time.Since(t0)
  250. btreeQPS := float64(lookupN) / btreeTime.Seconds()
  251. fmt.Printf(" B+树 查找 %d 次: %.3fs (%.0f qps)\n", lookupN, btreeTime.Seconds(), btreeQPS)
  252. if hashQPS > 0 && btreeQPS > 0 {
  253. ratio := hashQPS / btreeQPS
  254. check(fmt.Sprintf("Hash/B+树 性能比: %.2fx", ratio), true)
  255. }
  256. db.Close()
  257. // ============================================================
  258. // 总结
  259. // ============================================================
  260. section("测试总结")
  261. total := passed + failed
  262. fmt.Printf(" 总计: %d 项测试, ✅ 通过: %d, ❌ 失败: %d\n", total, passed, failed)
  263. if failed == 0 {
  264. fmt.Println(" 🎉 全部通过!Hash 桶式索引工作正常!")
  265. } else {
  266. fmt.Printf(" ⚠️ 有 %d 项测试失败,请检查\n", failed)
  267. }
  268. // [KEEP-DB] 保留数据库文件供 Analyzer 分析, 屏蔽运行结束后的清理删除
  269. // os.Remove(dbPath)
  270. }