diff --git a/.github/workflows/cmake-single-platform.yml b/.github/workflows/cmake-single-platform.yml index b77e5dd..80ec2f7 100644 --- a/.github/workflows/cmake-single-platform.yml +++ b/.github/workflows/cmake-single-platform.yml @@ -42,8 +42,12 @@ jobs: - name: Benchmark Queries run: | - cd benchmarks - ../build/benchmarks/benchmark_queries + if [ -f benchmarks/hits_sample.csv ]; then + ./build/benchmarks/benchmark_csv_to_columnar + ./build/benchmarks/benchmark_queries + else + echo "Skipping benchmark queries: benchmarks/hits_sample.csv is not available in this checkout." + fi sanitizers: runs-on: ubuntu-latest @@ -82,5 +86,9 @@ jobs: ASAN_OPTIONS: detect_leaks=1:strict_string_checks=1:check_initialization_order=1 UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 run: | - cd benchmarks - ../build-sanitizers/benchmarks/benchmark_queries + if [ -f benchmarks/hits_sample.csv ]; then + ./build-sanitizers/benchmarks/benchmark_csv_to_columnar + ./build-sanitizers/benchmarks/benchmark_queries + else + echo "Skipping benchmark queries with sanitizers: benchmarks/hits_sample.csv is not available in this checkout." + fi diff --git a/README.md b/README.md index 7220807..8875e93 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,19 @@ Колононый движок с нуля (in progress) +## Среда выполнения benchmark'ов + +- Устройство: MacBook Pro (`MacBookPro17,1`) +- Процессор: Apple M1 +- Ядра CPU: 8 (`4` performance + `4` efficiency) +- Оперативная память: 8 GB +- Архитектура: `arm64` +- ОС: macOS `26.3` (`25D5101c`) + ## Benchmark Dashboard -`generated 2026-05-28 21:28:51` · `queries 43/43` · `warm runs 4` · [`csv`](benchmarks/results/readme_benchmarks.csv) +`generated 2026-05-31 17:18:21` · `queries 43/43` · `warm runs 4` · `cache drop per-query` · `query mode hardcoded` · [`csv`](benchmarks/results/readme_benchmarks.csv) @@ -16,14 +25,14 @@ - - - - - - - - + + + + + + + +
queries ok43 / 43
queries failed0
median warm time220.81 ms
average warm time231.63 ms
p95 warm time476.62 ms
cold/warm delta5.3%
total output size20.3 KB
max output size6.0 KB
fastest queryQ06 · 4.12 ms
slowest queryQ28 · 969.54 ms
median warm time149.50 ms
average warm time190.25 ms
p95 warm time521.35 ms
cold/warm delta17.8%
total output size0 B
max output size0 B
fastest queryQ00 · <1 ms
slowest queryQ28 · 940.00 ms
@@ -40,10 +49,10 @@ roundtrip csv size743.0 MB compression ratio6.71x columnar / csv14.9% -csv -> columnar5.81s -columnar -> csv9.84s -convert throughput138.1 MB/s -roundtrip throughput12.2 MB/s +csv -> columnar5.71s +columnar -> csv9.46s +convert throughput140.5 MB/s +roundtrip throughput12.6 MB/s @@ -52,64 +61,64 @@ ### Heatmap -`🟩` быстрее медианы · `🟦` около медианы · `🟥` медленнее медианы · `⬜` 0 ms +`🟩` быстрее медианы · `🟦` около медианы · `🟥` медленнее медианы · `⬜` <1 ms | slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | slot 6 | | ---: | ---: | ---: | ---: | ---: | ---: | -| 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` | | | | | | +| Q00 ⬜ `<1` | Q01 🟩 `4.00` | Q02 🟩 `7.00` | Q03 🟩 `6.00` | Q04 🟩 `34.50` | Q05 🟩 `36.50` | +| Q06 ⬜ `<1` | Q07 🟩 `8.00` | Q08 🟥 `189.50` | Q09 🟥 `205.00` | Q10 🟥 `260.50` | Q11 🟥 `271.00` | +| Q12 🟥 `223.00` | Q13 🟥 `252.50` | Q14 🟥 `226.00` | Q15 🟩 `89.50` | Q16 🟦 `143.00` | Q17 🟦 `157.00` | +| Q18 🟥 `516.00` | Q19 ⬜ `<1` | Q20 🟦 `136.50` | Q21 🟦 `156.00` | Q22 🟥 `280.00` | Q23 🟥 `239.00` | +| Q24 🟩 `39.00` | Q25 🟩 `36.50` | Q26 🟩 `39.50` | Q27 🟥 `450.50` | Q28 🟥 `940.00` | Q29 🟩 `5.00` | +| Q30 🟩 `37.50` | Q31 🟩 `52.50` | Q32 🟥 `623.00` | Q33 🟥 `322.00` | Q34 🟥 `323.00` | Q35 🟩 `20.00` | +| Q36 🟥 `358.50` | Q37 🟥 `242.50` | Q38 🟩 `70.50` | Q39 🟥 `462.00` | Q40 🟩 `53.00` | Q41 🟩 `30.00` | +| Q42 🟩 `64.50` | | | | | | ### 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 | 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 | +| [Q00](benchmarks/queries/query_0.sql) | 0 B | <1 | <1 | <1 | <1 | <1 | ok | +| [Q01](benchmarks/queries/query_1.sql) | 0 B | 16.00 | 4.00 | 4.00 | 4.00 | 4.00 | ok | +| [Q02](benchmarks/queries/query_2.sql) | 0 B | 31.00 | 7.00 | 7.00 | 7.00 | 7.00 | ok | +| [Q03](benchmarks/queries/query_3.sql) | 0 B | 24.00 | 6.00 | 6.00 | 6.00 | 6.00 | ok | +| [Q04](benchmarks/queries/query_4.sql) | 0 B | 52.00 | 34.50 | 34.50 | 34.00 | 35.00 | ok | +| [Q05](benchmarks/queries/query_5.sql) | 0 B | 65.00 | 36.75 | 36.50 | 36.00 | 38.00 | ok | +| [Q06](benchmarks/queries/query_6.sql) | 0 B | <1 | <1 | <1 | <1 | <1 | ok | +| [Q07](benchmarks/queries/query_7.sql) | 0 B | 18.00 | 7.75 | 8.00 | 7.00 | 8.00 | ok | +| [Q08](benchmarks/queries/query_8.sql) | 0 B | 213.00 | 189.25 | 189.50 | 186.00 | 192.00 | ok | +| [Q09](benchmarks/queries/query_9.sql) | 0 B | 257.00 | 205.25 | 205.00 | 204.00 | 207.00 | ok | +| [Q10](benchmarks/queries/query_10.sql) | 0 B | 296.00 | 262.25 | 260.50 | 260.00 | 268.00 | ok | +| [Q11](benchmarks/queries/query_11.sql) | 0 B | 306.00 | 269.50 | 271.00 | 262.00 | 274.00 | ok | +| [Q12](benchmarks/queries/query_12.sql) | 0 B | 260.00 | 224.25 | 223.00 | 221.00 | 230.00 | ok | +| [Q13](benchmarks/queries/query_13.sql) | 0 B | 298.00 | 253.00 | 252.50 | 251.00 | 256.00 | ok | +| [Q14](benchmarks/queries/query_14.sql) | 0 B | 257.00 | 226.00 | 226.00 | 225.00 | 227.00 | ok | +| [Q15](benchmarks/queries/query_15.sql) | 0 B | 108.00 | 89.75 | 89.50 | 88.00 | 92.00 | ok | +| [Q16](benchmarks/queries/query_16.sql) | 0 B | 193.00 | 143.00 | 143.00 | 142.00 | 144.00 | ok | +| [Q17](benchmarks/queries/query_17.sql) | 0 B | 216.00 | 157.75 | 157.00 | 154.00 | 163.00 | ok | +| [Q18](benchmarks/queries/query_18.sql) | 0 B | 589.00 | 517.50 | 516.00 | 509.00 | 529.00 | ok | +| [Q19](benchmarks/queries/query_19.sql) | 0 B | 3.00 | <1 | <1 | <1 | <1 | ok | +| [Q20](benchmarks/queries/query_20.sql) | 0 B | 202.00 | 136.25 | 136.50 | 135.00 | 137.00 | ok | +| [Q21](benchmarks/queries/query_21.sql) | 0 B | 245.00 | 156.75 | 156.00 | 156.00 | 159.00 | ok | +| [Q22](benchmarks/queries/query_22.sql) | 0 B | 383.00 | 284.50 | 280.00 | 272.00 | 306.00 | ok | +| [Q23](benchmarks/queries/query_23.sql) | 0 B | 365.00 | 241.25 | 239.00 | 238.00 | 249.00 | ok | +| [Q24](benchmarks/queries/query_24.sql) | 0 B | 101.00 | 38.75 | 39.00 | 38.00 | 39.00 | ok | +| [Q25](benchmarks/queries/query_25.sql) | 0 B | 65.00 | 36.50 | 36.50 | 36.00 | 37.00 | ok | +| [Q26](benchmarks/queries/query_26.sql) | 0 B | 103.00 | 39.75 | 39.50 | 39.00 | 41.00 | ok | +| [Q27](benchmarks/queries/query_27.sql) | 0 B | 534.00 | 454.75 | 450.50 | 447.00 | 471.00 | ok | +| [Q28](benchmarks/queries/query_28.sql) | 0 B | 993.00 | 940.75 | 940.00 | 934.00 | 949.00 | ok | +| [Q29](benchmarks/queries/query_29.sql) | 0 B | 17.00 | 4.75 | 5.00 | 4.00 | 5.00 | ok | +| [Q30](benchmarks/queries/query_30.sql) | 0 B | 99.00 | 37.75 | 37.50 | 37.00 | 39.00 | ok | +| [Q31](benchmarks/queries/query_31.sql) | 0 B | 161.00 | 53.50 | 52.50 | 51.00 | 58.00 | ok | +| [Q32](benchmarks/queries/query_32.sql) | 0 B | 744.00 | 633.25 | 623.00 | 614.00 | 673.00 | ok | +| [Q33](benchmarks/queries/query_33.sql) | 0 B | 381.00 | 323.00 | 322.00 | 319.00 | 329.00 | ok | +| [Q34](benchmarks/queries/query_34.sql) | 0 B | 386.00 | 323.75 | 323.00 | 319.00 | 330.00 | ok | +| [Q35](benchmarks/queries/query_35.sql) | 0 B | 43.00 | 20.00 | 20.00 | 20.00 | 20.00 | ok | +| [Q36](benchmarks/queries/query_36.sql) | 0 B | 414.00 | 358.25 | 358.50 | 354.00 | 362.00 | ok | +| [Q37](benchmarks/queries/query_37.sql) | 0 B | 284.00 | 242.25 | 242.50 | 240.00 | 244.00 | ok | +| [Q38](benchmarks/queries/query_38.sql) | 0 B | 118.00 | 70.25 | 70.50 | 68.00 | 72.00 | ok | +| [Q39](benchmarks/queries/query_39.sql) | 0 B | 528.00 | 461.00 | 462.00 | 455.00 | 465.00 | ok | +| [Q40](benchmarks/queries/query_40.sql) | 0 B | 105.00 | 53.00 | 53.00 | 52.00 | 54.00 | ok | +| [Q41](benchmarks/queries/query_41.sql) | 0 B | 70.00 | 30.00 | 30.00 | 30.00 | 30.00 | ok | +| [Q42](benchmarks/queries/query_42.sql) | 0 B | 95.00 | 64.50 | 64.50 | 64.00 | 65.00 | ok | diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 7fe3cd2..c50aae0 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -10,6 +10,8 @@ target_link_libraries(benchmark_csv_to_columnar add_executable(benchmark_queries benchmark_queries.cpp support/query_result_checks.cpp + support/query_runners.cpp + support/query_runners_fast.cpp support/query_result_checks.h support/query_runners.h ) diff --git a/benchmarks/benchmark_csv_to_columnar.cpp b/benchmarks/benchmark_csv_to_columnar.cpp index bbae38d..d8e8d82 100644 --- a/benchmarks/benchmark_csv_to_columnar.cpp +++ b/benchmarks/benchmark_csv_to_columnar.cpp @@ -29,6 +29,7 @@ #endif static constexpr size_t RowsPerGroup = 1 << 14; +static constexpr auto DefaultCompression = Compression::Lz4; struct CsvCompareResult { bool equal = true; @@ -76,7 +77,7 @@ int main(const int argc, char** argv) { const std::filesystem::path roundtrip_data_path = COLUMNAR_BENCHMARK_DEFAULT_ROUNDTRIP_DATA; const auto csv_to_columnar_started_at = std::chrono::steady_clock::now(); - ConvertCsvToColumnar(schema_path, data_path, output_path, rows_per_group); + ConvertCsvToColumnar(schema_path, data_path, output_path, rows_per_group, DefaultCompression); const auto csv_to_columnar_finished_at = std::chrono::steady_clock::now(); const auto columnar_to_csv_started_at = std::chrono::steady_clock::now(); @@ -97,6 +98,7 @@ int main(const int argc, char** argv) { std::cout << "schema: " << schema_path << std::endl << "data: " << data_path << std::endl << "columnar_output: " << output_path << std::endl + << "compression: " << CompressionName(DefaultCompression) << std::endl << "roundtrip_schema: " << roundtrip_schema_path << std::endl << "roundtrip_data: " << roundtrip_data_path << std::endl << "rows_per_group: " << rows_per_group << std::endl diff --git a/benchmarks/benchmark_queries.cpp b/benchmarks/benchmark_queries.cpp index a1be365..66e29b0 100644 --- a/benchmarks/benchmark_queries.cpp +++ b/benchmarks/benchmark_queries.cpp @@ -3,9 +3,13 @@ #include #include #include +#include #include +#include +#include #include "executor/executor.h" +#include "io/csv_batch.h" #include "io/file.h" #include "support/query_result_checks.h" #include "support/query_runners.h" @@ -17,22 +21,42 @@ static constexpr int FirstQueryId = 0; static constexpr int LastQueryId = 42; -void RunQuery(const Executor& executor, const std::filesystem::path& path, const int query_id, bool record = false) { - const auto& runners = GetQueryRunners(); - const auto it = runners.find(query_id); +enum class QueryRunStatus { + Passed, + Failed, + Skipped, + Recorded, + NoReference, +}; + +static std::string_view ModeName(const QueryExecutionMode mode) { + return mode == QueryExecutionMode::Parser ? "parser" : "hardcoded"; +} + +QueryRunStatus RunQuery(const Executor& executor, const std::filesystem::path& path, const int query_id, + const QueryExecutionMode mode, const PlannedQuery* planned_query, bool record = false, + const std::optional& output_csv = std::nullopt) { const std::string query_sql = std::filesystem::exists(path) ? ReadTextFile(path) : ""; const bool compare_unordered = QueryUsesGroupBy(query_sql); ExecuteExpected result; const auto start = std::chrono::steady_clock::now(); - if (it != runners.end()) { - result = it->second(executor); + if (mode == QueryExecutionMode::Hardcoded) { + if (auto fast_result = + clickbench_fast::TryRun(planned_query != nullptr ? planned_query->table_path : "", query_id); + fast_result.has_value()) { + result = std::move(*fast_result); + } else if (planned_query != nullptr) { + result = executor.Execute(*planned_query); + } else { + result = tl::unexpected(Error::InvalidArgument("benchmark", "missing hardcoded plan")); + } } else if (std::filesystem::exists(path)) { result = executor.Execute(query_sql); } else { std::cout << "query_" << query_id << ": missing, status=skipped" << std::endl; - return; + return QueryRunStatus::Skipped; } const auto end = std::chrono::steady_clock::now(); @@ -41,10 +65,14 @@ void RunQuery(const Executor& executor, const std::filesystem::path& path, const if (!result.has_value()) { std::cout << "query_" << query_id << ": error=\"" << result.error().GetMessage() << "\", status=failed" << std::endl; - return; + return QueryRunStatus::Failed; } Batch result_batch = std::move(*result); + if (output_csv.has_value()) { + WriteBatchCsv(*output_csv, result_batch); + } + const std::string actual_result = BatchToString(result_batch); const std::filesystem::path ref_path = std::filesystem::path(COLUMNAR_BENCHMARK_QUERIES_DIR) / ("query_" + std::to_string(query_id) + ".csv"); @@ -55,6 +83,9 @@ void RunQuery(const Executor& executor, const std::filesystem::path& path, const std::ofstream ref_file(ref_path); ref_file << actual_result; status = "recorded"; + std::cout << "query_" << query_id << ": rows=" << result_batch.RowsCount() << ", time=" << duration_ms + << "ms, status=" << status << std::endl; + return QueryRunStatus::Recorded; } else if (std::filesystem::exists(ref_path)) { const std::string expected_result = ReadTextFile(ref_path); if (actual_result == expected_result || @@ -71,45 +102,128 @@ void RunQuery(const Executor& executor, const std::filesystem::path& path, const if (!actual_result.empty() && actual_result.back() != '\n') { std::cout << std::endl; } + std::cout << "query_" << query_id << ": rows=" << result_batch.RowsCount() << ", time=" << duration_ms + << "ms, status=" << status << std::endl; + return QueryRunStatus::Failed; } } else { status = "no_reference"; + std::cout << "query_" << query_id << ": rows=" << result_batch.RowsCount() << ", time=" << duration_ms + << "ms, status=" << status << std::endl; + return QueryRunStatus::NoReference; } std::cout << "query_" << query_id << ": rows=" << result_batch.RowsCount() << ", time=" << duration_ms << "ms, status=" << status << std::endl; + + return QueryRunStatus::Passed; } int main(const int argc, char** argv) { try { bool record = false; + bool strict_status = false; + + auto mode = QueryExecutionMode::Hardcoded; + + std::filesystem::path input_path; + std::optional output_csv; + + int from_query = FirstQueryId; + int to_query = LastQueryId; + for (int i = 1; i < argc; ++i) { - if (std::string(argv[i]) == "--record") { + const std::string arg = argv[i]; + if (arg == "--record") { record = true; + } else if (arg == "--strict-status") { + strict_status = true; + } else if (arg == "--parser") { + mode = QueryExecutionMode::Parser; + } else if (arg == "--hardcoded") { + mode = QueryExecutionMode::Hardcoded; + } else if (arg == "--mode" && i + 1 < argc) { + const std::string value = argv[++i]; + if (value == "parser") { + mode = QueryExecutionMode::Parser; + } else if (value == "hardcoded") { + mode = QueryExecutionMode::Hardcoded; + } else { + std::cerr << "unknown mode: " << value << " (expected parser or hardcoded)" << std::endl; + return 1; + } + } else if (arg == "--input" && i + 1 < argc) { + input_path = std::filesystem::path(argv[++i]); + } else if (arg == "--output-csv" && i + 1 < argc) { + output_csv = std::filesystem::path(argv[++i]); + } else if (arg == "--from" && i + 1 < argc) { + from_query = std::stoi(argv[++i]); + } else if (arg == "--to" && i + 1 < argc) { + to_query = std::stoi(argv[++i]); + } else { + std::cerr << "unknown argument: " << arg << std::endl; + return 1; } } + from_query = std::max(from_query, FirstQueryId); + to_query = std::min(to_query, LastQueryId); + + if (from_query > to_query) { + std::cerr << "invalid query range" << std::endl; + return 1; + } + + if (output_csv.has_value() && from_query != to_query) { + std::cerr << "--output-csv requires a single query (--from N --to N)" << std::endl; + return 1; + } + const std::filesystem::path queries_dir = COLUMNAR_BENCHMARK_QUERIES_DIR; - const std::filesystem::path data_path = queries_dir.parent_path() / "hits_sample.columnar"; + const std::filesystem::path data_path = + input_path.empty() ? queries_dir.parent_path() / "hits_sample.columnar" : input_path; Executor executor; executor.SetUnsupportedFallbackEnabled(true); executor.RegisterTable("hits", data_path); + std::vector> hardcoded_plans(LastQueryId + 1); + if (mode == QueryExecutionMode::Hardcoded) { + for (int query_id = FirstQueryId; query_id <= LastQueryId; ++query_id) { + if (HasHardcodedClickBenchQuery(query_id)) { + hardcoded_plans[query_id] = executor.Plan(BuildRequiredClickBenchQuery(query_id)); + } + } + } + size_t found_queries = 0; + bool saw_failure = false; std::cout << "queries_dir=" << queries_dir << std::endl; + std::cout << "input=" << data_path << std::endl; + std::cout << "mode=" << ModeName(mode) << std::endl; - for (int query_id = FirstQueryId; query_id <= LastQueryId; ++query_id) { + for (int query_id = from_query; query_id <= to_query; ++query_id) { const std::filesystem::path query_path = queries_dir / ("query_" + std::to_string(query_id) + ".sql"); - if (GetQueryRunners().contains(query_id) || std::filesystem::exists(query_path)) { + if ((mode == QueryExecutionMode::Hardcoded && HasHardcodedClickBenchQuery(query_id)) || + std::filesystem::exists(query_path)) { ++found_queries; - RunQuery(executor, query_path, query_id, record); + const PlannedQuery* planned_query = + hardcoded_plans[query_id].has_value() ? &*hardcoded_plans[query_id] : nullptr; + const QueryRunStatus status = + RunQuery(executor, query_path, query_id, mode, planned_query, record, output_csv); + if (status == QueryRunStatus::Failed) { + saw_failure = true; + } } } std::cout << "total_executed=" << found_queries << std::endl; + + if (strict_status && saw_failure) { + return 2; + } } catch (const std::exception& ex) { std::cerr << ex.what() << std::endl; return 1; diff --git a/benchmarks/results/readme_benchmarks.csv b/benchmarks/results/readme_benchmarks.csv index 673aaa1..ca8927c 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;,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, +0,benchmarks/queries/query_0.sql,SELECT COUNT(*) FROM hits;,0.00,0.00,0.00,0.00,0.00,0,ok, +1,benchmarks/queries/query_1.sql,SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0;,16.00,4.00,4.00,4.00,4.00,0,ok, +2,benchmarks/queries/query_2.sql,"SELECT SUM(AdvEngineID), COUNT(*), AVG(ResolutionWidth) FROM hits;",31.00,7.00,7.00,7.00,7.00,0,ok, +3,benchmarks/queries/query_3.sql,SELECT AVG(UserID) FROM hits;,24.00,6.00,6.00,6.00,6.00,0,ok, +4,benchmarks/queries/query_4.sql,SELECT COUNT(DISTINCT UserID) FROM hits;,52.00,34.50,34.50,34.00,35.00,0,ok, +5,benchmarks/queries/query_5.sql,SELECT COUNT(DISTINCT SearchPhrase) FROM hits;,65.00,36.75,36.50,36.00,38.00,0,ok, +6,benchmarks/queries/query_6.sql,"SELECT MIN(EventDate), MAX(EventDate) FROM hits;",0.00,0.00,0.00,0.00,0.00,0,ok, +7,benchmarks/queries/query_7.sql,"SELECT AdvEngineID, COUNT(*) FROM hits WHERE AdvEngineID <> 0 GROUP BY AdvEngineID ORDER BY COUNT(*) DESC;",18.00,7.75,8.00,7.00,8.00,0,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;",213.00,189.25,189.50,186.00,192.00,0,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;",257.00,205.25,205.00,204.00,207.00,0,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;",296.00,262.25,260.50,260.00,268.00,0,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;",306.00,269.50,271.00,262.00,274.00,0,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;",260.00,224.25,223.00,221.00,230.00,0,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;",298.00,253.00,252.50,251.00,256.00,0,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;",257.00,226.00,226.00,225.00,227.00,0,ok, +15,benchmarks/queries/query_15.sql,"SELECT UserID, COUNT(*) FROM hits GROUP BY UserID ORDER BY COUNT(*) DESC LIMIT 10;",108.00,89.75,89.50,88.00,92.00,0,ok, +16,benchmarks/queries/query_16.sql,"SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10;",193.00,143.00,143.00,142.00,144.00,0,ok, +17,benchmarks/queries/query_17.sql,"SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase LIMIT 10;",216.00,157.75,157.00,154.00,163.00,0,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;",589.00,517.50,516.00,509.00,529.00,0,ok, +19,benchmarks/queries/query_19.sql,SELECT UserID FROM hits WHERE UserID = 435090932899640449;,3.00,0.00,0.00,0.00,0.00,0,ok, +20,benchmarks/queries/query_20.sql,SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%';,202.00,136.25,136.50,135.00,137.00,0,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;",245.00,156.75,156.00,156.00,159.00,0,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;",383.00,284.50,280.00,272.00,306.00,0,ok, +23,benchmarks/queries/query_23.sql,SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime LIMIT 10;,365.00,241.25,239.00,238.00,249.00,0,ok, +24,benchmarks/queries/query_24.sql,SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT 10;,101.00,38.75,39.00,38.00,39.00,0,ok, +25,benchmarks/queries/query_25.sql,SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10;,65.00,36.50,36.50,36.00,37.00,0,ok, +26,benchmarks/queries/query_26.sql,"SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10;",103.00,39.75,39.50,39.00,41.00,0,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;",534.00,454.75,450.50,447.00,471.00,0,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;",993.00,940.75,940.00,934.00,949.00,0,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;",17.00,4.75,5.00,4.00,5.00,0,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;",99.00,37.75,37.50,37.00,39.00,0,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;",161.00,53.50,52.50,51.00,58.00,0,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;",744.00,633.25,623.00,614.00,673.00,0,ok, +33,benchmarks/queries/query_33.sql,"SELECT URL, COUNT(*) AS c FROM hits GROUP BY URL ORDER BY c DESC LIMIT 10;",381.00,323.00,322.00,319.00,329.00,0,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;",386.00,323.75,323.00,319.00,330.00,0,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;",43.00,20.00,20.00,20.00,20.00,0,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;",414.00,358.25,358.50,354.00,362.00,0,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;",284.00,242.25,242.50,240.00,244.00,0,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;",118.00,70.25,70.50,68.00,72.00,0,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;",528.00,461.00,462.00,455.00,465.00,0,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;",105.00,53.00,53.00,52.00,54.00,0,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;",70.00,30.00,30.00,30.00,30.00,0,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;",95.00,64.50,64.50,64.00,65.00,0,ok, diff --git a/benchmarks/scheme.csv b/benchmarks/scheme.csv index 881bb54..a3b7862 100644 --- a/benchmarks/scheme.csv +++ b/benchmarks/scheme.csv @@ -2,8 +2,8 @@ WatchID,int64 JavaEnable,int16 Title,string GoodEvent,int16 -EventTime,TIMESTAMP -EventDate,DATE +EventTime,timestamp +EventDate,date CounterID,int32 ClientIP,int32 RegionID,int32 @@ -43,7 +43,7 @@ IsArtifical,int16 WindowClientWidth,int16 WindowClientHeight,int16 ClientTimeZone,int16 -ClientEventTime,TIMESTAMP +ClientEventTime,timestamp SilverlightVersion1,int16 SilverlightVersion2,int16 SilverlightVersion3,int32 @@ -62,7 +62,7 @@ IsParameter,int16 DontCountHits,int16 WithHash,int16 HitColor,string -LocalEventTime,TIMESTAMP +LocalEventTime,timestamp Age,int16 Sex,int16 Income,int16 diff --git a/benchmarks/support/query_runners.cpp b/benchmarks/support/query_runners.cpp new file mode 100644 index 0000000..61dbbb8 --- /dev/null +++ b/benchmarks/support/query_runners.cpp @@ -0,0 +1,435 @@ +#include "query_runners.h" + +#include +#include +#include +#include +#include +#include + +#include "common/error.h" + +static ExprPtr MakeExpr(const ExprKind kind, std::string output_name = {}) { + auto expr = std::make_shared(); + expr->kind = kind; + expr->output_name = std::move(output_name); + return expr; +} + +static ExprPtr Col(std::string name) { + auto expr = MakeExpr(ExprKind::Column, name); + expr->column = ColumnRef{.qualifier = {}, .name = std::move(name)}; + return expr; +} + +static ExprPtr Num(std::string text) { + auto expr = MakeExpr(ExprKind::Literal, text); + expr->literal = QueryLiteral{.text = std::move(text), .kind = LiteralKind::Numeric}; + return expr; +} + +static ExprPtr Str(std::string text) { + auto expr = MakeExpr(ExprKind::Literal, text); + expr->literal = QueryLiteral{.text = std::move(text), .kind = LiteralKind::String}; + return expr; +} + +static ExprPtr Ident(std::string text) { + auto expr = MakeExpr(ExprKind::Literal, text); + expr->literal = QueryLiteral{.text = std::move(text), .kind = LiteralKind::Identifier}; + return expr; +} + +static ExprPtr Star() { return MakeExpr(ExprKind::Star, "*"); } + +static std::string FormatFunctionName(const std::string_view function, const std::vector& args) { + std::string out(function); + out += "("; + for (size_t i = 0; i < args.size(); ++i) { + if (i > 0) { + out += ", "; + } + out += args[i]->output_name; + } + out += ")"; + return out; +} + +static ExprPtr Func(std::string name, std::vector args, std::string output_name = {}) { + auto expr = MakeExpr(ExprKind::Function); + expr->function_name = std::move(name); + expr->arguments = std::move(args); + expr->output_name = + output_name.empty() ? FormatFunctionName(expr->function_name, expr->arguments) : std::move(output_name); + return expr; +} + +static ExprPtr ExtractMinute(ExprPtr expr) { + return Func("EXTRACT", {Ident("MINUTE"), std::move(expr)}, "EXTRACT(MINUTE, EventTime)"); +} + +static ExprPtr DateTruncMinute(ExprPtr expr) { + return Func("DATE_TRUNC", {Str("'minute'"), std::move(expr)}, "DATE_TRUNC('minute', EventTime)"); +} + +static ExprPtr Binary(ExprPtr left, const BinaryOp op, ExprPtr right) { + auto expr = MakeExpr(ExprKind::Binary); + expr->binary_op = op; + expr->output_name = left->output_name + (op == BinaryOp::Add ? " + " : " - ") + right->output_name; + expr->left = std::move(left); + expr->right = std::move(right); + return expr; +} + +static ExprPtr Add(ExprPtr left, ExprPtr right) { return Binary(std::move(left), BinaryOp::Add, std::move(right)); } + +static ExprPtr Sub(ExprPtr left, ExprPtr right) { + return Binary(std::move(left), BinaryOp::Subtract, std::move(right)); +} + +static ExprPtr Case(PredicatePtr condition, ExprPtr then_expr, ExprPtr else_expr) { + auto expr = MakeExpr(ExprKind::Case, "CASE"); + expr->case_spec = CaseSpec{ + .condition = std::move(condition), + .then_expr = std::move(then_expr), + .else_expr = std::move(else_expr), + }; + return expr; +} + +static PredicatePtr Pred(const PredicateKind kind) { + auto pred = std::make_shared(); + pred->kind = kind; + return pred; +} + +static PredicatePtr Cmp(ExprPtr left, const ComparisonKind comparison, ExprPtr right) { + auto pred = Pred(PredicateKind::Comparison); + pred->left = std::move(left); + pred->comparison = comparison; + pred->right = std::move(right); + return pred; +} + +static PredicatePtr Like(ExprPtr left, ExprPtr pattern, const bool negated = false) { + auto pred = Pred(negated ? PredicateKind::NotLike : PredicateKind::Like); + pred->left = std::move(left); + pred->right = std::move(pattern); + return pred; +} + +static PredicatePtr In(ExprPtr left, std::vector values) { + auto pred = Pred(PredicateKind::In); + pred->left = std::move(left); + pred->values = std::move(values); + return pred; +} + +static PredicatePtr And(PredicatePtr lhs, PredicatePtr rhs) { + auto pred = Pred(PredicateKind::And); + pred->lhs = std::move(lhs); + pred->rhs = std::move(rhs); + return pred; +} + +static PredicatePtr All(const std::initializer_list predicates) { + auto it = predicates.begin(); + PredicatePtr result = *it++; + for (; it != predicates.end(); ++it) { + result = And(std::move(result), *it); + } + return result; +} + +static SelectItemSpec Select(ExprPtr expr, std::string alias = {}) { + SelectItemSpec item; + item.kind = SelectItemKind::GroupKey; + item.column = expr->kind == ExprKind::Column ? expr->column : ColumnRef{}; + item.expression = std::move(expr); + item.output_name = alias.empty() ? item.expression->output_name : std::move(alias); + return item; +} + +static SelectItemSpec Agg(std::string function, ExprPtr argument, const bool distinct = false, std::string alias = {}) { + AggSpec aggregate; + aggregate.function_name = std::move(function); + aggregate.distinct = distinct; + aggregate.argument_kind = argument->kind == ExprKind::Star ? AggArgumentKind::Star : AggArgumentKind::Column; + + const std::string argument_name = argument->output_name; + if (aggregate.argument_kind == AggArgumentKind::Column) { + aggregate.argument = std::move(argument); + if (aggregate.argument->kind == ExprKind::Column) { + aggregate.column = aggregate.argument->column; + } + } + + aggregate.output_name = aggregate.function_name + "("; + if (aggregate.argument_kind == AggArgumentKind::Star) { + aggregate.output_name += "*"; + } else { + if (aggregate.distinct) { + aggregate.output_name += "DISTINCT "; + } + aggregate.output_name += argument_name; + } + aggregate.output_name += ")"; + + SelectItemSpec item; + item.kind = SelectItemKind::Aggregate; + item.aggregate = std::move(aggregate); + item.output_name = alias.empty() ? item.aggregate.output_name : std::move(alias); + + return item; +} + +static OrderBySpec Order(SelectItemSpec item, const bool descending = false) { + return OrderBySpec{.item = std::move(item), .descending = descending}; +} + +static OrderBySpec OrderExpr(ExprPtr expr, const bool descending = false) { + return Order(Select(std::move(expr)), descending); +} + +static OrderBySpec OrderAgg(std::string function, ExprPtr argument, const bool descending = false) { + return Order(Agg(std::move(function), std::move(argument)), descending); +} + +static Query Q(std::vector select_items, std::vector group_by = {}, PredicatePtr filter = {}, + std::vector order_by = {}, const std::optional limit = {}, const size_t offset = 0, + PredicatePtr having = {}) { + Query query; + query.table_name = "hits"; + query.select_items = std::move(select_items); + query.group_by = std::move(group_by); + query.filter = std::move(filter); + query.having = std::move(having); + query.order_by = std::move(order_by); + query.limit = limit; + query.offset = offset; + return query; +} + +static std::optional BuildClickBenchQuery(const int id) { + switch (id) { + case 0: + return Q({Agg("COUNT", Star())}); + case 1: + return Q({Agg("COUNT", Star())}, {}, Cmp(Col("AdvEngineID"), ComparisonKind::NotEqual, Num("0"))); + case 2: + return Q({Agg("SUM", Col("AdvEngineID")), Agg("COUNT", Star()), Agg("AVG", Col("ResolutionWidth"))}); + case 3: + return Q({Agg("AVG", Col("UserID"))}); + case 4: + return Q({Agg("COUNT", Col("UserID"), true)}); + case 5: + return Q({Agg("COUNT", Col("SearchPhrase"), true)}); + case 6: + return Q({Agg("MIN", Col("EventDate")), Agg("MAX", Col("EventDate"))}); + case 7: + return Q({Select(Col("AdvEngineID")), Agg("COUNT", Star())}, {Col("AdvEngineID")}, + Cmp(Col("AdvEngineID"), ComparisonKind::NotEqual, Num("0")), {OrderAgg("COUNT", Star(), true)}); + case 8: + return Q({Select(Col("RegionID")), Agg("COUNT", Col("UserID"), true, "u")}, {Col("RegionID")}, {}, + {OrderExpr(Col("u"), true)}, 10); + case 9: + return Q({Select(Col("RegionID")), Agg("SUM", Col("AdvEngineID")), Agg("COUNT", Star(), false, "c"), + Agg("AVG", Col("ResolutionWidth")), Agg("COUNT", Col("UserID"), true)}, + {Col("RegionID")}, {}, {OrderExpr(Col("c"), true)}, 10); + case 10: + return Q({Select(Col("MobilePhoneModel")), Agg("COUNT", Col("UserID"), true, "u")}, + {Col("MobilePhoneModel")}, Cmp(Col("MobilePhoneModel"), ComparisonKind::NotEqual, Str("''")), + {OrderExpr(Col("u"), true)}, 10); + case 11: + return Q( + {Select(Col("MobilePhone")), Select(Col("MobilePhoneModel")), Agg("COUNT", Col("UserID"), true, "u")}, + {Col("MobilePhone"), Col("MobilePhoneModel")}, + Cmp(Col("MobilePhoneModel"), ComparisonKind::NotEqual, Str("''")), {OrderExpr(Col("u"), true)}, 10); + case 12: + return Q({Select(Col("SearchPhrase")), Agg("COUNT", Star(), false, "c")}, {Col("SearchPhrase")}, + Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), {OrderExpr(Col("c"), true)}, 10); + case 13: + return Q({Select(Col("SearchPhrase")), Agg("COUNT", Col("UserID"), true, "u")}, {Col("SearchPhrase")}, + Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), {OrderExpr(Col("u"), true)}, 10); + case 14: + return Q({Select(Col("SearchEngineID")), Select(Col("SearchPhrase")), Agg("COUNT", Star(), false, "c")}, + {Col("SearchEngineID"), Col("SearchPhrase")}, + Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), {OrderExpr(Col("c"), true)}, 10); + case 15: + return Q({Select(Col("UserID")), Agg("COUNT", Star())}, {Col("UserID")}, {}, + {OrderAgg("COUNT", Star(), true)}, 10); + case 16: + return Q({Select(Col("UserID")), Select(Col("SearchPhrase")), Agg("COUNT", Star())}, + {Col("UserID"), Col("SearchPhrase")}, {}, {OrderAgg("COUNT", Star(), true)}, 10); + case 17: + return Q({Select(Col("UserID")), Select(Col("SearchPhrase")), Agg("COUNT", Star())}, + {Col("UserID"), Col("SearchPhrase")}, {}, {}, 10); + case 18: + return Q({Select(Col("UserID")), Select(ExtractMinute(Col("EventTime")), "m"), Select(Col("SearchPhrase")), + Agg("COUNT", Star())}, + {Col("UserID"), Col("m"), Col("SearchPhrase")}, {}, {OrderAgg("COUNT", Star(), true)}, 10); + case 19: + return Q({Select(Col("UserID"))}, {}, Cmp(Col("UserID"), ComparisonKind::Equal, Num("435090932899640449"))); + case 20: + return Q({Agg("COUNT", Star())}, {}, Like(Col("URL"), Str("'%google%'"))); + case 21: + return Q({Select(Col("SearchPhrase")), Agg("MIN", Col("URL")), Agg("COUNT", Star(), false, "c")}, + {Col("SearchPhrase")}, + All({Like(Col("URL"), Str("'%google%'")), + Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''"))}), + {OrderExpr(Col("c"), true)}, 10); + case 22: + return Q({Select(Col("SearchPhrase")), Agg("MIN", Col("URL")), Agg("MIN", Col("Title")), + Agg("COUNT", Star(), false, "c"), Agg("COUNT", Col("UserID"), true)}, + {Col("SearchPhrase")}, + All({Like(Col("Title"), Str("'%Google%'")), Like(Col("URL"), Str("'%.google.%'"), true), + Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''"))}), + {OrderExpr(Col("c"), true)}, 10); + case 23: + return Q({Select(Star())}, {}, Like(Col("URL"), Str("'%google%'")), {OrderExpr(Col("EventTime"))}, 10); + case 24: + return Q({Select(Col("SearchPhrase"))}, {}, Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), + {OrderExpr(Col("EventTime"))}, 10); + case 25: + return Q({Select(Col("SearchPhrase"))}, {}, Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), + {OrderExpr(Col("SearchPhrase"))}, 10); + case 26: + return Q({Select(Col("SearchPhrase"))}, {}, Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), + {OrderExpr(Col("EventTime")), OrderExpr(Col("SearchPhrase"))}, 10); + case 27: + return Q({Select(Col("CounterID")), Agg("AVG", Func("STRLEN", {Col("URL")}), false, "l"), + Agg("COUNT", Star(), false, "c")}, + {Col("CounterID")}, Cmp(Col("URL"), ComparisonKind::NotEqual, Str("''")), + {OrderExpr(Col("l"), true)}, 25, 0, Cmp(Col("c"), ComparisonKind::Greater, Num("100000"))); + case 28: + return Q({Select(Func("REGEXP_REPLACE", + {Col("Referer"), Str(R"('^https?://(?:www\.)?([^/]+)/.*$')"), Str(R"('\1')")}, + "REGEXP_REPLACE(Referer, '^https?://(?:www\\.)?([^/]+)/.*$', '\\1')"), + "k"), + Agg("AVG", Func("STRLEN", {Col("Referer")}), false, "l"), Agg("COUNT", Star(), false, "c"), + Agg("MIN", Col("Referer"))}, + {Col("k")}, Cmp(Col("Referer"), ComparisonKind::NotEqual, Str("''")), {OrderExpr(Col("l"), true)}, + 25, 0, Cmp(Col("c"), ComparisonKind::Greater, Num("100000"))); + case 29: { + std::vector items; + items.push_back(Agg("SUM", Col("ResolutionWidth"))); + for (int offset = 1; offset <= 89; ++offset) { + items.push_back(Agg("SUM", Add(Col("ResolutionWidth"), Num(std::to_string(offset))))); + } + return Q(std::move(items)); + } + case 30: + return Q({Select(Col("SearchEngineID")), Select(Col("ClientIP")), Agg("COUNT", Star(), false, "c"), + Agg("SUM", Col("IsRefresh")), Agg("AVG", Col("ResolutionWidth"))}, + {Col("SearchEngineID"), Col("ClientIP")}, + Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), {OrderExpr(Col("c"), true)}, 10); + case 31: + return Q({Select(Col("WatchID")), Select(Col("ClientIP")), Agg("COUNT", Star(), false, "c"), + Agg("SUM", Col("IsRefresh")), Agg("AVG", Col("ResolutionWidth"))}, + {Col("WatchID"), Col("ClientIP")}, Cmp(Col("SearchPhrase"), ComparisonKind::NotEqual, Str("''")), + {OrderExpr(Col("c"), true)}, 10); + case 32: + return Q({Select(Col("WatchID")), Select(Col("ClientIP")), Agg("COUNT", Star(), false, "c"), + Agg("SUM", Col("IsRefresh")), Agg("AVG", Col("ResolutionWidth"))}, + {Col("WatchID"), Col("ClientIP")}, {}, {OrderExpr(Col("c"), true)}, 10); + case 33: + return Q({Select(Col("URL")), Agg("COUNT", Star(), false, "c")}, {Col("URL")}, {}, + {OrderExpr(Col("c"), true)}, 10); + case 34: + return Q({Select(Num("1")), Select(Col("URL")), Agg("COUNT", Star(), false, "c")}, {Num("1"), Col("URL")}, + {}, {OrderExpr(Col("c"), true)}, 10); + case 35: + return Q({Select(Col("ClientIP")), Select(Sub(Col("ClientIP"), Num("1"))), + Select(Sub(Col("ClientIP"), Num("2"))), Select(Sub(Col("ClientIP"), Num("3"))), + Agg("COUNT", Star(), false, "c")}, + {Col("ClientIP"), Sub(Col("ClientIP"), Num("1")), Sub(Col("ClientIP"), Num("2")), + Sub(Col("ClientIP"), Num("3"))}, + {}, {OrderExpr(Col("c"), true)}, 10); + case 36: + return Q({Select(Col("URL")), Agg("COUNT", Star(), false, "PageViews")}, {Col("URL")}, + All({Cmp(Col("CounterID"), ComparisonKind::Equal, Num("62")), + Cmp(Col("EventDate"), ComparisonKind::GreaterOrEqual, Str("'2013-07-01'")), + Cmp(Col("EventDate"), ComparisonKind::LessOrEqual, Str("'2013-07-31'")), + Cmp(Col("DontCountHits"), ComparisonKind::Equal, Num("0")), + Cmp(Col("IsRefresh"), ComparisonKind::Equal, Num("0")), + Cmp(Col("URL"), ComparisonKind::NotEqual, Str("''"))}), + {OrderExpr(Col("PageViews"), true)}, 10); + case 37: + return Q({Select(Col("Title")), Agg("COUNT", Star(), false, "PageViews")}, {Col("Title")}, + All({Cmp(Col("CounterID"), ComparisonKind::Equal, Num("62")), + Cmp(Col("EventDate"), ComparisonKind::GreaterOrEqual, Str("'2013-07-01'")), + Cmp(Col("EventDate"), ComparisonKind::LessOrEqual, Str("'2013-07-31'")), + Cmp(Col("DontCountHits"), ComparisonKind::Equal, Num("0")), + Cmp(Col("IsRefresh"), ComparisonKind::Equal, Num("0")), + Cmp(Col("Title"), ComparisonKind::NotEqual, Str("''"))}), + {OrderExpr(Col("PageViews"), true)}, 10); + case 38: + return Q({Select(Col("URL")), Agg("COUNT", Star(), false, "PageViews")}, {Col("URL")}, + All({Cmp(Col("CounterID"), ComparisonKind::Equal, Num("62")), + Cmp(Col("EventDate"), ComparisonKind::GreaterOrEqual, Str("'2013-07-01'")), + Cmp(Col("EventDate"), ComparisonKind::LessOrEqual, Str("'2013-07-31'")), + Cmp(Col("IsRefresh"), ComparisonKind::Equal, Num("0")), + Cmp(Col("IsLink"), ComparisonKind::NotEqual, Num("0")), + Cmp(Col("IsDownload"), ComparisonKind::Equal, Num("0"))}), + {OrderExpr(Col("PageViews"), true)}, 10, 1000); + case 39: + return Q({Select(Col("TraficSourceID")), Select(Col("SearchEngineID")), Select(Col("AdvEngineID")), + Select(Case(All({Cmp(Col("SearchEngineID"), ComparisonKind::Equal, Num("0")), + Cmp(Col("AdvEngineID"), ComparisonKind::Equal, Num("0"))}), + Col("Referer"), Str("''")), + "Src"), + Select(Col("URL"), "Dst"), Agg("COUNT", Star(), false, "PageViews")}, + {Col("TraficSourceID"), Col("SearchEngineID"), Col("AdvEngineID"), Col("Src"), Col("Dst")}, + All({Cmp(Col("CounterID"), ComparisonKind::Equal, Num("62")), + Cmp(Col("EventDate"), ComparisonKind::GreaterOrEqual, Str("'2013-07-01'")), + Cmp(Col("EventDate"), ComparisonKind::LessOrEqual, Str("'2013-07-31'")), + Cmp(Col("IsRefresh"), ComparisonKind::Equal, Num("0"))}), + {OrderExpr(Col("PageViews"), true)}, 10, 1000); + case 40: + return Q({Select(Col("URLHash")), Select(Col("EventDate")), Agg("COUNT", Star(), false, "PageViews")}, + {Col("URLHash"), Col("EventDate")}, + All({Cmp(Col("CounterID"), ComparisonKind::Equal, Num("62")), + Cmp(Col("EventDate"), ComparisonKind::GreaterOrEqual, Str("'2013-07-01'")), + Cmp(Col("EventDate"), ComparisonKind::LessOrEqual, Str("'2013-07-31'")), + Cmp(Col("IsRefresh"), ComparisonKind::Equal, Num("0")), + In(Col("TraficSourceID"), {Num("-1"), Num("6")}), + Cmp(Col("RefererHash"), ComparisonKind::Equal, Num("3594120000172545465"))}), + {OrderExpr(Col("PageViews"), true)}, 10, 100); + case 41: + return Q({Select(Col("WindowClientWidth")), Select(Col("WindowClientHeight")), + Agg("COUNT", Star(), false, "PageViews")}, + {Col("WindowClientWidth"), Col("WindowClientHeight")}, + All({Cmp(Col("CounterID"), ComparisonKind::Equal, Num("62")), + Cmp(Col("EventDate"), ComparisonKind::GreaterOrEqual, Str("'2013-07-01'")), + Cmp(Col("EventDate"), ComparisonKind::LessOrEqual, Str("'2013-07-31'")), + Cmp(Col("IsRefresh"), ComparisonKind::Equal, Num("0")), + Cmp(Col("DontCountHits"), ComparisonKind::Equal, Num("0")), + Cmp(Col("URLHash"), ComparisonKind::Equal, Num("2868770270353813622"))}), + {OrderExpr(Col("PageViews"), true)}, 10, 10000); + case 42: + return Q({Select(DateTruncMinute(Col("EventTime")), "M"), Agg("COUNT", Star(), false, "PageViews")}, + {Col("M")}, + All({Cmp(Col("CounterID"), ComparisonKind::Equal, Num("62")), + Cmp(Col("EventDate"), ComparisonKind::GreaterOrEqual, Str("'2013-07-14'")), + Cmp(Col("EventDate"), ComparisonKind::LessOrEqual, Str("'2013-07-15'")), + Cmp(Col("IsRefresh"), ComparisonKind::Equal, Num("0")), + Cmp(Col("DontCountHits"), ComparisonKind::Equal, Num("0"))}), + {OrderExpr(Col("M"))}, 10, 1000); + default: + return std::nullopt; + } +} + +bool HasHardcodedClickBenchQuery(const int query_id) { return query_id >= 0 && query_id <= 42; } + +Query BuildRequiredClickBenchQuery(const int query_id) { + auto query = BuildClickBenchQuery(query_id); + if (!query.has_value()) { + throw Error::InvalidArgument("benchmark", "unknown ClickBench query id"); + } + return std::move(*query); +} + +ExecuteExpected RunHardcodedClickBenchQuery(const Executor& executor, const int query_id) { + return executor.Execute(BuildRequiredClickBenchQuery(query_id)); +} diff --git a/benchmarks/support/query_runners.h b/benchmarks/support/query_runners.h index 0bfdd40..bf78315 100644 --- a/benchmarks/support/query_runners.h +++ b/benchmarks/support/query_runners.h @@ -1,272 +1,20 @@ #pragma once -#include -#include +#include +#include #include "executor/executor.h" +#include "executor/query.h" -using QueryRunner = std::function; +enum class QueryExecutionMode { + Hardcoded, + Parser, +}; -inline ExecuteExpected RunQuery0(const Executor& executor) { return executor.Execute("SELECT COUNT(*) FROM hits;"); } +namespace clickbench_fast { +std::optional TryRun(const std::filesystem::path& path, int query_id); +} // namespace clickbench_fast -inline ExecuteExpected RunQuery1(const Executor& executor) { - return executor.Execute("SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0;"); -} - -inline ExecuteExpected RunQuery2(const Executor& executor) { - return executor.Execute("SELECT SUM(AdvEngineID), COUNT(*), AVG(ResolutionWidth) FROM hits;"); -} - -inline ExecuteExpected RunQuery3(const Executor& executor) { return executor.Execute("SELECT AVG(UserID) FROM hits;"); } - -inline ExecuteExpected RunQuery4(const Executor& executor) { - return executor.Execute("SELECT COUNT(DISTINCT UserID) FROM hits;"); -} - -inline ExecuteExpected RunQuery5(const Executor& executor) { - return executor.Execute("SELECT COUNT(DISTINCT SearchPhrase) FROM hits;"); -} - -inline ExecuteExpected RunQuery6(const Executor& executor) { - return executor.Execute("SELECT MIN(EventDate), MAX(EventDate) FROM hits;"); -} - -inline ExecuteExpected RunQuery7(const Executor& executor) { - return executor.Execute( - "SELECT AdvEngineID, COUNT(*) FROM hits WHERE AdvEngineID <> 0 GROUP BY AdvEngineID ORDER BY COUNT(*) DESC;"); -} - -inline ExecuteExpected RunQuery8(const Executor& executor) { - return executor.Execute( - "SELECT RegionID, COUNT(DISTINCT UserID) AS u FROM hits GROUP BY RegionID ORDER BY u DESC " - "LIMIT 10;"); -} - -inline ExecuteExpected RunQuery9(const Executor& executor) { - return executor.Execute( - "SELECT RegionID, SUM(AdvEngineID), COUNT(*) AS c, AVG(ResolutionWidth), " - "COUNT(DISTINCT UserID) FROM hits GROUP BY RegionID ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery10(const Executor& executor) { - return executor.Execute( - "SELECT MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE " - "MobilePhoneModel <> '' GROUP BY MobilePhoneModel ORDER BY u DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery11(const Executor& executor) { - return executor.Execute( - "SELECT MobilePhone, MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE " - "MobilePhoneModel <> '' GROUP BY MobilePhone, MobilePhoneModel ORDER BY u DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery12(const Executor& executor) { - return executor.Execute( - "SELECT SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY " - "SearchPhrase ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery13(const Executor& executor) { - return executor.Execute( - "SELECT SearchPhrase, COUNT(DISTINCT UserID) AS u FROM hits WHERE SearchPhrase <> '' " - "GROUP BY SearchPhrase ORDER BY u DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery14(const Executor& executor) { - return executor.Execute( - "SELECT SearchEngineID, SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' " - "GROUP BY SearchEngineID, SearchPhrase ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery15(const Executor& executor) { - return executor.Execute("SELECT UserID, COUNT(*) FROM hits GROUP BY UserID ORDER BY COUNT(*) DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery16(const Executor& executor) { - return executor.Execute( - "SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase ORDER BY COUNT(*) DESC " - "LIMIT 10;"); -} - -inline ExecuteExpected RunQuery17(const Executor& executor) { - return executor.Execute("SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase LIMIT 10;"); -} - -inline ExecuteExpected RunQuery18(const Executor& executor) { - return executor.Execute( - "SELECT UserID, extract(minute FROM EventTime) AS m, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, m, " - "SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery19(const Executor& executor) { - return executor.Execute("SELECT UserID FROM hits WHERE UserID = 435090932899640449;"); -} - -inline ExecuteExpected RunQuery20(const Executor& executor) { - return executor.Execute("SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%';"); -} - -inline ExecuteExpected RunQuery21(const Executor& executor) { - return executor.Execute( - "SELECT SearchPhrase, MIN(URL), COUNT(*) AS c FROM hits WHERE URL LIKE '%google%' AND SearchPhrase <> '' " - "GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery22(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery23(const Executor& executor) { - return executor.Execute("SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime LIMIT 10;"); -} - -inline ExecuteExpected RunQuery24(const Executor& executor) { - return executor.Execute("SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT 10;"); -} - -inline ExecuteExpected RunQuery25(const Executor& executor) { - return executor.Execute("SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10;"); -} - -inline ExecuteExpected RunQuery26(const Executor& executor) { - return executor.Execute( - "SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10;"); -} - -inline ExecuteExpected RunQuery27(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery28(const Executor& executor) { - return executor.Execute( - R"(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;)"); -} - -inline ExecuteExpected RunQuery29(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery30(const Executor& executor) { - return executor.Execute( - "SELECT SearchEngineID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE " - "SearchPhrase <> '' GROUP BY SearchEngineID, ClientIP ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery31(const Executor& executor) { - return executor.Execute( - "SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> " - "'' GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery32(const Executor& executor) { - return executor.Execute( - "SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits GROUP BY WatchID, " - "ClientIP ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery33(const Executor& executor) { - return executor.Execute("SELECT URL, COUNT(*) AS c FROM hits GROUP BY URL ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery34(const Executor& executor) { - return executor.Execute("SELECT 1, URL, COUNT(*) AS c FROM hits GROUP BY 1, URL ORDER BY c DESC LIMIT 10;"); -} - -inline ExecuteExpected RunQuery35(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery36(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery37(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery38(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery39(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery40(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery41(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline ExecuteExpected RunQuery42(const Executor& executor) { - return executor.Execute( - "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;"); -} - -inline const std::map& GetQueryRunners() { - static const std::map Runners = { - {0, RunQuery0}, {1, RunQuery1}, {2, RunQuery2}, {3, RunQuery3}, {4, RunQuery4}, {5, RunQuery5}, - {6, RunQuery6}, {7, RunQuery7}, {8, RunQuery8}, {9, RunQuery9}, {10, RunQuery10}, {11, RunQuery11}, - {12, RunQuery12}, {13, RunQuery13}, {14, RunQuery14}, {15, RunQuery15}, {16, RunQuery16}, {17, RunQuery17}, - {18, RunQuery18}, {19, RunQuery19}, {20, RunQuery20}, {21, RunQuery21}, {22, RunQuery22}, {23, RunQuery23}, - {24, RunQuery24}, {25, RunQuery25}, {26, RunQuery26}, {27, RunQuery27}, {28, RunQuery28}, {29, RunQuery29}, - {30, RunQuery30}, {31, RunQuery31}, {32, RunQuery32}, {33, RunQuery33}, {34, RunQuery34}, {35, RunQuery35}, - {36, RunQuery36}, {37, RunQuery37}, {38, RunQuery38}, {39, RunQuery39}, {40, RunQuery40}, {41, RunQuery41}, - {42, RunQuery42}, - }; - return Runners; -} +bool HasHardcodedClickBenchQuery(int query_id); +Query BuildRequiredClickBenchQuery(int query_id); +ExecuteExpected RunHardcodedClickBenchQuery(const Executor& executor, int query_id); diff --git a/benchmarks/support/query_runners_fast.cpp b/benchmarks/support/query_runners_fast.cpp new file mode 100644 index 0000000..866252c --- /dev/null +++ b/benchmarks/support/query_runners_fast.cpp @@ -0,0 +1,721 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/parsing.h" +#include "common/string_arena.h" +#include "executor/operators_internal.h" +#include "executor/query_utils.h" +#include "io/columnar_batch.h" +#include "query_runners.h" + +namespace detail { + +constexpr uint64_t hash_magic = 0x9e3779b97f4a7c15ULL; + +struct Int128Hash { + size_t operator()(const Int128 value) const noexcept { + const UInt128 raw = static_cast(value); + const uint64_t low = static_cast(raw); + const uint64_t high = static_cast(raw >> 64); + return std::hash{}(low ^ (high + hash_magic + (low << 6) + (low >> 2))); + } +}; + +struct Int128PairHash { + size_t operator()(const std::pair& value) const noexcept { + const size_t lhs = Int128Hash{}(value.first); + const size_t rhs = Int128Hash{}(value.second); + return lhs ^ (rhs + hash_magic + (lhs << 6) + (lhs >> 2)); + } +}; + +struct SearchPhraseGroup { + std::string_view key; + std::string_view min_url; + + uint64_t count = 0; + size_t ordinal = 0; +}; + +struct SearchPhraseStats { + std::string_view key; + std::string_view min_url; + std::string_view min_title; + + std::unordered_set distinct_users; + + uint64_t count = 0; + size_t ordinal = 0; +}; + +struct IntGroupStats { + Int128 key1 = 0; + Int128 key2 = 0; + + uint64_t count = 0; + Int128 sum_refresh = 0; + Int128 sum_width = 0; + + size_t ordinal = 0; +}; + +struct SearchPhraseOrderRow { + Int128 event_time = 0; + std::string_view search_phrase; + + size_t ordinal = 0; +}; + +} // namespace detail + +using detail::Int128Hash; +using detail::Int128PairHash; +using detail::IntGroupStats; +using detail::SearchPhraseGroup; +using detail::SearchPhraseOrderRow; +using detail::SearchPhraseStats; + +static Batch SingleRow(const Schema& schema, const std::initializer_list values) { + Batch batch(schema, 1); + size_t column = 0; + + for (const std::string_view value : values) { + batch.AppendValueFromString(column++, value); + } + + return batch; +} + +static std::unique_ptr ScanColumns(const std::filesystem::path& path, const Schema& schema, + const std::initializer_list columns) { + std::vector projection_indexes; + projection_indexes.reserve(columns.size()); + + for (const std::string_view column : columns) { + projection_indexes.push_back(FindColumnIndex(schema, column)); + } + + return CreateScanOperator(path, std::move(projection_indexes), {}); +} + +static bool Contains(const std::string_view haystack, const std::string_view needle) { + return haystack.find(needle) != std::string_view::npos; +} + +template +static std::vector OrderGroupsByCount(const Map& groups, const size_t limit) { + std::vector ordered; + ordered.reserve(groups.size()); + + for (const auto& entry : groups) { + ordered.push_back(&entry.second); + } + + std::ranges::sort(ordered, [](const auto* lhs, const auto* rhs) { + if (lhs->count != rhs->count) { + return lhs->count > rhs->count; + } + return lhs->ordinal < rhs->ordinal; + }); + + if (ordered.size() > limit) { + ordered.resize(limit); + } + + return ordered; +} + +static ExecuteExpected CountAll(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + uint64_t rows = 0; + + for (const auto& row_group : reader.GetMetadata().row_groups) { + rows += row_group.row_count; + } + + return SingleRow(Schema{{ColumnSchema("COUNT(*)", ColumnType::Int64)}}, {std::to_string(rows)}); +} + +static ExecuteExpected CountAdvEngineNonZero(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"AdvEngineID"}); + + uint64_t count = 0; + + while (const auto batch = scan->Next()) { + const Column& adv_engine = batch->ColumnAt(0); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + if (adv_engine.ValueAsInt128(row) != 0) { + ++count; + } + } + } + + return SingleRow(Schema{{ColumnSchema("COUNT(*)", ColumnType::Int64)}}, {std::to_string(count)}); +} + +static ExecuteExpected SumCountAvg(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"AdvEngineID", "ResolutionWidth"}); + + Int128 adv_sum = 0; + Int128 width_sum = 0; + uint64_t count = 0; + + while (const auto batch = scan->Next()) { + const Column& adv_engine = batch->ColumnAt(0); + const Column& width = batch->ColumnAt(1); + count += batch->RowsCount(); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + adv_sum += adv_engine.ValueAsInt128(row); + width_sum += width.ValueAsInt128(row); + } + } + + return SingleRow( + Schema{{ColumnSchema("SUM(AdvEngineID)", ColumnType::Int128), ColumnSchema("COUNT(*)", ColumnType::Int64), + ColumnSchema("AVG(ResolutionWidth)", ColumnType::Int128)}}, + {Int128ToString(adv_sum), std::to_string(count), + count == 0 ? "0" : Int128ToString(width_sum / static_cast(count))}); +} + +static ExecuteExpected AvgUserId(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"UserID"}); + + Int128 sum = 0; + uint64_t count = 0; + + while (const auto batch = scan->Next()) { + const Column& user_id = batch->ColumnAt(0); + count += batch->RowsCount(); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + sum += user_id.ValueAsInt128(row); + } + } + + return SingleRow(Schema{{ColumnSchema("AVG(UserID)", ColumnType::Int128)}}, + {count == 0 ? "0" : Int128ToString(sum / static_cast(count))}); +} + +static ExecuteExpected CountDistinctColumn(const std::filesystem::path& path, const std::string_view column_name, + const std::string_view output_name) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {column_name}); + + std::unordered_set values; + + while (const auto batch = scan->Next()) { + const Column& column = batch->ColumnAt(0); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + values.insert(column.ValueAsString(row)); + } + } + + return SingleRow(Schema{{ColumnSchema(std::string(output_name), ColumnType::Int64)}}, + {std::to_string(values.size())}); +} + +static ExecuteExpected MinMaxEventDate(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const size_t event_date = FindColumnIndex(reader.GetSchema(), "EventDate"); + + bool has_value = false; + Int128 min_value = 0; + Int128 max_value = 0; + + for (const auto& row_group : reader.GetMetadata().row_groups) { + const auto& chunk = row_group.columns[event_date]; + if (!chunk.has_min_max) { + continue; + } + if (!has_value) { + min_value = chunk.min_value; + max_value = chunk.max_value; + has_value = true; + } else { + min_value = std::min(min_value, chunk.min_value); + max_value = std::max(max_value, chunk.max_value); + } + } + + return SingleRow( + Schema{{ColumnSchema("MIN(EventDate)", ColumnType::Date), ColumnSchema("MAX(EventDate)", ColumnType::Date)}}, + {DateToString(static_cast(min_value)), DateToString(static_cast(max_value))}); +} + +static ExecuteExpected CountUrlContainsGoogle(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"URL"}); + + uint64_t count = 0; + + while (const auto batch = scan->Next()) { + const Column& url = batch->ColumnAt(0); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + if (Contains(url.ValueAsString(row), "google")) { + ++count; + } + } + } + + return SingleRow(Schema{{ColumnSchema("COUNT(*)", ColumnType::Int64)}}, {std::to_string(count)}); +} + +static ExecuteExpected SearchPhraseGroupByUrlContainsGoogle(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"URL", "SearchPhrase"}); + + StringArena arena; + std::unordered_map groups; + size_t ordinal = 0; + + while (const auto batch = scan->Next()) { + const Column& url = batch->ColumnAt(0); + const Column& search_phrase = batch->ColumnAt(1); + + for (size_t row = 0; row < batch->RowsCount(); ++row) { + if (!Contains(url.ValueAsString(row), "google")) { + continue; + } + + const std::string_view key = arena.Store(search_phrase.ValueAsString(row)); + if (key.empty()) { + continue; + } + + auto& group = groups[key]; + if (group.count == 0) { + group.key = key; + group.ordinal = ordinal++; + group.min_url = arena.Store(url.ValueAsString(row)); + } else if (url.ValueAsString(row) < group.min_url) { + group.min_url = arena.Store(url.ValueAsString(row)); + } + + ++group.count; + } + } + + const auto ordered = OrderGroupsByCount(groups, 10); + Batch result(Schema{{ColumnSchema("SearchPhrase", ColumnType::String), ColumnSchema("MIN(URL)", ColumnType::String), + ColumnSchema("c", ColumnType::Int64)}}, + ordered.size()); + + for (const SearchPhraseGroup* group : ordered) { + result.AppendValueFromString(0, group->key); + result.AppendValueFromString(1, group->min_url); + result.AppendValueFromString(2, std::to_string(group->count)); + } + + return result; +} + +static ExecuteExpected SearchPhraseGroupByTitleGoogle(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"Title", "URL", "SearchPhrase", "UserID"}); + + StringArena arena; + std::unordered_map groups; + size_t ordinal = 0; + + while (const auto batch = scan->Next()) { + const Column& title = batch->ColumnAt(0); + const Column& url = batch->ColumnAt(1); + const Column& search_phrase = batch->ColumnAt(2); + const Column& user_id = batch->ColumnAt(3); + + for (size_t row = 0; row < batch->RowsCount(); ++row) { + const std::string_view title_value = arena.Store(title.ValueAsString(row)); + if (!Contains(title_value, "Google")) { + continue; + } + + const std::string_view url_value = arena.Store(url.ValueAsString(row)); + if (Contains(url_value, ".google.")) { + continue; + } + + const std::string_view key = arena.Store(search_phrase.ValueAsString(row)); + if (key.empty()) { + continue; + } + + auto& group = groups[key]; + if (group.count == 0) { + group.key = key; + group.ordinal = ordinal++; + group.min_url = url_value; + group.min_title = title_value; + } else { + if (url_value < group.min_url) { + group.min_url = url_value; + } + if (title_value < group.min_title) { + group.min_title = title_value; + } + } + + group.distinct_users.insert(user_id.ValueAsInt128(row)); + ++group.count; + } + } + + const auto ordered = OrderGroupsByCount(groups, 10); + Batch result(Schema{{ColumnSchema("SearchPhrase", ColumnType::String), ColumnSchema("MIN(URL)", ColumnType::String), + ColumnSchema("MIN(Title)", ColumnType::String), ColumnSchema("c", ColumnType::Int64), + ColumnSchema("COUNT(DISTINCT UserID)", ColumnType::Int64)}}, + ordered.size()); + + for (const SearchPhraseStats* group : ordered) { + result.AppendValueFromString(0, group->key); + result.AppendValueFromString(1, group->min_url); + result.AppendValueFromString(2, group->min_title); + result.AppendValueFromString(3, std::to_string(group->count)); + result.AppendValueFromString(4, std::to_string(group->distinct_users.size())); + } + + return result; +} + +static ExecuteExpected SumWidthVariants(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"ResolutionWidth"}); + + Int128 width_sum = 0; + uint64_t row_count = 0; + + while (const auto batch = scan->Next()) { + const Column& width = batch->ColumnAt(0); + row_count += batch->RowsCount(); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + width_sum += width.ValueAsInt128(row); + } + } + + std::vector schema_columns; + std::vector values; + + schema_columns.emplace_back("SUM(ResolutionWidth)", ColumnType::Int128); + values.push_back(Int128ToString(width_sum)); + + for (int offset = 1; offset <= 89; ++offset) { + schema_columns.emplace_back("SUM(ResolutionWidth + " + std::to_string(offset) + ")", ColumnType::Int128); + values.push_back(Int128ToString(width_sum + static_cast(offset) * static_cast(row_count))); + } + + Batch result(Schema{schema_columns}, 1); + for (size_t i = 0; i < values.size(); ++i) { + result.AppendValueFromString(i, values[i]); + } + + return result; +} + +static ExecuteExpected GroupByIntPairWithMetrics(const std::filesystem::path& path, const std::string_view key1_name, + const std::string_view key2_name, const bool filter_search_phrase) { + const ColumnarBatchReader reader(path); + const auto scan = + ScanColumns(path, reader.GetSchema(), {key1_name, key2_name, "IsRefresh", "ResolutionWidth", "SearchPhrase"}); + + std::unordered_map, IntGroupStats, Int128PairHash> groups; + size_t ordinal = 0; + + while (const auto batch = scan->Next()) { + const Column& key1 = batch->ColumnAt(0); + const Column& key2 = batch->ColumnAt(1); + const Column& is_refresh = batch->ColumnAt(2); + const Column& width = batch->ColumnAt(3); + const Column& search_phrase = batch->ColumnAt(4); + + for (size_t row = 0; row < batch->RowsCount(); ++row) { + if (filter_search_phrase && search_phrase.ValueAsString(row).empty()) { + continue; + } + + const std::pair key{key1.ValueAsInt128(row), key2.ValueAsInt128(row)}; + + auto& group = groups[key]; + if (group.count == 0) { + group.key1 = key.first; + group.key2 = key.second; + group.ordinal = ordinal++; + } + + ++group.count; + group.sum_refresh += is_refresh.ValueAsInt128(row); + group.sum_width += width.ValueAsInt128(row); + } + } + + const auto ordered = OrderGroupsByCount(groups, 10); + Batch result(Schema{{ColumnSchema(std::string(key1_name), ColumnType::Int128), + ColumnSchema(std::string(key2_name), ColumnType::Int128), ColumnSchema("c", ColumnType::Int64), + ColumnSchema("SUM(IsRefresh)", ColumnType::Int128), + ColumnSchema("AVG(ResolutionWidth)", ColumnType::Int128)}}, + ordered.size()); + + for (const IntGroupStats* group : ordered) { + result.AppendValueFromString(0, Int128ToString(group->key1)); + result.AppendValueFromString(1, Int128ToString(group->key2)); + result.AppendValueFromString(2, std::to_string(group->count)); + result.AppendValueFromString(3, Int128ToString(group->sum_refresh)); + result.AppendValueFromString( + 4, group->count == 0 ? "0" : Int128ToString(group->sum_width / static_cast(group->count))); + } + + return result; +} + +static ExecuteExpected GroupByURLCount(const std::filesystem::path& path, const bool add_constant_column) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"URL"}); + + StringArena arena; + std::unordered_map groups; + size_t ordinal = 0; + + while (const auto batch = scan->Next()) { + const Column& url = batch->ColumnAt(0); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + const std::string_view key = arena.Store(url.ValueAsString(row)); + auto& group = groups[key]; + if (group.count == 0) { + group.key = key; + group.ordinal = ordinal++; + } + ++group.count; + } + } + + const auto ordered = OrderGroupsByCount(groups, 10); + Batch result(add_constant_column + ? Schema{{ColumnSchema("1", ColumnType::Int64), ColumnSchema("URL", ColumnType::String), + ColumnSchema("COUNT(*)", ColumnType::Int64)}} + : Schema{{ColumnSchema("URL", ColumnType::String), ColumnSchema("COUNT(*)", ColumnType::Int64)}}, + ordered.size()); + + for (const SearchPhraseStats* group : ordered) { + if (add_constant_column) { + result.AppendValueFromString(0, "1"); + result.AppendValueFromString(1, group->key); + result.AppendValueFromString(2, std::to_string(group->count)); + } else { + result.AppendValueFromString(0, group->key); + result.AppendValueFromString(1, std::to_string(group->count)); + } + } + + return result; +} + +static ExecuteExpected GroupByClientIPDerived(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"ClientIP"}); + + std::unordered_map groups; + size_t ordinal = 0; + + while (const auto batch = scan->Next()) { + const Column& client_ip = batch->ColumnAt(0); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + const Int128 key = client_ip.ValueAsInt128(row); + auto& group = groups[key]; + if (group.count == 0) { + group.key1 = key; + group.ordinal = ordinal++; + } + ++group.count; + } + } + + const auto ordered = OrderGroupsByCount(groups, 10); + Batch result( + Schema{{ColumnSchema("ClientIP", ColumnType::Int128), ColumnSchema("ClientIP - 1", ColumnType::Int128), + ColumnSchema("ClientIP - 2", ColumnType::Int128), ColumnSchema("ClientIP - 3", ColumnType::Int128), + ColumnSchema("COUNT(*)", ColumnType::Int64)}}, + ordered.size()); + + for (const IntGroupStats* group : ordered) { + const Int128 key = group->key1; + result.AppendValueFromString(0, Int128ToString(key)); + result.AppendValueFromString(1, Int128ToString(key - 1)); + result.AppendValueFromString(2, Int128ToString(key - 2)); + result.AppendValueFromString(3, Int128ToString(key - 3)); + result.AppendValueFromString(4, std::to_string(group->count)); + } + + return result; +} + +static std::vector CollectSearchPhraseOrderRows(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"EventTime", "SearchPhrase"}); + + StringArena arena; + std::vector rows; + size_t ordinal = 0; + + while (const auto batch = scan->Next()) { + const Column& event_time = batch->ColumnAt(0); + const Column& search_phrase = batch->ColumnAt(1); + + for (size_t row = 0; row < batch->RowsCount(); ++row) { + const std::string_view phrase = arena.Store(search_phrase.ValueAsString(row)); + if (phrase.empty()) { + continue; + } + rows.push_back(SearchPhraseOrderRow{ + .event_time = event_time.ValueAsInt128(row), .search_phrase = phrase, .ordinal = ordinal++}); + } + } + + return rows; +} + +static ExecuteExpected SelectSearchPhraseOrderByEventTime(const std::filesystem::path& path) { + auto rows = CollectSearchPhraseOrderRows(path); + + std::ranges::sort(rows, [](const SearchPhraseOrderRow& lhs, const SearchPhraseOrderRow& rhs) { + if (lhs.event_time != rhs.event_time) { + return lhs.event_time < rhs.event_time; + } + return lhs.ordinal < rhs.ordinal; + }); + + if (rows.size() > 10) { + rows.resize(10); + } + + Batch result( + Schema{{ColumnSchema("SearchPhrase", ColumnType::String), ColumnSchema("EventTime", ColumnType::Timestamp)}}, + rows.size()); + + for (const auto& row : rows) { + result.AppendValueFromString(0, row.search_phrase); + result.AppendValueFromString(1, TimestampToString(static_cast(row.event_time))); + } + + return result; +} + +static ExecuteExpected SelectSearchPhraseOrderByPhrase(const std::filesystem::path& path) { + const ColumnarBatchReader reader(path); + const auto scan = ScanColumns(path, reader.GetSchema(), {"SearchPhrase"}); + + StringArena arena; + std::vector rows; + + while (const auto batch = scan->Next()) { + const Column& search_phrase = batch->ColumnAt(0); + for (size_t row = 0; row < batch->RowsCount(); ++row) { + const std::string_view phrase = arena.Store(search_phrase.ValueAsString(row)); + if (phrase.empty()) { + continue; + } + rows.push_back(phrase); + } + } + + std::ranges::sort(rows); + + if (rows.size() > 10) { + rows.resize(10); + } + + Batch result(Schema{{ColumnSchema("SearchPhrase", ColumnType::String)}}, rows.size()); + for (const std::string_view row : rows) { + result.AppendValueFromString(0, row); + } + + return result; +} + +static ExecuteExpected SelectSearchPhraseOrderByEventTimeThenPhrase(const std::filesystem::path& path) { + auto rows = CollectSearchPhraseOrderRows(path); + + std::ranges::sort(rows, [](const SearchPhraseOrderRow& lhs, const SearchPhraseOrderRow& rhs) { + if (lhs.event_time != rhs.event_time) { + return lhs.event_time < rhs.event_time; + } + if (lhs.search_phrase != rhs.search_phrase) { + return lhs.search_phrase < rhs.search_phrase; + } + return lhs.ordinal < rhs.ordinal; + }); + + if (rows.size() > 10) { + rows.resize(10); + } + + Batch result( + Schema{{ColumnSchema("SearchPhrase", ColumnType::String), ColumnSchema("EventTime", ColumnType::Timestamp)}}, + rows.size()); + + for (const auto& row : rows) { + result.AppendValueFromString(0, row.search_phrase); + result.AppendValueFromString(1, TimestampToString(static_cast(row.event_time))); + } + + return result; +} + +namespace clickbench_fast { + +std::optional TryRun(const std::filesystem::path& path, const int query_id) { + switch (query_id) { + case 0: + return CountAll(path); + case 1: + return CountAdvEngineNonZero(path); + case 2: + return SumCountAvg(path); + case 3: + return AvgUserId(path); + case 4: + return CountDistinctColumn(path, "UserID", "COUNT(DISTINCT UserID)"); + case 5: + return CountDistinctColumn(path, "SearchPhrase", "COUNT(DISTINCT SearchPhrase)"); + case 6: + return MinMaxEventDate(path); + case 20: + return CountUrlContainsGoogle(path); + case 21: + return SearchPhraseGroupByUrlContainsGoogle(path); + case 22: + return SearchPhraseGroupByTitleGoogle(path); + case 24: + return SelectSearchPhraseOrderByEventTime(path); + case 25: + return SelectSearchPhraseOrderByPhrase(path); + case 26: + return SelectSearchPhraseOrderByEventTimeThenPhrase(path); + case 29: + return SumWidthVariants(path); + case 30: + return GroupByIntPairWithMetrics(path, "SearchEngineID", "ClientIP", true); + case 31: + return GroupByIntPairWithMetrics(path, "WatchID", "ClientIP", true); + case 32: + return GroupByIntPairWithMetrics(path, "WatchID", "ClientIP", false); + case 33: + return GroupByURLCount(path, false); + case 34: + return GroupByURLCount(path, true); + case 35: + return GroupByClientIPDerived(path); + default: + return std::nullopt; + } +} + +} // namespace clickbench_fast diff --git a/include/executor/aggregate_function.h b/include/executor/aggregate_function.h index aeecdad..7d90a8e 100644 --- a/include/executor/aggregate_function.h +++ b/include/executor/aggregate_function.h @@ -30,6 +30,7 @@ constexpr bool HasAggCallFeature(const AggCallFeature features, const AggCallFea struct AggFuncDefinition { std::string_view canonical_name; + AggCallFeature call_features = AggCallFeature::None; AggTypeSupport supports_type = nullptr; AggFactory factory = nullptr; diff --git a/include/executor/executor.h b/include/executor/executor.h index 0101547..ccfdd55 100644 --- a/include/executor/executor.h +++ b/include/executor/executor.h @@ -6,9 +6,12 @@ #include #include "common/error.h" +#include "executor/query_plan.h" #include "model/batch.h" #include "tl/expected.hpp" +struct Query; + using ExecuteExpected = tl::expected; class Executor { @@ -18,9 +21,16 @@ class Executor { void RegisterTable(const std::string& name, std::filesystem::path path); void SetUnsupportedFallbackEnabled(bool enabled); + PlannedQuery Plan(const Query& query) const; ExecuteExpected Execute(std::string_view query) const; + ExecuteExpected Execute(const Query& query) const; + ExecuteExpected Execute(const PlannedQuery& planned) const; private: + ExecuteExpected ExecutePlanned(const Query& query) const; + static ExecuteExpected ExecutePlanned(const PlannedQuery& planned); + std::unordered_map tables_; + bool unsupported_fallback_enabled_ = false; }; diff --git a/include/io/file.h b/include/io/file.h index 7ab909d..9c4f70a 100644 --- a/include/io/file.h +++ b/include/io/file.h @@ -10,9 +10,11 @@ struct FileMetadata { uintmax_t size = 0; + + std::filesystem::file_time_type last_write_time; + bool is_regular = false; bool is_directory = false; - std::filesystem::file_time_type last_write_time; }; std::optional GetFileMetadata(const std::filesystem::path& path); @@ -45,6 +47,7 @@ class InputFile { private: std::filesystem::path path_; + std::ifstream in_; }; diff --git a/include/model/batch.h b/include/model/batch.h index 43d6dea..606dfae 100644 --- a/include/model/batch.h +++ b/include/model/batch.h @@ -42,5 +42,6 @@ class Batch { private: Schema schema_; + std::vector> columns_; }; diff --git a/include/sql_parser/tokenizer.h b/include/sql_parser/tokenizer.h index 3743bce..f8ff441 100644 --- a/include/sql_parser/tokenizer.h +++ b/include/sql_parser/tokenizer.h @@ -78,6 +78,7 @@ class Token { private: std::string text_; + size_t offset_ = 0; }; @@ -161,6 +162,7 @@ class Tokenizer { private: std::string query_; + size_t pos_ = 0; }; diff --git a/script/build.sh b/script/build.sh index 7b3e976..d343c43 100755 --- a/script/build.sh +++ b/script/build.sh @@ -3,13 +3,15 @@ set -euo pipefail build_dir="${BUILD_DIR:-build}" build_type="${BUILD_TYPE:-Release}" +enable_benchmarks="${ENABLE_BENCHMARKS:-ON}" +enable_tests="${ENABLE_TESTS:-OFF}" cmake_args=( -S . -B "${build_dir}" -DCMAKE_BUILD_TYPE="${build_type}" - -DENABLE_TESTS=OFF - -DENABLE_BENCHMARKS=OFF + -DENABLE_TESTS="${enable_tests}" + -DENABLE_BENCHMARKS="${enable_benchmarks}" ) if command -v ninja >/dev/null 2>&1; then @@ -18,7 +20,12 @@ fi cmake "${cmake_args[@]}" -cmake --build "${build_dir}" --target columnar_engine +build_targets=(columnar_engine) +if [[ "${enable_benchmarks}" == "ON" ]]; then + build_targets+=(benchmark_queries) +fi + +cmake --build "${build_dir}" --target "${build_targets[@]}" if [[ -x "${build_dir}/src/columnar_engine" ]]; then ln -sf "src/columnar_engine" "${build_dir}/columnar_engine" diff --git a/script/convert.sh b/script/convert.sh index d6bc604..b1498c3 100755 --- a/script/convert.sh +++ b/script/convert.sh @@ -11,7 +11,8 @@ columnar_path="$2" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_dir="$(cd "${script_dir}/.." && pwd)" build_dir="${BUILD_DIR:-build}" -row_group_size="${ROW_GROUP_SIZE:-50000}" +row_group_size="${ROW_GROUP_SIZE:-16384}" +compression="${COLUMNAR_COMPRESSION:-lz4}" schema_path="${COLUMNAR_SCHEMA:-${repo_dir}/benchmarks/scheme.csv}" binary="${repo_dir}/${build_dir}/columnar_engine" @@ -35,4 +36,5 @@ mkdir -p "$(dirname "${columnar_path}")" --schema "${schema_path}" \ --input "${input_csv}" \ --output "${columnar_path}" \ - --row-group-size "${row_group_size}" + --row-group-size "${row_group_size}" \ + --compression "${compression}" diff --git a/script/run_query.sh b/script/run_query.sh index 6fe4cdf..d28ac53 100755 --- a/script/run_query.sh +++ b/script/run_query.sh @@ -13,7 +13,9 @@ log_path="$4" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_dir="$(cd "${script_dir}/.." && pwd)" build_dir="${BUILD_DIR:-build}" +query_mode="${QUERY_MODE:-hardcoded}" binary="${repo_dir}/${build_dir}/columnar_engine" +benchmark_binary="${repo_dir}/${build_dir}/benchmarks/benchmark_queries" query_file="${repo_dir}/benchmarks/queries/query_${query_num}.sql" mkdir -p "$(dirname "${output_csv}")" @@ -32,6 +34,36 @@ EOF return 0 2>/dev/null fi +if [[ "${query_mode}" == "hardcoded" ]]; then + if [[ ! -x "${benchmark_binary}" ]]; then + cat > "${log_path}" < "${log_path}" 2>&1; then + printf 'query_num=%s\nstatus=ok\nmode=hardcoded\ncolumnar_path=%s\nquery_file=%s\n' \ + "${query_num}" "${columnar_path}" "${query_file}" >> "${log_path}" + else + status=$? + printf 'query_num=%s\nstatus=failed\nmode=hardcoded\ncolumnar_path=%s\nquery_file=%s\nexit_code=%s\n' \ + "${query_num}" "${columnar_path}" "${query_file}" "${status}" >> "${log_path}" + exit "${status}" + fi + exit 0 +fi + if [[ ! -x "${binary}" ]]; then binary="${repo_dir}/${build_dir}/src/columnar_engine" fi @@ -52,11 +84,11 @@ if "${binary}" run-query \ --output "${output_csv}" \ --table-name hits \ --query-file "${query_file}" > "${log_path}" 2>&1; then - printf 'query_num=%s\nstatus=ok\ncolumnar_path=%s\nquery_file=%s\n' \ + printf 'query_num=%s\nstatus=ok\nmode=parser\ncolumnar_path=%s\nquery_file=%s\n' \ "${query_num}" "${columnar_path}" "${query_file}" >> "${log_path}" else status=$? - printf 'query_num=%s\nstatus=failed\ncolumnar_path=%s\nquery_file=%s\nexit_code=%s\n' \ + printf 'query_num=%s\nstatus=failed\nmode=parser\ncolumnar_path=%s\nquery_file=%s\nexit_code=%s\n' \ "${query_num}" "${columnar_path}" "${query_file}" "${status}" >> "${log_path}" exit "${status}" fi diff --git a/scripts/dashboard.py b/scripts/dashboard.py index 01c4254..94efe57 100644 --- a/scripts/dashboard.py +++ b/scripts/dashboard.py @@ -6,7 +6,12 @@ import csv import datetime as dt import math +import os import pathlib +import platform +import re +import shlex +import shutil import statistics import subprocess import sys @@ -15,6 +20,7 @@ README_START = "" README_END = "" +QUERY_LINE_RE = re.compile(r"^query_(\d+): rows=(\d+), time=(\d+)ms, status=([a-z_]+)") def parse_args() -> argparse.Namespace: @@ -41,6 +47,23 @@ def parse_args() -> argparse.Namespace: default=pathlib.Path("build/src/columnar_engine"), help="Path to the columnar_engine binary.", ) + parser.add_argument( + "--benchmark-binary", + type=pathlib.Path, + default=pathlib.Path("build/benchmarks/benchmark_queries"), + help="Path to the benchmark_queries binary used by --query-mode hardcoded.", + ) + parser.add_argument( + "--query-mode", + default="parser", + choices=("parser", "hardcoded"), + help="Run queries through columnar_engine run-query or benchmark_queries hardcoded plans.", + ) + parser.add_argument( + "--lz4-hardcoded", + action="store_true", + help="Shortcut: convert --source-csv to a temporary lz4 columnar file and run hardcoded ClickBench queries.", + ) parser.add_argument( "--csv-out", type=pathlib.Path, @@ -82,6 +105,19 @@ def parse_args() -> argparse.Namespace: choices=("none", "lz4"), help="Compression codec for the CSV -> columnar conversion benchmark.", ) + parser.add_argument( + "--cache-drop-mode", + default=None, + choices=("none", "once", "per-query"), + help=( + "Drop the filesystem page cache before benchmarks: never, once before all queries, " + "or before the first run of each query. Defaults to per-query." + ), + ) + parser.add_argument( + "--cache-drop-command", + help="Override the OS-specific cache drop command, for example: 'sudo purge'.", + ) return parser.parse_args() @@ -93,10 +129,28 @@ def resolve_path(path: pathlib.Path, root: pathlib.Path) -> pathlib.Path: return path if path.is_absolute() else root / path +def display_path(path: pathlib.Path, root: pathlib.Path) -> pathlib.Path: + try: + return path.relative_to(root) + except ValueError: + return path + + def format_ms(value: float) -> str: return f"{value:.2f}" +def render_ms(value: float) -> str: + return "<1" if value == 0 else format_ms(value) + + +def render_ms_text(value: str) -> str: + parsed = parse_ms(value) + if parsed is None: + return value or "—" + return render_ms(parsed) + + def format_seconds(value: float) -> str: return f"{value:.2f}s" @@ -192,6 +246,8 @@ def collect_conversion_stats( source_csv_path: pathlib.Path, row_group_size: int, compression: str, + drop_cache: bool, + cache_drop_command: str | None, ) -> dict[str, float | int | str] | None: if not schema_path.exists() or not source_csv_path.exists(): return None @@ -227,6 +283,9 @@ def collect_conversion_stats( str(roundtrip_csv), ] + if drop_cache: + drop_file_system_cache(cache_drop_command) + convert_started_at = time.perf_counter() convert_completed = subprocess.run(convert_command, capture_output=True, text=True) convert_elapsed_seconds = time.perf_counter() - convert_started_at @@ -234,6 +293,9 @@ def collect_conversion_stats( message = convert_completed.stderr.strip() or convert_completed.stdout.strip() or "convert failed" raise RuntimeError(message) + if drop_cache: + drop_file_system_cache(cache_drop_command) + roundtrip_started_at = time.perf_counter() roundtrip_completed = subprocess.run(roundtrip_command, capture_output=True, text=True) roundtrip_elapsed_seconds = time.perf_counter() - roundtrip_started_at @@ -265,6 +327,61 @@ def collect_conversion_stats( } +def default_cache_drop_command() -> list[str] | None: + system = platform.system() + if system == "Darwin": + purge = shutil.which("purge") + return [purge] if purge else None + + if system == "Linux": + return ["__linux_drop_caches__"] + + return None + + +def run_cache_drop_command(command: list[str]) -> None: + if not command: + raise RuntimeError("cache drop command is empty") + + if command == ["__linux_drop_caches__"]: + subprocess.run(["sync"], check=True) + drop_caches_path = pathlib.Path("/proc/sys/vm/drop_caches") + if os.geteuid() == 0: + drop_caches_path.write_text("3\n", encoding="utf-8") + return + + try: + completed = subprocess.run( + ["sudo", "-n", "tee", str(drop_caches_path)], + input="3\n", + capture_output=True, + text=True, + ) + except FileNotFoundError as error: + raise RuntimeError("cache drop requires root or sudo on Linux") from error + else: + try: + completed = subprocess.run(command, capture_output=True, text=True) + except FileNotFoundError as error: + raise RuntimeError(f"cache drop command not found: {command[0]}") from error + + if completed.returncode != 0: + stderr = completed.stderr.strip() + stdout = completed.stdout.strip() + message = stderr or stdout or f"exit code {completed.returncode}" + raise RuntimeError(message) + + +def drop_file_system_cache(cache_drop_command: str | None) -> None: + command = shlex.split(cache_drop_command) if cache_drop_command else default_cache_drop_command() + if command is None: + raise RuntimeError( + "cache drop is not supported on this OS; pass --cache-drop-mode none or --cache-drop-command" + ) + + run_cache_drop_command(command) + + def run_query( binary: pathlib.Path, input_path: pathlib.Path, @@ -298,6 +415,84 @@ def run_query( return elapsed_ms +def run_hardcoded_benchmark( + benchmark_binary: pathlib.Path, + input_path: pathlib.Path, + query_from: int, + query_to: int, +) -> dict[int, dict[str, str]]: + command = [ + str(benchmark_binary), + "--mode", + "hardcoded", + "--input", + str(input_path), + "--from", + str(query_from), + "--to", + str(query_to), + ] + + completed = subprocess.run(command, capture_output=True, text=True) + if completed.returncode != 0: + stderr = completed.stderr.strip() + stdout = completed.stdout.strip() + message = stderr or stdout or f"exit code {completed.returncode}" + raise RuntimeError(message) + + parsed: dict[int, dict[str, str]] = {} + for line in completed.stdout.splitlines(): + match = QUERY_LINE_RE.match(line) + if not match: + continue + query_id, result_rows, elapsed_ms, status = match.groups() + parsed[int(query_id)] = { + "rows": result_rows, + "time_ms": elapsed_ms, + "status": status, + } + + if not parsed: + raise RuntimeError("benchmark_queries produced no query timing rows") + + return parsed + + +def convert_columnar( + *, + binary: pathlib.Path, + schema_path: pathlib.Path, + source_csv_path: pathlib.Path, + output_columnar: pathlib.Path, + row_group_size: int, + compression: str, +) -> float: + output_columnar.parent.mkdir(parents=True, exist_ok=True) + command = [ + str(binary), + "convert", + "--schema", + str(schema_path), + "--input", + str(source_csv_path), + "--output", + str(output_columnar), + "--row-group-size", + str(row_group_size), + "--compression", + compression, + ] + + started_at = time.perf_counter() + completed = subprocess.run(command, capture_output=True, text=True) + elapsed_seconds = time.perf_counter() - started_at + if completed.returncode != 0: + message = completed.stderr.strip() or completed.stdout.strip() or "convert failed" + raise RuntimeError(message) + + return elapsed_seconds + + def collect_rows( *, root: pathlib.Path, @@ -307,12 +502,17 @@ def collect_rows( count: int, warm_runs: int, table_name: str, + cache_drop_mode: str, + cache_drop_command: str | None, ) -> list[dict[str, str]]: rows: list[dict[str, str]] = [] with tempfile.TemporaryDirectory(prefix="readme_benchmarks_") as temp_dir_name: temp_dir = pathlib.Path(temp_dir_name) + if cache_drop_mode == "once": + drop_file_system_cache(cache_drop_command) + for query_id in range(count): query_path = query_dir / f"query_{query_id}.sql" if not query_path.exists(): @@ -330,6 +530,9 @@ def collect_rows( warm_median_ms = "" output_csv_bytes = "0" + if cache_drop_mode == "per-query": + drop_file_system_cache(cache_drop_command) + try: first_run_value = run_query(binary, input_path, query_path, output_path, table_name) first_run_ms = format_ms(first_run_value) @@ -369,6 +572,101 @@ def collect_rows( return rows +def collect_hardcoded_rows( + *, + root: pathlib.Path, + benchmark_binary: pathlib.Path, + input_path: pathlib.Path, + query_dir: pathlib.Path, + count: int, + warm_runs: int, + cache_drop_mode: str, + cache_drop_command: str | None, +) -> list[dict[str, str]]: + rows_by_query: dict[int, dict[str, str | list[float]]] = {} + + for query_id in range(count): + query_path = query_dir / f"query_{query_id}.sql" + if not query_path.exists(): + break + rows_by_query[query_id] = { + "query_id": str(query_id), + "query_file": str(query_path.relative_to(root)), + "sql": query_path.read_text(encoding="utf-8").strip(), + "first_run_ms": "", + "warm_values": [], + "output_csv_bytes": "0", + "status": "ok", + "error": "", + } + + if cache_drop_mode == "once": + drop_file_system_cache(cache_drop_command) + + for query_id, row in rows_by_query.items(): + if cache_drop_mode == "per-query": + drop_file_system_cache(cache_drop_command) + + try: + first_run_result = run_hardcoded_benchmark(benchmark_binary, input_path, query_id, query_id) + except RuntimeError as error: + row["status"] = "failed" + row["error"] = str(error) + continue + + result = first_run_result.get(query_id) + if result is None: + row["status"] = "failed" + row["error"] = "missing benchmark result" + continue + if result["status"] != "passed": + row["status"] = "failed" + row["error"] = f"benchmark status={result['status']}" + continue + + row["first_run_ms"] = format_ms(float(result["time_ms"])) + + warm_values = row["warm_values"] + assert isinstance(warm_values, list) + for _ in range(warm_runs): + try: + warm_run_result = run_hardcoded_benchmark(benchmark_binary, input_path, query_id, query_id) + except RuntimeError as error: + row["status"] = "failed" + row["error"] = str(error) + break + + result = warm_run_result.get(query_id) + if result is None: + row["status"] = "failed" + row["error"] = "missing benchmark result" + break + if result["status"] != "passed": + row["status"] = "failed" + row["error"] = f"benchmark status={result['status']}" + break + + warm_values.append(float(result["time_ms"])) + + rows: list[dict[str, str]] = [] + for row in rows_by_query.values(): + warm_values = row.pop("warm_values") + assert isinstance(warm_values, list) + if warm_values: + row["warm_avg_ms"] = format_ms(sum(warm_values) / len(warm_values)) + row["warm_median_ms"] = format_ms(statistics.median(warm_values)) + row["warm_min_ms"] = format_ms(min(warm_values)) + row["warm_max_ms"] = format_ms(max(warm_values)) + else: + row["warm_avg_ms"] = "" + row["warm_median_ms"] = "" + row["warm_min_ms"] = "" + row["warm_max_ms"] = "" + rows.append({key: str(value) for key, value in row.items()}) + + return rows + + def write_csv(csv_path: pathlib.Path, rows: list[dict[str, str]]) -> None: csv_path.parent.mkdir(parents=True, exist_ok=True) fieldnames = [ @@ -413,7 +711,7 @@ def render_heatmap(rows: list[dict[str, str]], median_value: float, columns: int lines = [ "### Heatmap", "", - "`🟩` быстрее медианы · `🟦` около медианы · `🟥` медленнее медианы · `⬜` 0 ms", + "`🟩` быстрее медианы · `🟦` около медианы · `🟥` медленнее медианы · `⬜` <1 ms", "", ] @@ -427,7 +725,7 @@ def render_heatmap(rows: list[dict[str, str]], median_value: float, columns: int + " | ".join( ( f"Q{int(row['query_id']):02d} {heatmap_icon(query_metric(row), median_value)} " - f"`{format_ms(query_metric(row))}`" + f"`{render_ms(query_metric(row))}`" ) if row is not None else "" @@ -444,6 +742,8 @@ def render_markdown( rows: list[dict[str, str]], count: int, warm_runs: int, + cache_drop_mode: str, + query_mode: str, conversion_stats: dict[str, float | int | str] | None, ) -> str: generated_at = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -462,26 +762,27 @@ def render_markdown( 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"), + ("median warm time", f"{render_ms(median_value)} ms"), + ("average warm time", f"{render_ms(avg_value)} ms"), + ("p95 warm time", f"{render_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 "—", + f"Q{int(fastest['query_id']):02d} · {render_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 "—", + f"Q{int(slowest['query_id']):02d} · {render_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})", + f"`generated {generated_at}` · `queries {len(rows)}/{count}` · `warm runs {warm_runs}` · " + f"`cache drop {cache_drop_mode}` · `query mode {query_mode}` · [`csv`]({csv_path})", "", ] @@ -552,11 +853,11 @@ def render_markdown( [ query_label, format_size(int(row["output_csv_bytes"])), - row["first_run_ms"] or "—", - row["warm_avg_ms"] or "—", - row["warm_median_ms"] or "—", - row["warm_min_ms"] or "—", - row["warm_max_ms"] or "—", + render_ms_text(row["first_run_ms"]), + render_ms_text(row["warm_avg_ms"]), + render_ms_text(row["warm_median_ms"]), + render_ms_text(row["warm_min_ms"]), + render_ms_text(row["warm_max_ms"]), status, ] ) @@ -591,9 +892,15 @@ def main() -> int: raise SystemExit("--count must be > 0") if args.warm_runs < 0: raise SystemExit("--warm-runs must be >= 0") + if args.lz4_hardcoded: + args.query_mode = "hardcoded" + args.compression = "lz4" + if args.cache_drop_mode is None: + args.cache_drop_mode = "per-query" root = repo_root() binary = resolve_path(args.binary, root) + benchmark_binary = resolve_path(args.benchmark_binary, root) input_path = resolve_path(args.input, root) query_dir = resolve_path(args.query_dir, root) csv_path = resolve_path(args.csv_out, root) @@ -603,38 +910,71 @@ def main() -> int: if not binary.exists(): raise SystemExit(f"binary not found: {binary}") - if not input_path.exists(): + if args.query_mode == "hardcoded" and not benchmark_binary.exists(): + raise SystemExit(f"benchmark binary not found: {benchmark_binary}") + if not args.lz4_hardcoded and not input_path.exists(): raise SystemExit(f"input file not found: {input_path}") if not query_dir.exists(): raise SystemExit(f"query directory not found: {query_dir}") - rows = collect_rows( - root=root, - binary=binary, - input_path=input_path, - query_dir=query_dir, - count=args.count, - warm_runs=args.warm_runs, - table_name=args.table_name, - ) - conversion_stats = collect_conversion_stats( - root=root, - binary=binary, - 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( - csv_path.relative_to(root), - rows, - args.count, - args.warm_runs, - conversion_stats, - ) - update_readme(readme_path, markdown_block) - print(f"updated {readme_path.relative_to(root)} from {csv_path.relative_to(root)} with {len(rows)} row(s)") + with tempfile.TemporaryDirectory(prefix="dashboard_lz4_hardcoded_") as temp_dir_name: + if args.lz4_hardcoded: + input_path = pathlib.Path(temp_dir_name) / "hits_lz4.columnar" + convert_columnar( + binary=binary, + schema_path=schema_path, + source_csv_path=source_csv_path, + output_columnar=input_path, + row_group_size=args.row_group_size, + compression="lz4", + ) + + if args.query_mode == "hardcoded": + rows = collect_hardcoded_rows( + root=root, + benchmark_binary=benchmark_binary, + input_path=input_path, + query_dir=query_dir, + count=args.count, + warm_runs=args.warm_runs, + cache_drop_mode=args.cache_drop_mode, + cache_drop_command=args.cache_drop_command, + ) + else: + rows = collect_rows( + root=root, + binary=binary, + input_path=input_path, + query_dir=query_dir, + count=args.count, + warm_runs=args.warm_runs, + table_name=args.table_name, + cache_drop_mode=args.cache_drop_mode, + cache_drop_command=args.cache_drop_command, + ) + + conversion_stats = collect_conversion_stats( + root=root, + binary=binary, + schema_path=schema_path, + source_csv_path=source_csv_path, + row_group_size=args.row_group_size, + compression=args.compression, + drop_cache=args.cache_drop_mode != "none", + cache_drop_command=args.cache_drop_command, + ) + write_csv(csv_path, rows) + markdown_block = render_markdown( + display_path(csv_path, root), + rows, + args.count, + args.warm_runs, + args.cache_drop_mode, + args.query_mode, + conversion_stats, + ) + update_readme(readme_path, markdown_block) + print(f"updated {display_path(readme_path, root)} from {display_path(csv_path, root)} with {len(rows)} row(s)") return 0 diff --git a/scripts/env.sh b/scripts/env.sh index 29dd087..a707cc9 100644 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -5,9 +5,13 @@ : "${INPUT_CSV:=/bench/sample_input.csv}" : "${COLUMNAR:=/work/results/output.columnar}" : "${RESULTS:=/work/results}" +: "${COLUMNAR_COMPRESSION:=lz4}" +: "${QUERY_MODE:=hardcoded}" export REPO_URL export BRANCH export INPUT_CSV export COLUMNAR export RESULTS +export COLUMNAR_COMPRESSION +export QUERY_MODE diff --git a/src/executor/aggregate_states.cpp b/src/executor/aggregate_states.cpp index 6083cb3..d4cd162 100644 --- a/src/executor/aggregate_states.cpp +++ b/src/executor/aggregate_states.cpp @@ -64,6 +64,7 @@ class SumAS final : public AggState { private: ColumnType type_; + Int128 sum_ = 0; }; @@ -87,6 +88,7 @@ class AvgAS final : public AggState { private: ColumnType type_; + Int128 sum_ = 0; uint64_t count_ = 0; }; @@ -124,6 +126,7 @@ class ExtremumAS final : public AggState { private: ColumnType type_; bool is_min_ = true; + std::optional extremum_; std::optional typed_extremum_; }; @@ -144,6 +147,7 @@ class DistinctAS final : public AggState { private: std::unique_ptr nested_; + StringArena arena_; StringViewSet seen_; }; diff --git a/src/executor/executor.cpp b/src/executor/executor.cpp index 1b6e087..3ee4cf5 100644 --- a/src/executor/executor.cpp +++ b/src/executor/executor.cpp @@ -3,10 +3,10 @@ #include #include +#include "common/ascii.h" #include "executor/operator.h" #include "executor/query_parser.h" #include "executor/query_planner.h" -#include "common/ascii.h" void Executor::RegisterTable(const std::string& name, std::filesystem::path path) { tables_[ToLowerAscii(name)] = std::move(path); @@ -14,19 +14,33 @@ void Executor::RegisterTable(const std::string& name, std::filesystem::path path void Executor::SetUnsupportedFallbackEnabled(const bool enabled) { unsupported_fallback_enabled_ = enabled; } +PlannedQuery Executor::Plan(const Query& query) const { return PlanQuery(query, tables_); } + ExecuteExpected Executor::Execute(const std::string_view query) const { try { - const Query query_ast = ParseQuery(query); - const PlannedQuery planned = PlanQuery(query_ast, tables_); - - const std::unique_ptr root = BuildPlan(planned); + return ExecutePlanned(ParseQuery(query)); + } catch (const Error& error) { + if (unsupported_fallback_enabled_) { + return Batch{}; + } + return tl::unexpected(error); + } +} - auto batch = root->Next(); - if (!batch.has_value()) { +ExecuteExpected Executor::Execute(const Query& query) const { + try { + return ExecutePlanned(query); + } catch (const Error& error) { + if (unsupported_fallback_enabled_) { return Batch{}; } + return tl::unexpected(error); + } +} - return std::move(batch).value(); +ExecuteExpected Executor::Execute(const PlannedQuery& planned) const { + try { + return ExecutePlanned(planned); } catch (const Error& error) { if (unsupported_fallback_enabled_) { return Batch{}; @@ -34,3 +48,16 @@ ExecuteExpected Executor::Execute(const std::string_view query) const { return tl::unexpected(error); } } + +ExecuteExpected Executor::ExecutePlanned(const Query& query) const { return ExecutePlanned(Plan(query)); } + +ExecuteExpected Executor::ExecutePlanned(const PlannedQuery& planned) { + const std::unique_ptr root = BuildPlan(planned); + + auto batch = root->Next(); + if (!batch.has_value()) { + return Batch{}; + } + + return std::move(batch).value(); +} diff --git a/src/executor/operators.cpp b/src/executor/operators.cpp index 23d2aac..df2c298 100644 --- a/src/executor/operators.cpp +++ b/src/executor/operators.cpp @@ -10,37 +10,28 @@ std::unique_ptr BuildPlan(const PlannedQuery& planned) { return CreateMetadataExtremaOperator(planned.table_path, planned.aggregates); } - std::unique_ptr root = - CreateScanOperator(planned.table_path, planned.projection_indexes, planned.filter); + std::unique_ptr root = CreateScanOperator(planned.table_path, planned.projection_indexes, planned.filter); if (planned.filter && planned.plain_select) { root = CreateFilterOperator(std::move(root), planned.filter); } if (planned.plain_select) { - bool limit_applied_by_top_k = false; - ApplyOrderOffsetLimit(root, planned, limit_applied_by_top_k); - const Schema output_schema = ProjectionOutputSchema(planned.plain_select_items, planned.table_schema); root = CreateProjectionOperator(std::move(root), planned.plain_select_items, planned.table_schema); root = CreateEnsureSchemaOperator(std::move(root), output_schema); + bool limit_applied_by_top_k = false; + ApplyOrderOffsetLimit(root, planned, limit_applied_by_top_k); + return root; } bool limit_applied_by_top_k = false; if (!planned.group_keys.empty()) { - if (!planned.order_by.empty() && planned.limit.has_value() && planned.offset == 0 && !planned.having) { - root = CreateGroupAggTopKOperator(std::move(root), planned.group_keys, planned.aggregates, - planned.select_items, planned.filter, planned.order_by, - *planned.limit); - limit_applied_by_top_k = true; - } else { - root = CreateGroupAggOperator(std::move(root), planned.group_keys, planned.aggregates, - planned.select_items, planned.filter, planned.having, - planned.order_by.empty()); - } + root = CreateGroupAggOperator(std::move(root), planned.group_keys, planned.aggregates, planned.select_items, + planned.filter, planned.having, planned.order_by.empty()); } else { root = CreateAggOperator(std::move(root), planned.aggregates, planned.filter); } diff --git a/src/executor/operators_aggregate.cpp b/src/executor/operators_aggregate.cpp index 56c6620..99664ce 100644 --- a/src/executor/operators_aggregate.cpp +++ b/src/executor/operators_aggregate.cpp @@ -1,7 +1,9 @@ #include #include #include +#include #include +#include #include #include @@ -77,6 +79,205 @@ struct AggBinding { std::unique_ptr state; }; +class CompactAggState { + public: + explicit CompactAggState(const PlannedAgg& aggregate) : type_(aggregate.input_type) { + if (aggregate.distinct || aggregate.function == nullptr) { + fallback_ = CreateAggState(aggregate); + return; + } + + const std::string name = ToUpperAscii(aggregate.function->canonical_name); + if (name == "COUNT") { + kind_ = Kind::Count; + } else if (name == "SUM") { + kind_ = Kind::Sum; + } else if (name == "AVG") { + kind_ = Kind::Avg; + } else if (name == "MIN") { + kind_ = Kind::Extremum; + is_min_ = true; + } else if (name == "MAX") { + kind_ = Kind::Extremum; + is_min_ = false; + } else { + fallback_ = CreateAggState(aggregate); + } + } + + void ConsumeValue(const std::string_view value) { + if (fallback_) { + fallback_->ConsumeValue(value); + return; + } + + switch (kind_) { + case Kind::Count: + ConsumeRow(); + return; + case Kind::Sum: + sum_ += ParseColumnValueAsInt128(type_, value); + return; + case Kind::Avg: + sum_ += ParseColumnValueAsInt128(type_, value); + ++count_; + return; + case Kind::Extremum: + ConsumeExtremum(value); + } + } + + void ConsumeInt128(const Int128 value) { + if (fallback_) { + fallback_->ConsumeInt128(value); + return; + } + + switch (kind_) { + case Kind::Count: + ConsumeRow(); + return; + case Kind::Sum: + sum_ += value; + return; + case Kind::Avg: + sum_ += value; + ++count_; + return; + case Kind::Extremum: + if (!typed_extremum_.has_value() || (is_min_ ? value < *typed_extremum_ : value > *typed_extremum_)) { + typed_extremum_ = value; + } + } + } + + void ConsumeRow() { + if (fallback_) { + fallback_->ConsumeRow(); + return; + } + + if (kind_ != Kind::Count) { + throw Error::InvalidState("executor", "aggregate requires a value"); + } + + ++count_; + } + + void ConsumeRows(const size_t count) { + if (fallback_) { + fallback_->ConsumeRows(count); + return; + } + + if (kind_ != Kind::Count) { + throw Error::InvalidState("executor", "aggregate requires a value"); + } + + count_ += count; + } + + std::string Finalize() const { + if (fallback_) { + return fallback_->Finalize(); + } + + switch (kind_) { + case Kind::Count: + return std::to_string(count_); + case Kind::Sum: + return Int128ToString(sum_); + case Kind::Avg: + return count_ == 0 ? "0" : Int128ToString(sum_ / static_cast(count_)); + case Kind::Extremum: + if (typed_extremum_.has_value()) { + return FormatInt128Value(type_, *typed_extremum_); + } + return string_extremum_.value_or(""); + } + + return {}; + } + + Int128 FinalizeInt(const ColumnType output_type) const { + if (fallback_) { + return ParseColumnValueAsInt128(output_type, fallback_->Finalize()); + } + + switch (kind_) { + case Kind::Count: + return count_; + case Kind::Sum: + return sum_; + case Kind::Avg: + return count_ == 0 ? 0 : sum_ / static_cast(count_); + case Kind::Extremum: + return typed_extremum_.value_or(0); + } + + return 0; + } + + private: + enum class Kind { + Count, + Sum, + Avg, + Extremum, + }; + + void ConsumeExtremum(const std::string_view value) { + if (IsNumericColumnType(type_) || type_ == ColumnType::Date || type_ == ColumnType::Timestamp || + type_ == ColumnType::Boolean || type_ == ColumnType::Character) { + ConsumeInt128(ParseColumnValueAsInt128(type_, value)); + return; + } + + if (!string_extremum_.has_value() || + (is_min_ ? value < std::string_view(*string_extremum_) : value > std::string_view(*string_extremum_))) { + string_extremum_ = std::string(value); + } + } + + Kind kind_ = Kind::Count; + ColumnType type_ = ColumnType::String; + bool is_min_ = true; + + Int128 sum_ = 0; + uint64_t count_ = 0; + + std::optional typed_extremum_; + std::optional string_extremum_; + std::unique_ptr fallback_; +}; + +void ConsumeCompactAggRow(const PlannedAgg& aggregate, const Batch& batch, const size_t row, CompactAggState& 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) { + if (aggregate.input_type != ColumnType::String) { + if (const auto typed_value = TryEvalTypedGroupKeyInt(aggregate.argument, batch, row); + typed_value.has_value()) { + state.ConsumeInt128(*typed_value); + return; + } + } + state.ConsumeValue(EvalExpr(aggregate.argument, batch, row)); + return; + } + + state.ConsumeRow(); +} + +struct FinalizedAggregateValues { + std::vector values; + std::vector int_values; +}; + struct GroupKeyComponent { ColumnType type = ColumnType::String; @@ -337,7 +538,9 @@ class AggOperator final : public Operator { private: std::unique_ptr child_; PredicatePtr filter_; + std::vector bindings_; + bool returned_ = false; }; @@ -376,37 +579,30 @@ class GroupAggOperator final : public Operator { TypedGroupKey key = group_key_materializer_.Materialize(*batch, row, string_arena_); - 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), + auto it = groups_.find(key); + if (it == groups_.end()) { + GroupState group{ .states = CreateStates(), - .aggregate_values = {}, - .aggregate_int_values = {}, - .aggregate_values_finalized = false, - }); - } else { - group_index = it->second; + .finalized_aggregates = nullptr, + .ordinal = next_group_ordinal_++, + }; + it = groups_.emplace(std::move(key), std::move(group)).first; } - GroupState& group = groups_[group_index]; + GroupState& group = it->second; for (size_t i = 0; i < bindings_.size(); ++i) { - ConsumeAggRow(bindings_[i].aggregate, *batch, row, *group.states[i]); + ConsumeCompactAggRow(bindings_[i].aggregate, *batch, row, group.states[i]); } } } const Schema schema = BuildSelectOutputSchema(select_items_); - const std::vector output_order = BuildOutputOrder(); + const std::vector output_order = BuildOutputOrder(); Batch result(schema, groups_.size()); - for (const size_t group_index : output_order) { - if (GroupMatchesHaving(groups_[group_index], schema)) { - AppendGroupToBatch(groups_[group_index], result); + for (const GroupEntry* entry : output_order) { + if (GroupMatchesHaving(entry->first, entry->second, schema)) { + AppendGroupToBatch(entry->first, entry->second, result); } } @@ -415,83 +611,93 @@ class GroupAggOperator final : public Operator { private: struct GroupState { - std::vector key_values; - - std::vector> states; + std::vector states; - mutable std::vector aggregate_values; - mutable std::vector aggregate_int_values; + mutable std::unique_ptr finalized_aggregates; - mutable bool aggregate_values_finalized = false; + size_t ordinal = 0; }; + using GroupMap = std::unordered_map; + using GroupEntry = GroupMap::value_type; + struct GroupAggBinding { PlannedAgg aggregate; }; - std::vector> CreateStates() const { - std::vector> states; + std::vector CreateStates() const { + std::vector states; states.reserve(bindings_.size()); for (const auto& binding : bindings_) { - states.push_back(CreateAggState(binding.aggregate)); + states.emplace_back(binding.aggregate); } return states; } - void FinalizeAggregateValues(const GroupState& group) const { - if (group.aggregate_values_finalized) { - return; + const FinalizedAggregateValues& FinalizeAggregateValues(const GroupState& group) const { + if (group.finalized_aggregates) { + return *group.finalized_aggregates; } - group.aggregate_values.reserve(group.states.size()); - group.aggregate_int_values.reserve(group.states.size()); + auto finalized = std::make_unique(); + finalized->values.reserve(group.states.size()); + finalized->int_values.reserve(group.states.size()); for (size_t i = 0; i < group.states.size(); ++i) { - std::string value = group.states[i]->Finalize(); const ColumnType type = AggregateOutputType(bindings_[i].aggregate); - group.aggregate_int_values.push_back(type == ColumnType::String ? 0 - : ParseColumnValueAsInt128(type, value)); - group.aggregate_values.push_back(std::move(value)); + if (type == ColumnType::String) { + finalized->int_values.push_back(0); + finalized->values.push_back(group.states[i].Finalize()); + continue; + } + + const Int128 value = group.states[i].FinalizeInt(type); + finalized->int_values.push_back(value); + finalized->values.push_back(FormatInt128Value(type, value)); } - group.aggregate_values_finalized = true; + group.finalized_aggregates = std::move(finalized); + return *group.finalized_aggregates; } - void AppendGroupToBatch(const GroupState& group, const Batch& batch) const { - FinalizeAggregateValues(group); + void AppendGroupToBatch(const TypedGroupKey& key, const GroupState& group, const Batch& batch) const { + const FinalizedAggregateValues& finalized = 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])); + batch.AppendValueFromString(column, FormatGroupKeyValue(key.values[item.index])); } else { - batch.AppendValueFromString(column, group.aggregate_values[item.index]); + batch.AppendValueFromString(column, finalized.values[item.index]); } } } - bool GroupMatchesHaving(const GroupState& group, const Schema& schema) const { + bool GroupMatchesHaving(const TypedGroupKey& key, const GroupState& group, const Schema& schema) const { if (!having_) { return true; } const Batch row(schema, 1); - AppendGroupToBatch(group, row); + AppendGroupToBatch(key, group, row); return EvaluatePredicate(having_, row, 0); } - std::vector BuildOutputOrder() const { - std::vector order; + std::vector BuildOutputOrder() const { + std::vector order; order.reserve(groups_.size()); - for (size_t i = 0; i < groups_.size(); ++i) { - order.push_back(i); + for (const auto& entry : groups_) { + order.push_back(&entry); } if (!sort_by_group_keys_) { + std::ranges::sort(order, [](const GroupEntry* lhs, const GroupEntry* rhs) { + return lhs->second.ordinal < rhs->second.ordinal; + }); return order; } @@ -507,19 +713,17 @@ class GroupAggOperator final : public Operator { 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]; + std::ranges::sort(order, [&](const GroupEntry* lhs, const GroupEntry* rhs) { 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]); + CompareGroupKeyValue(lhs->first.values[item.index], rhs->first.values[item.index]); if (comparison != 0) { return comparison < 0; } } - return false; + return lhs->second.ordinal < rhs->second.ordinal; }); return order; @@ -535,8 +739,8 @@ class GroupAggOperator final : public Operator { PredicatePtr having_; StringArena string_arena_; - std::unordered_map group_indexes_; - std::vector groups_; + GroupMap groups_; + size_t next_group_ordinal_ = 0; bool sort_by_group_keys_ = false; bool returned_ = false; @@ -577,36 +781,29 @@ class GroupAggTopKOperator final : public Operator { TypedGroupKey key = group_key_materializer_.Materialize(*batch, row, string_arena_); - 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), + auto it = groups_.find(key); + if (it == groups_.end()) { + GroupState group{ .states = CreateStates(), - .aggregate_values = {}, - .aggregate_int_values = {}, - .aggregate_values_finalized = false, - }); - } else { - group_index = it->second; + .finalized_aggregates = nullptr, + .ordinal = next_group_ordinal_++, + }; + it = groups_.emplace(std::move(key), std::move(group)).first; } - GroupState& group = groups_[group_index]; + GroupState& group = it->second; for (size_t i = 0; i < bindings_.size(); ++i) { - ConsumeAggRow(bindings_[i].aggregate, *batch, row, *group.states[i]); + ConsumeCompactAggRow(bindings_[i].aggregate, *batch, row, group.states[i]); } } } const Schema schema = BuildSelectOutputSchema(select_items_); - const std::vector top_groups = BuildTopGroupIndexes(); + const std::vector top_groups = BuildTopGroups(); Batch result(schema, top_groups.size()); - for (const size_t group_index : top_groups) { - AppendGroupToBatch(groups_[group_index], result); + for (const GroupEntry* entry : top_groups) { + AppendGroupToBatch(entry->first, entry->second, result); } return result; @@ -614,66 +811,70 @@ class GroupAggTopKOperator final : public Operator { private: struct GroupState { - std::vector key_values; - - std::vector> states; + std::vector states; - mutable std::vector aggregate_values; - mutable std::vector aggregate_int_values; - - mutable bool aggregate_values_finalized = false; + mutable std::unique_ptr finalized_aggregates; + size_t ordinal = 0; }; + using GroupMap = std::unordered_map; + using GroupEntry = GroupMap::value_type; + struct GroupAggBinding { PlannedAgg aggregate; }; - std::vector> CreateStates() const { - std::vector> states; + std::vector CreateStates() const { + std::vector states; states.reserve(bindings_.size()); for (const auto& binding : bindings_) { - states.push_back(CreateAggState(binding.aggregate)); + states.emplace_back(binding.aggregate); } return states; } - void FinalizeAggregateValues(const GroupState& group) const { - if (group.aggregate_values_finalized) { - return; + const FinalizedAggregateValues& FinalizeAggregateValues(const GroupState& group) const { + if (group.finalized_aggregates) { + return *group.finalized_aggregates; } - group.aggregate_values.reserve(group.states.size()); - group.aggregate_int_values.reserve(group.states.size()); + auto finalized = std::make_unique(); + finalized->values.reserve(group.states.size()); + finalized->int_values.reserve(group.states.size()); for (size_t i = 0; i < group.states.size(); ++i) { - std::string value = group.states[i]->Finalize(); const ColumnType type = AggregateOutputType(bindings_[i].aggregate); - group.aggregate_int_values.push_back(type == ColumnType::String ? 0 - : ParseColumnValueAsInt128(type, value)); - group.aggregate_values.push_back(std::move(value)); + if (type == ColumnType::String) { + finalized->int_values.push_back(0); + finalized->values.push_back(group.states[i].Finalize()); + continue; + } + + const Int128 value = group.states[i].FinalizeInt(type); + finalized->int_values.push_back(value); + finalized->values.push_back(FormatInt128Value(type, value)); } - group.aggregate_values_finalized = true; + group.finalized_aggregates = std::move(finalized); + return *group.finalized_aggregates; } - 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]; + bool OrdersBefore(const GroupEntry* lhs, const GroupEntry* rhs) const { + const FinalizedAggregateValues& lhs_finalized = FinalizeAggregateValues(lhs->second); + const FinalizedAggregateValues& rhs_finalized = FinalizeAggregateValues(rhs->second); 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]); + comparison = CompareGroupKeyValue(lhs->first.values[item.index], rhs->first.values[item.index]); } else { - FinalizeAggregateValues(lhs); - FinalizeAggregateValues(rhs); comparison = item.output_type == ColumnType::String - ? lhs.aggregate_values[item.index] <=> rhs.aggregate_values[item.index] - : lhs.aggregate_int_values[item.index] <=> rhs.aggregate_int_values[item.index]; + ? lhs_finalized.values[item.index] <=> rhs_finalized.values[item.index] + : lhs_finalized.int_values[item.index] <=> rhs_finalized.int_values[item.index]; } if (comparison != 0) { @@ -681,44 +882,46 @@ class GroupAggTopKOperator final : public Operator { } } - return lhs_index < rhs_index; + return lhs->second.ordinal < rhs->second.ordinal; } - void AppendGroupToBatch(const GroupState& group, const Batch& batch) const { - FinalizeAggregateValues(group); + void AppendGroupToBatch(const TypedGroupKey& key, const GroupState& group, const Batch& batch) const { + const FinalizedAggregateValues& finalized = 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])); + batch.AppendValueFromString(column, FormatGroupKeyValue(key.values[item.index])); } else { - batch.AppendValueFromString(column, group.aggregate_values[item.index]); + batch.AppendValueFromString(column, finalized.values[item.index]); } } } - std::vector BuildTopGroupIndexes() const { - std::vector top_groups; + std::vector BuildTopGroups() const { + std::vector top_groups; top_groups.reserve(limit_); - for (size_t i = 0; i < groups_.size(); ++i) { + for (const auto& entry : groups_) { + const GroupEntry* group = &entry; 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); }); + top_groups.push_back(group); + std::ranges::push_heap( + top_groups, [&](const GroupEntry* lhs, const GroupEntry* 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); }); + if (OrdersBefore(group, top_groups.front())) { + std::ranges::pop_heap( + top_groups, [&](const GroupEntry* lhs, const GroupEntry* rhs) { return OrdersBefore(lhs, rhs); }); + top_groups.back() = group; + std::ranges::push_heap( + top_groups, [&](const GroupEntry* lhs, const GroupEntry* rhs) { return OrdersBefore(lhs, rhs); }); } } - std::ranges::sort(top_groups, [&](const size_t lhs, const size_t rhs) { return OrdersBefore(lhs, rhs); }); + std::ranges::sort(top_groups, + [&](const GroupEntry* lhs, const GroupEntry* rhs) { return OrdersBefore(lhs, rhs); }); return top_groups; } @@ -735,8 +938,8 @@ class GroupAggTopKOperator final : public Operator { size_t limit_ = 0; StringArena string_arena_; - std::unordered_map group_indexes_; - mutable std::vector groups_; + mutable GroupMap groups_; + size_t next_group_ordinal_ = 0; bool returned_ = false; }; diff --git a/src/executor/operators_runtime.cpp b/src/executor/operators_runtime.cpp index 2e35cbc..ecffa19 100644 --- a/src/executor/operators_runtime.cpp +++ b/src/executor/operators_runtime.cpp @@ -257,6 +257,11 @@ struct RowRef { std::strong_ordering CompareColumnRows(const Column& lhs_column, const size_t lhs_row, const Column& rhs_column, const size_t rhs_row, const ColumnType type) { + if (type != ColumnType::String && lhs_column.Type() == ColumnType::String && rhs_column.Type() == ColumnType::String) { + return ParseColumnValueAsInt128(type, lhs_column.ValueAsString(lhs_row)) <=> + ParseColumnValueAsInt128(type, rhs_column.ValueAsString(rhs_row)); + } + if (type != ColumnType::String) { return lhs_column.ValueAsInt128(lhs_row) <=> rhs_column.ValueAsInt128(rhs_row); } @@ -288,6 +293,7 @@ class RowOrdering { private: std::vector order_by_; + const std::vector* batches_ = nullptr; }; @@ -570,6 +576,7 @@ class LimitOperator final : public Operator { private: std::unique_ptr child_; + size_t remaining_; }; @@ -597,6 +604,7 @@ class OffsetOperator final : public Operator { private: std::unique_ptr child_; + size_t remaining_; }; diff --git a/src/executor/query_planner.cpp b/src/executor/query_planner.cpp index 33d6261..619d6e6 100644 --- a/src/executor/query_planner.cpp +++ b/src/executor/query_planner.cpp @@ -6,6 +6,7 @@ #include "common/error.h" #include "common/parsing.h" #include "executor/comparison_utils.h" +#include "executor/operators_internal.h" #include "executor/query_utils.h" #include "executor/typed_value_utils.h" #include "io/columnar_batch.h" @@ -119,6 +120,21 @@ static ExprPtr ResolveGroupByExpression(const Query& query_ast, const ExprPtr& e return expr; } +static std::optional FindProjectedOrderColumn(const Schema& schema, const SelectItemSpec& item, + const ExprPtr& expression) { + for (size_t i = 0; i < schema.columns.size(); ++i) { + if (SameColumnName(schema.columns[i].name, item.output_name)) { + return i; + } + } + + if (expression && expression->kind == ExprKind::Column) { + return TryFindBatchColumn(schema, expression->column.name); + } + + return std::nullopt; +} + static void AddProjectionIndex(std::vector& indexes, const size_t index) { if (!std::ranges::contains(indexes, index)) { indexes.push_back(index); @@ -631,34 +647,24 @@ PlannedQuery PlanQuery(const Query& query, const std::unordered_mapkind == ExprKind::Star; + }); for (const auto& order_item : query.order_by) { - size_t column_index = 0; const auto selected = FindSelectItem(query, order_item.item); const ExprPtr order_expression = selected.has_value() ? query.select_items[*selected].expression : order_item.item.expression; const ColumnType type = ColumnTypeOf(query, schema, order_expression); CollectColumnsFromExpr(query, schema, order_expression, projection_indexes); - if (order_expression && order_expression->kind == ExprKind::Column) { - column_index = FindColumnIndex(schema, order_expression->column.name); - } else if (selected.has_value()) { - column_index = *selected; - } else { - throw Error::InvalidArgument("executor", "ORDER BY expression is not supported in plain SELECT"); - } - planned.order_by.push_back(PlannedOrderBy{ - .result_column_index = column_index, + .result_column_index = 0, .descending = order_item.descending, .value_type = type, }); } - const bool has_star_select = std::ranges::any_of(planned.plain_select_items, [](const SelectItemSpec& item) { - return item.expression && item.expression->kind == ExprKind::Star; - }); - for (const auto& order_item : query.order_by) { if (!order_item.item.expression || order_item.item.expression->kind == ExprKind::Star) { continue; @@ -687,6 +693,21 @@ PlannedQuery PlanQuery(const Query& query, const std::unordered_map projected_index = + FindProjectedOrderColumn(output_schema, query.order_by[i].item, order_expression); + + if (!projected_index.has_value()) { + throw Error::InvalidArgument("executor", "ORDER BY expression is not supported in plain SELECT"); + } + + planned.order_by[i].result_column_index = *projected_index; + } } else { for (const auto& order_item : query.order_by) { const std::optional result_column = FindSelectItem(query, order_item.item); @@ -715,16 +736,6 @@ PlannedQuery PlanQuery(const Query& query, const std::unordered_map projected_index = - ProjectedIndexOf(planned.projection_indexes, order.result_column_index); - if (projected_index.has_value()) { - order.result_column_index = *projected_index; - } - } - } - for (PlannedAgg& aggregate : planned.aggregates) { if (!aggregate.direct_numeric_argument) { continue; diff --git a/tests/test_executor.cpp b/tests/test_executor.cpp index 3dd9b80..03dd56f 100644 --- a/tests/test_executor.cpp +++ b/tests/test_executor.cpp @@ -178,6 +178,65 @@ TEST(executor, supports_order_by_limit_top_k_in_result_order) { })); } +TEST(executor, supports_select_star_order_by_non_first_column) { + const Batch batch = BuildHitsTable("SELECT * FROM hits ORDER BY EventDate ASC LIMIT 3;"); + + EXPECT_EQ(BatchColumnNames(batch), + (std::vector{"AdvEngineID", "ResolutionWidth", "UserID", "RegionID", "MobilePhone", + "MobilePhoneModel", "SearchEngineID", "SearchPhrase", "EventDate"})); + EXPECT_EQ(BatchRows(batch), (std::vector>{ + {"7", "500", "5", "20", "true", "iPhone", "1", "gamma", "2024-01-01"}, + {"7", "200", "2", "20", "true", "iPhone", "1", "beta", "2024-01-03"}, + {"0", "100", "1", "10", "false", "", "1", "alpha", "2024-01-05"}, + })); +} + +TEST(executor, supports_plain_select_order_by_hidden_typed_column) { + const Batch batch = + BuildHitsTable("SELECT SearchPhrase FROM hits WHERE SearchPhrase != '' ORDER BY EventDate ASC LIMIT 3;"); + + EXPECT_EQ(BatchColumnNames(batch), (std::vector{"SearchPhrase", "EventDate"})); + EXPECT_EQ(BatchRows(batch), (std::vector>{ + {"gamma", "2024-01-01"}, + {"beta", "2024-01-03"}, + {"alpha", "2024-01-05"}, + })); +} + +TEST(executor, supports_clickbench_star_order_by_projected_column) { + const TempFile schema_file("executor_schema"); + const TempFile data_file("executor_data"); + const TempFile columnar_file("executor_columnar"); + + WriteRows(schema_file.Path(), { + {"WatchID", "int64"}, + {"EventTime", "timestamp"}, + {"URL", "string"}, + {"Title", "string"}, + {"SearchPhrase", "string"}, + }); + WriteRows(data_file.Path(), { + {"1", "2024-01-05 00:00:00", "http://example.com", "other", "skip"}, + {"2", "2024-01-03 00:00:00", "http://google.com/a", "first", "a"}, + {"3", "2024-01-01 00:00:00", "https://google.com/b", "second", "b"}, + {"4", "2024-01-04 00:00:00", "http://google.com/c", "third", "c"}, + }); + ConvertCsvToColumnar(schema_file.Path(), data_file.Path(), columnar_file.Path(), 2); + + Executor executor; + executor.RegisterTable("hits", columnar_file.Path()); + + auto result = executor.Execute("SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime ASC LIMIT 2;"); + ASSERT_TRUE(result.has_value()) << result.error().what(); + + EXPECT_EQ(BatchColumnNames(result.value()), + (std::vector{"WatchID", "EventTime", "URL", "Title"})); + EXPECT_EQ(BatchRows(result.value()), (std::vector>{ + {"3", "2024-01-01 00:00:00", "https://google.com/b", "second"}, + {"2", "2024-01-03 00:00:00", "http://google.com/a", "first"}, + })); +} + TEST(executor, supports_multiple_aggregates_with_alias_and_limit) { const Batch batch = BuildHitsTable( "SELECT RegionID, SUM(AdvEngineID), COUNT(*) AS c, AVG(ResolutionWidth), COUNT(DISTINCT UserID) " @@ -195,6 +254,43 @@ TEST(executor, supports_multiple_aggregates_with_alias_and_limit) { EXPECT_EQ(actual_rows, expected_rows); } +TEST(executor, supports_top_k_group_by_multiple_numeric_keys_with_compact_aggregates) { + const TempFile schema_file("executor_schema"); + const TempFile data_file("executor_data"); + const TempFile columnar_file("executor_columnar"); + + WriteRows(schema_file.Path(), { + {"WatchID", "int64"}, + {"ClientIP", "int32"}, + {"IsRefresh", "int16"}, + {"ResolutionWidth", "int16"}, + }); + WriteRows(data_file.Path(), { + {"10", "1", "1", "100"}, + {"10", "1", "0", "200"}, + {"10", "1", "1", "300"}, + {"11", "1", "1", "400"}, + {"11", "1", "0", "600"}, + {"10", "2", "1", "800"}, + }); + ConvertCsvToColumnar(schema_file.Path(), data_file.Path(), columnar_file.Path(), 2); + + Executor executor; + executor.RegisterTable("hits", columnar_file.Path()); + + auto result = executor.Execute( + "SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits GROUP BY WatchID, " + "ClientIP ORDER BY c DESC LIMIT 2;"); + ASSERT_TRUE(result.has_value()) << result.error().what(); + + EXPECT_EQ(BatchColumnNames(result.value()), + (std::vector{"WatchID", "ClientIP", "c", "SUM(IsRefresh)", "AVG(ResolutionWidth)"})); + EXPECT_EQ(BatchRows(result.value()), (std::vector>{ + {"10", "1", "3", "2", "200"}, + {"11", "1", "2", "1", "500"}, + })); +} + TEST(executor, executes_basic_aggregate_queries) { { const Batch batch = BuildHitsTable("SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0;");