query_optimizer_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. package core
  2. import (
  3. "cmp"
  4. "slices"
  5. "testing"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  8. )
  9. // qopNewTable 建测试表: id (uint64 主键) + age (int64, B+ 二级索引 idx_age)
  10. // + score (int64, Hash 二级索引 idx_score) + name (string 32B, 无索引)。
  11. func qopNewTable(t *testing.T, db *DB, name string) *Table {
  12. t.Helper()
  13. tbl, err := db.CreateTable(name, []types.FieldInfo{
  14. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  15. {FieldType: types.FieldType_Int64, FieldName: []byte("age")},
  16. {FieldType: types.FieldType_Int64, FieldName: []byte("score")},
  17. {FieldType: types.FieldType_String, FieldName: []byte("name"), Flag: types.WidthCodeFor(32)},
  18. })
  19. if err != nil {
  20. t.Fatalf("建表 %s: %v", name, err)
  21. }
  22. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  23. t.Fatalf("建主键索引: %v", err)
  24. }
  25. if _, err := tbl.CreateIndex("idx_age", page.IndexTypeBTree, []byte{1}); err != nil {
  26. t.Fatalf("建 B+ 索引 idx_age: %v", err)
  27. }
  28. if _, err := tbl.CreateIndex("idx_score", page.IndexTypeHash, []byte{2}); err != nil {
  29. t.Fatalf("建 Hash 索引 idx_score: %v", err)
  30. }
  31. return tbl
  32. }
  33. // qopInsert 插入一行 (id 显式, age/score/name 按 i 生成)。
  34. func qopInsert(t *testing.T, tbl *Table, id uint64, age, score int64, name string) {
  35. t.Helper()
  36. if _, err := tbl.InsertRowWithID(id, []types.FieldValue{
  37. {FieldName: []byte("id"), Value: id},
  38. {FieldName: []byte("age"), Value: age},
  39. {FieldName: []byte("score"), Value: score},
  40. {FieldName: []byte("name"), Value: name},
  41. }); err != nil {
  42. t.Fatalf("插入 id=%d: %v", id, err)
  43. }
  44. }
  45. // qopPKKey 主键索引键 = pkID 的 8B 大端。
  46. func qopPKKey(id uint64) []byte {
  47. k := make([]byte, 8)
  48. types.EncodeValue(types.FieldType_Uint64, 0, id, k)
  49. return k
  50. }
  51. // qopSeed 灌 n 行: age = i%50, score = i%100, 便于构造多值命中。
  52. func qopSeed(t *testing.T, tbl *Table, n int) {
  53. t.Helper()
  54. for i := 1; i <= n; i++ {
  55. qopInsert(t, tbl, uint64(i), int64(i%50), int64(i%100), "")
  56. }
  57. }
  58. // collectAges 执行查询并返回 (pkID → age) 映射, 用于与全量预期比对。
  59. func collectAges(t *testing.T, q *Query) map[uint64]int64 {
  60. t.Helper()
  61. out := map[uint64]int64{}
  62. err := q.ScanWithPKID(func(pkID uint64, row []types.FieldValue) bool {
  63. for _, fv := range row {
  64. if string(fv.FieldName) == "age" {
  65. out[pkID] = fv.Value.(int64)
  66. }
  67. }
  68. return true
  69. })
  70. if err != nil {
  71. t.Fatalf("scan: %v", err)
  72. }
  73. return out
  74. }
  75. // TestInUsesIndex IN 必须走索引: 规划器为每个值展开一个区间, 而不是退回全表扫描。
  76. func TestInUsesIndex(t *testing.T) {
  77. db := fixOpenDB(t)
  78. tbl := qopNewTable(t, db, "t")
  79. qopSeed(t, tbl, 200)
  80. // 白盒: 规划结果应为多区间计划
  81. vals := []interface{}{int64(7), int64(23), int64(49)}
  82. plan := tbl.Query().Where(In("age", vals, 0)).buildScanPlan()
  83. if plan == nil {
  84. t.Fatalf("IN(age, 3值) 应生成索引计划, 得到 nil")
  85. }
  86. if len(plan.ranges) != 3 {
  87. t.Fatalf("IN 3 值应展开 3 个区间, 得到 %d", len(plan.ranges))
  88. }
  89. if plan.meta == nil || string(plan.meta.IndexName[:7]) != "idx_age" {
  90. t.Fatalf("计划应选 idx_age, 得到 %+v", plan.meta)
  91. }
  92. // 黑盒: 结果集与逐行过滤一致
  93. got := collectAges(t, tbl.Query().Where(In("age", vals, 0)))
  94. want := map[uint64]int64{}
  95. for id := uint64(1); id <= 200; id++ {
  96. age := int64(id % 50)
  97. for _, v := range vals {
  98. if age == v.(int64) {
  99. want[id] = age
  100. }
  101. }
  102. }
  103. if len(got) != len(want) {
  104. t.Fatalf("IN 命中 %d 行, 想要 %d", len(got), len(want))
  105. }
  106. for id, age := range want {
  107. if got[id] != age {
  108. t.Fatalf("pkID=%d: 得到 age=%d, 想要 %d", id, got[id], age)
  109. }
  110. }
  111. }
  112. // TestInOnHashIndex Hash 索引上的等值集合: 每个值一次桶定位。
  113. func TestInOnHashIndex(t *testing.T) {
  114. db := fixOpenDB(t)
  115. tbl := qopNewTable(t, db, "t")
  116. qopSeed(t, tbl, 200)
  117. plan := tbl.Query().Where(In("score", []interface{}{int64(5), int64(66)}, 0)).buildScanPlan()
  118. if plan == nil {
  119. t.Fatalf("IN(score, ...) 应生成索引计划")
  120. }
  121. if len(plan.ranges) != 2 {
  122. t.Fatalf("应展开 2 个区间, 得到 %d", len(plan.ranges))
  123. }
  124. got := collectAges(t, tbl.Query().Where(In("score", []interface{}{int64(5), int64(66)}, 0)))
  125. wantCount := 0
  126. for id := uint64(1); id <= 200; id++ {
  127. s := int64(id % 100)
  128. if s == 5 || s == 66 {
  129. wantCount++
  130. }
  131. }
  132. if len(got) != wantCount {
  133. t.Fatalf("Hash IN 命中 %d 行, 想要 %d", len(got), wantCount)
  134. }
  135. }
  136. // TestInEdgeCases 空集合恒假 / 不存在的值 / 重复值不重复返回 / 乱序值。
  137. func TestInEdgeCases(t *testing.T) {
  138. db := fixOpenDB(t)
  139. tbl := qopNewTable(t, db, "t")
  140. qopSeed(t, tbl, 100)
  141. // 空集合: 0 行 (不能退化成查全表)
  142. if n, err := tbl.Query().Where(In("age", nil, 0)).Count(); err != nil || n != 0 {
  143. t.Fatalf("空 IN 应得 0 行, 得到 %d (%v)", n, err)
  144. }
  145. // 全部不存在的值
  146. if n, err := tbl.Query().Where(In("age", []interface{}{int64(999), int64(1000)}, 0)).Count(); err != nil || n != 0 {
  147. t.Fatalf("不存在值的 IN 应得 0 行, 得到 %d", n)
  148. }
  149. // 重复值: 每行只返回一次
  150. dup := []interface{}{int64(7), int64(7), int64(7)}
  151. got := collectAges(t, tbl.Query().Where(In("age", dup, 0)))
  152. // age=7 → id%50==7 → id ∈ {7,57} (1..100 内)
  153. if len(got) != 2 {
  154. t.Fatalf("重复值的 IN 应命中 2 行, 得到 %d", len(got))
  155. }
  156. // 乱序值: 结果集不变
  157. a := collectAges(t, tbl.Query().Where(In("age", []interface{}{int64(49), int64(7), int64(23)}, 0)))
  158. b := collectAges(t, tbl.Query().Where(In("age", []interface{}{int64(7), int64(23), int64(49)}, 0)))
  159. if len(a) != len(b) {
  160. t.Fatalf("乱序 IN 结果数不一致: %d vs %d", len(a), len(b))
  161. }
  162. for id := range a {
  163. if _, ok := b[id]; !ok {
  164. t.Fatalf("乱序 IN 结果集不一致: pkID %d 只在一边", id)
  165. }
  166. }
  167. }
  168. // TestInWithOtherConditions IN 与其它条件组合: 同列范围条件让位给行级过滤,
  169. // 其它列条件进 remainingCond, 结果都必须正确。
  170. func TestInWithOtherConditions(t *testing.T) {
  171. db := fixOpenDB(t)
  172. tbl := qopNewTable(t, db, "t")
  173. qopSeed(t, tbl, 100)
  174. // IN + 同列范围: 范围条件走索引区间, IN 由行级过滤兜底
  175. got := collectAges(t, tbl.Query().Where(
  176. And(In("age", []interface{}{int64(20), int64(25), int64(30)}, 0),
  177. Gt("age", int64(22), 0))))
  178. // 期望: age ∈ {25, 30}
  179. if len(got) != 4 { // id%50==25 → 25,75; id%50==30 → 30,80
  180. t.Fatalf("IN+范围 应命中 4 行, 得到 %d", len(got))
  181. }
  182. for _, age := range got {
  183. if age != 25 && age != 30 {
  184. t.Fatalf("出现不该出现的 age=%d", age)
  185. }
  186. }
  187. // IN + 其它列等值 (score 走 hash 或行级过滤)
  188. got2 := collectAges(t, tbl.Query().Where(
  189. And(In("age", []interface{}{int64(10), int64(11)}, 0),
  190. Eq("score", int64(10), 0))))
  191. // age=10 & score=10 → id%50==10 且 id%100==10 → id=10, 110>100 → 仅 id=10
  192. if len(got2) != 1 {
  193. t.Fatalf("IN+其它列 应命中 1 行, 得到 %d", len(got2))
  194. }
  195. if got2[10] != 10 {
  196. t.Fatalf("应为 pkID=10 age=10, 得到 %+v", got2)
  197. }
  198. }
  199. // TestBatchResolveMatchesPerPK 批量回表与逐个回表结果一致 (含已删除的行号):
  200. // 删除若干行后, 二级索引扫描回表必须自动跳过不存在的 pkID。
  201. func TestBatchResolveMatchesPerPK(t *testing.T) {
  202. db := fixOpenDB(t)
  203. tbl := qopNewTable(t, db, "t")
  204. qopSeed(t, tbl, 300)
  205. // 删除 1/3 的行 (交错)
  206. for id := uint64(2); id <= 300; id += 3 {
  207. if err := tbl.DeleteRow("pk", qopPKKey(id)); err != nil {
  208. t.Fatalf("删除 %d: %v", id, err)
  209. }
  210. }
  211. if err := db.Commit(); err != nil {
  212. t.Fatalf("commit: %v", err)
  213. }
  214. // 大范围扫 + 回表
  215. got := collectAges(t, tbl.Query().Where(Ge("age", int64(0), 0)))
  216. want := 0
  217. for id := uint64(1); id <= 300; id++ {
  218. if id%3 == 2 {
  219. continue // 已删除
  220. }
  221. want++
  222. }
  223. if len(got) != want {
  224. t.Fatalf("范围扫回表 %d 行, 想要 %d", len(got), want)
  225. }
  226. for id := range got {
  227. if id%3 == 2 {
  228. t.Fatalf("已删除的行 %d 被回表出来", id)
  229. }
  230. }
  231. // IN 批量回表 (排序去重路径)
  232. in := collectAges(t, tbl.Query().Where(
  233. In("age", []interface{}{int64(1), int64(2), int64(3)}, 0)))
  234. for id := range in {
  235. if id%3 == 2 {
  236. t.Fatalf("IN 回表带出已删除行 %d", id)
  237. }
  238. }
  239. }
  240. // TestTopKMatchesFullSort ORDER BY + LIMIT 的流式 top-k 与"全量排序再切片"逐行一致,
  241. // 覆盖正/反序、OFFSET、大量重复键 (稳定性) 与 IN 多区间走排序管线的组合。
  242. func TestTopKMatchesFullSort(t *testing.T) {
  243. db := fixOpenDB(t)
  244. tbl := qopNewTable(t, db, "t")
  245. qopSeed(t, tbl, 500) // age = i%50: 每个 age 10 行, 天然大量重复键
  246. full := []uint64{}
  247. err := tbl.Query().OrderBy("age").ScanWithPKID(func(pkID uint64, _ []types.FieldValue) bool {
  248. full = append(full, pkID)
  249. return true
  250. })
  251. if err != nil {
  252. t.Fatalf("full sort: %v", err)
  253. }
  254. // 稳定性参照: 相等 age 按扫描顺序 (pkID 升序) 排列
  255. for _, tc := range []struct {
  256. name string
  257. build func() *Query
  258. wantSlice func() []uint64
  259. }{
  260. {
  261. name: "asc limit 10",
  262. build: func() *Query { return tbl.Query().OrderBy("age").Limit(10) },
  263. wantSlice: func() []uint64 {
  264. return full[:10]
  265. },
  266. },
  267. {
  268. name: "desc limit 20",
  269. build: func() *Query { return tbl.Query().OrderBy("age", true).Limit(20) },
  270. wantSlice: func() []uint64 {
  271. // 稳定降序: 键降序, 相等键保持输入序 (pkID 升序) —
  272. // 不能对升序结果简单反转 (那会把相等键也倒过来)。
  273. type pa struct {
  274. pk uint64
  275. age int64
  276. }
  277. in := make([]pa, 500)
  278. for i := 0; i < 500; i++ {
  279. in[i] = pa{uint64(i + 1), int64((i + 1) % 50)}
  280. }
  281. slices.SortStableFunc(in, func(a, b pa) int { return cmp.Compare(b.age, a.age) })
  282. out := make([]uint64, 20)
  283. for i := range out {
  284. out[i] = in[i].pk
  285. }
  286. return out
  287. },
  288. },
  289. {
  290. name: "asc offset 100 limit 50",
  291. build: func() *Query { return tbl.Query().OrderBy("age").Offset(100).Limit(50) },
  292. wantSlice: func() []uint64 {
  293. return full[100:150]
  294. },
  295. },
  296. {
  297. name: "asc limit 1 (First 场景)",
  298. build: func() *Query {
  299. return tbl.Query().OrderBy("age").Limit(1)
  300. },
  301. wantSlice: func() []uint64 {
  302. return full[:1]
  303. },
  304. },
  305. } {
  306. var got []uint64
  307. err := tc.build().ScanWithPKID(func(pkID uint64, _ []types.FieldValue) bool {
  308. got = append(got, pkID)
  309. return true
  310. })
  311. if err != nil {
  312. t.Fatalf("%s: %v", tc.name, err)
  313. }
  314. want := tc.wantSlice()
  315. if len(got) != len(want) {
  316. t.Fatalf("%s: %d 行, 想要 %d", tc.name, len(got), len(want))
  317. }
  318. for i := range want {
  319. if got[i] != want[i] {
  320. t.Fatalf("%s: 第 %d 位 pkID=%d, 想要 %d (稳定性破坏)", tc.name, i, got[i], want[i])
  321. }
  322. }
  323. }
  324. // IN 多区间 + ORDER BY (强制排序管线) + LIMIT
  325. var got []uint64
  326. err = tbl.Query().Where(In("age", []interface{}{int64(3), int64(1), int64(2)}, 0)).
  327. OrderBy("age").Limit(5).
  328. ScanWithPKID(func(pkID uint64, _ []types.FieldValue) bool {
  329. got = append(got, pkID)
  330. return true
  331. })
  332. if err != nil {
  333. t.Fatalf("IN+ORDER BY+LIMIT: %v", err)
  334. }
  335. if len(got) != 5 {
  336. t.Fatalf("IN+ORDER BY+LIMIT 应得 5 行, 得到 %d", len(got))
  337. }
  338. // 期望: age=1 的 10 行 (pkID 1,51,101,151,201,251,301>500→1,51,101,151,201,251... 500 内: 1,51,101,151,201,251,301,351,401,451) 取前 5
  339. want5 := []uint64{1, 51, 101, 151, 201}
  340. for i := range want5 {
  341. if got[i] != want5[i] {
  342. t.Fatalf("IN+ORDER BY 第 %d 位 = %d, 想要 %d", i, got[i], want5[i])
  343. }
  344. }
  345. }
  346. // TestSelectProjectionPushdown 投影下推: 只解码选中列 (+ORDER BY 列),
  347. // 未选中列不出现; 行内 NULL 与字符串列在投影下语义不变。
  348. func TestSelectProjectionPushdown(t *testing.T) {
  349. db := fixOpenDB(t)
  350. tbl, err := db.CreateTable("t", []types.FieldInfo{
  351. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  352. {FieldType: types.FieldType_Int64, FieldName: []byte("age")},
  353. {FieldType: types.FieldType_String, FieldName: []byte("name"), Flag: types.WidthCodeFor(32)},
  354. })
  355. if err != nil {
  356. t.Fatalf("建表: %v", err)
  357. }
  358. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  359. t.Fatalf("建主键索引: %v", err)
  360. }
  361. // 交错插入: 部分行 name 为空串, 部分有值
  362. for i := 1; i <= 20; i++ {
  363. nm := ""
  364. if i%2 == 0 {
  365. nm = "emp"
  366. }
  367. if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{
  368. {FieldName: []byte("id"), Value: uint64(i)},
  369. {FieldName: []byte("age"), Value: int64(i * 3)},
  370. {FieldName: []byte("name"), Value: nm},
  371. }); err != nil {
  372. t.Fatalf("插入 %d: %v", i, err)
  373. }
  374. }
  375. if err := db.Commit(); err != nil {
  376. t.Fatalf("commit: %v", err)
  377. }
  378. // 只选 name: 行里只有 name 列
  379. err = tbl.Query().Select("name").Scan(func(row []types.FieldValue) bool {
  380. if len(row) != 1 || string(row[0].FieldName) != "name" {
  381. t.Fatalf("投影行应只含 name, 得到 %v", row)
  382. }
  383. return true
  384. })
  385. if err != nil {
  386. t.Fatalf("Select(name) scan: %v", err)
  387. }
  388. // Select + OrderBy(未选中列): 排序键列必须随投影保留, 排序结果正确
  389. type pair struct {
  390. id, age int64
  391. }
  392. var got []pair
  393. err = tbl.Query().Select("id").OrderBy("age", true).Limit(3).Scan(func(row []types.FieldValue) bool {
  394. var p pair
  395. for _, fv := range row {
  396. switch string(fv.FieldName) {
  397. case "id":
  398. p.id = int64(fv.Value.(uint64))
  399. case "age":
  400. p.age = fv.Value.(int64)
  401. }
  402. }
  403. got = append(got, p)
  404. return true
  405. })
  406. if err != nil {
  407. t.Fatalf("Select+OrderBy: %v", err)
  408. }
  409. // age 倒序前三: id=20,19,18 → age=60,57,54
  410. want := []pair{{20, 60}, {19, 57}, {18, 54}}
  411. for i, w := range want {
  412. if got[i] != w {
  413. t.Fatalf("第 %d 位 = %+v, 想要 %+v", i, got[i], w)
  414. }
  415. }
  416. // Find + Select: project 后只剩选中列
  417. rows, err := tbl.Query().Select("age").OrderBy("id").Limit(2).Find()
  418. if err != nil {
  419. t.Fatalf("Find+Select: %v", err)
  420. }
  421. for _, row := range rows {
  422. if len(row) != 1 || string(row[0].FieldName) != "age" {
  423. t.Fatalf("Find+Select 行应只含 age, 得到 %v", row)
  424. }
  425. }
  426. }
  427. // TestInCountUsesIndex IN 的 Count 也走索引区间 (不物化行)。
  428. func TestInCountUsesIndex(t *testing.T) {
  429. db := fixOpenDB(t)
  430. tbl := qopNewTable(t, db, "t")
  431. qopSeed(t, tbl, 1000)
  432. n, err := tbl.Query().Where(In("age", []interface{}{int64(7), int64(8)}, 0)).Count()
  433. if err != nil {
  434. t.Fatalf("count: %v", err)
  435. }
  436. // age=7 → id%50==7 → 1..1000 内 20 个; age=8 → 20 个
  437. if n != 40 {
  438. t.Fatalf("IN count 应得 40, 得到 %d", n)
  439. }
  440. }