main.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. // Hash 索引增删改查性能基准
  2. //
  3. // 目标: 在不同数据量 (1 万 ~ 20 万行) 下, 测量 Hash 二级索引的
  4. // 1. 插入吞吐 (行/秒) 与提交耗时
  5. // 2. 哈希索引等值查询延迟 (单条命中, Limit=1)
  6. // 3. 哈希索引全匹配查询延迟 (统计同一 key 的全部重复行)
  7. // 4. 主键回表查询延迟 (B+ 树)
  8. // 5. 更新吞吐 (改 Age → 触发哈希索引 删旧键 + 插新键)
  9. // 6. 删除吞吐 (触发哈希索引级联删除 + 桶合并)
  10. //
  11. // 说明: Age 取值空间 keySpace=1000, 平均每个 key 有 N/1000 行重复,
  12. // 用于模拟真实的重复键场景 (哈希溢出链)。
  13. package main
  14. import (
  15. "fmt"
  16. "math/rand"
  17. "os"
  18. "slices"
  19. "strconv"
  20. "time"
  21. "git.ycl.pub/yl32940/TsDB2/DB"
  22. )
  23. type HB struct {
  24. DB.BaseModel
  25. Name string `vrtx:"maxlen:32"`
  26. Age int64 `vrtx:"index:idx_age,type:hash"`
  27. Score int64
  28. Dept string `vrtx:"maxlen:16"`
  29. }
  30. const keySpace = 1000 // Age 取值空间 → 平均 N/keySpace 行重复
  31. // --pure 模式: 关闭脏页阈值自动刷盘, 用于隔离索引自身开销
  32. var noAutoFlush bool
  33. type result struct {
  34. rows int
  35. insRate float64 // 行/秒
  36. commitMs float64
  37. hashAvgUs float64
  38. hashP50Us float64
  39. hashP99Us float64
  40. countAvgMs float64
  41. countP99Ms float64
  42. pkAvgUs float64
  43. pkP50Us float64
  44. pkP99Us float64
  45. updRate float64
  46. delRate float64
  47. sizeMB float64
  48. remain int64
  49. }
  50. func pct(durs []time.Duration, p int) time.Duration {
  51. if len(durs) == 0 {
  52. return 0
  53. }
  54. idx := len(durs) * p / 100
  55. if idx >= len(durs) {
  56. idx = len(durs) - 1
  57. }
  58. return durs[idx]
  59. }
  60. func summarize(durs []time.Duration) (avg, p50, p99 time.Duration) {
  61. if len(durs) == 0 {
  62. return
  63. }
  64. slices.Sort(durs)
  65. var sum time.Duration
  66. for _, d := range durs {
  67. sum += d
  68. }
  69. avg = sum / time.Duration(len(durs))
  70. p50 = pct(durs, 50)
  71. p99 = pct(durs, 99)
  72. return
  73. }
  74. // runBatched 把 total 次操作切成 batch 大小的批次分别计时,
  75. // 返回每批的"单次均摊"耗时。用于规避 Windows time.Now() 约 1ms 的
  76. // 时钟粒度, 使微秒级延迟测量稳定可靠。
  77. func runBatched(total, batch int, op func()) []time.Duration {
  78. n := total / batch
  79. if n == 0 {
  80. n = 1
  81. }
  82. per := make([]time.Duration, 0, n)
  83. for b := 0; b < n; b++ {
  84. s := time.Now()
  85. for k := 0; k < batch; k++ {
  86. op()
  87. }
  88. per = append(per, time.Since(s)/time.Duration(batch))
  89. }
  90. return per
  91. }
  92. func runVolume(rows int) result {
  93. rng := rand.New(rand.NewSource(int64(rows)))
  94. path := fmt.Sprintf("hash_bench_%d.db", rows)
  95. _ = os.Remove(path)
  96. db, err := DB.Open(path)
  97. if err != nil {
  98. panic(err)
  99. }
  100. db.SetAutoCommitDelay(0)
  101. db.SetBufferPoolCapacity(16384)
  102. if noAutoFlush {
  103. // 关闭脏页阈值自动 Commit, 隔离 "索引自身开销" 与 "刷盘策略开销"
  104. db.SetDirtyThreshold(1 << 30)
  105. }
  106. tbl, err := DB.UseTable[HB](db, "hb")
  107. if err != nil {
  108. panic(err)
  109. }
  110. ages := make([]int64, rows+1)
  111. // ---------------- 插入 ----------------
  112. t0 := time.Now()
  113. for i := 1; i <= rows; i++ {
  114. a := int64(rng.Intn(keySpace))
  115. ages[i] = a
  116. if err := tbl.Insert(&HB{
  117. Name: fmt.Sprintf("N%07d", i),
  118. Age: a,
  119. Score: int64(i % 500),
  120. Dept: fmt.Sprintf("D%d", i%20),
  121. }); err != nil {
  122. panic(err)
  123. }
  124. }
  125. insDur := time.Since(t0)
  126. tc := time.Now()
  127. if err := db.Commit(); err != nil {
  128. panic(err)
  129. }
  130. commitDur := time.Since(tc)
  131. // ---------------- 哈希索引: 等值单条查询 (批量计时) ----------------
  132. hashDurs := runBatched(20000, 1000, func() {
  133. a := int64(rng.Intn(keySpace))
  134. _, _ = tbl.Where("Age", "=", a).First()
  135. })
  136. // ---------------- 哈希索引: 全匹配统计 ----------------
  137. countDurs := runBatched(500, 20, func() {
  138. a := int64(rng.Intn(keySpace))
  139. _, _ = tbl.Where("Age", "=", a).Count()
  140. })
  141. // ---------------- 主键回表 ----------------
  142. pkDurs := runBatched(20000, 1000, func() {
  143. id := uint64(rng.Intn(rows) + 1)
  144. _, _ = tbl.Find(id)
  145. })
  146. // ---------------- 更新 (改 Age, 触发哈希索引重排) ----------------
  147. updN := rows
  148. if updN > 20000 {
  149. updN = 20000
  150. }
  151. tu := time.Now()
  152. for j := 1; j <= updN; j++ {
  153. newAge := (ages[j] + 1) % keySpace
  154. ages[j] = newAge
  155. if err := tbl.Update(&HB{
  156. BaseModel: DB.BaseModel{ID: uint64(j)},
  157. Name: fmt.Sprintf("U%07d", j),
  158. Age: newAge,
  159. Score: int64(j % 1000),
  160. Dept: fmt.Sprintf("D%d", j%20),
  161. }); err != nil {
  162. panic(err)
  163. }
  164. }
  165. updDur := time.Since(tu)
  166. if err := db.Commit(); err != nil {
  167. panic(err)
  168. }
  169. // ---------------- 删除 (级联删哈希索引) ----------------
  170. delN := rows
  171. if delN > 20000 {
  172. delN = 20000
  173. }
  174. td := time.Now()
  175. for j := 1; j <= delN; j++ {
  176. if err := tbl.Delete(uint64(j)); err != nil {
  177. panic(err)
  178. }
  179. }
  180. delDur := time.Since(td)
  181. if err := db.Commit(); err != nil {
  182. panic(err)
  183. }
  184. remain, _ := tbl.Count()
  185. if err := db.Close(); err != nil {
  186. panic(err)
  187. }
  188. fi, _ := os.Stat(path)
  189. sizeMB := 0.0
  190. if fi != nil {
  191. sizeMB = float64(fi.Size()) / 1024 / 1024
  192. }
  193. // [KEEP-DB] 保留数据库文件供 Analyzer 分析, 屏蔽运行结束后的清理删除
  194. // _ = os.Remove(path)
  195. hAvg, hP50, hP99 := summarize(hashDurs)
  196. cAvg, _, cP99 := summarize(countDurs)
  197. pAvg, pP50, pP99 := summarize(pkDurs)
  198. return result{
  199. rows: rows,
  200. insRate: float64(rows) / insDur.Seconds(),
  201. commitMs: float64(commitDur.Microseconds()) / 1000,
  202. hashAvgUs: float64(hAvg.Microseconds()),
  203. hashP50Us: float64(hP50.Microseconds()),
  204. hashP99Us: float64(hP99.Microseconds()),
  205. countAvgMs: float64(cAvg.Microseconds()) / 1000,
  206. countP99Ms: float64(cP99.Microseconds()) / 1000,
  207. pkAvgUs: float64(pAvg.Microseconds()),
  208. pkP50Us: float64(pP50.Microseconds()),
  209. pkP99Us: float64(pP99.Microseconds()),
  210. updRate: float64(updN) / updDur.Seconds(),
  211. delRate: float64(delN) / delDur.Seconds(),
  212. sizeMB: sizeMB,
  213. remain: remain,
  214. }
  215. }
  216. func main() {
  217. volumes := []int{10000, 50000, 100000, 200000, 400000}
  218. for _, a := range os.Args[1:] {
  219. if a == "--pure" {
  220. noAutoFlush = true
  221. continue
  222. }
  223. if n, err := strconv.Atoi(a); err == nil && n > 0 {
  224. volumes = []int{n} // 指定单一数据量
  225. }
  226. }
  227. fmt.Println("============================================================")
  228. fmt.Println(" Hash 索引 (线性哈希) 增删改查性能基准")
  229. fmt.Printf(" Age 取值空间=%d (平均每 key 重复 N/%d 行)\n", keySpace, keySpace)
  230. if noAutoFlush {
  231. fmt.Println(" 模式: --pure (关闭脏页阈值自动刷盘, 仅手动 Commit)")
  232. } else {
  233. fmt.Println(" 模式: 默认策略 (脏页阈值 500 页, 超限自动 Commit)")
  234. }
  235. fmt.Println("============================================================")
  236. results := make([]result, 0, len(volumes))
  237. for _, n := range volumes {
  238. fmt.Printf("\n>>> 运行数据量 N=%d ...\n", n)
  239. results = append(results, runVolume(n))
  240. }
  241. // ---------------- 吞吐总览 ----------------
  242. fmt.Println("\n============================================================")
  243. fmt.Println(" 吞吐总览")
  244. fmt.Println("============================================================")
  245. fmt.Printf("%-10s %14s %12s %14s %14s %10s\n",
  246. "数据量", "插入(行/秒)", "提交(ms)", "更新(行/秒)", "删除(行/秒)", "文件(MB)")
  247. for _, r := range results {
  248. fmt.Printf("%-10d %14.0f %12.1f %14.0f %14.0f %10.2f\n",
  249. r.rows, r.insRate, r.commitMs, r.updRate, r.delRate, r.sizeMB)
  250. }
  251. // ---------------- 查询延迟总览 ----------------
  252. fmt.Println("\n============================================================")
  253. fmt.Println(" 查询延迟 (单位: 微秒 µs / 毫秒 ms)")
  254. fmt.Println("============================================================")
  255. fmt.Printf("%-10s %26s %26s %18s\n",
  256. "数据量", "哈希等值查询(Limit=1)", "主键回表(Find)", "哈希全匹配(Count)")
  257. fmt.Printf("%-10s %10s %8s %8s %10s %8s %8s %10s %8s\n",
  258. "", "avg", "p50", "p99", "avg", "p50", "p99", "avg(ms)", "p99(ms)")
  259. for _, r := range results {
  260. fmt.Printf("%-10d %10.1f %8.1f %8.1f %10.1f %8.1f %8.1f %10.2f %8.2f\n",
  261. r.rows,
  262. r.hashAvgUs, r.hashP50Us, r.hashP99Us,
  263. r.pkAvgUs, r.pkP50Us, r.pkP99Us,
  264. r.countAvgMs, r.countP99Ms)
  265. }
  266. // ---------------- 查询吞吐 (QPS) ----------------
  267. fmt.Println("\n============================================================")
  268. fmt.Println(" 查询吞吐 (QPS = 1000/avg µs, 按均摊延迟换算)")
  269. fmt.Println("============================================================")
  270. fmt.Printf("%-10s %18s %18s %18s\n", "数据量", "哈希等值查询", "主键回表", "哈希全匹配")
  271. for _, r := range results {
  272. hQPS, pQPS, cQPS := 0.0, 0.0, 0.0
  273. if r.hashAvgUs > 0 {
  274. hQPS = 1e6 / r.hashAvgUs
  275. }
  276. if r.pkAvgUs > 0 {
  277. pQPS = 1e6 / r.pkAvgUs
  278. }
  279. if r.countAvgMs > 0 {
  280. cQPS = 1000 / r.countAvgMs
  281. }
  282. fmt.Printf("%-10d %18.0f %18.0f %18.0f\n", r.rows, hQPS, pQPS, cQPS)
  283. }
  284. fmt.Println("\n注: 每档数据量均执行 插入 N 行 / 2万次哈希查询 / 500次全匹配 /")
  285. fmt.Println(" 2万次主键查询 / 2万次更新 / 2万次删除。删除后剩余行数校验一致。")
  286. fmt.Println(" 点查询延迟为批量均摊值 (每批 1000 次), 已规避系统时钟粒度影响。")
  287. }