-
Notifications
You must be signed in to change notification settings - Fork 2.2k
perf: preserve dictionary encoding for lower/upper to avoid materializing low-cardinality columns #22905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
perf: preserve dictionary encoding for lower/upper to avoid materializing low-cardinality columns #22905
Changes from all commits
1896e06
4d32c12
8f0f701
b1bb38d
789014d
adbdd13
364d9d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -33,7 +33,7 @@ use datafusion_common::utils::{ | ||||||||||
| use datafusion_common::{ | |||||||||||
| Result, exec_err, internal_err, plan_err, types::NativeType, utils::list_ndims, | |||||||||||
| }; | |||||||||||
| use datafusion_expr_common::signature::ArrayFunctionArgument; | |||||||||||
| use datafusion_expr_common::signature::{ArrayFunctionArgument, EncodingPreservation}; | |||||||||||
| use datafusion_expr_common::type_coercion::binary::type_union_resolution; | |||||||||||
| use datafusion_expr_common::{ | |||||||||||
| signature::{ArrayFunctionSignature, FIXED_SIZE_LIST_WILDCARD, TIMEZONE_WILDCARD}, | |||||||||||
|
|
@@ -873,19 +873,53 @@ fn get_valid_types( | ||||||||||
| TypeSignature::Coercible(param_types) => { | |||||||||||
| function_length_check(function_name, current_types.len(), param_types.len())?; | |||||||||||
|
|
|||||||||||
| fn cast_origin( | |||||||||||
| current_type: &DataType, | |||||||||||
| encoding_preservation: EncodingPreservation, | |||||||||||
| ) -> &DataType { | |||||||||||
| if encoding_preservation.preserve_dictionary() | |||||||||||
| && let DataType::Dictionary(_, value_type) = current_type | |||||||||||
| { | |||||||||||
| value_type | |||||||||||
| } else { | |||||||||||
| current_type | |||||||||||
| } | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| fn preserve_encoding( | |||||||||||
| current_type: &DataType, | |||||||||||
| casted_type: DataType, | |||||||||||
| encoding_preservation: EncodingPreservation, | |||||||||||
| ) -> DataType { | |||||||||||
| if encoding_preservation.preserve_dictionary() | |||||||||||
| && let DataType::Dictionary(key_type, _) = current_type | |||||||||||
| && !matches!(casted_type, DataType::Dictionary(_, _)) | |||||||||||
| { | |||||||||||
| DataType::Dictionary(key_type.clone(), Box::new(casted_type)) | |||||||||||
| } else { | |||||||||||
| casted_type | |||||||||||
| } | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| let mut new_types = Vec::with_capacity(current_types.len()); | |||||||||||
| for (current_type, param) in current_types.iter().zip(param_types.iter()) { | |||||||||||
| let current_native_type: NativeType = current_type.into(); | |||||||||||
| let encoding_preservation = param.encoding_preservation(); | |||||||||||
| let cast_origin = cast_origin(current_type, encoding_preservation); | |||||||||||
|
|
|||||||||||
| if param | |||||||||||
| .desired_type() | |||||||||||
| .matches_native_type(¤t_native_type) | |||||||||||
| { | |||||||||||
| let casted_type = param | |||||||||||
| .desired_type() | |||||||||||
| .default_casted_type(¤t_native_type, current_type)?; | |||||||||||
| .default_casted_type(¤t_native_type, cast_origin)?; | |||||||||||
|
|
|||||||||||
| new_types.push(casted_type); | |||||||||||
| new_types.push(preserve_encoding( | |||||||||||
| current_type, | |||||||||||
| casted_type, | |||||||||||
| encoding_preservation, | |||||||||||
| )); | |||||||||||
| } else if param | |||||||||||
| .allowed_source_types() | |||||||||||
| .iter() | |||||||||||
|
|
@@ -894,8 +928,12 @@ fn get_valid_types( | ||||||||||
| // If the condition is met which means `implicit coercion`` is provided so we can safely unwrap | |||||||||||
| let default_casted_type = param.default_casted_type().unwrap(); | |||||||||||
| let casted_type = | |||||||||||
| default_casted_type.default_cast_for(current_type)?; | |||||||||||
| new_types.push(casted_type); | |||||||||||
| default_casted_type.default_cast_for(cast_origin)?; | |||||||||||
| new_types.push(preserve_encoding( | |||||||||||
| current_type, | |||||||||||
| casted_type, | |||||||||||
| encoding_preservation, | |||||||||||
| )); | |||||||||||
| } else { | |||||||||||
| let hint = if matches!(current_native_type, NativeType::Binary) { | |||||||||||
| "\n\nHint: Binary types are not automatically coerced to String. Use CAST(column AS VARCHAR) to convert Binary data to String." | |||||||||||
|
|
@@ -1214,11 +1252,11 @@ mod tests { | ||||||||||
| use arrow::datatypes::IntervalUnit; | |||||||||||
| use datafusion_common::{ | |||||||||||
| assert_contains, | |||||||||||
| types::{logical_binary, logical_int64}, | |||||||||||
| types::{logical_binary, logical_int64, logical_string}, | |||||||||||
| }; | |||||||||||
| use datafusion_expr_common::{ | |||||||||||
| columnar_value::ColumnarValue, | |||||||||||
| signature::{Coercion, TypeSignatureClass}, | |||||||||||
| signature::{Coercion, EncodingPreservation, TypeSignatureClass}, | |||||||||||
| }; | |||||||||||
|
|
|||||||||||
| #[test] | |||||||||||
|
|
@@ -1831,6 +1869,112 @@ mod tests { | ||||||||||
| Ok(()) | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| #[test] | |||||||||||
| fn test_coercible_dictionary_preserves_encoding() -> Result<()> { | |||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we also have a test for a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added tests for both cases: The behavior now looks like this:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for checking this; I'm a little worried because of the discrepancy here now; is it a big task to try align them? Though we might need to check the UDFs of existing functions that use a non-native typesignatureclass in case they already knew this assumption and had dictionary paths 🤔
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this discrepancy already exists today. Typed non-Native TypeSignatureClass values can pass If we want I did find a couple of Spark compatibility functions, such as From my side, the set of built-ins that may need changes looks bounded, so aligning the behavior seems manageable.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for checking this; I think we can separate that work into a followup PR 👍 |
|||||||||||
| fn dictionary_input( | |||||||||||
| value_type: DataType, | |||||||||||
| coercion: Coercion, | |||||||||||
| ) -> Result<Vec<DataType>> { | |||||||||||
| fields_with_udf( | |||||||||||
| &[Field::new( | |||||||||||
| "field", | |||||||||||
| DataType::Dictionary(Box::new(DataType::Int8), Box::new(value_type)), | |||||||||||
| true, | |||||||||||
| ) | |||||||||||
| .into()], | |||||||||||
| &MockUdf(Signature::coercible(vec![coercion], Volatility::Immutable)), | |||||||||||
| ) | |||||||||||
| .map(|v| v.into_iter().map(|f| f.data_type().clone()).collect()) | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| let coercion = Coercion::new_exact(TypeSignatureClass::Native(logical_string())) | |||||||||||
| .with_encoding_preservation(EncodingPreservation::dictionary()); | |||||||||||
|
|
|||||||||||
| assert_eq!( | |||||||||||
| dictionary_input(DataType::LargeUtf8, coercion.clone())?, | |||||||||||
| vec![DataType::Dictionary( | |||||||||||
| Box::new(DataType::Int8), | |||||||||||
| Box::new(DataType::LargeUtf8), | |||||||||||
| )] | |||||||||||
| ); | |||||||||||
| assert_eq!( | |||||||||||
| dictionary_input( | |||||||||||
| DataType::BinaryView, | |||||||||||
| Coercion::new_implicit( | |||||||||||
| TypeSignatureClass::Native(logical_string()), | |||||||||||
| vec![TypeSignatureClass::Native(logical_binary())], | |||||||||||
| NativeType::String, | |||||||||||
| ) | |||||||||||
| .with_encoding_preservation(EncodingPreservation::dictionary()), | |||||||||||
| )?, | |||||||||||
| vec![DataType::Dictionary( | |||||||||||
| Box::new(DataType::Int8), | |||||||||||
| Box::new(DataType::Utf8View), | |||||||||||
| )] | |||||||||||
| ); | |||||||||||
| // Contrast: without encoding_preservation, Native strips dictionary entirely | |||||||||||
| assert_eq!( | |||||||||||
| dictionary_input( | |||||||||||
| DataType::Int32, | |||||||||||
| Coercion::new_implicit( | |||||||||||
| TypeSignatureClass::Native(logical_int64()), | |||||||||||
| vec![TypeSignatureClass::Integer], | |||||||||||
| NativeType::Int64, | |||||||||||
| ), | |||||||||||
| )?, | |||||||||||
| vec![DataType::Int64] | |||||||||||
| ); | |||||||||||
| // With encoding_preservation, dictionary wrapper is preserved, value coerced | |||||||||||
| assert_eq!( | |||||||||||
| dictionary_input( | |||||||||||
| DataType::Int32, | |||||||||||
| Coercion::new_implicit( | |||||||||||
| TypeSignatureClass::Native(logical_int64()), | |||||||||||
| vec![TypeSignatureClass::Integer], | |||||||||||
| NativeType::Int64, | |||||||||||
| ) | |||||||||||
| .with_encoding_preservation(EncodingPreservation::dictionary()), | |||||||||||
| )?, | |||||||||||
| vec![DataType::Dictionary( | |||||||||||
| Box::new(DataType::Int8), | |||||||||||
| Box::new(DataType::Int64), | |||||||||||
| )] | |||||||||||
| ); | |||||||||||
| // Contrast: without encoding_preservation, non-Native already passes through | |||||||||||
| assert_eq!( | |||||||||||
| dictionary_input( | |||||||||||
| DataType::Int32, | |||||||||||
| Coercion::new_implicit( | |||||||||||
| TypeSignatureClass::Integer, | |||||||||||
| vec![], | |||||||||||
| NativeType::Int64, | |||||||||||
| ), | |||||||||||
| )?, | |||||||||||
| vec![DataType::Dictionary( | |||||||||||
| Box::new(DataType::Int8), | |||||||||||
| Box::new(DataType::Int32), | |||||||||||
| )] | |||||||||||
| ); | |||||||||||
| // With encoding_preservation, same result — no difference for non-Native | |||||||||||
| assert_eq!( | |||||||||||
| dictionary_input( | |||||||||||
| DataType::Int32, | |||||||||||
| Coercion::new_implicit( | |||||||||||
| TypeSignatureClass::Integer, | |||||||||||
| vec![], | |||||||||||
| NativeType::Int64, | |||||||||||
| ) | |||||||||||
| .with_encoding_preservation(EncodingPreservation::dictionary()), | |||||||||||
| )?, | |||||||||||
| vec![DataType::Dictionary( | |||||||||||
| Box::new(DataType::Int8), | |||||||||||
| Box::new(DataType::Int32), | |||||||||||
| )] | |||||||||||
| ); | |||||||||||
|
|
|||||||||||
| Ok(()) | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| #[test] | |||||||||||
| fn test_coercible_run_end_encoded() -> Result<()> { | |||||||||||
| let run_end_encoded = DataType::RunEndEncoded( | |||||||||||
|
|
|||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something to consider is if an enum is best suited for this 🤔
For example, if we want to also include run encoded arrays as well (since they are similar to dictionaries), would this mean two new variants, one just for run arrays and one for dictionary + run arrays?
So I was also thinking perhaps a bitflag approach (or just a struct with boolean flags) could be another approach:
But I don't know how common a use case would be to implement preservation only for dictionaries and not run arrays (or vice versa) so maybe thats overengineering 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering RunEndEncoded and other potential encoding types, I agree that using a struct with boolean flags is a better approach. It keeps the complexity low while making it easier to add future preservation options without introducing enum variants for every possible combination.