jayzhan211 commented on code in PR #25874:
URL: https://github.com/apache/datafusion/pull/25874#discussion_r4365790124


##########
datafusion/functions/src/unicode/substr.rs:
##########
@@ -73,7 +83,8 @@ impl Default for SubstrFunc {
 
 impl SubstrFunc {
     pub fn new() -> Self {
-        let string = 
Coercion::new_exact(TypeSignatureClass::Native(logical_string()));
+        let string = 
Coercion::new_exact(TypeSignatureClass::Native(logical_string()))
+            .with_encoding_preservation(EncodingPreservation::dictionary());

Review Comment:
   Returning a dictionary also makes a high-cardinality GROUP BY on the result 
slower, with or without the guard. With `enable_rle_to_dictionary=true` on 
ClickBench, `SELECT substr("SearchPhrase", 1, 10) s, COUNT(*) c FROM hits WHERE 
"SearchPhrase" <> '' GROUP BY s ORDER BY c DESC LIMIT 10` is 1.2x slower than 
main (1.3x with 2 partitions), although `substr` itself is faster. The time 
goes into repartitioning and re-aggregating dictionary group keys, the same 
cost main already pays when grouping on `"SearchPhrase"` directly (#24822).
   
   Compacting the dictionary when the aggregate slices its output (#26696) 
fixes the 2-partition case, but the default stays at 1.2x because the 
repartition coalescer still rebuilds dictionaries. The next step is to dig into 
that further, and until then we might need to hold this PR.
   



##########
datafusion/functions/src/unicode/substr.rs:
##########
@@ -112,10 +123,42 @@ impl ScalarUDFImpl for SubstrFunc {
     }
 
     fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
-        Ok(arg_types[0].clone())
+        Ok(match &arg_types[0] {
+            DataType::Dictionary(key_type, value_type) if 
!is_wide_key(key_type) => {
+                value_type.as_ref().clone()
+            }
+            other => other.clone(),
+        })
     }
 
     fn invoke_with_args(&self, args: ScalarFunctionArgs) -> 
Result<ColumnarValue> {
+        if let DataType::Dictionary(key_type, value_type) = 
args.args[0].data_type()
+            && !is_wide_key(&key_type)
+        {
+            // Per-row start/length can produce more distinct values than a
+            // narrow key type can index, so decode instead.
+            let mut args = args.args;
+            args[0] = args[0].cast_to(&value_type, None)?;
+            return make_scalar_function(substr, vec![])(&args);
+        }
+
+        if let ColumnarValue::Array(array) = &args.args[0]
+            && matches!(array.data_type(), DataType::Dictionary(_, _))

Review Comment:
   This fast path runs `substr` over every dictionary value, so its cost 
follows the dictionary size rather than the batch size: `filter` keeps the full 
dictionary, and Parquet shares one dictionary across batches. That's what the 
bot's Q22 regression is. On data where `c_phone` is `Dictionary(Int32, Utf8)`, 
Q22 goes from 11.8 ms on main to 15.1 ms here (38.6 → 55.3 ms with one 
partition).
   
   Only taking the fast path when the dictionary isn't larger than the batch 
brings it back to 12.1 ms (40.4 ms), with the same results:
   ```diff
            if let ColumnarValue::Array(array) = &args.args[0]
                && matches!(array.data_type(), DataType::Dictionary(_, _))
   +            && array.as_any_dictionary().values().len() <= array.len()
                && args.args[1..]
   ```
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to