|
| 1 | +// Licensed to the Apache Software Foundation (ASF) under one |
| 2 | +// or more contributor license agreements. See the NOTICE file |
| 3 | +// distributed with this work for additional information |
| 4 | +// regarding copyright ownership. The ASF licenses this file |
| 5 | +// to you under the Apache License, Version 2.0 (the |
| 6 | +// "License"); you may not use this file except in compliance |
| 7 | +// with the License. You may obtain a copy of the License at |
| 8 | +// |
| 9 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +// |
| 11 | +// Unless required by applicable law or agreed to in writing, |
| 12 | +// software distributed under the License is distributed on an |
| 13 | +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 14 | +// KIND, either express or implied. See the License for the |
| 15 | +// specific language governing permissions and limitations |
| 16 | +// under the License. |
| 17 | + |
| 18 | +//! Benchmarks of SQL queries again parquet data |
| 19 | +
|
| 20 | +use arrow::array::{ArrayRef, DictionaryArray, PrimitiveArray, StringArray}; |
| 21 | +use arrow::datatypes::{ |
| 22 | + ArrowPrimitiveType, DataType, Field, Float64Type, Int32Type, Int64Type, Schema, |
| 23 | + SchemaRef, |
| 24 | +}; |
| 25 | +use arrow::record_batch::RecordBatch; |
| 26 | +use criterion::{criterion_group, criterion_main, Criterion}; |
| 27 | +use datafusion::prelude::ExecutionContext; |
| 28 | +use parquet::arrow::ArrowWriter; |
| 29 | +use parquet::file::properties::{WriterProperties, WriterVersion}; |
| 30 | +use rand::distributions::uniform::SampleUniform; |
| 31 | +use rand::distributions::Alphanumeric; |
| 32 | +use rand::prelude::*; |
| 33 | +use std::fs::File; |
| 34 | +use std::io::Read; |
| 35 | +use std::ops::Range; |
| 36 | +use std::path::Path; |
| 37 | +use std::sync::Arc; |
| 38 | +use std::time::Instant; |
| 39 | +use tempfile::NamedTempFile; |
| 40 | +use tokio_stream::StreamExt; |
| 41 | + |
| 42 | +/// The number of batches to write |
| 43 | +const NUM_BATCHES: usize = 2048; |
| 44 | +/// The number of rows in each record batch to write |
| 45 | +const WRITE_RECORD_BATCH_SIZE: usize = 1024; |
| 46 | +/// The number of rows in a row group |
| 47 | +const ROW_GROUP_SIZE: usize = 1024 * 1024; |
| 48 | +/// The number of row groups expected |
| 49 | +const EXPECTED_ROW_GROUPS: usize = 2; |
| 50 | + |
| 51 | +fn schema() -> SchemaRef { |
| 52 | + let string_dictionary_type = |
| 53 | + DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)); |
| 54 | + |
| 55 | + Arc::new(Schema::new(vec![ |
| 56 | + Field::new("dict_10_required", string_dictionary_type.clone(), false), |
| 57 | + Field::new("dict_10_optional", string_dictionary_type.clone(), true), |
| 58 | + Field::new("dict_100_required", string_dictionary_type.clone(), false), |
| 59 | + Field::new("dict_100_optional", string_dictionary_type.clone(), true), |
| 60 | + Field::new("dict_1000_required", string_dictionary_type.clone(), false), |
| 61 | + Field::new("dict_1000_optional", string_dictionary_type, true), |
| 62 | + Field::new("string_required", DataType::Utf8, false), |
| 63 | + Field::new("string_optional", DataType::Utf8, true), |
| 64 | + Field::new("i64_required", DataType::Int64, false), |
| 65 | + Field::new("i64_optional", DataType::Int64, true), |
| 66 | + Field::new("f64_required", DataType::Float64, false), |
| 67 | + Field::new("f64_optional", DataType::Float64, true), |
| 68 | + ])) |
| 69 | +} |
| 70 | + |
| 71 | +fn generate_batch() -> RecordBatch { |
| 72 | + let schema = schema(); |
| 73 | + let len = WRITE_RECORD_BATCH_SIZE; |
| 74 | + RecordBatch::try_new( |
| 75 | + schema, |
| 76 | + vec![ |
| 77 | + generate_string_dictionary("prefix", 10, len, 1.0), |
| 78 | + generate_string_dictionary("prefix", 10, len, 0.5), |
| 79 | + generate_string_dictionary("prefix", 100, len, 1.0), |
| 80 | + generate_string_dictionary("prefix", 100, len, 0.5), |
| 81 | + generate_string_dictionary("prefix", 1000, len, 1.0), |
| 82 | + generate_string_dictionary("prefix", 1000, len, 0.5), |
| 83 | + generate_strings(0..100, len, 1.0), |
| 84 | + generate_strings(0..100, len, 0.5), |
| 85 | + generate_primitive::<Int64Type>(len, 1.0, -2000..2000), |
| 86 | + generate_primitive::<Int64Type>(len, 0.5, -2000..2000), |
| 87 | + generate_primitive::<Float64Type>(len, 1.0, -1000.0..1000.0), |
| 88 | + generate_primitive::<Float64Type>(len, 0.5, -1000.0..1000.0), |
| 89 | + ], |
| 90 | + ) |
| 91 | + .unwrap() |
| 92 | +} |
| 93 | + |
| 94 | +fn generate_string_dictionary( |
| 95 | + prefix: &str, |
| 96 | + cardinality: usize, |
| 97 | + len: usize, |
| 98 | + valid_percent: f64, |
| 99 | +) -> ArrayRef { |
| 100 | + let mut rng = thread_rng(); |
| 101 | + let strings: Vec<_> = (0..cardinality) |
| 102 | + .map(|x| format!("{}#{}", prefix, x)) |
| 103 | + .collect(); |
| 104 | + |
| 105 | + Arc::new(DictionaryArray::<Int32Type>::from_iter((0..len).map( |
| 106 | + |_| { |
| 107 | + rng.gen_bool(valid_percent) |
| 108 | + .then(|| strings[rng.gen_range(0..cardinality)].as_str()) |
| 109 | + }, |
| 110 | + ))) |
| 111 | +} |
| 112 | + |
| 113 | +fn generate_strings( |
| 114 | + string_length_range: Range<usize>, |
| 115 | + len: usize, |
| 116 | + valid_percent: f64, |
| 117 | +) -> ArrayRef { |
| 118 | + let mut rng = thread_rng(); |
| 119 | + Arc::new(StringArray::from_iter((0..len).map(|_| { |
| 120 | + rng.gen_bool(valid_percent).then(|| { |
| 121 | + let string_len = rng.gen_range(string_length_range.clone()); |
| 122 | + (0..string_len) |
| 123 | + .map(|_| char::from(rng.sample(Alphanumeric))) |
| 124 | + .collect::<String>() |
| 125 | + }) |
| 126 | + }))) |
| 127 | +} |
| 128 | + |
| 129 | +fn generate_primitive<T>( |
| 130 | + len: usize, |
| 131 | + valid_percent: f64, |
| 132 | + range: Range<T::Native>, |
| 133 | +) -> ArrayRef |
| 134 | +where |
| 135 | + T: ArrowPrimitiveType, |
| 136 | + T::Native: SampleUniform, |
| 137 | +{ |
| 138 | + let mut rng = thread_rng(); |
| 139 | + Arc::new(PrimitiveArray::<T>::from_iter((0..len).map(|_| { |
| 140 | + rng.gen_bool(valid_percent) |
| 141 | + .then(|| rng.gen_range(range.clone())) |
| 142 | + }))) |
| 143 | +} |
| 144 | + |
| 145 | +fn generate_file() -> NamedTempFile { |
| 146 | + let now = Instant::now(); |
| 147 | + let named_file = tempfile::Builder::new() |
| 148 | + .prefix("parquet_query_sql") |
| 149 | + .suffix(".parquet") |
| 150 | + .tempfile() |
| 151 | + .unwrap(); |
| 152 | + |
| 153 | + println!("Generating parquet file - {}", named_file.path().display()); |
| 154 | + let schema = schema(); |
| 155 | + |
| 156 | + let properties = WriterProperties::builder() |
| 157 | + .set_writer_version(WriterVersion::PARQUET_2_0) |
| 158 | + .set_max_row_group_size(ROW_GROUP_SIZE) |
| 159 | + .build(); |
| 160 | + |
| 161 | + let file = named_file.as_file().try_clone().unwrap(); |
| 162 | + let mut writer = ArrowWriter::try_new(file, schema, Some(properties)).unwrap(); |
| 163 | + |
| 164 | + for _ in 0..NUM_BATCHES { |
| 165 | + let batch = generate_batch(); |
| 166 | + writer.write(&batch).unwrap(); |
| 167 | + } |
| 168 | + |
| 169 | + let metadata = writer.close().unwrap(); |
| 170 | + assert_eq!( |
| 171 | + metadata.num_rows as usize, |
| 172 | + WRITE_RECORD_BATCH_SIZE * NUM_BATCHES |
| 173 | + ); |
| 174 | + assert_eq!(metadata.row_groups.len(), EXPECTED_ROW_GROUPS); |
| 175 | + |
| 176 | + println!( |
| 177 | + "Generated parquet file in {} seconds", |
| 178 | + now.elapsed().as_secs_f32() |
| 179 | + ); |
| 180 | + |
| 181 | + named_file |
| 182 | +} |
| 183 | + |
| 184 | +fn criterion_benchmark(c: &mut Criterion) { |
| 185 | + let (file_path, temp_file) = match std::env::var("PARQUET_FILE") { |
| 186 | + Ok(file) => (file, None), |
| 187 | + Err(_) => { |
| 188 | + let temp_file = generate_file(); |
| 189 | + (temp_file.path().display().to_string(), Some(temp_file)) |
| 190 | + } |
| 191 | + }; |
| 192 | + |
| 193 | + assert!(Path::new(&file_path).exists(), "path not found"); |
| 194 | + println!("Using parquet file {}", file_path); |
| 195 | + |
| 196 | + let mut context = ExecutionContext::new(); |
| 197 | + |
| 198 | + let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap(); |
| 199 | + rt.block_on(context.register_parquet("t", file_path.as_str())) |
| 200 | + .unwrap(); |
| 201 | + |
| 202 | + // We read the queries from a file so they can be changed without recompiling the benchmark |
| 203 | + let mut queries_file = File::open("benches/parquet_query_sql.sql").unwrap(); |
| 204 | + let mut queries = String::new(); |
| 205 | + queries_file.read_to_string(&mut queries).unwrap(); |
| 206 | + |
| 207 | + for query in queries.split(';') { |
| 208 | + let query = query.trim(); |
| 209 | + |
| 210 | + // Remove comment lines |
| 211 | + let query: Vec<_> = query.split('\n').filter(|x| !x.starts_with("--")).collect(); |
| 212 | + let query = query.join(" "); |
| 213 | + |
| 214 | + // Ignore blank lines |
| 215 | + if query.is_empty() { |
| 216 | + continue; |
| 217 | + } |
| 218 | + |
| 219 | + let query = query.as_str(); |
| 220 | + c.bench_function(query, |b| { |
| 221 | + b.iter(|| { |
| 222 | + let mut context = context.clone(); |
| 223 | + rt.block_on(async move { |
| 224 | + let query = context.sql(query).await.unwrap(); |
| 225 | + let mut stream = query.execute_stream().await.unwrap(); |
| 226 | + while criterion::black_box(stream.next().await).is_some() {} |
| 227 | + }) |
| 228 | + }); |
| 229 | + }); |
| 230 | + } |
| 231 | + |
| 232 | + // Clean up temporary file if any |
| 233 | + std::mem::drop(temp_file); |
| 234 | +} |
| 235 | + |
| 236 | +criterion_group!(benches, criterion_benchmark); |
| 237 | +criterion_main!(benches); |
0 commit comments