query_optimizer_test.go 14 KB

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