diff --git a/README.md b/README.md
index 4456393..7220807 100644
--- a/README.md
+++ b/README.md
@@ -5,38 +5,50 @@
## Benchmark Dashboard
-`generated 2026-05-28 03:03:29` · `queries 43/43` · `warm runs 4` · [`csv`](benchmarks/results/readme_benchmarks.csv)
-
-### Summary
-
-| Metric | Value |
-| --- | ---: |
-| queries ok | 43 / 43 |
-| queries failed | 0 |
-| median warm time | 237.07 ms |
-| average warm time | 268.20 ms |
-| p95 warm time | 777.08 ms |
-| cold/warm delta | 11.3% |
-| total output size | 20.3 KB |
-| max output size | 6.0 KB |
-| fastest query | Q06 · 3.68 ms |
-| slowest query | Q18 · 1112.19 ms |
-
-### Storage
-
-| Metric | Value |
-| --- | ---: |
-| source csv | `benchmarks/hits_sample.csv` |
-| schema | `benchmarks/scheme.csv` |
-| source size | 802.6 MB |
-| columnar size | 694.9 MB |
-| roundtrip csv size | 743.0 MB |
-| compression ratio | 1.16x |
-| columnar / csv | 86.6% |
-| csv -> columnar | 5.77s |
-| columnar -> csv | 9.67s |
-| convert throughput | 139.2 MB/s |
-| roundtrip throughput | 71.9 MB/s |
+`generated 2026-05-28 21:28:51` · `queries 43/43` · `warm runs 4` · [`csv`](benchmarks/results/readme_benchmarks.csv)
+
+
+
+
+Summary
+
+| Metric | Value |
+
+| queries ok | 43 / 43 |
+| queries failed | 0 |
+| median warm time | 220.81 ms |
+| average warm time | 231.63 ms |
+| p95 warm time | 476.62 ms |
+| cold/warm delta | 5.3% |
+| total output size | 20.3 KB |
+| max output size | 6.0 KB |
+| fastest query | Q06 · 4.12 ms |
+| slowest query | Q28 · 969.54 ms |
+
+
+ |
+
+Storage
+
+| Metric | Value |
+
+| source csv | benchmarks/hits_sample.csv |
+| schema | benchmarks/scheme.csv |
+| compression | lz4 |
+| source size | 802.6 MB |
+| columnar size | 119.7 MB |
+| roundtrip csv size | 743.0 MB |
+| compression ratio | 6.71x |
+| columnar / csv | 14.9% |
+| csv -> columnar | 5.81s |
+| columnar -> csv | 9.84s |
+| convert throughput | 138.1 MB/s |
+| roundtrip throughput | 12.2 MB/s |
+
+
+ |
+
+
### Heatmap
@@ -44,89 +56,60 @@
| slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | slot 6 |
| ---: | ---: | ---: | ---: | ---: | ---: |
-| Q00 🟩 `3.91` | Q01 🟩 `6.32` | Q02 🟩 `17.25` | Q03 🟩 `13.37` | Q04 🟩 `148.12` | Q05 🟩 `130.96` |
-| Q06 🟩 `3.68` | Q07 🟩 `6.69` | Q08 🟩 `193.36` | Q09 🟩 `209.45` | Q10 🟥 `268.08` | Q11 🟥 `269.48` |
-| Q12 🟦 `233.32` | Q13 🟦 `245.55` | Q14 🟦 `238.18` | Q15 🟩 `73.32` | Q16 🟩 `122.39` | Q17 🟩 `144.28` |
-| Q18 🟥 `1112.19` | Q19 🟩 `4.11` | Q20 🟩 `134.97` | Q21 🟥 `268.11` | Q22 🟥 `357.07` | Q23 🟦 `236.84` |
-| Q24 🟥 `273.73` | Q25 🟦 `226.36` | Q26 🟥 `289.97` | Q27 🟥 `398.98` | Q28 🟥 `907.32` | Q29 🟥 `414.52` |
-| Q30 🟦 `237.07` | Q31 🟥 `262.05` | Q32 🟥 `787.69` | Q33 🟥 `419.25` | Q34 🟥 `423.99` | Q35 🟥 `681.60` |
-| Q36 🟥 `337.69` | Q37 🟦 `244.01` | Q38 🟩 `67.28` | Q39 🟥 `426.31` | Q40 🟩 `92.18` | Q41 🟩 `33.18` |
-| Q42 🟥 `568.52` | | | | | |
-### Largest Result Sets
-
-| Query | Output | Warm median, ms | SQL |
-| --- | ---: | ---: | --- |
-| [Q22](benchmarks/queries/query_22.sql) | 6.0 KB | 357.07 | SELECT SearchPhrase, MIN(URL), MIN(Title), COUNT(*) AS c, COUNT… |
-| [Q23](benchmarks/queries/query_23.sql) | 2.6 KB | 236.84 | SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime… |
-| [Q29](benchmarks/queries/query_29.sql) | 990 B | 414.52 | SELECT SUM(ResolutionWidth), SUM(ResolutionWidth + 1), SUM(Reso… |
-| [Q39](benchmarks/queries/query_39.sql) | 835 B | 426.31 | SELECT TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (… |
-| [Q24](benchmarks/queries/query_24.sql) | 658 B | 273.73 | SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY… |
-
-### Fastest Queries
-
-| Query | SQL | Warm median, ms | First run, ms |
-| --- | --- | ---: | ---: |
-| [Q06](benchmarks/queries/query_6.sql) | SELECT MIN(EventDate), MAX(EventDate) FROM hits; | 3.68 | 4.43 |
-| [Q00](benchmarks/queries/query_0.sql) | SELECT COUNT(*) FROM hits; | 3.91 | 8.14 |
-| [Q19](benchmarks/queries/query_19.sql) | SELECT UserID FROM hits WHERE UserID = 435090932899640449; | 4.11 | 4.50 |
-| [Q01](benchmarks/queries/query_1.sql) | SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0; | 6.32 | 34.17 |
-| [Q07](benchmarks/queries/query_7.sql) | SELECT AdvEngineID, COUNT(*) FROM hits WHERE AdvEngineID <> 0 GROUP BY … | 6.69 | 6.40 |
-
-### Slowest Queries
-
-| Query | SQL | Warm median, ms | Warm max, ms |
-| --- | --- | ---: | ---: |
-| [Q18](benchmarks/queries/query_18.sql) | SELECT UserID, extract(minute FROM EventTime) AS m, SearchPhrase, COUNT… | 1112.19 | 1178.59 |
-| [Q28](benchmarks/queries/query_28.sql) | SELECT REGEXP_REPLACE(Referer, '^https?://(?:www\.)?([^/]+)/.*$', '\1')… | 907.32 | 910.64 |
-| [Q32](benchmarks/queries/query_32.sql) | SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(Resolution… | 787.69 | 790.48 |
-| [Q35](benchmarks/queries/query_35.sql) | SELECT ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3, COUNT(*) AS … | 681.60 | 683.99 |
-| [Q42](benchmarks/queries/query_42.sql) | SELECT DATE_TRUNC('minute', EventTime) AS M, COUNT(*) AS PageViews FROM… | 568.52 | 569.02 |
+| Q00 🟩 `7.14` | Q01 🟩 `12.85` | Q02 🟩 `21.68` | Q03 🟩 `14.30` | Q04 🟩 `147.02` | Q05 🟩 `128.76` |
+| Q06 🟩 `4.12` | Q07 🟩 `11.69` | Q08 🟩 `189.51` | Q09 🟦 `208.53` | Q10 🟥 `267.84` | Q11 🟥 `267.06` |
+| Q12 🟦 `225.37` | Q13 🟥 `253.71` | Q14 🟦 `227.55` | Q15 🟩 `74.16` | Q16 🟩 `116.97` | Q17 🟩 `146.00` |
+| Q18 🟥 `349.51` | Q19 🟩 `4.39` | Q20 🟥 `252.69` | Q21 🟥 `277.31` | Q22 🟥 `364.71` | Q23 🟦 `240.62` |
+| Q24 🟦 `220.81` | Q25 🟦 `219.69` | Q26 🟦 `220.03` | Q27 🟥 `450.51` | Q28 🟥 `969.54` | Q29 🟥 `427.54` |
+| Q30 🟦 `235.10` | Q31 🟥 `262.43` | Q32 🟥 `881.20` | Q33 🟥 `349.73` | Q34 🟥 `420.41` | Q35 🟩 `149.57` |
+| Q36 🟥 `339.52` | Q37 🟥 `247.46` | Q38 🟩 `73.93` | Q39 🟥 `479.52` | Q40 🟩 `95.10` | Q41 🟩 `34.88` |
+| Q42 🟩 `69.69` | | | | | |
### Query Table
| Query | Output CSV | First run, ms | Warm avg, ms | Warm median, ms | Warm min, ms | Warm max, ms | Status |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
-| [Q00](benchmarks/queries/query_0.sql) | 7 B | 8.14 | 3.95 | 3.91 | 3.78 | 4.22 | ok |
-| [Q01](benchmarks/queries/query_1.sql) | 6 B | 34.17 | 6.33 | 6.32 | 6.01 | 6.67 | ok |
-| [Q02](benchmarks/queries/query_2.sql) | 18 B | 48.00 | 17.26 | 17.25 | 16.83 | 17.71 | ok |
-| [Q03](benchmarks/queries/query_3.sql) | 20 B | 49.77 | 13.43 | 13.37 | 13.21 | 13.76 | ok |
-| [Q04](benchmarks/queries/query_4.sql) | 6 B | 148.05 | 149.27 | 148.12 | 147.47 | 153.38 | ok |
-| [Q05](benchmarks/queries/query_5.sql) | 6 B | 165.31 | 130.88 | 130.96 | 130.57 | 131.02 | ok |
-| [Q06](benchmarks/queries/query_6.sql) | 22 B | 4.43 | 3.69 | 3.68 | 3.60 | 3.82 | ok |
-| [Q07](benchmarks/queries/query_7.sql) | 31 B | 6.40 | 6.80 | 6.69 | 6.46 | 7.35 | ok |
-| [Q08](benchmarks/queries/query_8.sql) | 81 B | 230.36 | 193.81 | 193.36 | 191.19 | 197.33 | ok |
-| [Q09](benchmarks/queries/query_9.sql) | 237 B | 210.07 | 210.02 | 209.45 | 209.09 | 212.08 | ok |
-| [Q10](benchmarks/queries/query_10.sql) | 102 B | 299.72 | 268.08 | 268.08 | 267.32 | 268.86 | ok |
-| [Q11](benchmarks/queries/query_11.sql) | 116 B | 287.55 | 270.30 | 269.48 | 268.57 | 273.67 | ok |
-| [Q12](benchmarks/queries/query_12.sql) | 387 B | 229.11 | 232.94 | 233.32 | 230.58 | 234.54 | ok |
-| [Q13](benchmarks/queries/query_13.sql) | 350 B | 247.57 | 246.07 | 245.55 | 244.77 | 248.40 | ok |
-| [Q14](benchmarks/queries/query_14.sql) | 400 B | 249.88 | 237.64 | 238.18 | 235.56 | 238.64 | ok |
-| [Q15](benchmarks/queries/query_15.sql) | 236 B | 75.58 | 73.07 | 73.32 | 71.09 | 74.55 | ok |
-| [Q16](benchmarks/queries/query_16.sql) | 244 B | 130.68 | 122.37 | 122.39 | 121.98 | 122.72 | ok |
-| [Q17](benchmarks/queries/query_17.sql) | 414 B | 155.01 | 144.52 | 144.28 | 142.55 | 146.97 | ok |
-| [Q18](benchmarks/queries/query_18.sql) | 264 B | 1288.46 | 1127.01 | 1112.19 | 1105.08 | 1178.59 | ok |
-| [Q19](benchmarks/queries/query_19.sql) | 0 B | 4.50 | 4.34 | 4.11 | 4.01 | 5.15 | ok |
-| [Q20](benchmarks/queries/query_20.sql) | 3 B | 227.21 | 135.07 | 134.97 | 134.86 | 135.50 | ok |
-| [Q21](benchmarks/queries/query_21.sql) | 162 B | 266.42 | 268.70 | 268.11 | 264.90 | 273.70 | ok |
-| [Q22](benchmarks/queries/query_22.sql) | 6.0 KB | 471.23 | 356.98 | 357.07 | 354.97 | 358.80 | ok |
-| [Q23](benchmarks/queries/query_23.sql) | 2.6 KB | 272.00 | 236.75 | 236.84 | 235.63 | 237.68 | ok |
-| [Q24](benchmarks/queries/query_24.sql) | 658 B | 280.44 | 274.08 | 273.73 | 272.01 | 276.87 | ok |
-| [Q25](benchmarks/queries/query_25.sql) | 382 B | 226.93 | 226.56 | 226.36 | 226.06 | 227.44 | ok |
-| [Q26](benchmarks/queries/query_26.sql) | 658 B | 272.76 | 300.66 | 289.97 | 271.68 | 351.01 | ok |
-| [Q27](benchmarks/queries/query_27.sql) | 26 B | 449.09 | 398.29 | 398.98 | 394.65 | 400.55 | ok |
-| [Q28](benchmarks/queries/query_28.sql) | 556 B | 1015.82 | 907.49 | 907.32 | 904.67 | 910.64 | ok |
-| [Q29](benchmarks/queries/query_29.sql) | 990 B | 414.40 | 414.81 | 414.52 | 414.18 | 416.03 | ok |
-| [Q30](benchmarks/queries/query_30.sql) | 244 B | 290.88 | 236.69 | 237.07 | 234.72 | 237.89 | ok |
-| [Q31](benchmarks/queries/query_31.sql) | 398 B | 271.91 | 261.89 | 262.05 | 258.68 | 264.77 | ok |
-| [Q32](benchmarks/queries/query_32.sql) | 376 B | 1001.96 | 785.02 | 787.69 | 774.24 | 790.48 | ok |
-| [Q33](benchmarks/queries/query_33.sql) | 464 B | 391.70 | 454.24 | 419.25 | 390.48 | 587.97 | ok |
-| [Q34](benchmarks/queries/query_34.sql) | 484 B | 436.48 | 424.10 | 423.99 | 413.11 | 435.32 | ok |
-| [Q35](benchmarks/queries/query_35.sql) | 493 B | 689.66 | 681.69 | 681.60 | 679.59 | 683.99 | ok |
-| [Q36](benchmarks/queries/query_36.sql) | 569 B | 379.10 | 338.62 | 337.69 | 333.24 | 345.84 | ok |
-| [Q37](benchmarks/queries/query_37.sql) | 550 B | 256.58 | 244.32 | 244.01 | 243.86 | 245.39 | ok |
-| [Q38](benchmarks/queries/query_38.sql) | 480 B | 83.42 | 67.53 | 67.28 | 67.05 | 68.52 | ok |
-| [Q39](benchmarks/queries/query_39.sql) | 835 B | 536.54 | 426.07 | 426.31 | 423.11 | 428.55 | ok |
-| [Q40](benchmarks/queries/query_40.sql) | 356 B | 113.53 | 92.15 | 92.18 | 91.78 | 92.46 | ok |
-| [Q41](benchmarks/queries/query_41.sql) | 132 B | 46.85 | 33.59 | 33.18 | 32.37 | 35.65 | ok |
-| [Q42](benchmarks/queries/query_42.sql) | 240 B | 572.27 | 568.26 | 568.52 | 566.96 | 569.02 | ok |
+| [Q00](benchmarks/queries/query_0.sql) | 7 B | 372.47 | 7.12 | 7.14 | 5.32 | 8.89 | ok |
+| [Q01](benchmarks/queries/query_1.sql) | 6 B | 16.18 | 13.17 | 12.85 | 11.37 | 15.61 | ok |
+| [Q02](benchmarks/queries/query_2.sql) | 18 B | 28.44 | 21.85 | 21.68 | 21.58 | 22.45 | ok |
+| [Q03](benchmarks/queries/query_3.sql) | 20 B | 14.56 | 14.35 | 14.30 | 14.24 | 14.55 | ok |
+| [Q04](benchmarks/queries/query_4.sql) | 6 B | 151.94 | 148.55 | 147.02 | 146.46 | 153.70 | ok |
+| [Q05](benchmarks/queries/query_5.sql) | 6 B | 131.23 | 128.78 | 128.76 | 128.60 | 129.00 | ok |
+| [Q06](benchmarks/queries/query_6.sql) | 22 B | 4.63 | 4.11 | 4.12 | 4.00 | 4.22 | ok |
+| [Q07](benchmarks/queries/query_7.sql) | 31 B | 11.54 | 11.68 | 11.69 | 11.61 | 11.71 | ok |
+| [Q08](benchmarks/queries/query_8.sql) | 81 B | 189.62 | 189.70 | 189.51 | 189.29 | 190.49 | ok |
+| [Q09](benchmarks/queries/query_9.sql) | 237 B | 209.27 | 209.04 | 208.53 | 207.81 | 211.30 | ok |
+| [Q10](benchmarks/queries/query_10.sql) | 102 B | 273.63 | 268.25 | 267.84 | 264.68 | 272.65 | ok |
+| [Q11](benchmarks/queries/query_11.sql) | 116 B | 267.74 | 267.06 | 267.06 | 266.30 | 267.81 | ok |
+| [Q12](benchmarks/queries/query_12.sql) | 387 B | 225.38 | 225.50 | 225.37 | 225.15 | 226.12 | ok |
+| [Q13](benchmarks/queries/query_13.sql) | 350 B | 255.52 | 253.82 | 253.71 | 253.17 | 254.72 | ok |
+| [Q14](benchmarks/queries/query_14.sql) | 400 B | 227.71 | 227.58 | 227.55 | 227.02 | 228.18 | ok |
+| [Q15](benchmarks/queries/query_15.sql) | 236 B | 73.92 | 74.08 | 74.16 | 73.23 | 74.78 | ok |
+| [Q16](benchmarks/queries/query_16.sql) | 244 B | 116.27 | 117.15 | 116.97 | 116.74 | 117.90 | ok |
+| [Q17](benchmarks/queries/query_17.sql) | 414 B | 146.45 | 145.98 | 146.00 | 145.32 | 146.60 | ok |
+| [Q18](benchmarks/queries/query_18.sql) | 264 B | 371.65 | 354.57 | 349.51 | 345.73 | 373.53 | ok |
+| [Q19](benchmarks/queries/query_19.sql) | 0 B | 5.11 | 4.40 | 4.39 | 4.35 | 4.45 | ok |
+| [Q20](benchmarks/queries/query_20.sql) | 3 B | 253.57 | 252.47 | 252.69 | 251.74 | 252.77 | ok |
+| [Q21](benchmarks/queries/query_21.sql) | 162 B | 275.26 | 277.07 | 277.31 | 274.21 | 279.46 | ok |
+| [Q22](benchmarks/queries/query_22.sql) | 6.0 KB | 364.65 | 364.26 | 364.71 | 362.10 | 365.51 | ok |
+| [Q23](benchmarks/queries/query_23.sql) | 2.6 KB | 247.58 | 240.46 | 240.62 | 239.89 | 240.72 | ok |
+| [Q24](benchmarks/queries/query_24.sql) | 672 B | 221.33 | 220.77 | 220.81 | 220.51 | 220.95 | ok |
+| [Q25](benchmarks/queries/query_25.sql) | 382 B | 219.30 | 219.65 | 219.69 | 218.74 | 220.49 | ok |
+| [Q26](benchmarks/queries/query_26.sql) | 658 B | 221.11 | 220.32 | 220.03 | 219.98 | 221.26 | ok |
+| [Q27](benchmarks/queries/query_27.sql) | 26 B | 448.24 | 450.32 | 450.51 | 448.06 | 452.19 | ok |
+| [Q28](benchmarks/queries/query_28.sql) | 556 B | 972.11 | 969.67 | 969.54 | 964.82 | 974.79 | ok |
+| [Q29](benchmarks/queries/query_29.sql) | 990 B | 436.39 | 427.32 | 427.54 | 426.26 | 427.95 | ok |
+| [Q30](benchmarks/queries/query_30.sql) | 244 B | 237.16 | 235.05 | 235.10 | 234.09 | 235.90 | ok |
+| [Q31](benchmarks/queries/query_31.sql) | 398 B | 264.83 | 262.59 | 262.43 | 261.71 | 263.79 | ok |
+| [Q32](benchmarks/queries/query_32.sql) | 376 B | 1089.10 | 866.39 | 881.20 | 808.51 | 894.64 | ok |
+| [Q33](benchmarks/queries/query_33.sql) | 464 B | 354.03 | 349.81 | 349.73 | 349.37 | 350.43 | ok |
+| [Q34](benchmarks/queries/query_34.sql) | 484 B | 369.46 | 435.58 | 420.41 | 370.29 | 531.22 | ok |
+| [Q35](benchmarks/queries/query_35.sql) | 493 B | 151.95 | 149.11 | 149.57 | 142.89 | 154.43 | ok |
+| [Q36](benchmarks/queries/query_36.sql) | 569 B | 331.43 | 345.67 | 339.52 | 333.21 | 370.41 | ok |
+| [Q37](benchmarks/queries/query_37.sql) | 550 B | 249.16 | 247.76 | 247.46 | 246.64 | 249.47 | ok |
+| [Q38](benchmarks/queries/query_38.sql) | 480 B | 79.44 | 74.20 | 73.93 | 73.32 | 75.60 | ok |
+| [Q39](benchmarks/queries/query_39.sql) | 835 B | 400.94 | 541.20 | 479.52 | 391.72 | 814.03 | ok |
+| [Q40](benchmarks/queries/query_40.sql) | 356 B | 103.53 | 95.10 | 95.10 | 94.63 | 95.58 | ok |
+| [Q41](benchmarks/queries/query_41.sql) | 132 B | 39.71 | 34.90 | 34.88 | 34.50 | 35.36 | ok |
+| [Q42](benchmarks/queries/query_42.sql) | 240 B | 68.37 | 69.46 | 69.69 | 68.04 | 70.42 | ok |
diff --git a/benchmarks/results/readme_benchmarks.csv b/benchmarks/results/readme_benchmarks.csv
index 0a6a1d8..673aaa1 100644
--- a/benchmarks/results/readme_benchmarks.csv
+++ b/benchmarks/results/readme_benchmarks.csv
@@ -1,44 +1,44 @@
query_id,query_file,sql,first_run_ms,warm_avg_ms,warm_median_ms,warm_min_ms,warm_max_ms,output_csv_bytes,status,error
-0,benchmarks/queries/query_0.sql,SELECT COUNT(*) FROM hits;,8.14,3.95,3.91,3.78,4.22,7,ok,
-1,benchmarks/queries/query_1.sql,SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0;,34.17,6.33,6.32,6.01,6.67,6,ok,
-2,benchmarks/queries/query_2.sql,"SELECT SUM(AdvEngineID), COUNT(*), AVG(ResolutionWidth) FROM hits;",48.00,17.26,17.25,16.83,17.71,18,ok,
-3,benchmarks/queries/query_3.sql,SELECT AVG(UserID) FROM hits;,49.77,13.43,13.37,13.21,13.76,20,ok,
-4,benchmarks/queries/query_4.sql,SELECT COUNT(DISTINCT UserID) FROM hits;,148.05,149.27,148.12,147.47,153.38,6,ok,
-5,benchmarks/queries/query_5.sql,SELECT COUNT(DISTINCT SearchPhrase) FROM hits;,165.31,130.88,130.96,130.57,131.02,6,ok,
-6,benchmarks/queries/query_6.sql,"SELECT MIN(EventDate), MAX(EventDate) FROM hits;",4.43,3.69,3.68,3.60,3.82,22,ok,
-7,benchmarks/queries/query_7.sql,"SELECT AdvEngineID, COUNT(*) FROM hits WHERE AdvEngineID <> 0 GROUP BY AdvEngineID ORDER BY COUNT(*) DESC;",6.40,6.80,6.69,6.46,7.35,31,ok,
-8,benchmarks/queries/query_8.sql,"SELECT RegionID, COUNT(DISTINCT UserID) AS u FROM hits GROUP BY RegionID ORDER BY u DESC LIMIT 10;",230.36,193.81,193.36,191.19,197.33,81,ok,
-9,benchmarks/queries/query_9.sql,"SELECT RegionID, SUM(AdvEngineID), COUNT(*) AS c, AVG(ResolutionWidth), COUNT(DISTINCT UserID) FROM hits GROUP BY RegionID ORDER BY c DESC LIMIT 10;",210.07,210.02,209.45,209.09,212.08,237,ok,
-10,benchmarks/queries/query_10.sql,"SELECT MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhoneModel ORDER BY u DESC LIMIT 10;",299.72,268.08,268.08,267.32,268.86,102,ok,
-11,benchmarks/queries/query_11.sql,"SELECT MobilePhone, MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhone, MobilePhoneModel ORDER BY u DESC LIMIT 10;",287.55,270.30,269.48,268.57,273.67,116,ok,
-12,benchmarks/queries/query_12.sql,"SELECT SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10;",229.11,232.94,233.32,230.58,234.54,387,ok,
-13,benchmarks/queries/query_13.sql,"SELECT SearchPhrase, COUNT(DISTINCT UserID) AS u FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY u DESC LIMIT 10;",247.57,246.07,245.55,244.77,248.40,350,ok,
-14,benchmarks/queries/query_14.sql,"SELECT SearchEngineID, SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, SearchPhrase ORDER BY c DESC LIMIT 10;",249.88,237.64,238.18,235.56,238.64,400,ok,
-15,benchmarks/queries/query_15.sql,"SELECT UserID, COUNT(*) FROM hits GROUP BY UserID ORDER BY COUNT(*) DESC LIMIT 10;",75.58,73.07,73.32,71.09,74.55,236,ok,
-16,benchmarks/queries/query_16.sql,"SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10;",130.68,122.37,122.39,121.98,122.72,244,ok,
-17,benchmarks/queries/query_17.sql,"SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase LIMIT 10;",155.01,144.52,144.28,142.55,146.97,414,ok,
-18,benchmarks/queries/query_18.sql,"SELECT UserID, extract(minute FROM EventTime) AS m, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, m, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10;",1288.46,1127.01,1112.19,1105.08,1178.59,264,ok,
-19,benchmarks/queries/query_19.sql,SELECT UserID FROM hits WHERE UserID = 435090932899640449;,4.50,4.34,4.11,4.01,5.15,0,ok,
-20,benchmarks/queries/query_20.sql,SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%';,227.21,135.07,134.97,134.86,135.50,3,ok,
-21,benchmarks/queries/query_21.sql,"SELECT SearchPhrase, MIN(URL), COUNT(*) AS c FROM hits WHERE URL LIKE '%google%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10;",266.42,268.70,268.11,264.90,273.70,162,ok,
-22,benchmarks/queries/query_22.sql,"SELECT SearchPhrase, MIN(URL), MIN(Title), COUNT(*) AS c, COUNT(DISTINCT UserID) FROM hits WHERE Title LIKE '%Google%' AND URL NOT LIKE '%.google.%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10;",471.23,356.98,357.07,354.97,358.80,6188,ok,
-23,benchmarks/queries/query_23.sql,SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime LIMIT 10;,272.00,236.75,236.84,235.63,237.68,2633,ok,
-24,benchmarks/queries/query_24.sql,SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT 10;,280.44,274.08,273.73,272.01,276.87,658,ok,
-25,benchmarks/queries/query_25.sql,SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10;,226.93,226.56,226.36,226.06,227.44,382,ok,
-26,benchmarks/queries/query_26.sql,"SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10;",272.76,300.66,289.97,271.68,351.01,658,ok,
-27,benchmarks/queries/query_27.sql,"SELECT CounterID, AVG(STRLEN(URL)) AS l, COUNT(*) AS c FROM hits WHERE URL <> '' GROUP BY CounterID HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;",449.09,398.29,398.98,394.65,400.55,26,ok,
-28,benchmarks/queries/query_28.sql,"SELECT REGEXP_REPLACE(Referer, '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(STRLEN(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;",1015.82,907.49,907.32,904.67,910.64,556,ok,
-29,benchmarks/queries/query_29.sql,"SELECT SUM(ResolutionWidth), SUM(ResolutionWidth + 1), SUM(ResolutionWidth + 2), SUM(ResolutionWidth + 3), SUM(ResolutionWidth + 4), SUM(ResolutionWidth + 5), SUM(ResolutionWidth + 6), SUM(ResolutionWidth + 7), SUM(ResolutionWidth + 8), SUM(ResolutionWidth + 9), SUM(ResolutionWidth + 10), SUM(ResolutionWidth + 11), SUM(ResolutionWidth + 12), SUM(ResolutionWidth + 13), SUM(ResolutionWidth + 14), SUM(ResolutionWidth + 15), SUM(ResolutionWidth + 16), SUM(ResolutionWidth + 17), SUM(ResolutionWidth + 18), SUM(ResolutionWidth + 19), SUM(ResolutionWidth + 20), SUM(ResolutionWidth + 21), SUM(ResolutionWidth + 22), SUM(ResolutionWidth + 23), SUM(ResolutionWidth + 24), SUM(ResolutionWidth + 25), SUM(ResolutionWidth + 26), SUM(ResolutionWidth + 27), SUM(ResolutionWidth + 28), SUM(ResolutionWidth + 29), SUM(ResolutionWidth + 30), SUM(ResolutionWidth + 31), SUM(ResolutionWidth + 32), SUM(ResolutionWidth + 33), SUM(ResolutionWidth + 34), SUM(ResolutionWidth + 35), SUM(ResolutionWidth + 36), SUM(ResolutionWidth + 37), SUM(ResolutionWidth + 38), SUM(ResolutionWidth + 39), SUM(ResolutionWidth + 40), SUM(ResolutionWidth + 41), SUM(ResolutionWidth + 42), SUM(ResolutionWidth + 43), SUM(ResolutionWidth + 44), SUM(ResolutionWidth + 45), SUM(ResolutionWidth + 46), SUM(ResolutionWidth + 47), SUM(ResolutionWidth + 48), SUM(ResolutionWidth + 49), SUM(ResolutionWidth + 50), SUM(ResolutionWidth + 51), SUM(ResolutionWidth + 52), SUM(ResolutionWidth + 53), SUM(ResolutionWidth + 54), SUM(ResolutionWidth + 55), SUM(ResolutionWidth + 56), SUM(ResolutionWidth + 57), SUM(ResolutionWidth + 58), SUM(ResolutionWidth + 59), SUM(ResolutionWidth + 60), SUM(ResolutionWidth + 61), SUM(ResolutionWidth + 62), SUM(ResolutionWidth + 63), SUM(ResolutionWidth + 64), SUM(ResolutionWidth + 65), SUM(ResolutionWidth + 66), SUM(ResolutionWidth + 67), SUM(ResolutionWidth + 68), SUM(ResolutionWidth + 69), SUM(ResolutionWidth + 70), SUM(ResolutionWidth + 71), SUM(ResolutionWidth + 72), SUM(ResolutionWidth + 73), SUM(ResolutionWidth + 74), SUM(ResolutionWidth + 75), SUM(ResolutionWidth + 76), SUM(ResolutionWidth + 77), SUM(ResolutionWidth + 78), SUM(ResolutionWidth + 79), SUM(ResolutionWidth + 80), SUM(ResolutionWidth + 81), SUM(ResolutionWidth + 82), SUM(ResolutionWidth + 83), SUM(ResolutionWidth + 84), SUM(ResolutionWidth + 85), SUM(ResolutionWidth + 86), SUM(ResolutionWidth + 87), SUM(ResolutionWidth + 88), SUM(ResolutionWidth + 89) FROM hits;",414.40,414.81,414.52,414.18,416.03,990,ok,
-30,benchmarks/queries/query_30.sql,"SELECT SearchEngineID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, ClientIP ORDER BY c DESC LIMIT 10;",290.88,236.69,237.07,234.72,237.89,244,ok,
-31,benchmarks/queries/query_31.sql,"SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10;",271.91,261.89,262.05,258.68,264.77,398,ok,
-32,benchmarks/queries/query_32.sql,"SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10;",1001.96,785.02,787.69,774.24,790.48,376,ok,
-33,benchmarks/queries/query_33.sql,"SELECT URL, COUNT(*) AS c FROM hits GROUP BY URL ORDER BY c DESC LIMIT 10;",391.70,454.24,419.25,390.48,587.97,464,ok,
-34,benchmarks/queries/query_34.sql,"SELECT 1, URL, COUNT(*) AS c FROM hits GROUP BY 1, URL ORDER BY c DESC LIMIT 10;",436.48,424.10,423.99,413.11,435.32,484,ok,
-35,benchmarks/queries/query_35.sql,"SELECT ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3, COUNT(*) AS c FROM hits GROUP BY ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3 ORDER BY c DESC LIMIT 10;",689.66,681.69,681.60,679.59,683.99,493,ok,
-36,benchmarks/queries/query_36.sql,"SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND URL <> '' GROUP BY URL ORDER BY PageViews DESC LIMIT 10;",379.10,338.62,337.69,333.24,345.84,569,ok,
-37,benchmarks/queries/query_37.sql,"SELECT Title, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND Title <> '' GROUP BY Title ORDER BY PageViews DESC LIMIT 10;",256.58,244.32,244.01,243.86,245.39,550,ok,
-38,benchmarks/queries/query_38.sql,"SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND IsLink <> 0 AND IsDownload = 0 GROUP BY URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;",83.42,67.53,67.28,67.05,68.52,480,ok,
-39,benchmarks/queries/query_39.sql,"SELECT TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END AS Src, URL AS Dst, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 GROUP BY TraficSourceID, SearchEngineID, AdvEngineID, Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;",536.54,426.07,426.31,423.11,428.55,835,ok,
-40,benchmarks/queries/query_40.sql,"SELECT URLHash, EventDate, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND TraficSourceID IN (-1, 6) AND RefererHash = 3594120000172545465 GROUP BY URLHash, EventDate ORDER BY PageViews DESC LIMIT 10 OFFSET 100;",113.53,92.15,92.18,91.78,92.46,356,ok,
-41,benchmarks/queries/query_41.sql,"SELECT WindowClientWidth, WindowClientHeight, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND DontCountHits = 0 AND URLHash = 2868770270353813622 GROUP BY WindowClientWidth, WindowClientHeight ORDER BY PageViews DESC LIMIT 10 OFFSET 10000;",46.85,33.59,33.18,32.37,35.65,132,ok,
-42,benchmarks/queries/query_42.sql,"SELECT DATE_TRUNC('minute', EventTime) AS M, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-14' AND EventDate <= '2013-07-15' AND IsRefresh = 0 AND DontCountHits = 0 GROUP BY DATE_TRUNC('minute', EventTime) ORDER BY DATE_TRUNC('minute', EventTime) LIMIT 10 OFFSET 1000;",572.27,568.26,568.52,566.96,569.02,240,ok,
+0,benchmarks/queries/query_0.sql,SELECT COUNT(*) FROM hits;,372.47,7.12,7.14,5.32,8.89,7,ok,
+1,benchmarks/queries/query_1.sql,SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0;,16.18,13.17,12.85,11.37,15.61,6,ok,
+2,benchmarks/queries/query_2.sql,"SELECT SUM(AdvEngineID), COUNT(*), AVG(ResolutionWidth) FROM hits;",28.44,21.85,21.68,21.58,22.45,18,ok,
+3,benchmarks/queries/query_3.sql,SELECT AVG(UserID) FROM hits;,14.56,14.35,14.30,14.24,14.55,20,ok,
+4,benchmarks/queries/query_4.sql,SELECT COUNT(DISTINCT UserID) FROM hits;,151.94,148.55,147.02,146.46,153.70,6,ok,
+5,benchmarks/queries/query_5.sql,SELECT COUNT(DISTINCT SearchPhrase) FROM hits;,131.23,128.78,128.76,128.60,129.00,6,ok,
+6,benchmarks/queries/query_6.sql,"SELECT MIN(EventDate), MAX(EventDate) FROM hits;",4.63,4.11,4.12,4.00,4.22,22,ok,
+7,benchmarks/queries/query_7.sql,"SELECT AdvEngineID, COUNT(*) FROM hits WHERE AdvEngineID <> 0 GROUP BY AdvEngineID ORDER BY COUNT(*) DESC;",11.54,11.68,11.69,11.61,11.71,31,ok,
+8,benchmarks/queries/query_8.sql,"SELECT RegionID, COUNT(DISTINCT UserID) AS u FROM hits GROUP BY RegionID ORDER BY u DESC LIMIT 10;",189.62,189.70,189.51,189.29,190.49,81,ok,
+9,benchmarks/queries/query_9.sql,"SELECT RegionID, SUM(AdvEngineID), COUNT(*) AS c, AVG(ResolutionWidth), COUNT(DISTINCT UserID) FROM hits GROUP BY RegionID ORDER BY c DESC LIMIT 10;",209.27,209.04,208.53,207.81,211.30,237,ok,
+10,benchmarks/queries/query_10.sql,"SELECT MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhoneModel ORDER BY u DESC LIMIT 10;",273.63,268.25,267.84,264.68,272.65,102,ok,
+11,benchmarks/queries/query_11.sql,"SELECT MobilePhone, MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhone, MobilePhoneModel ORDER BY u DESC LIMIT 10;",267.74,267.06,267.06,266.30,267.81,116,ok,
+12,benchmarks/queries/query_12.sql,"SELECT SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10;",225.38,225.50,225.37,225.15,226.12,387,ok,
+13,benchmarks/queries/query_13.sql,"SELECT SearchPhrase, COUNT(DISTINCT UserID) AS u FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY u DESC LIMIT 10;",255.52,253.82,253.71,253.17,254.72,350,ok,
+14,benchmarks/queries/query_14.sql,"SELECT SearchEngineID, SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, SearchPhrase ORDER BY c DESC LIMIT 10;",227.71,227.58,227.55,227.02,228.18,400,ok,
+15,benchmarks/queries/query_15.sql,"SELECT UserID, COUNT(*) FROM hits GROUP BY UserID ORDER BY COUNT(*) DESC LIMIT 10;",73.92,74.08,74.16,73.23,74.78,236,ok,
+16,benchmarks/queries/query_16.sql,"SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10;",116.27,117.15,116.97,116.74,117.90,244,ok,
+17,benchmarks/queries/query_17.sql,"SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase LIMIT 10;",146.45,145.98,146.00,145.32,146.60,414,ok,
+18,benchmarks/queries/query_18.sql,"SELECT UserID, extract(minute FROM EventTime) AS m, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, m, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10;",371.65,354.57,349.51,345.73,373.53,264,ok,
+19,benchmarks/queries/query_19.sql,SELECT UserID FROM hits WHERE UserID = 435090932899640449;,5.11,4.40,4.39,4.35,4.45,0,ok,
+20,benchmarks/queries/query_20.sql,SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%';,253.57,252.47,252.69,251.74,252.77,3,ok,
+21,benchmarks/queries/query_21.sql,"SELECT SearchPhrase, MIN(URL), COUNT(*) AS c FROM hits WHERE URL LIKE '%google%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10;",275.26,277.07,277.31,274.21,279.46,162,ok,
+22,benchmarks/queries/query_22.sql,"SELECT SearchPhrase, MIN(URL), MIN(Title), COUNT(*) AS c, COUNT(DISTINCT UserID) FROM hits WHERE Title LIKE '%Google%' AND URL NOT LIKE '%.google.%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10;",364.65,364.26,364.71,362.10,365.51,6188,ok,
+23,benchmarks/queries/query_23.sql,SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime LIMIT 10;,247.58,240.46,240.62,239.89,240.72,2633,ok,
+24,benchmarks/queries/query_24.sql,SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT 10;,221.33,220.77,220.81,220.51,220.95,672,ok,
+25,benchmarks/queries/query_25.sql,SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10;,219.30,219.65,219.69,218.74,220.49,382,ok,
+26,benchmarks/queries/query_26.sql,"SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10;",221.11,220.32,220.03,219.98,221.26,658,ok,
+27,benchmarks/queries/query_27.sql,"SELECT CounterID, AVG(STRLEN(URL)) AS l, COUNT(*) AS c FROM hits WHERE URL <> '' GROUP BY CounterID HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;",448.24,450.32,450.51,448.06,452.19,26,ok,
+28,benchmarks/queries/query_28.sql,"SELECT REGEXP_REPLACE(Referer, '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(STRLEN(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;",972.11,969.67,969.54,964.82,974.79,556,ok,
+29,benchmarks/queries/query_29.sql,"SELECT SUM(ResolutionWidth), SUM(ResolutionWidth + 1), SUM(ResolutionWidth + 2), SUM(ResolutionWidth + 3), SUM(ResolutionWidth + 4), SUM(ResolutionWidth + 5), SUM(ResolutionWidth + 6), SUM(ResolutionWidth + 7), SUM(ResolutionWidth + 8), SUM(ResolutionWidth + 9), SUM(ResolutionWidth + 10), SUM(ResolutionWidth + 11), SUM(ResolutionWidth + 12), SUM(ResolutionWidth + 13), SUM(ResolutionWidth + 14), SUM(ResolutionWidth + 15), SUM(ResolutionWidth + 16), SUM(ResolutionWidth + 17), SUM(ResolutionWidth + 18), SUM(ResolutionWidth + 19), SUM(ResolutionWidth + 20), SUM(ResolutionWidth + 21), SUM(ResolutionWidth + 22), SUM(ResolutionWidth + 23), SUM(ResolutionWidth + 24), SUM(ResolutionWidth + 25), SUM(ResolutionWidth + 26), SUM(ResolutionWidth + 27), SUM(ResolutionWidth + 28), SUM(ResolutionWidth + 29), SUM(ResolutionWidth + 30), SUM(ResolutionWidth + 31), SUM(ResolutionWidth + 32), SUM(ResolutionWidth + 33), SUM(ResolutionWidth + 34), SUM(ResolutionWidth + 35), SUM(ResolutionWidth + 36), SUM(ResolutionWidth + 37), SUM(ResolutionWidth + 38), SUM(ResolutionWidth + 39), SUM(ResolutionWidth + 40), SUM(ResolutionWidth + 41), SUM(ResolutionWidth + 42), SUM(ResolutionWidth + 43), SUM(ResolutionWidth + 44), SUM(ResolutionWidth + 45), SUM(ResolutionWidth + 46), SUM(ResolutionWidth + 47), SUM(ResolutionWidth + 48), SUM(ResolutionWidth + 49), SUM(ResolutionWidth + 50), SUM(ResolutionWidth + 51), SUM(ResolutionWidth + 52), SUM(ResolutionWidth + 53), SUM(ResolutionWidth + 54), SUM(ResolutionWidth + 55), SUM(ResolutionWidth + 56), SUM(ResolutionWidth + 57), SUM(ResolutionWidth + 58), SUM(ResolutionWidth + 59), SUM(ResolutionWidth + 60), SUM(ResolutionWidth + 61), SUM(ResolutionWidth + 62), SUM(ResolutionWidth + 63), SUM(ResolutionWidth + 64), SUM(ResolutionWidth + 65), SUM(ResolutionWidth + 66), SUM(ResolutionWidth + 67), SUM(ResolutionWidth + 68), SUM(ResolutionWidth + 69), SUM(ResolutionWidth + 70), SUM(ResolutionWidth + 71), SUM(ResolutionWidth + 72), SUM(ResolutionWidth + 73), SUM(ResolutionWidth + 74), SUM(ResolutionWidth + 75), SUM(ResolutionWidth + 76), SUM(ResolutionWidth + 77), SUM(ResolutionWidth + 78), SUM(ResolutionWidth + 79), SUM(ResolutionWidth + 80), SUM(ResolutionWidth + 81), SUM(ResolutionWidth + 82), SUM(ResolutionWidth + 83), SUM(ResolutionWidth + 84), SUM(ResolutionWidth + 85), SUM(ResolutionWidth + 86), SUM(ResolutionWidth + 87), SUM(ResolutionWidth + 88), SUM(ResolutionWidth + 89) FROM hits;",436.39,427.32,427.54,426.26,427.95,990,ok,
+30,benchmarks/queries/query_30.sql,"SELECT SearchEngineID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, ClientIP ORDER BY c DESC LIMIT 10;",237.16,235.05,235.10,234.09,235.90,244,ok,
+31,benchmarks/queries/query_31.sql,"SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10;",264.83,262.59,262.43,261.71,263.79,398,ok,
+32,benchmarks/queries/query_32.sql,"SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10;",1089.10,866.39,881.20,808.51,894.64,376,ok,
+33,benchmarks/queries/query_33.sql,"SELECT URL, COUNT(*) AS c FROM hits GROUP BY URL ORDER BY c DESC LIMIT 10;",354.03,349.81,349.73,349.37,350.43,464,ok,
+34,benchmarks/queries/query_34.sql,"SELECT 1, URL, COUNT(*) AS c FROM hits GROUP BY 1, URL ORDER BY c DESC LIMIT 10;",369.46,435.58,420.41,370.29,531.22,484,ok,
+35,benchmarks/queries/query_35.sql,"SELECT ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3, COUNT(*) AS c FROM hits GROUP BY ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3 ORDER BY c DESC LIMIT 10;",151.95,149.11,149.57,142.89,154.43,493,ok,
+36,benchmarks/queries/query_36.sql,"SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND URL <> '' GROUP BY URL ORDER BY PageViews DESC LIMIT 10;",331.43,345.67,339.52,333.21,370.41,569,ok,
+37,benchmarks/queries/query_37.sql,"SELECT Title, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND Title <> '' GROUP BY Title ORDER BY PageViews DESC LIMIT 10;",249.16,247.76,247.46,246.64,249.47,550,ok,
+38,benchmarks/queries/query_38.sql,"SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND IsLink <> 0 AND IsDownload = 0 GROUP BY URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;",79.44,74.20,73.93,73.32,75.60,480,ok,
+39,benchmarks/queries/query_39.sql,"SELECT TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END AS Src, URL AS Dst, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 GROUP BY TraficSourceID, SearchEngineID, AdvEngineID, Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;",400.94,541.20,479.52,391.72,814.03,835,ok,
+40,benchmarks/queries/query_40.sql,"SELECT URLHash, EventDate, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND TraficSourceID IN (-1, 6) AND RefererHash = 3594120000172545465 GROUP BY URLHash, EventDate ORDER BY PageViews DESC LIMIT 10 OFFSET 100;",103.53,95.10,95.10,94.63,95.58,356,ok,
+41,benchmarks/queries/query_41.sql,"SELECT WindowClientWidth, WindowClientHeight, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND DontCountHits = 0 AND URLHash = 2868770270353813622 GROUP BY WindowClientWidth, WindowClientHeight ORDER BY PageViews DESC LIMIT 10 OFFSET 10000;",39.71,34.90,34.88,34.50,35.36,132,ok,
+42,benchmarks/queries/query_42.sql,"SELECT DATE_TRUNC('minute', EventTime) AS M, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-14' AND EventDate <= '2013-07-15' AND IsRefresh = 0 AND DontCountHits = 0 GROUP BY DATE_TRUNC('minute', EventTime) ORDER BY DATE_TRUNC('minute', EventTime) LIMIT 10 OFFSET 1000;",68.37,69.46,69.69,68.04,70.42,240,ok,
diff --git a/benchmarks/support/query_result_checks.cpp b/benchmarks/support/query_result_checks.cpp
index bbdcc03..78549a1 100644
--- a/benchmarks/support/query_result_checks.cpp
+++ b/benchmarks/support/query_result_checks.cpp
@@ -9,8 +9,8 @@
#include "common/ascii.h"
#include "common/error.h"
-#include "common/operator_utils.h"
-#include "common/planner_utils.h"
+#include "executor/comparison_utils.h"
+#include "executor/query_utils.h"
#include "io/csv.h"
#include "io/csv_batch.h"
diff --git a/include/common/string_arena.h b/include/common/string_arena.h
new file mode 100644
index 0000000..5411ae7
--- /dev/null
+++ b/include/common/string_arena.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+class StringArena {
+ public:
+ explicit StringArena(size_t chunk_size = 4096);
+
+ std::string_view Store(std::string_view value);
+ void Clear();
+
+ private:
+ struct Chunk {
+ std::unique_ptr data;
+ size_t capacity = 0;
+ size_t used = 0;
+ };
+
+ Chunk& EnsureChunk(size_t size);
+
+ size_t chunk_size_ = 0;
+ std::vector chunks_;
+};
+
+struct StringViewHash {
+ size_t operator()(std::string_view value) const noexcept;
+};
+
+struct StringViewEqual {
+ bool operator()(std::string_view lhs, std::string_view rhs) const noexcept;
+};
+
+using StringViewSet = std::unordered_set;
diff --git a/include/common/string_pattern_utils.h b/include/common/string_pattern_utils.h
new file mode 100644
index 0000000..7a70759
--- /dev/null
+++ b/include/common/string_pattern_utils.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include
+
+bool LikeMatches(std::string_view value, std::string_view pattern);
diff --git a/include/convert/csv_columnar.h b/include/convert/csv_columnar.h
index 058af34..a069e1e 100644
--- a/include/convert/csv_columnar.h
+++ b/include/convert/csv_columnar.h
@@ -3,10 +3,12 @@
#include
#include
+#include "io/compression.h"
+
inline constexpr size_t DefaultMaxRowsPerGroup = 1 << 14;
void ConvertCsvToColumnar(const std::filesystem::path& schema_path, const std::filesystem::path& data_path,
- const std::filesystem::path& output_path,
- size_t max_rows_per_group = DefaultMaxRowsPerGroup);
+ const std::filesystem::path& output_path, size_t max_rows_per_group = DefaultMaxRowsPerGroup,
+ Compression compression = Compression::None);
void ConvertColumnarToCsv(const std::filesystem::path& columnar_path, const std::filesystem::path& schema_path,
const std::filesystem::path& data_path);
diff --git a/include/common/operator_utils.h b/include/executor/comparison_utils.h
similarity index 83%
rename from include/common/operator_utils.h
rename to include/executor/comparison_utils.h
index 3d1a93d..43f13d4 100644
--- a/include/common/operator_utils.h
+++ b/include/executor/comparison_utils.h
@@ -7,5 +7,5 @@
std::strong_ordering CompareValues(ColumnType type, std::string_view lhs, std::string_view rhs);
bool MatchesComparison(ColumnType type, std::string_view lhs, std::string_view rhs, ComparisonKind kind);
-bool LikeMatches(std::string_view value, std::string_view pattern);
+
ColumnType AggregateOutputType(const PlannedAgg& aggregate);
diff --git a/include/executor/operators_internal.h b/include/executor/operators_internal.h
new file mode 100644
index 0000000..c0fd102
--- /dev/null
+++ b/include/executor/operators_internal.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "executor/operator.h"
+#include "executor/query_utils.h"
+
+std::unique_ptr CreateMetadataCountOperator(std::filesystem::path path, std::string output_name);
+std::unique_ptr CreateMetadataExtremaOperator(std::filesystem::path path, std::vector aggregates);
+std::unique_ptr CreateScanOperator(std::filesystem::path path, std::vector projection_indexes,
+ PredicatePtr filter);
+std::unique_ptr CreateFilterOperator(std::unique_ptr child, PredicatePtr filter);
+std::unique_ptr CreateEnsureSchemaOperator(std::unique_ptr child, Schema schema);
+std::unique_ptr CreateProjectionOperator(std::unique_ptr child, std::vector items,
+ Schema source_schema);
+std::unique_ptr CreateAggOperator(std::unique_ptr child, std::vector aggregates,
+ PredicatePtr filter);
+std::unique_ptr CreateGroupAggOperator(std::unique_ptr child,
+ std::vector group_keys,
+ const std::vector& aggregates,
+ std::vector select_items, PredicatePtr filter,
+ PredicatePtr having, bool sort_by_group_keys);
+std::unique_ptr CreateGroupAggTopKOperator(std::unique_ptr child,
+ std::vector group_keys,
+ const std::vector& aggregates,
+ std::vector select_items, PredicatePtr filter,
+ std::vector order_by, size_t limit);
+
+void ApplyOrderOffsetLimit(std::unique_ptr& root, const PlannedQuery& planned, bool& limit_applied_by_top_k);
+Schema ProjectionOutputSchema(const std::vector& items, const Schema& source_schema);
+
+std::string EvalExpr(const ExprPtr& expr, const Batch& batch, size_t row);
+bool EvaluatePredicate(const PredicatePtr& predicate, const Batch& batch, size_t row);
+void SelectRowsMatchingPredicate(const PredicatePtr& predicate, const Batch& batch, std::vector& rows);
+Schema BuildSelectOutputSchema(const std::vector& select_items);
diff --git a/include/executor/query.h b/include/executor/query.h
index 6ed56ad..36ca0e3 100644
--- a/include/executor/query.h
+++ b/include/executor/query.h
@@ -42,6 +42,7 @@ enum class QueryValueKind {
struct QueryValue {
QueryValueKind kind = QueryValueKind::Literal;
+
ColumnRef column;
QueryLiteral literal;
};
@@ -76,6 +77,7 @@ struct ExprSpec {
ExprKind kind = ExprKind::Literal;
ColumnRef column;
+
bool column_index_bound = false;
size_t column_index = 0;
@@ -109,8 +111,8 @@ struct PredicateSpec {
PredicateKind kind = PredicateKind::Comparison;
ExprPtr left;
- ComparisonKind comparison = ComparisonKind::Equal;
ExprPtr right;
+ ComparisonKind comparison = ComparisonKind::Equal;
std::vector values;
@@ -118,24 +120,29 @@ struct PredicateSpec {
PredicatePtr rhs;
bool typed_literal_comparison_bound = false;
+
size_t typed_column_index = 0;
Int128 typed_literal_value = 0;
ComparisonKind typed_comparison = ComparisonKind::Equal;
bool metadata_typed_literal_comparison_bound = false;
+
size_t metadata_typed_column_index = 0;
Int128 metadata_typed_literal_value = 0;
ComparisonKind metadata_typed_comparison = ComparisonKind::Equal;
bool literal_in_set_bound = false;
+
size_t in_column_index = 0;
std::unordered_set literal_in_values;
bool metadata_typed_in_set_bound = false;
+
size_t metadata_typed_in_column_index = 0;
std::vector metadata_typed_in_values;
bool literal_like_pattern_bound = false;
+
size_t like_column_index = 0;
std::string like_pattern;
bool like_negated = false;
@@ -149,9 +156,12 @@ enum class AggArgumentKind {
struct AggSpec {
std::string function_name;
bool distinct = false;
+
AggArgumentKind argument_kind = AggArgumentKind::Column;
+
ColumnRef column;
ExprPtr argument;
+
std::string output_name;
};
@@ -162,9 +172,11 @@ enum class SelectItemKind {
struct SelectItemSpec {
SelectItemKind kind = SelectItemKind::GroupKey;
+
ColumnRef column;
ExprPtr expression;
AggSpec aggregate;
+
std::string output_name;
};
diff --git a/include/executor/query_plan.h b/include/executor/query_plan.h
index fcebdcd..9fc7f32 100644
--- a/include/executor/query_plan.h
+++ b/include/executor/query_plan.h
@@ -40,13 +40,16 @@ struct PlannedGroupKey {
struct PlannedSelectItem {
SelectItemKind kind = SelectItemKind::GroupKey;
size_t index = 0;
+
ColumnType output_type = ColumnType::String;
std::string output_name;
};
struct PlannedOrderBy {
size_t result_column_index = 0;
+
bool descending = false;
+
ColumnType value_type = ColumnType::String;
};
@@ -66,8 +69,9 @@ struct PlannedValue {
struct PlannedFilter {
PlannedValue left;
- ComparisonKind comparison = ComparisonKind::Equal;
PlannedValue right;
+
+ ComparisonKind comparison = ComparisonKind::Equal;
ColumnType comparison_type = ColumnType::String;
};
@@ -89,6 +93,7 @@ struct PlannedQuery {
std::vector select_items;
std::vector plain_select_items;
+
std::vector order_by;
std::vector plain_order_by;
diff --git a/include/common/planner_utils.h b/include/executor/query_utils.h
similarity index 67%
rename from include/common/planner_utils.h
rename to include/executor/query_utils.h
index 1e5bc12..e8ff824 100644
--- a/include/common/planner_utils.h
+++ b/include/executor/query_utils.h
@@ -1,5 +1,6 @@
#pragma once
+#include
#include
#include
#include
@@ -9,10 +10,16 @@
size_t FindColumnIndex(const Schema& schema, std::string_view column_name);
void ValidateColumnQualifier(const Query& query, const ColumnRef& column);
+std::string UnescapeSqlString(std::string_view text);
std::string NormalizeLiteral(const QueryLiteral& literal, ColumnType type);
+std::string NormalizeLiteralForEval(const QueryLiteral& literal);
+void NormalizeRegexReplacement(std::string& replacement);
bool SameColumnName(std::string_view lhs, std::string_view rhs);
bool SameColumnRef(const ColumnRef& lhs, const ColumnRef& rhs);
bool SameSelectItem(const SelectItemSpec& lhs, const SelectItemSpec& rhs);
bool SameOutputName(const SelectItemSpec& lhs, const SelectItemSpec& rhs);
+bool SameExpr(const ExprPtr& lhs, const ExprPtr& rhs);
+
std::vector StarColumnIndexes(const Schema& schema);
+std::optional TryFindBatchColumn(const Schema& schema, std::string_view name);
diff --git a/include/executor/typed_value_utils.h b/include/executor/typed_value_utils.h
new file mode 100644
index 0000000..95aa863
--- /dev/null
+++ b/include/executor/typed_value_utils.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include "executor/query.h"
+#include "model/schema.h"
+
+bool IsNumericColumnType(ColumnType type);
+Int128 ParseColumnValueAsInt128(ColumnType type, std::string_view value);
+std::optional TryParseLiteralValueAsInt128(const QueryLiteral& literal, ColumnType type);
+std::string FormatInt128Value(ColumnType type, Int128 value);
+ComparisonKind FlipComparison(ComparisonKind comparison);
diff --git a/include/io/columnar_batch.h b/include/io/columnar_batch.h
index 9dfebb0..ae8c0b8 100644
--- a/include/io/columnar_batch.h
+++ b/include/io/columnar_batch.h
@@ -2,8 +2,11 @@
#include
#include
+#include
+#include
#include "io/batch.h"
+#include "io/compression.h"
#include "io/file.h"
#include "model/metadata.h"
@@ -33,7 +36,7 @@ class ColumnarBatchReader final : public BatchReader {
class ColumnarBatchWriter final : public BatchWriter {
public:
- ColumnarBatchWriter(const std::filesystem::path& path, Schema schema);
+ ColumnarBatchWriter(const std::filesystem::path& path, Schema schema, Compression compression = Compression::None);
ColumnarBatchWriter(const ColumnarBatchWriter&) = delete;
ColumnarBatchWriter(ColumnarBatchWriter&&) noexcept = default;
ColumnarBatchWriter& operator=(const ColumnarBatchWriter&) = delete;
@@ -43,6 +46,7 @@ class ColumnarBatchWriter final : public BatchWriter {
void Write(const Batch& batch) override;
void Flush() override;
+ void Finalize() &;
void Finalize() &&;
const ColumnarMetadata& GetMetadata() const { return metadata_; }
@@ -52,5 +56,11 @@ class ColumnarBatchWriter final : public BatchWriter {
std::ofstream out_;
ColumnarMetadata metadata_;
+ Compression compression_ = Compression::None;
bool finalized_ = false;
};
+
+std::vector ReadColumnChunk(const std::filesystem::path& path, InputFile& input,
+ const ColumnChunkMetadata& chunk);
+void ReadBatchColumnChunk(const std::filesystem::path& path, InputFile& input, const ColumnChunkMetadata& chunk,
+ uint32_t row_count, const Batch& batch, size_t column_index);
diff --git a/include/io/compression.h b/include/io/compression.h
new file mode 100644
index 0000000..4f9d0f1
--- /dev/null
+++ b/include/io/compression.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+enum class Compression : uint8_t {
+ None = 0,
+ Lz4 = 1,
+};
+
+const char* CompressionName(Compression compression);
+Compression CompressionFromName(std::string_view name);
+
+std::vector Compress(std::span input, Compression compression);
+std::vector Decompress(std::span input, Compression compression, uint64_t uncompressed_size);
diff --git a/include/io/csv_batch.h b/include/io/csv_batch.h
index 97dbab2..de63f9a 100644
--- a/include/io/csv_batch.h
+++ b/include/io/csv_batch.h
@@ -22,6 +22,7 @@ class CsvBatchReader final : public BatchReader {
private:
CsvReader csv_reader_;
+
Schema schema_;
BatchSizing sizing_;
diff --git a/include/model/metadata.h b/include/model/metadata.h
index dbc3844..9db52f7 100644
--- a/include/model/metadata.h
+++ b/include/model/metadata.h
@@ -5,12 +5,18 @@
#include
#include "common/int128.h"
+#include "io/compression.h"
#include "model/schema.h"
struct ColumnChunkMetadata {
uint64_t offset = 0;
- uint64_t size = 0;
+
+ uint64_t compressed_size = 0;
+ uint64_t uncompressed_size = 0;
+ Compression compression = Compression::None;
+
bool has_min_max = false;
+
Int128 min_value = 0;
Int128 max_value = 0;
};
diff --git a/scripts/dashboard.py b/scripts/dashboard.py
index 4f8f3eb..01c4254 100644
--- a/scripts/dashboard.py
+++ b/scripts/dashboard.py
@@ -76,6 +76,12 @@ def parse_args() -> argparse.Namespace:
default=1 << 14,
help="Row group size for the CSV -> columnar conversion benchmark.",
)
+ parser.add_argument(
+ "--compression",
+ default="none",
+ choices=("none", "lz4"),
+ help="Compression codec for the CSV -> columnar conversion benchmark.",
+ )
return parser.parse_args()
@@ -128,6 +134,30 @@ def markdown_escape(text: str) -> str:
return text.replace("|", "\\|")
+def html_escape(text: str) -> str:
+ return (
+ text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace('"', """)
+ )
+
+
+def render_html_metric_table(title: str, rows: list[tuple[str, str]]) -> list[str]:
+ lines = [
+ f"{html_escape(title)}
",
+ '',
+ '| Metric | Value |
',
+ "",
+ ]
+ for metric, value in rows:
+ lines.append(
+ f"| {html_escape(str(metric))} | {html_escape(str(value))} |
"
+ )
+ lines.extend(["", "
"])
+ return lines
+
+
def parse_ms(raw: str) -> float | None:
if not raw:
return None
@@ -161,6 +191,7 @@ def collect_conversion_stats(
schema_path: pathlib.Path,
source_csv_path: pathlib.Path,
row_group_size: int,
+ compression: str,
) -> dict[str, float | int | str] | None:
if not schema_path.exists() or not source_csv_path.exists():
return None
@@ -182,6 +213,8 @@ def collect_conversion_stats(
str(output_columnar),
"--row-group-size",
str(row_group_size),
+ "--compression",
+ compression,
]
roundtrip_command = [
str(binary),
@@ -215,6 +248,7 @@ def collect_conversion_stats(
return {
"source_csv_path": str(source_csv_path.relative_to(root)),
"schema_path": str(schema_path.relative_to(root)),
+ "compression": compression,
"source_csv_bytes": source_csv_bytes,
"columnar_bytes": columnar_bytes,
"roundtrip_csv_bytes": roundtrip_csv_bytes,
@@ -425,98 +459,75 @@ def render_markdown(
cold_penalty = ((statistics.fmean(first_run_values) / avg_value) - 1.0) * 100.0 if avg_value and first_run_values else 0.0
fastest = min(ok_rows, key=query_metric) if ok_rows else None
slowest = max(ok_rows, key=query_metric) if ok_rows else None
- top_slowest = sorted(ok_rows, key=query_metric, reverse=True)[:5]
- top_fastest = sorted(ok_rows, key=query_metric)[:5]
- largest_outputs = sorted(ok_rows, key=lambda row: int(row["output_csv_bytes"]), reverse=True)[:5]
+ summary_rows = [
+ ("queries ok", f"{len(ok_rows)} / {len(rows)}"),
+ ("queries failed", str(len(failed_rows))),
+ ("median warm time", f"{format_ms(median_value)} ms"),
+ ("average warm time", f"{format_ms(avg_value)} ms"),
+ ("p95 warm time", f"{format_ms(p95_value)} ms"),
+ ("cold/warm delta", format_percent(cold_penalty)),
+ ("total output size", format_size(sum(output_sizes))),
+ ("max output size", format_size(max(output_sizes)) if output_sizes else "—"),
+ (
+ "fastest query",
+ f"Q{int(fastest['query_id']):02d} · {format_ms(query_metric(fastest))} ms" if fastest else "—",
+ ),
+ (
+ "slowest query",
+ f"Q{int(slowest['query_id']):02d} · {format_ms(query_metric(slowest))} ms" if slowest else "—",
+ ),
+ ]
lines = [
"## Benchmark Dashboard",
"",
f"`generated {generated_at}` · `queries {len(rows)}/{count}` · `warm runs {warm_runs}` · [`csv`]({csv_path})",
"",
- "### Summary",
- "",
- "| Metric | Value |",
- "| --- | ---: |",
- f"| queries ok | {len(ok_rows)} / {len(rows)} |",
- f"| queries failed | {len(failed_rows)} |",
- f"| median warm time | {format_ms(median_value)} ms |",
- f"| average warm time | {format_ms(avg_value)} ms |",
- f"| p95 warm time | {format_ms(p95_value)} ms |",
- f"| cold/warm delta | {format_percent(cold_penalty)} |",
- f"| total output size | {format_size(sum(output_sizes))} |",
- f"| max output size | {format_size(max(output_sizes)) if output_sizes else '—'} |",
- f"| fastest query | Q{int(fastest['query_id']):02d} · {format_ms(query_metric(fastest))} ms |" if fastest else "| fastest query | — |",
- f"| slowest query | Q{int(slowest['query_id']):02d} · {format_ms(query_metric(slowest))} ms |" if slowest else "| slowest query | — |",
- "",
]
if conversion_stats is not None:
+ storage_rows = [
+ ("source csv", conversion_stats["source_csv_path"]),
+ ("schema", conversion_stats["schema_path"]),
+ ("compression", conversion_stats["compression"]),
+ ("source size", format_size(int(conversion_stats["source_csv_bytes"]))),
+ ("columnar size", format_size(int(conversion_stats["columnar_bytes"]))),
+ ("roundtrip csv size", format_size(int(conversion_stats["roundtrip_csv_bytes"]))),
+ ("compression ratio", format_ratio(float(conversion_stats["compression_ratio"]))),
+ ("columnar / csv", format_percent(float(conversion_stats["columnar_vs_csv_percent"]))),
+ ("csv -> columnar", format_seconds(float(conversion_stats["convert_elapsed_seconds"]))),
+ ("columnar -> csv", format_seconds(float(conversion_stats["roundtrip_elapsed_seconds"]))),
+ ("convert throughput", f"{float(conversion_stats['convert_throughput_mb_s']):.1f} MB/s"),
+ ("roundtrip throughput", f"{float(conversion_stats['roundtrip_throughput_mb_s']):.1f} MB/s"),
+ ]
+ lines.extend(
+ [
+ '',
+ '',
+ '| ',
+ *render_html_metric_table("Summary", summary_rows),
+ " | ",
+ '',
+ *render_html_metric_table("Storage", storage_rows),
+ " | ",
+ "
",
+ "
",
+ "",
+ ]
+ )
+ else:
lines.extend(
[
- "### Storage",
+ "### Summary",
"",
"| Metric | Value |",
"| --- | ---: |",
- f"| source csv | `{conversion_stats['source_csv_path']}` |",
- f"| schema | `{conversion_stats['schema_path']}` |",
- f"| source size | {format_size(int(conversion_stats['source_csv_bytes']))} |",
- f"| columnar size | {format_size(int(conversion_stats['columnar_bytes']))} |",
- f"| roundtrip csv size | {format_size(int(conversion_stats['roundtrip_csv_bytes']))} |",
- f"| compression ratio | {format_ratio(float(conversion_stats['compression_ratio']))} |",
- f"| columnar / csv | {format_percent(float(conversion_stats['columnar_vs_csv_percent']))} |",
- f"| csv -> columnar | {format_seconds(float(conversion_stats['convert_elapsed_seconds']))} |",
- f"| columnar -> csv | {format_seconds(float(conversion_stats['roundtrip_elapsed_seconds']))} |",
- f"| convert throughput | {float(conversion_stats['convert_throughput_mb_s']):.1f} MB/s |",
- f"| roundtrip throughput | {float(conversion_stats['roundtrip_throughput_mb_s']):.1f} MB/s |",
+ *[f"| {markdown_escape(metric)} | {markdown_escape(str(value))} |" for metric, value in summary_rows],
"",
]
)
lines.extend(render_heatmap(rows, median_value))
- lines.extend(
- [
- "### Largest Result Sets",
- "",
- "| Query | Output | Warm median, ms | SQL |",
- "| --- | ---: | ---: | --- |",
- ]
- )
- for row in largest_outputs:
- lines.append(
- f"| [Q{int(row['query_id']):02d}]({row['query_file']}) | {format_size(int(row['output_csv_bytes']))} | "
- f"{format_ms(query_metric(row))} | {markdown_escape(compact_sql(row['sql'], 64))} |"
- )
-
- lines.extend(
- [
- "",
- "### Fastest Queries",
- "",
- "| Query | SQL | Warm median, ms | First run, ms |",
- "| --- | --- | ---: | ---: |",
- ]
- )
- for row in top_fastest:
- lines.append(
- f"| [Q{int(row['query_id']):02d}]({row['query_file']}) | {markdown_escape(compact_sql(row['sql'], 72))} | "
- f"{format_ms(query_metric(row))} | {row['first_run_ms'] or '—'} |"
- )
-
- lines.extend(
- [
- "",
- "### Slowest Queries",
- "",
- "| Query | SQL | Warm median, ms | Warm max, ms |",
- "| --- | --- | ---: | ---: |",
- ]
- )
- for row in top_slowest:
- lines.append(
- f"| [Q{int(row['query_id']):02d}]({row['query_file']}) | {markdown_escape(compact_sql(row['sql'], 72))} | "
- f"{format_ms(query_metric(row))} | {row['warm_max_ms'] or '—'} |"
- )
lines.extend(
[
@@ -612,6 +623,7 @@ def main() -> int:
schema_path=schema_path,
source_csv_path=source_csv_path,
row_group_size=args.row_group_size,
+ compression=args.compression,
)
write_csv(csv_path, rows)
markdown_block = render_markdown(
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b81e294..79c6e65 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,19 +1,32 @@
+find_path(LZ4_INCLUDE_DIR lz4.h)
+find_library(LZ4_LIBRARY lz4)
+
+if (NOT LZ4_INCLUDE_DIR OR NOT LZ4_LIBRARY)
+ message(FATAL_ERROR "lz4 headers/library not found")
+endif ()
+
+add_library(lz4_external INTERFACE)
+target_include_directories(lz4_external INTERFACE ${LZ4_INCLUDE_DIR})
+target_link_libraries(lz4_external INTERFACE ${LZ4_LIBRARY})
+
add_library(columnar_engine_core
model/batch.cpp
model/column.cpp
model/column_string.cpp
model/metadata.cpp
io/batch.cpp
+ io/compression.cpp
io/file.cpp
io/stream.cpp
common/error.cpp
common/ascii.cpp
common/parsing.cpp
- common/operator_utils.cpp
- common/planner_utils.cpp
+ common/string_pattern_utils.cpp
+ common/string_arena.cpp
)
target_include_directories(columnar_engine_core PUBLIC ${COLUMNAR_INCLUDE_DIRS})
+target_link_libraries(columnar_engine_core PUBLIC lz4_external)
add_library(columnar_engine_columnar
io/columnar_batch.cpp
@@ -40,11 +53,17 @@ target_link_libraries(columnar_engine_convert
)
add_library(columnar_engine_executor
+ executor/comparison_utils.cpp
+ executor/query_utils.cpp
+ executor/typed_value_utils.cpp
executor/executor.cpp
executor/query_parser.cpp
executor/query_planner.cpp
executor/aggregate_states.cpp
executor/operators.cpp
+ executor/operators_aggregate.cpp
+ executor/operators_runtime.cpp
+ executor/operators_scan.cpp
sql_parser/tokenizer_factory.cpp
sql_parser/tokenizer_tokens.cpp
sql_parser/tokenizer.cpp
diff --git a/src/app/main.cpp b/src/app/main.cpp
index 9277427..75ef337 100644
--- a/src/app/main.cpp
+++ b/src/app/main.cpp
@@ -23,6 +23,7 @@ void ConfigureConvertCommand(argparse::ArgumentParser& command) {
command.add_argument("--input").required();
command.add_argument("--output").required();
command.add_argument("--row-group-size").scan<'u', size_t>().default_value(size_t{1 << 14});
+ command.add_argument("--compression").default_value(std::string("none"));
}
void ConfigureRoundtripCommand(argparse::ArgumentParser& command) {
@@ -57,7 +58,8 @@ int RunConvert(const argparse::ArgumentParser& command) {
EnsureParentDirectory(output_path);
ConvertCsvToColumnar(std::filesystem::path(command.get("--schema")),
std::filesystem::path(command.get("--input")), output_path,
- command.get("--row-group-size"));
+ command.get("--row-group-size"),
+ CompressionFromName(command.get("--compression")));
return 0;
}
diff --git a/src/common/string_arena.cpp b/src/common/string_arena.cpp
new file mode 100644
index 0000000..b1165dd
--- /dev/null
+++ b/src/common/string_arena.cpp
@@ -0,0 +1,48 @@
+#include "common/string_arena.h"
+
+#include
+#include
+#include
+
+StringArena::StringArena(const size_t chunk_size) : chunk_size_(chunk_size) {}
+
+std::string_view StringArena::Store(const std::string_view value) {
+ if (value.empty()) {
+ return {};
+ }
+
+ Chunk& chunk = EnsureChunk(value.size());
+ const size_t offset = chunk.used;
+ std::memcpy(chunk.data.get() + static_cast(offset), value.data(), value.size());
+ chunk.used = offset + value.size();
+ return {chunk.data.get() + static_cast(offset), value.size()};
+}
+
+void StringArena::Clear() {
+ for (Chunk& chunk : chunks_) {
+ chunk.used = 0;
+ }
+}
+
+StringArena::Chunk& StringArena::EnsureChunk(const size_t size) {
+ if (!chunks_.empty() && chunks_.back().capacity - chunks_.back().used >= size) {
+ return chunks_.back();
+ }
+
+ const size_t capacity = std::max(chunk_size_, size);
+ chunks_.push_back(Chunk{
+ .data = std::make_unique(capacity),
+ .capacity = capacity,
+ .used = 0,
+ });
+
+ return chunks_.back();
+}
+
+size_t StringViewHash::operator()(const std::string_view value) const noexcept {
+ return std::hash{}(value);
+}
+
+bool StringViewEqual::operator()(const std::string_view lhs, const std::string_view rhs) const noexcept {
+ return lhs == rhs;
+}
diff --git a/src/common/operator_utils.cpp b/src/common/string_pattern_utils.cpp
similarity index 58%
rename from src/common/operator_utils.cpp
rename to src/common/string_pattern_utils.cpp
index 9639f50..48252cd 100644
--- a/src/common/operator_utils.cpp
+++ b/src/common/string_pattern_utils.cpp
@@ -1,49 +1,11 @@
-#include "common/operator_utils.h"
+#include "common/string_pattern_utils.h"
-#include
-#include
+#include
#include
-#include "common/ascii.h"
-#include "common/error.h"
-#include "model/column_traits.h"
-
constexpr char LikeWildcardAny = '%';
constexpr char LikeWildcardSingle = '_';
-static bool EvaluateOrdering(const std::strong_ordering ordering, const ComparisonKind kind) {
- if (kind == ComparisonKind::Equal) {
- return ordering == 0;
- }
-
- if (kind == ComparisonKind::NotEqual) {
- return ordering != 0;
- }
-
- if (kind == ComparisonKind::Less) {
- return ordering < 0;
- }
-
- if (kind == ComparisonKind::LessOrEqual) {
- return ordering <= 0;
- }
-
- if (kind == ComparisonKind::Greater) {
- return ordering > 0;
- }
-
- if (kind == ComparisonKind::GreaterOrEqual) {
- return ordering >= 0;
- }
-
- throw Error::Unsupported("executor", "unsupported comparison kind");
-}
-
-template
-static std::strong_ordering CompareValuesImpl(const T left, const T right) {
- return left <=> right;
-}
-
static bool LikeMatchesPercentOnly(const std::string_view value, const std::string_view pattern) {
std::vector segments;
@@ -118,17 +80,6 @@ static bool LikeMatchesWithUnderscore(const std::string_view value, const std::s
return pattern_pos == pattern.size();
}
-std::strong_ordering CompareValues(const ColumnType type, const std::string_view lhs, const std::string_view rhs) {
- return VisitColumnType(type, [&]() {
- return CompareValuesImpl(ColumnValueTraits::Parse(lhs), ColumnValueTraits::Parse(rhs));
- });
-}
-
-bool MatchesComparison(const ColumnType type, const std::string_view lhs, const std::string_view rhs,
- const ComparisonKind kind) {
- return EvaluateOrdering(CompareValues(type, lhs, rhs), kind);
-}
-
bool LikeMatches(const std::string_view value, const std::string_view pattern) {
if (pattern.find(LikeWildcardSingle) == std::string_view::npos) {
return LikeMatchesPercentOnly(value, pattern);
@@ -136,21 +87,3 @@ bool LikeMatches(const std::string_view value, const std::string_view pattern) {
return LikeMatchesWithUnderscore(value, pattern);
}
-
-ColumnType AggregateOutputType(const PlannedAgg& aggregate) {
- const std::string name = ToUpperAscii(aggregate.function->canonical_name);
-
- if (name == "COUNT") {
- return ColumnType::Int64;
- }
-
- if (name == "SUM" || name == "AVG") {
- return ColumnType::Int128;
- }
-
- if (name == "MIN" || name == "MAX") {
- return aggregate.input_type;
- }
-
- throw Error::Unsupported("executor", "unsupported aggregate result type");
-}
diff --git a/src/convert/csv_columnar.cpp b/src/convert/csv_columnar.cpp
index eff1ace..ef7f599 100644
--- a/src/convert/csv_columnar.cpp
+++ b/src/convert/csv_columnar.cpp
@@ -8,7 +8,8 @@
#include "model/schema_csv.h"
void ConvertCsvToColumnar(const std::filesystem::path& schema_path, const std::filesystem::path& data_path,
- const std::filesystem::path& output_path, size_t max_rows_per_group) {
+ const std::filesystem::path& output_path, const size_t max_rows_per_group,
+ const Compression compression) {
if (max_rows_per_group == 0) {
throw Error::InvalidArgument("convert", "row group size must be > 0");
}
@@ -19,13 +20,13 @@ void ConvertCsvToColumnar(const std::filesystem::path& schema_path, const std::f
sizing.max_rows = max_rows_per_group;
CsvBatchReader batch_reader(data_path, schema, sizing);
- ColumnarBatchWriter batch_writer(output_path, schema);
+ ColumnarBatchWriter batch_writer(output_path, schema, compression);
while (auto batch = batch_reader.ReadNext()) {
batch_writer.Write(*batch);
}
- std::move(batch_writer).Finalize();
+ batch_writer.Finalize();
}
void ConvertColumnarToCsv(const std::filesystem::path& columnar_path, const std::filesystem::path& schema_path,
diff --git a/src/executor/aggregate_states.cpp b/src/executor/aggregate_states.cpp
index d86669f..6083cb3 100644
--- a/src/executor/aggregate_states.cpp
+++ b/src/executor/aggregate_states.cpp
@@ -8,8 +8,10 @@
#include "common/error.h"
#include "common/int128.h"
#include "common/parsing.h"
+#include "common/string_arena.h"
#include "executor/aggregate_function.h"
#include "executor/aggregate_state.h"
+#include "executor/typed_value_utils.h"
#include "model/column_traits.h"
void AggState::ConsumeInt128(const Int128 value) { ConsumeValue(Int128ToString(value)); }
@@ -30,29 +32,6 @@ static bool ShouldReplaceExtremum(const ColumnType type, const std::string_view
});
}
-static Int128 ParseToInt128(const ColumnType type, const std::string_view value) {
- switch (type) {
- case ColumnType::Boolean:
- return ParseBoolean(value) ? 1 : 0;
- case ColumnType::Int16:
- return ParseInt16(value);
- case ColumnType::Int32:
- return ParseInt32(value);
- case ColumnType::Int64:
- return ParseInt64(value);
- case ColumnType::Int128:
- return ParseInt128(value);
- case ColumnType::Character:
- return ParseCharacter(value);
- case ColumnType::String:
- case ColumnType::Date:
- case ColumnType::Timestamp:
- break;
- }
-
- throw Error::Unsupported("executor", "unsupported numeric conversion");
-}
-
static std::string FormatAverage(const Int128 sum, const uint64_t count) {
if (count == 0) {
return "0";
@@ -60,28 +39,6 @@ static std::string FormatAverage(const Int128 sum, const uint64_t count) {
return Int128ToString(sum / static_cast(count));
}
-static std::string FormatInt128AsType(const ColumnType type, const Int128 value) {
- switch (type) {
- case ColumnType::Boolean:
- return BooleanToString(value != 0);
- case ColumnType::Int16:
- case ColumnType::Int32:
- case ColumnType::Int64:
- case ColumnType::Int128:
- return Int128ToString(value);
- case ColumnType::Date:
- return DateToString(static_cast(value));
- case ColumnType::Timestamp:
- return TimestampToString(static_cast(value));
- case ColumnType::Character:
- return std::string(1, static_cast(value));
- case ColumnType::String:
- break;
- }
-
- throw Error::Unsupported("executor", "unsupported typed aggregate result");
-}
-
class CountAS final : public AggState {
public:
void ConsumeValue(const std::string_view) override { ConsumeRow(); }
@@ -99,7 +56,7 @@ class SumAS final : public AggState {
public:
explicit SumAS(const ColumnType type) : type_(type) {}
- void ConsumeValue(const std::string_view value) override { sum_ += ParseToInt128(type_, value); }
+ void ConsumeValue(const std::string_view value) override { sum_ += ParseColumnValueAsInt128(type_, value); }
void ConsumeInt128(const Int128 value) override { sum_ += value; }
void ConsumeRow() override { throw Error::InvalidState("executor", "SUM requires a value"); }
@@ -115,7 +72,7 @@ class AvgAS final : public AggState {
explicit AvgAS(const ColumnType type) : type_(type) {}
void ConsumeValue(const std::string_view value) override {
- sum_ += ParseToInt128(type_, value);
+ sum_ += ParseColumnValueAsInt128(type_, value);
++count_;
}
@@ -159,7 +116,7 @@ class ExtremumAS final : public AggState {
std::string Finalize() const override {
if (typed_extremum_.has_value()) {
- return FormatInt128AsType(type_, *typed_extremum_);
+ return FormatInt128Value(type_, *typed_extremum_);
}
return extremum_.value_or("");
}
@@ -176,7 +133,7 @@ class DistinctAS final : public AggState {
explicit DistinctAS(std::unique_ptr nested) : nested_(std::move(nested)) {}
void ConsumeValue(const std::string_view value) override {
- if (seen_.insert(std::string(value)).second) {
+ if (seen_.insert(arena_.Store(value)).second) {
nested_->ConsumeValue(value);
}
}
@@ -187,29 +144,12 @@ class DistinctAS final : public AggState {
private:
std::unique_ptr nested_;
- std::unordered_set seen_;
+ StringArena arena_;
+ StringViewSet seen_;
};
static bool SupportsAnyType(const ColumnType) { return true; }
-static bool SupportsNumericType(const ColumnType type) {
- switch (type) {
- case ColumnType::Boolean:
- case ColumnType::Int16:
- case ColumnType::Int32:
- case ColumnType::Int64:
- case ColumnType::Int128:
- case ColumnType::Character:
- return true;
- case ColumnType::String:
- case ColumnType::Date:
- case ColumnType::Timestamp:
- return false;
- }
-
- return false;
-}
-
static std::unique_ptr CreateCountState(const PlannedAgg&) { return std::make_unique(); }
static std::unique_ptr CreateSumState(const PlannedAgg& aggregate) {
@@ -254,13 +194,13 @@ AggRegistrar::AggRegistrar(const AggFuncDefinition& def) { AggRegistry::Instance
[[maybe_unused]] static AggRegistrar register_sum({
.canonical_name = "SUM",
- .supports_type = &SupportsNumericType,
+ .supports_type = &IsNumericColumnType,
.factory = &CreateSumState,
});
[[maybe_unused]] static AggRegistrar register_avg({
.canonical_name = "AVG",
- .supports_type = &SupportsNumericType,
+ .supports_type = &IsNumericColumnType,
.factory = &CreateAvgState,
});
diff --git a/src/executor/comparison_utils.cpp b/src/executor/comparison_utils.cpp
new file mode 100644
index 0000000..50565fa
--- /dev/null
+++ b/src/executor/comparison_utils.cpp
@@ -0,0 +1,70 @@
+#include "executor/comparison_utils.h"
+
+#include
+#include
+
+#include "common/ascii.h"
+#include "common/error.h"
+#include "model/column_traits.h"
+
+static bool EvaluateOrdering(const std::strong_ordering ordering, const ComparisonKind kind) {
+ if (kind == ComparisonKind::Equal) {
+ return ordering == 0;
+ }
+
+ if (kind == ComparisonKind::NotEqual) {
+ return ordering != 0;
+ }
+
+ if (kind == ComparisonKind::Less) {
+ return ordering < 0;
+ }
+
+ if (kind == ComparisonKind::LessOrEqual) {
+ return ordering <= 0;
+ }
+
+ if (kind == ComparisonKind::Greater) {
+ return ordering > 0;
+ }
+
+ if (kind == ComparisonKind::GreaterOrEqual) {
+ return ordering >= 0;
+ }
+
+ throw Error::Unsupported("executor", "unsupported comparison kind");
+}
+
+template
+static std::strong_ordering CompareValuesImpl(const T left, const T right) {
+ return left <=> right;
+}
+
+std::strong_ordering CompareValues(const ColumnType type, const std::string_view lhs, const std::string_view rhs) {
+ return VisitColumnType(type, [&]() {
+ return CompareValuesImpl(ColumnValueTraits::Parse(lhs), ColumnValueTraits::Parse(rhs));
+ });
+}
+
+bool MatchesComparison(const ColumnType type, const std::string_view lhs, const std::string_view rhs,
+ const ComparisonKind kind) {
+ return EvaluateOrdering(CompareValues(type, lhs, rhs), kind);
+}
+
+ColumnType AggregateOutputType(const PlannedAgg& aggregate) {
+ const std::string name = ToUpperAscii(aggregate.function->canonical_name);
+
+ if (name == "COUNT") {
+ return ColumnType::Int64;
+ }
+
+ if (name == "SUM" || name == "AVG") {
+ return ColumnType::Int128;
+ }
+
+ if (name == "MIN" || name == "MAX") {
+ return aggregate.input_type;
+ }
+
+ throw Error::Unsupported("executor", "unsupported aggregate result type");
+}
diff --git a/src/executor/executor.cpp b/src/executor/executor.cpp
index 79045a2..1b6e087 100644
--- a/src/executor/executor.cpp
+++ b/src/executor/executor.cpp
@@ -26,7 +26,7 @@ ExecuteExpected Executor::Execute(const std::string_view query) const {
return Batch{};
}
- return std::move(*batch);
+ return std::move(batch).value();
} catch (const Error& error) {
if (unsupported_fallback_enabled_) {
return Batch{};
diff --git a/src/executor/operators.cpp b/src/executor/operators.cpp
index 72d39b2..23d2aac 100644
--- a/src/executor/operators.cpp
+++ b/src/executor/operators.cpp
@@ -1,1631 +1,29 @@
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "common/ascii.h"
-#include "common/error.h"
-#include "common/int128.h"
-#include "common/operator_utils.h"
-#include "common/parsing.h"
-#include "common/planner_utils.h"
-#include "executor/aggregate_state.h"
#include "executor/operator.h"
-#include "io/columnar_batch.h"
-#include "io/file.h"
-
-constexpr std::string_view CountStarName = "COUNT(*)";
-constexpr std::string_view FallbackAggregateAlias = "c";
-constexpr std::string_view PageViewsAlias = "PageViews";
-constexpr std::string_view ExtractMinutePart = "MINUTE";
-constexpr std::string_view ExtractHourPart = "HOUR";
-
-constexpr int64_t MinuteMicros = 60'000'000;
-
-constexpr std::string_view HttpScheme = "http://";
-constexpr std::string_view HttpsScheme = "https://";
-constexpr std::string_view WwwPrefix = "www.";
-constexpr std::string_view RegexBackrefPlaceholder = "\\1";
-constexpr std::string_view RegexBackrefReplacement = "$1";
-
-class MetadataCountOperator final : public Operator {
- public:
- MetadataCountOperator(std::filesystem::path path, std::string output_name)
- : path_(std::move(path)), output_name_(std::move(output_name)) {}
-
- std::optional Next() override {
- if (returned_) {
- return std::nullopt;
- }
-
- returned_ = true;
-
- uint64_t rows = 0;
-
- const ColumnarBatchReader reader(path_);
- for (const auto& row_group : reader.GetMetadata().row_groups) {
- rows += row_group.row_count;
- }
-
- Batch result(Schema{{ColumnSchema(output_name_, ColumnType::Int64)}}, 1);
- result.AppendValueFromString(0, std::to_string(rows));
-
- return result;
- }
-
- private:
- std::filesystem::path path_;
- std::string output_name_;
- bool returned_ = false;
-};
-
-class MetadataExtremaOperator final : public Operator {
- public:
- MetadataExtremaOperator(std::filesystem::path path, std::vector aggregates)
- : path_(std::move(path)), aggregates_(std::move(aggregates)) {}
-
- std::optional Next() override {
- if (returned_) {
- return std::nullopt;
- }
-
- returned_ = true;
-
- const ColumnarBatchReader reader(path_);
- const ColumnarMetadata& metadata = reader.GetMetadata();
-
- Schema schema;
- schema.columns.reserve(aggregates_.size());
-
- std::vector> states;
- states.reserve(aggregates_.size());
-
- for (const PlannedAgg& aggregate : aggregates_) {
- schema.columns.push_back(ColumnSchema(aggregate.output_name, AggregateOutputType(aggregate)));
- states.push_back(CreateAggState(aggregate));
- }
-
- for (const auto& row_group : metadata.row_groups) {
- for (size_t i = 0; i < aggregates_.size(); ++i) {
- const PlannedAgg& aggregate = aggregates_[i];
- const auto& chunk = row_group.columns[aggregate.column_index];
-
- if (!chunk.has_min_max) {
- throw Error::InvalidState("executor", "metadata min/max statistics are missing", path_.string());
- }
-
- const std::string name = ToUpperAscii(aggregate.function->canonical_name);
- states[i]->ConsumeInt128(name == "MIN" ? chunk.min_value : chunk.max_value);
- }
- }
-
- Batch result(schema);
- for (size_t i = 0; i < states.size(); ++i) {
- result.AppendValueFromString(i, states[i]->Finalize());
- }
-
- return result;
- }
-
- private:
- std::filesystem::path path_;
- std::vector aggregates_;
- bool returned_ = false;
-};
-
-static bool MatchesRowGroupComparison(const ColumnChunkMetadata& chunk, const Int128 value,
- const ComparisonKind comparison) {
- if (!chunk.has_min_max) {
- return true;
- }
-
- switch (comparison) {
- case ComparisonKind::Equal:
- return chunk.min_value <= value && value <= chunk.max_value;
- case ComparisonKind::NotEqual:
- return chunk.min_value != value || chunk.max_value != value;
- case ComparisonKind::Less:
- return chunk.min_value < value;
- case ComparisonKind::LessOrEqual:
- return chunk.min_value <= value;
- case ComparisonKind::Greater:
- return chunk.max_value > value;
- case ComparisonKind::GreaterOrEqual:
- return chunk.max_value >= value;
- }
-
- return true;
-}
-
-static bool MayMatchRowGroup(const PredicatePtr& predicate, const RowGroupMetadata& row_group) {
- if (!predicate) {
- return true;
- }
-
- switch (predicate->kind) {
- case PredicateKind::And:
- return MayMatchRowGroup(predicate->lhs, row_group) && MayMatchRowGroup(predicate->rhs, row_group);
- case PredicateKind::Comparison:
- if (!predicate->metadata_typed_literal_comparison_bound ||
- predicate->metadata_typed_column_index >= row_group.columns.size()) {
- return true;
- }
- return MatchesRowGroupComparison(row_group.columns[predicate->metadata_typed_column_index],
- predicate->metadata_typed_literal_value,
- predicate->metadata_typed_comparison);
- case PredicateKind::In:
- if (!predicate->metadata_typed_in_set_bound ||
- predicate->metadata_typed_in_column_index >= row_group.columns.size()) {
- return true;
- }
-
- return std::ranges::any_of(predicate->metadata_typed_in_values, [&](const Int128 value) {
- return MatchesRowGroupComparison(row_group.columns[predicate->metadata_typed_in_column_index], value,
- ComparisonKind::Equal);
- });
- case PredicateKind::Like:
- case PredicateKind::NotLike:
- return true;
- }
-
- return true;
-}
-
-class ScanOperator final : public Operator {
- public:
- ScanOperator(std::filesystem::path path, std::vector projection_indexes, PredicatePtr filter)
- : path_(std::move(path)),
- input_(path_),
- metadata_(ColumnarBatchReader(path_).GetMetadata()),
- filter_(std::move(filter)) {
- if (metadata_.schema.columns.empty()) {
- throw Error::MalformedData("executor", "columnar schema is empty", path_.string());
- }
-
- projection_indexes_ = std::move(projection_indexes);
- projected_schema_.columns.reserve(projection_indexes_.size());
-
- for (const size_t source_index : projection_indexes_) {
- if (source_index >= metadata_.schema.columns.size()) {
- throw Error::OutOfRange("executor", "projection index out of range", path_.string());
- }
-
- projected_schema_.columns.push_back(metadata_.schema.columns[source_index]);
- }
- }
-
- std::optional Next() override {
- if (next_group_ >= metadata_.row_groups.size()) {
- return std::nullopt;
- }
-
- const RowGroupMetadata* row_group = nullptr;
- while (next_group_ < metadata_.row_groups.size()) {
- const RowGroupMetadata& candidate = metadata_.row_groups[next_group_++];
- if (MayMatchRowGroup(filter_, candidate)) {
- row_group = &candidate;
- break;
- }
- }
-
- if (row_group == nullptr) {
- return std::nullopt;
- }
-
- Batch batch(projected_schema_, row_group->row_count);
-
- for (size_t projected_index = 0; projected_index < projection_indexes_.size(); ++projected_index) {
- const size_t source_index = projection_indexes_[projected_index];
- const auto& chunk = row_group->columns[source_index];
-
- batch.ReadColumnFrom(projected_index, input_.StreamAt(chunk.offset), row_group->row_count, chunk.size);
- }
-
- return batch;
- }
-
- private:
- std::filesystem::path path_;
- InputFile input_;
-
- ColumnarMetadata metadata_;
- std::vector projection_indexes_;
- Schema projected_schema_;
- PredicatePtr filter_;
- size_t next_group_ = 0;
-};
-
-static std::string NormalizeLiteralForEval(const QueryLiteral& literal) {
- if (literal.kind != LiteralKind::String) {
- return literal.text;
- }
-
- std::string result;
- const std::string_view text = literal.text;
-
- for (size_t i = 1; i + 1 < text.size(); ++i) {
- if (text[i] == '\'' && i + 2 < text.size() && text[i + 1] == '\'') {
- result.push_back('\'');
- ++i;
- continue;
- }
- result.push_back(text[i]);
- }
-
- return result;
-}
-
-static std::optional TryFindBatchColumn(const Schema& schema, const std::string_view name) {
- for (size_t i = 0; i < schema.columns.size(); ++i) {
- if (SameColumnName(schema.columns[i].name, name)) {
- return i;
- }
- }
- return std::nullopt;
-}
-
-static Int128 EvalInt(const std::string& value) { return ParseInt128(value); }
-
-static std::string FormatAggregateExprName(const ExprSpec& expr) {
- const std::string name = ToUpperAscii(expr.function_name);
- if (name == "COUNT" && !expr.arguments.empty() && expr.arguments.front()->kind == ExprKind::Star) {
- return std::string(CountStarName);
- }
-
- std::string out = name + "(";
-
- for (size_t i = 0; i < expr.arguments.size(); ++i) {
- if (i > 0) {
- out += ", ";
- }
- out += expr.arguments[i]->output_name;
- }
-
- out += ")";
-
- return out;
-}
-
-static std::string EvalExpr(const ExprPtr& expr, const Batch& batch, size_t row);
-static bool EvaluatePredicate(const PredicatePtr& predicate, const Batch& batch, size_t row);
-
-static std::string EvalFunction(const ExprSpec& expr, const Batch& batch, const size_t row) {
- const std::string name = ToUpperAscii(expr.function_name);
-
- if (name == "COUNT" || name == "SUM" || name == "AVG" || name == "MIN" || name == "MAX") {
- if (const auto column = TryFindBatchColumn(batch.GetSchema(), FormatAggregateExprName(expr));
- column.has_value()) {
- return batch.ColumnAt(*column).ValueAsString(row);
- }
- if (const auto column = TryFindBatchColumn(batch.GetSchema(), FallbackAggregateAlias); column.has_value()) {
- return batch.ColumnAt(*column).ValueAsString(row);
- }
- if (const auto column = TryFindBatchColumn(batch.GetSchema(), PageViewsAlias); column.has_value()) {
- return batch.ColumnAt(*column).ValueAsString(row);
- }
- }
-
- if (name == "STRLEN" || name == "LENGTH") {
- return std::to_string(EvalExpr(expr.arguments.at(0), batch, row).size());
- }
-
- if (name == "EXTRACT") {
- const std::string part = ToUpperAscii(EvalExpr(expr.arguments.at(0), batch, row));
- const int64_t ts = ParseTimestamp(EvalExpr(expr.arguments.at(1), batch, row));
-
- const std::chrono::sys_time time{std::chrono::microseconds{ts}};
- const auto day = std::chrono::floor(time);
-
- const std::chrono::hh_mm_ss tod{time - day};
-
- if (part == ExtractMinutePart) {
- return std::to_string(tod.minutes().count());
- }
- if (part == ExtractHourPart) {
- return std::to_string(tod.hours().count());
- }
-
- throw Error::Unsupported("executor", "unsupported EXTRACT part");
- }
-
- if (name == "DATE_TRUNC") {
- const std::string part = ToUpperAscii(EvalExpr(expr.arguments.at(0), batch, row));
- int64_t micros = ParseTimestamp(EvalExpr(expr.arguments.at(1), batch, row));
-
- if (part == ExtractMinutePart) {
- micros = micros / MinuteMicros * MinuteMicros;
- return TimestampToString(micros);
- }
-
- throw Error::Unsupported("executor", "unsupported DATE_TRUNC part");
- }
-
- if (name == "REGEXP_REPLACE") {
- const std::string source = EvalExpr(expr.arguments.at(0), batch, row);
- const std::string pattern_text = EvalExpr(expr.arguments.at(1), batch, row);
- std::string replacement = EvalExpr(expr.arguments.at(2), batch, row);
-
- if (pattern_text == R"(^https?://(?:www\.)?([^/]+)/.*$)" && replacement == R"(\1)") {
- std::string_view rest = source;
- if (rest.starts_with(HttpScheme)) {
- rest.remove_prefix(HttpScheme.size());
- } else if (rest.starts_with(HttpsScheme)) {
- rest.remove_prefix(HttpsScheme.size());
- } else {
- return source;
- }
-
- if (rest.starts_with(WwwPrefix)) {
- rest.remove_prefix(WwwPrefix.size());
- }
-
- const size_t slash = rest.find('/');
- if (slash == std::string_view::npos || slash == 0) {
- return source;
- }
-
- const std::string_view suffix = rest.substr(slash + 1);
- if (suffix.find('\n') != std::string_view::npos || suffix.find('\r') != std::string_view::npos) {
- return source;
- }
-
- return std::string(rest.substr(0, slash));
- }
-
- if (expr.regex_replace_bound) {
- return std::regex_replace(source, expr.regex_pattern, expr.regex_replacement);
- }
-
- for (size_t pos = 0; (pos = replacement.find(RegexBackrefPlaceholder, pos)) != std::string::npos;) {
- replacement.replace(pos, RegexBackrefPlaceholder.size(), RegexBackrefReplacement);
- pos += RegexBackrefReplacement.size();
- }
-
- const std::regex pattern(pattern_text);
-
- return std::regex_replace(source, pattern, replacement);
- }
-
- throw Error::Unsupported("executor", "unsupported function '" + expr.function_name + "'");
-}
-
-static std::string EvalExpr(const ExprPtr& expr, const Batch& batch, const size_t row) {
- switch (expr->kind) {
- case ExprKind::Column: {
- if (expr->column_index_bound && expr->column_index < batch.ColumnsCount() &&
- SameColumnName(batch.GetSchema().columns[expr->column_index].name, expr->column.name)) {
- return batch.ColumnAt(expr->column_index).ValueAsString(row);
- }
-
- const auto column = TryFindBatchColumn(batch.GetSchema(), expr->column.name);
- if (!column.has_value()) {
- throw Error::InvalidArgument("executor", "unknown column '" + expr->column.name + "'");
- }
-
- return batch.ColumnAt(*column).ValueAsString(row);
- }
- case ExprKind::Literal:
- return NormalizeLiteralForEval(expr->literal);
- case ExprKind::Binary: {
- const Int128 lhs = EvalInt(EvalExpr(expr->left, batch, row));
- const Int128 rhs = EvalInt(EvalExpr(expr->right, batch, row));
- return Int128ToString(expr->binary_op == BinaryOp::Add ? lhs + rhs : lhs - rhs);
- }
- case ExprKind::Function:
- return EvalFunction(*expr, batch, row);
- case ExprKind::Case:
- return EvaluatePredicate(expr->case_spec.condition, batch, row)
- ? EvalExpr(expr->case_spec.then_expr, batch, row)
- : EvalExpr(expr->case_spec.else_expr, batch, row);
- case ExprKind::Star:
- return "*";
- }
- return {};
-}
-
-static ComparisonKind FlipComparison(const ComparisonKind comparison) {
- switch (comparison) {
- case ComparisonKind::Equal:
- return ComparisonKind::Equal;
- case ComparisonKind::NotEqual:
- return ComparisonKind::NotEqual;
- case ComparisonKind::Less:
- return ComparisonKind::Greater;
- case ComparisonKind::LessOrEqual:
- return ComparisonKind::GreaterOrEqual;
- case ComparisonKind::Greater:
- return ComparisonKind::Less;
- case ComparisonKind::GreaterOrEqual:
- return ComparisonKind::LessOrEqual;
- }
-
- return comparison;
-}
-
-static bool MatchesInt128Comparison(const Int128 lhs, const Int128 rhs, const ComparisonKind comparison) {
- switch (comparison) {
- case ComparisonKind::Equal:
- return lhs == rhs;
- case ComparisonKind::NotEqual:
- return lhs != rhs;
- case ComparisonKind::Less:
- return lhs < rhs;
- case ComparisonKind::LessOrEqual:
- return lhs <= rhs;
- case ComparisonKind::Greater:
- return lhs > rhs;
- case ComparisonKind::GreaterOrEqual:
- return lhs >= rhs;
- }
-
- return false;
-}
-
-static ValueComparison ToValueComparison(const ComparisonKind comparison) {
- switch (comparison) {
- case ComparisonKind::Equal:
- return ValueComparison::Equal;
- case ComparisonKind::NotEqual:
- return ValueComparison::NotEqual;
- case ComparisonKind::Less:
- return ValueComparison::Less;
- case ComparisonKind::LessOrEqual:
- return ValueComparison::LessOrEqual;
- case ComparisonKind::Greater:
- return ValueComparison::Greater;
- case ComparisonKind::GreaterOrEqual:
- return ValueComparison::GreaterOrEqual;
- }
-
- return ValueComparison::Equal;
-}
-
-static std::optional LiteralValueForColumnType(const QueryLiteral& literal, const ColumnType type) {
- try {
- if (literal.kind == LiteralKind::Numeric) {
- return ParseInt128(literal.text);
- }
-
- if (literal.kind == LiteralKind::String) {
- const QueryLiteral normalized_literal{NormalizeLiteralForEval(literal), LiteralKind::String};
- if (type == ColumnType::Date) {
- return ParseDate(normalized_literal.text);
- }
- if (type == ColumnType::Timestamp) {
- return ParseTimestamp(normalized_literal.text);
- }
- }
- } catch (const Error&) {
- return std::nullopt;
- }
-
- return std::nullopt;
-}
-
-static std::optional TryEvaluateTypedComparison(const ExprPtr& left, const ExprPtr& right, const Batch& batch,
- const size_t row, const ComparisonKind comparison) {
- if (!left || !right) {
- return std::nullopt;
- }
-
- if (left->kind != ExprKind::Column || right->kind != ExprKind::Literal) {
- if (left->kind == ExprKind::Literal && right->kind == ExprKind::Column) {
- return TryEvaluateTypedComparison(right, left, batch, row, FlipComparison(comparison));
- }
- return std::nullopt;
- }
-
- const auto column_index = TryFindBatchColumn(batch.GetSchema(), left->column.name);
- if (!column_index.has_value()) {
- return std::nullopt;
- }
-
- const Column& column = batch.ColumnAt(*column_index);
- if (column.Type() == ColumnType::String) {
- return std::nullopt;
- }
-
- const std::optional rhs = LiteralValueForColumnType(right->literal, column.Type());
- if (!rhs.has_value()) {
- return std::nullopt;
- }
-
- return MatchesInt128Comparison(column.ValueAsInt128(row), *rhs, comparison);
-}
-
-static ColumnType InferComparisonType(const std::string& lhs, const std::string& rhs) {
- if (TryParseInt128(lhs).has_value() && TryParseInt128(rhs).has_value()) {
- return ColumnType::Int128;
- }
-
- if (TryParseDate(lhs).has_value() && TryParseDate(rhs).has_value()) {
- return ColumnType::Date;
- }
-
- if (TryParseTimestamp(lhs).has_value() && TryParseTimestamp(rhs).has_value()) {
- return ColumnType::Timestamp;
- }
-
- return ColumnType::String;
-}
-
-static bool EvaluatePredicate(const PredicatePtr& predicate, const Batch& batch, const size_t row) {
- if (!predicate) {
- return true;
- }
-
- switch (predicate->kind) {
- case PredicateKind::And:
- return EvaluatePredicate(predicate->lhs, batch, row) && EvaluatePredicate(predicate->rhs, batch, row);
- case PredicateKind::Like: {
- return LikeMatches(EvalExpr(predicate->left, batch, row), EvalExpr(predicate->right, batch, row));
- }
- case PredicateKind::NotLike:
- return !LikeMatches(EvalExpr(predicate->left, batch, row), EvalExpr(predicate->right, batch, row));
- case PredicateKind::In: {
- if (predicate->literal_in_set_bound && predicate->in_column_index < batch.ColumnsCount()) {
- return predicate->literal_in_values.contains(
- batch.ColumnAt(predicate->in_column_index).ValueAsString(row));
- }
-
- const std::string lhs = EvalExpr(predicate->left, batch, row);
-
- return std::ranges::any_of(predicate->values,
- [&](const ExprPtr& value) { return lhs == EvalExpr(value, batch, row); });
- }
- case PredicateKind::Comparison: {
- if (predicate->typed_literal_comparison_bound && predicate->typed_column_index < batch.ColumnsCount()) {
- return MatchesInt128Comparison(batch.ColumnAt(predicate->typed_column_index).ValueAsInt128(row),
- predicate->typed_literal_value, predicate->typed_comparison);
- }
-
- if (const std::optional result =
- TryEvaluateTypedComparison(predicate->left, predicate->right, batch, row, predicate->comparison);
- result.has_value()) {
- return *result;
- }
-
- const std::string lhs = EvalExpr(predicate->left, batch, row);
- const std::string rhs = EvalExpr(predicate->right, batch, row);
-
- return MatchesComparison(InferComparisonType(lhs, rhs), lhs, rhs, predicate->comparison);
- }
- }
-
- return false;
-}
-
-static void SelectRowsMatchingPredicate(const PredicatePtr& predicate, const Batch& batch, std::vector& rows) {
- rows.clear();
- rows.reserve(batch.RowsCount());
-
- if (!predicate) {
- for (size_t row = 0; row < batch.RowsCount(); ++row) {
- rows.push_back(row);
- }
- return;
- }
-
- if (predicate->kind == PredicateKind::Comparison && predicate->typed_literal_comparison_bound &&
- predicate->typed_column_index < batch.ColumnsCount()) {
- batch.ColumnAt(predicate->typed_column_index)
- .SelectRowsByInt128Comparison(predicate->typed_literal_value,
- ToValueComparison(predicate->typed_comparison), rows);
- return;
- }
-
- if ((predicate->kind == PredicateKind::Like || predicate->kind == PredicateKind::NotLike) &&
- predicate->literal_like_pattern_bound && predicate->like_column_index < batch.ColumnsCount()) {
- batch.ColumnAt(predicate->like_column_index)
- .SelectRowsByLikePattern(predicate->like_pattern, predicate->like_negated, rows);
- return;
- }
-
- if (predicate->kind == PredicateKind::In && predicate->literal_in_set_bound &&
- predicate->in_column_index < batch.ColumnsCount()) {
- batch.ColumnAt(predicate->in_column_index).SelectRowsByStringSet(predicate->literal_in_values, rows);
- return;
- }
-
- for (size_t row = 0; row < batch.RowsCount(); ++row) {
- if (EvaluatePredicate(predicate, batch, row)) {
- rows.push_back(row);
- }
- }
-}
-
-class FilterOperator final : public Operator {
- public:
- FilterOperator(std::unique_ptr child, PredicatePtr filter)
- : child_(std::move(child)), filter_(std::move(filter)) {}
-
- std::optional Next() override {
- while (auto batch = child_->Next()) {
- std::vector selected_rows;
- SelectRowsMatchingPredicate(filter_, *batch, selected_rows);
-
- if (selected_rows.empty()) {
- continue;
- }
-
- Batch filtered(batch->GetSchema(), selected_rows.size());
- filtered.AppendRowsSelectedFromBatch(*batch, selected_rows);
-
- return filtered;
- }
-
- return std::nullopt;
- }
-
- private:
- std::unique_ptr child_;
- PredicatePtr filter_;
-};
-
-class EnsureSchemaOperator final : public Operator {
- public:
- EnsureSchemaOperator(std::unique_ptr child, Schema schema)
- : child_(std::move(child)), schema_(std::move(schema)) {}
-
- std::optional Next() override {
- if (returned_empty_) {
- return std::nullopt;
- }
-
- if (auto batch = child_->Next()) {
- returned_any_ = true;
- return batch;
- }
-
- if (returned_any_) {
- return std::nullopt;
- }
-
- returned_any_ = true;
- returned_empty_ = true;
-
- return Batch(schema_);
- }
-
- private:
- std::unique_ptr child_;
- Schema schema_;
- bool returned_any_ = false;
- bool returned_empty_ = false;
-};
-
-static bool UsesRowAggInput(const PlannedAgg& aggregate) {
- return !aggregate.direct_numeric_argument && aggregate.argument_kind != AggArgumentKind::Column;
-}
-
-static void ConsumeAggRow(const PlannedAgg& aggregate, const Batch& batch, const size_t row, AggState& state) {
- if (aggregate.direct_numeric_argument) {
- state.ConsumeInt128(batch.ColumnAt(aggregate.column_index).ValueAsInt128(row) +
- aggregate.direct_numeric_offset);
- return;
- }
-
- if (aggregate.argument_kind == AggArgumentKind::Column) {
- state.ConsumeValue(EvalExpr(aggregate.argument, batch, row));
- return;
- }
-
- state.ConsumeRow();
-}
-
-static void ConsumeAggRows(const PlannedAgg& aggregate, const Batch& batch, const std::span rows,
- AggState& state) {
- if (UsesRowAggInput(aggregate)) {
- state.ConsumeRows(rows.size());
- return;
- }
-
- for (const size_t row : rows) {
- ConsumeAggRow(aggregate, batch, row, state);
- }
-}
-
-struct AggBinding {
- PlannedAgg aggregate;
- std::unique_ptr state;
-};
-
-struct GroupKeyComponent {
- ColumnType type = ColumnType::String;
- Int128 int_value = 0;
- std::string string_value;
-};
-
-static Int128 ParseGroupKeyIntValue(const ColumnType type, const std::string_view value) {
- switch (type) {
- case ColumnType::Boolean:
- return ParseBoolean(value) ? 1 : 0;
- case ColumnType::Int16:
- return ParseInt16(value);
- case ColumnType::Int32:
- return ParseInt32(value);
- case ColumnType::Int64:
- return ParseInt64(value);
- case ColumnType::Int128:
- return ParseInt128(value);
- case ColumnType::Date:
- return ParseDate(value);
- case ColumnType::Timestamp:
- return ParseTimestamp(value);
- case ColumnType::Character:
- return ParseCharacter(value);
- case ColumnType::String:
- break;
- }
-
- throw Error::Unsupported("executor", "unsupported group key type");
-}
-
-static std::string FormatGroupKeyValue(const GroupKeyComponent& value) {
- switch (value.type) {
- case ColumnType::Boolean:
- return BooleanToString(value.int_value != 0);
- case ColumnType::Int16:
- case ColumnType::Int32:
- case ColumnType::Int64:
- case ColumnType::Int128:
- return Int128ToString(value.int_value);
- case ColumnType::Date:
- return DateToString(static_cast(value.int_value));
- case ColumnType::Timestamp:
- return TimestampToString(static_cast(value.int_value));
- case ColumnType::Character:
- return std::string(1, static_cast(value.int_value));
- case ColumnType::String:
- return value.string_value;
- }
-
- return {};
-}
-
-static std::strong_ordering CompareGroupKeyValue(const GroupKeyComponent& lhs, const GroupKeyComponent& rhs) {
- if (lhs.type != rhs.type) {
- throw Error::InconsistentData("executor", "group key type mismatch");
- }
-
- if (lhs.type == ColumnType::String) {
- return lhs.string_value <=> rhs.string_value;
- }
-
- return lhs.int_value <=> rhs.int_value;
-}
-
-static bool GroupKeyValueEquals(const GroupKeyComponent& lhs, const GroupKeyComponent& rhs) {
- return CompareGroupKeyValue(lhs, rhs) == 0;
-}
-
-static size_t HashCombine(const size_t seed, const size_t value) {
- return seed ^ (value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2));
-}
-
-static size_t HashInt128(const Int128 value) {
- const auto bits = static_cast(value);
- const uint64_t low = static_cast(bits);
- const uint64_t high = static_cast(bits >> 64);
-
- return HashCombine(std::hash{}(high), std::hash{}(low));
-}
-
-struct TypedGroupKey {
- std::vector values;
-};
-
-struct TypedGroupKeyHash {
- size_t operator()(const TypedGroupKey& key) const {
- size_t hash = 0;
-
- for (const GroupKeyComponent& value : key.values) {
- hash = HashCombine(hash, std::hash{}(static_cast(value.type)));
- hash = HashCombine(hash, value.type == ColumnType::String ? std::hash{}(value.string_value)
- : HashInt128(value.int_value));
- }
-
- return hash;
- }
-};
-
-struct TypedGroupKeyEqual {
- bool operator()(const TypedGroupKey& lhs, const TypedGroupKey& rhs) const {
- if (lhs.values.size() != rhs.values.size()) {
- return false;
- }
-
- for (size_t i = 0; i < lhs.values.size(); ++i) {
- if (!GroupKeyValueEquals(lhs.values[i], rhs.values[i])) {
- return false;
- }
- }
-
- return true;
- }
-};
-
-class GroupKeyMaterializer {
- public:
- explicit GroupKeyMaterializer(std::vector group_keys) : group_keys_(std::move(group_keys)) {}
-
- TypedGroupKey Materialize(const Batch& batch, const size_t row) const {
- TypedGroupKey key;
- key.values.reserve(group_keys_.size());
-
- for (const auto& group_key : group_keys_) {
- if (group_key.expression && group_key.expression->kind == ExprKind::Column &&
- group_key.expression->column_index_bound && group_key.expression->column_index < batch.ColumnsCount()) {
- const Column& column = batch.ColumnAt(group_key.expression->column_index);
- if (group_key.column_type == ColumnType::String) {
- key.values.push_back(GroupKeyComponent{
- .type = group_key.column_type,
- .int_value = 0,
- .string_value = column.ValueAsString(row),
- });
- } else {
- key.values.push_back(GroupKeyComponent{
- .type = group_key.column_type,
- .int_value = column.ValueAsInt128(row),
- .string_value = {},
- });
- }
- continue;
- }
-
- std::string value = EvalExpr(group_key.expression, batch, row);
- if (group_key.column_type == ColumnType::String) {
- key.values.push_back(GroupKeyComponent{
- .type = group_key.column_type,
- .int_value = 0,
- .string_value = std::move(value),
- });
- } else {
- key.values.push_back(GroupKeyComponent{
- .type = group_key.column_type,
- .int_value = ParseGroupKeyIntValue(group_key.column_type, value),
- .string_value = {},
- });
- }
- }
-
- return key;
- }
-
- std::vector group_keys_;
-};
-
-class AggOperator final : public Operator {
- public:
- AggOperator(std::unique_ptr child, const std::vector& aggregates, PredicatePtr filter)
- : child_(std::move(child)), filter_(std::move(filter)) {
- bindings_.reserve(aggregates.size());
-
- for (const auto& aggregate : aggregates) {
- bindings_.push_back(AggBinding{
- .aggregate = aggregate,
- .state = CreateAggState(aggregate),
- });
- }
- }
-
- std::optional Next() override {
- if (returned_) {
- return std::nullopt;
- }
-
- returned_ = true;
-
- while (auto batch = child_->Next()) {
- std::vector selected_rows;
- SelectRowsMatchingPredicate(filter_, *batch, selected_rows);
-
- for (const auto& binding : bindings_) {
- ConsumeAggRows(binding.aggregate, *batch, selected_rows, *binding.state);
- }
- }
-
- Schema schema;
- schema.columns.reserve(bindings_.size());
-
- for (const auto& binding : bindings_) {
- schema.columns.push_back(
- ColumnSchema(binding.aggregate.output_name, AggregateOutputType(binding.aggregate)));
- }
-
- Batch result(schema);
-
- for (size_t i = 0; i < bindings_.size(); ++i) {
- result.AppendValueFromString(i, bindings_[i].state->Finalize());
- }
-
- return result;
- }
-
- private:
- std::unique_ptr child_;
- PredicatePtr filter_;
- std::vector bindings_;
- bool returned_ = false;
-};
-
-class GroupAggOperator final : public Operator {
- public:
- GroupAggOperator(std::unique_ptr child, std::vector group_keys,
- const std::vector& aggregates, std::vector select_items,
- PredicatePtr filter, PredicatePtr having, const bool sort_by_group_keys)
- : child_(std::move(child)),
- group_key_materializer_(std::move(group_keys)),
- select_items_(std::move(select_items)),
- filter_(std::move(filter)),
- having_(std::move(having)),
- sort_by_group_keys_(sort_by_group_keys) {
- bindings_.reserve(aggregates.size());
-
- for (const auto& aggregate : aggregates) {
- bindings_.push_back(GroupAggBinding{
- .aggregate = aggregate,
- });
- }
- }
-
- std::optional Next() override {
- if (returned_) {
- return std::nullopt;
- }
-
- returned_ = true;
-
- while (auto batch = child_->Next()) {
- std::vector selected_rows;
- SelectRowsMatchingPredicate(filter_, *batch, selected_rows);
-
- for (const size_t row : selected_rows) {
- TypedGroupKey key = group_key_materializer_.Materialize(*batch, row);
-
- size_t group_index = 0;
- const auto it = group_indexes_.find(key);
- if (it == group_indexes_.end()) {
- group_index = groups_.size();
- group_indexes_.emplace(key, group_index);
-
- groups_.push_back(GroupState{
- .key_values = std::move(key.values),
- .states = CreateStates(),
- });
- } else {
- group_index = it->second;
- }
-
- GroupState& group = groups_[group_index];
- for (size_t i = 0; i < bindings_.size(); ++i) {
- ConsumeAggRow(bindings_[i].aggregate, *batch, row, *group.states[i]);
- }
- }
- }
-
- Schema schema;
- schema.columns.reserve(select_items_.size());
-
- for (const auto& item : select_items_) {
- schema.columns.push_back(ColumnSchema(item.output_name, item.output_type));
- }
-
- std::vector output_order = BuildOutputOrder();
- Batch result(schema, output_order.size());
-
- for (const size_t group_index : output_order) {
- AppendGroupToBatch(groups_[group_index], result);
- }
-
- if (having_) {
- std::vector selected_rows;
- selected_rows.reserve(result.RowsCount());
-
- for (size_t row = 0; row < result.RowsCount(); ++row) {
- if (EvaluatePredicate(having_, result, row)) {
- selected_rows.push_back(row);
- }
- }
-
- Batch filtered(schema, selected_rows.size());
- filtered.AppendRowsSelectedFromBatch(result, selected_rows);
-
- return filtered;
- }
-
- return result;
- }
-
- private:
- struct GroupState {
- std::vector key_values;
- std::vector> states;
- };
-
- struct GroupAggBinding {
- PlannedAgg aggregate;
- };
-
- std::vector> CreateStates() const {
- std::vector> states;
- states.reserve(bindings_.size());
-
- for (const auto& binding : bindings_) {
- states.push_back(CreateAggState(binding.aggregate));
- }
-
- return states;
- }
-
- void AppendGroupToBatch(const GroupState& group, const Batch& batch) const {
- for (size_t column = 0; column < select_items_.size(); ++column) {
- const PlannedSelectItem& item = select_items_[column];
- if (item.kind == SelectItemKind::GroupKey) {
- batch.AppendValueFromString(column, FormatGroupKeyValue(group.key_values[item.index]));
- } else {
- batch.AppendValueFromString(column, group.states[item.index]->Finalize());
- }
- }
- }
-
- std::vector BuildOutputOrder() const {
- std::vector order;
- order.reserve(groups_.size());
-
- for (size_t i = 0; i < groups_.size(); ++i) {
- order.push_back(i);
- }
-
- if (!sort_by_group_keys_) {
- return order;
- }
-
- std::vector group_key_columns;
-
- for (size_t i = 0; i < select_items_.size(); ++i) {
- if (select_items_[i].kind == SelectItemKind::GroupKey) {
- group_key_columns.push_back(i);
- }
- }
-
- if (group_key_columns.empty()) {
- return order;
- }
-
- std::ranges::sort(order, [&](const size_t lhs_index, const size_t rhs_index) {
- const GroupState& lhs = groups_[lhs_index];
- const GroupState& rhs = groups_[rhs_index];
- for (const size_t column : group_key_columns) {
- const PlannedSelectItem& item = select_items_[column];
- const std::strong_ordering comparison =
- CompareGroupKeyValue(lhs.key_values[item.index], rhs.key_values[item.index]);
- if (comparison != 0) {
- return comparison < 0;
- }
- }
-
- return false;
- });
-
- return order;
- }
-
- std::unique_ptr child_;
- GroupKeyMaterializer group_key_materializer_;
- std::vector bindings_;
- std::vector select_items_;
- PredicatePtr filter_;
- PredicatePtr having_;
- std::unordered_map group_indexes_;
- std::vector groups_;
- bool sort_by_group_keys_ = false;
- bool returned_ = false;
-};
-
-class GroupAggTopKOperator final : public Operator {
- public:
- GroupAggTopKOperator(std::unique_ptr child, std::vector group_keys,
- const std::vector& aggregates, std::vector select_items,
- PredicatePtr filter, std::vector order_by, const size_t limit)
- : child_(std::move(child)),
- group_key_materializer_(std::move(group_keys)),
- select_items_(std::move(select_items)),
- filter_(std::move(filter)),
- order_by_(std::move(order_by)),
- limit_(limit) {
- bindings_.reserve(aggregates.size());
-
- for (const auto& aggregate : aggregates) {
- bindings_.push_back(GroupAggBinding{
- .aggregate = aggregate,
- });
- }
- }
-
- std::optional Next() override {
- if (returned_ || limit_ == 0) {
- return std::nullopt;
- }
-
- returned_ = true;
-
- while (auto batch = child_->Next()) {
- std::vector selected_rows;
- SelectRowsMatchingPredicate(filter_, *batch, selected_rows);
-
- for (const size_t row : selected_rows) {
- TypedGroupKey key = group_key_materializer_.Materialize(*batch, row);
-
- size_t group_index = 0;
- const auto it = group_indexes_.find(key);
- if (it == group_indexes_.end()) {
- group_index = groups_.size();
- group_indexes_.emplace(key, group_index);
-
- groups_.push_back(GroupState{
- .key_values = std::move(key.values),
- .states = CreateStates(),
- .aggregate_values = {},
- .aggregate_values_finalized = false,
- });
- } else {
- group_index = it->second;
- }
-
- GroupState& group = groups_[group_index];
- for (size_t i = 0; i < bindings_.size(); ++i) {
- ConsumeAggRow(bindings_[i].aggregate, *batch, row, *group.states[i]);
- }
- }
- }
-
- Schema schema;
- schema.columns.reserve(select_items_.size());
- for (const auto& item : select_items_) {
- schema.columns.push_back(ColumnSchema(item.output_name, item.output_type));
- }
-
- const std::vector top_groups = BuildTopGroupIndexes();
- Batch result(schema, top_groups.size());
-
- for (const size_t group_index : top_groups) {
- AppendGroupToBatch(groups_[group_index], result);
- }
-
- return result;
- }
-
- private:
- struct GroupState {
- std::vector key_values;
- std::vector> states;
- mutable std::vector aggregate_values;
- mutable bool aggregate_values_finalized = false;
- };
-
- struct GroupAggBinding {
- PlannedAgg aggregate;
- };
-
- std::vector> CreateStates() const {
- std::vector> states;
- states.reserve(bindings_.size());
-
- for (const auto& binding : bindings_) {
- states.push_back(CreateAggState(binding.aggregate));
- }
-
- return states;
- }
-
- static void FinalizeAggregateValues(const GroupState& group) {
- if (group.aggregate_values_finalized) {
- return;
- }
-
- group.aggregate_values.reserve(group.states.size());
- for (const auto& state : group.states) {
- group.aggregate_values.push_back(state->Finalize());
- }
- group.aggregate_values_finalized = true;
- }
-
- bool OrdersBefore(const size_t lhs_index, const size_t rhs_index) const {
- const GroupState& lhs = groups_[lhs_index];
- const GroupState& rhs = groups_[rhs_index];
-
- for (const PlannedOrderBy& order : order_by_) {
- const PlannedSelectItem& item = select_items_[order.result_column_index];
- std::strong_ordering comparison = std::strong_ordering::equal;
-
- if (item.kind == SelectItemKind::GroupKey) {
- comparison = CompareGroupKeyValue(lhs.key_values[item.index], rhs.key_values[item.index]);
- } else {
- FinalizeAggregateValues(lhs);
- FinalizeAggregateValues(rhs);
- comparison =
- CompareValues(item.output_type, lhs.aggregate_values[item.index], rhs.aggregate_values[item.index]);
- }
-
- if (comparison != 0) {
- return order.descending ? comparison > 0 : comparison < 0;
- }
- }
-
- return lhs_index < rhs_index;
- }
-
- void AppendGroupToBatch(const GroupState& group, const Batch& batch) const {
- FinalizeAggregateValues(group);
-
- for (size_t column = 0; column < select_items_.size(); ++column) {
- const PlannedSelectItem& item = select_items_[column];
- if (item.kind == SelectItemKind::GroupKey) {
- batch.AppendValueFromString(column, FormatGroupKeyValue(group.key_values[item.index]));
- } else {
- batch.AppendValueFromString(column, group.aggregate_values[item.index]);
- }
- }
- }
-
- std::vector BuildTopGroupIndexes() const {
- std::vector top_groups;
- top_groups.reserve(limit_);
-
- for (size_t i = 0; i < groups_.size(); ++i) {
- if (top_groups.size() < limit_) {
- top_groups.push_back(i);
- std::ranges::push_heap(top_groups,
- [&](const size_t lhs, const size_t rhs) { return OrdersBefore(lhs, rhs); });
- continue;
- }
-
- if (OrdersBefore(i, top_groups.front())) {
- std::ranges::pop_heap(top_groups,
- [&](const size_t lhs, const size_t rhs) { return OrdersBefore(lhs, rhs); });
- top_groups.back() = i;
- std::ranges::push_heap(top_groups,
- [&](const size_t lhs, const size_t rhs) { return OrdersBefore(lhs, rhs); });
- }
- }
-
- std::ranges::sort(top_groups, [&](const size_t lhs, const size_t rhs) { return OrdersBefore(lhs, rhs); });
-
- return top_groups;
- }
-
- std::unique_ptr child_;
- GroupKeyMaterializer group_key_materializer_;
- std::vector bindings_;
- std::vector select_items_;
- PredicatePtr filter_;
- std::vector order_by_;
- size_t limit_ = 0;
- std::unordered_map group_indexes_;
- mutable std::vector groups_;
- bool returned_ = false;
-};
-
-struct SortedRow {
- std::vector values;
- size_t ordinal = 0;
-};
-
-class RowOrdering {
- public:
- explicit RowOrdering(std::vector order_by) : order_by_(std::move(order_by)) {}
-
- bool operator()(const SortedRow& lhs, const SortedRow& rhs) const {
- for (const auto& order : order_by_) {
- const std::strong_ordering comparison = CompareValues(
- order.value_type, lhs.values[order.result_column_index], rhs.values[order.result_column_index]);
- if (comparison != 0) {
- return order.descending ? comparison > 0 : comparison < 0;
- }
- }
-
- return lhs.ordinal < rhs.ordinal;
- }
-
- private:
- std::vector order_by_;
-};
-
-static std::vector MaterializeBatchRow(const Batch& batch, const size_t row) {
- std::vector values;
- values.reserve(batch.ColumnsCount());
-
- for (size_t column_index = 0; column_index < batch.ColumnsCount(); ++column_index) {
- values.push_back(batch.ColumnAt(column_index).ValueAsString(row));
- }
-
- return values;
-}
-
-static Batch BuildBatchFromSortedRows(const Schema& schema, const std::vector& rows) {
- Batch result(schema, rows.size());
-
- for (const auto& row : rows) {
- for (size_t i = 0; i < row.values.size(); ++i) {
- result.AppendValueFromString(i, row.values[i]);
- }
- }
-
- return result;
-}
-
-class ProjectionOperator final : public Operator {
- public:
- ProjectionOperator(std::unique_ptr child, std::vector items, Schema source_schema)
- : child_(std::move(child)), items_(std::move(items)), source_schema_(std::move(source_schema)) {
- for (const auto& item : items_) {
- if (item.expression && item.expression->kind == ExprKind::Star) {
- for (const size_t source_index : StarColumnIndexes(source_schema_)) {
- schema_.columns.push_back(source_schema_.columns[source_index]);
- }
- } else {
- schema_.columns.emplace_back(item.output_name, ColumnType::String);
- }
- }
- }
-
- std::optional Next() override {
- while (auto batch = child_->Next()) {
- if (!star_column_indexes_initialized_) {
- star_column_indexes_ = StarColumnIndexes(batch->GetSchema());
- star_column_indexes_initialized_ = true;
- }
-
- Batch projected(schema_, batch->RowsCount());
-
- size_t output_column = 0;
- for (const auto& item : items_) {
- if (item.expression && item.expression->kind == ExprKind::Star) {
- for (const size_t source_column : star_column_indexes_) {
- projected.AppendColumnRange(output_column++, batch->ColumnAt(source_column), 0,
- batch->RowsCount());
- }
- continue;
- }
-
- const size_t expression_column = output_column++;
- for (size_t row = 0; row < batch->RowsCount(); ++row) {
- projected.AppendValueFromString(expression_column, EvalExpr(item.expression, *batch, row));
- }
- }
-
- if (projected.RowsCount() > 0) {
- return projected;
- }
- }
-
- return std::nullopt;
- }
-
- const Schema& GetSchema() const { return schema_; }
-
- private:
- std::unique_ptr child_;
- std::vector items_;
- Schema source_schema_;
- Schema schema_;
- std::vector star_column_indexes_;
- bool star_column_indexes_initialized_ = false;
-};
-
-class OrderByOperator final : public Operator {
- public:
- OrderByOperator(std::unique_ptr child, std::vector order_by,
- std::vector /*select_items*/)
- : child_(std::move(child)), ordering_(std::move(order_by)) {}
-
- std::optional Next() override {
- if (returned_) {
- return std::nullopt;
- }
-
- returned_ = true;
-
- std::optional schema;
- std::vector rows;
- size_t ordinal = 0;
-
- while (auto batch = child_->Next()) {
- if (!schema.has_value()) {
- schema = batch->GetSchema();
- }
-
- for (size_t row = 0; row < batch->RowsCount(); ++row) {
- rows.push_back(SortedRow{
- .values = MaterializeBatchRow(*batch, row),
- .ordinal = ordinal++,
- });
- }
- }
-
- if (!schema.has_value()) {
- return std::nullopt;
- }
-
- SortRows(rows);
-
- return BuildBatchFromSortedRows(*schema, rows);
- }
-
- private:
- void SortRows(std::vector& rows) const { std::ranges::sort(rows, ordering_); }
-
- std::unique_ptr child_;
- RowOrdering ordering_;
- bool returned_ = false;
-};
-
-class TopKOperator final : public Operator {
- public:
- TopKOperator(std::unique_ptr child, std::vector order_by,
- std::vector /*select_items*/, const size_t limit)
- : child_(std::move(child)), ordering_(std::move(order_by)), limit_(limit) {}
-
- std::optional Next() override {
- if (returned_ || limit_ == 0) {
- return std::nullopt;
- }
-
- returned_ = true;
-
- std::optional schema;
- std::vector rows;
- rows.reserve(limit_);
-
- size_t ordinal = 0;
-
- while (auto batch = child_->Next()) {
- if (!schema.has_value()) {
- schema = batch->GetSchema();
- }
-
- for (size_t row = 0; row < batch->RowsCount(); ++row) {
- SortedRow candidate{
- .values = MaterializeBatchRow(*batch, row),
- .ordinal = ordinal++,
- };
-
- if (rows.size() < limit_) {
- rows.push_back(std::move(candidate));
- std::ranges::push_heap(rows, ordering_);
- continue;
- }
-
- if (ordering_(candidate, rows.front())) {
- std::ranges::pop_heap(rows, ordering_);
- rows.back() = std::move(candidate);
- std::ranges::push_heap(rows, ordering_);
- }
- }
- }
-
- if (!schema.has_value()) {
- return std::nullopt;
- }
-
- std::ranges::sort(rows, ordering_);
-
- return BuildBatchFromSortedRows(*schema, rows);
- }
-
- private:
- std::unique_ptr child_;
- RowOrdering ordering_;
- size_t limit_;
- bool returned_ = false;
-};
-
-class LimitOperator final : public Operator {
- public:
- LimitOperator(std::unique_ptr child, const size_t limit) : child_(std::move(child)), remaining_(limit) {}
-
- std::optional Next() override {
- if (remaining_ == 0) {
- return std::nullopt;
- }
-
- while (auto batch = child_->Next()) {
- if (batch->RowsCount() <= remaining_) {
- remaining_ -= batch->RowsCount();
-
- return batch;
- }
-
- Batch limited(batch->GetSchema(), remaining_);
- limited.AppendRowsRangeFromBatch(*batch, 0, remaining_);
-
- remaining_ = 0;
-
- return limited;
- }
-
- remaining_ = 0;
-
- return std::nullopt;
- }
-
- private:
- std::unique_ptr child_;
- size_t remaining_;
-};
-
-class OffsetOperator final : public Operator {
- public:
- OffsetOperator(std::unique_ptr child, const size_t offset)
- : child_(std::move(child)), remaining_(offset) {}
-
- std::optional Next() override {
- while (const auto batch = child_->Next()) {
- if (remaining_ >= batch->RowsCount()) {
- remaining_ -= batch->RowsCount();
- continue;
- }
-
- Batch shifted(batch->GetSchema(), batch->RowsCount() - remaining_);
- shifted.AppendRowsRangeFromBatch(*batch, remaining_, batch->RowsCount() - remaining_);
- remaining_ = 0;
-
- return shifted;
- }
-
- return std::nullopt;
- }
-
- private:
- std::unique_ptr child_;
- size_t remaining_;
-};
+#include "executor/operators_internal.h"
std::unique_ptr BuildPlan(const PlannedQuery& planned) {
if (planned.metadata_count_only) {
- return std::make_unique(planned.table_path, planned.aggregates.front().output_name);
+ return CreateMetadataCountOperator(planned.table_path, planned.aggregates.front().output_name);
}
if (planned.metadata_extrema_only) {
- return std::make_unique(planned.table_path, planned.aggregates);
+ return CreateMetadataExtremaOperator(planned.table_path, planned.aggregates);
}
std::unique_ptr root =
- std::make_unique(planned.table_path, planned.projection_indexes, planned.filter);
+ CreateScanOperator(planned.table_path, planned.projection_indexes, planned.filter);
if (planned.filter && planned.plain_select) {
- root = std::make_unique(std::move(root), planned.filter);
+ root = CreateFilterOperator(std::move(root), planned.filter);
}
if (planned.plain_select) {
bool limit_applied_by_top_k = false;
- if (!planned.order_by.empty()) {
- if (planned.limit.has_value() && planned.offset == 0) {
- root = std::make_unique(std::move(root), planned.order_by, planned.select_items,
- *planned.limit);
- limit_applied_by_top_k = true;
- } else {
- root = std::make_unique(std::move(root), planned.order_by, planned.select_items);
- }
- }
- if (planned.offset > 0) {
- root = std::make_unique(std::move(root), planned.offset);
- }
- if (planned.limit.has_value() && !limit_applied_by_top_k) {
- root = std::make_unique(std::move(root), *planned.limit);
- }
+ ApplyOrderOffsetLimit(root, planned, limit_applied_by_top_k);
- auto projection =
- std::make_unique