You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Implement a scalar function for creating ScalarValue::Map #11268
we can follow the syntax from DuckDB, map_from_entries, and convert the syntax MAP {k: v, ...} to function map_from_entries later
It's a good idea. I also considered how to implement SQL syntax to support this scalar function, but I had no idea how to do it. It seems UserDefinedSQLPlanner is what I need. It's helpful for me.
I will try to implement the syntax for MAP { x : 1 }. I guess I can follow how the RawDictionaryExpr works.
Besides the SQL syntax, I'm considering what the scalar function would look like. Should we consider the data layout of arrow::MapArray? If needed, I think its parameters could be different from DuckDB's map_from_entries because the data layout of arrow::MapArray allows many slots for a map value, like:
It's a good idea. I also considered how to implement SQL syntax to support this scalar function, but I had no idea how to do it. It seems UserDefinedSQLPlanner is what I need. It's helpful for me.
I will try to implement the syntax for MAP { x : 1 }. I guess I can follow how the RawDictionaryExpr works.
👍 ❤️
Besides the SQL syntax, I'm considering what the scalar function would look like. Should we consider the data layout of arrow::MapArray? If needed, I think its parameters could be different from DuckDB's map_from_entries because the data layout of arrow::MapArray allows many slots for a map value, like:
Implement the rewrite from the map literal to make_map
Notes / caveats
I am not sure how complete the MapArray support in arrow-rs is, so it wouldn't surprise me if we hit various unimplemented errors when we tried to do basic operations like compare maps or sort them. I don't think that is a reason not to proceed here, but I wanted to point it out
It may not fit the syntax you propose. I will follow the named_struct model to implement another one.
The reason I didn't use MapBuilder is that I can't find a smart way to get the corresponding ArrayBuilder for the arguments ( MapBuilder requires two initial builders for the key and value ). If we have some existing solutions for this issue, I think using MapBuilder is better.
Implementation suggestion
I recommend doing this in 2 PRs:
Implement test the function (e.g. make_map)
Implement the rewrite from the map literal to make_map
I agree with you. I will implement the function in this issue and have a follow-up one for the map literal.
Notes / caveats
I am not sure how complete the MapArray support in arrow-rs is, so it wouldn't surprise me if we hit various
unimplemented errors when we tried to do basic operations like compare maps or sort them. I don't think that is a reason not to proceed here, but I wanted to point it out
Thanks for reminding this problem. I think if I occurred this kind of problem, I might implement the required functions on the DataFusion side first.
The reason I didn't use MapBuilder is that I can't find a smart way to get the corresponding ArrayBuilder for the arguments ( MapBuilder requires two initial builders for the key and value ). If we have some existing solutions for this issue, I think using MapBuilder is better.
The datatype of key and value should be known before invoke, so we can get the corresponding Builder based on the data type
The datatype of key and value should be known before invoke, so we can get the corresponding Builder based on the data type
Indeed. I'll try to use MapBuilder. Thanks :)
btw, there is probably a downside for using MapBuilder, since we need to create different builder for different types (therefore many macros) which easily causes code bloat. We have similar issue while building array function before #7988. I'm not sure what is the best approach yet (probably current draft function is the best), worth to explore it.
btw, there is probably a downside for using MapBuilder, since we need to create different builder for different types (therefore many macros) which easily causes code bloat. We have similar issue while building array function before #7988. I'm not sure what is the best approach yet (probably current draft function is the best), worth to explore it.
Besides the code bloat, I'm also concerned about the performance. Because MapBuilder doesn't provide batch append (I think it's not its main purpose), we can only append data entry by entry. I might expect it to have O(n) complexity.
I haven't tried any optimizations for them. The results are as follows:
// The first run is for warm-upGenerating1 random key-value pair
Time elapsed in make_map() is:449.737µs
Time elapsed inmake_map_batch() is:129.096µs
Generating10 random key-value pairs
Time elapsed inmake_map() is:24.932µs
Time elapsed inmake_map_batch() is:18.007µs
Generating50 random key-value pairs
Time elapsed inmake_map() is:34.587µs
Time elapsed inmake_map_batch() is:17.037µs
Generating100 random key-value pairs
Time elapsed inmake_map() is:66.02µs
Time elapsed inmake_map_batch() is:19.027µs
Generating500 random key-value pairs
Time elapsed inmake_map() is:586.476µs
Time elapsed inmake_map_batch() is:63.832µs
Generating1000 random key-value pairs
Time elapsed inmake_map() is:722.771µs
Time elapsed inmake_map_batch() is:47.455µs
The scenario for this function may not be for large maps, but I think the MapArray::from() version always has better performance and won't cause code bloat. I prefer this version.
btw, there is probably a downside for using MapBuilder, since we need to create different builder for different types (therefore many macros) which easily causes code bloat. We have similar issue while building array function before #7988. I'm not sure what is the best approach yet (probably current draft function is the best), worth to explore it.
Besides the code bloat, I'm also concerned about the performance. Because MapBuilder doesn't provide batch append (I think it's not its main purpose), we can only append data entry by entry. I might expect it to have O(n) complexity.
I haven't tried any optimizations for them. The results are as follows:
// The first run is for warm-upGenerating1 random key-value pair
Time elapsed in make_map() is:449.737µs
Time elapsed inmake_map_batch() is:129.096µs
Generating10 random key-value pairs
Time elapsed inmake_map() is:24.932µs
Time elapsed inmake_map_batch() is:18.007µs
Generating50 random key-value pairs
Time elapsed inmake_map() is:34.587µs
Time elapsed inmake_map_batch() is:17.037µs
Generating100 random key-value pairs
Time elapsed inmake_map() is:66.02µs
Time elapsed inmake_map_batch() is:19.027µs
Generating500 random key-value pairs
Time elapsed inmake_map() is:586.476µs
Time elapsed inmake_map_batch() is:63.832µs
Generating1000 random key-value pairs
Time elapsed inmake_map() is:722.771µs
Time elapsed inmake_map_batch() is:47.455µs
The scenario for this function may not be for large maps, but I think the MapArray::from() version always has better performance and won't cause code bloat. I prefer this version.
make_map_batch version is way faster, we should go with that one.
Upd: I tried to remove clone in MapBuilder version, it still seems slower than manually constructed one
Here's the revised version of the text with improved grammar:
Thanks for your effort. I think make_map_batch is faster because it accepts two known-type arrays. We only need to rearrange the layout for the MapArray, which requires some Arc cloning. It's a good way to implement the DuckDB syntax:
SELECT MAP(['a', 'b', 'c'], [1, 2, 3]);
However, I found it hard to provide a function like make_map(k1, v1, k2, v2 ...) that is both efficient and simple. I tried two solutions for it:
Aggregate the keys and values, then pass them to make_map_batch:
The codebase is simple, but the performance is terrible. The bottleneck is ColumnarValue::values_to_arrays. This function is convenient but slow. Actually, this version is slower than the MapBuilder one. :(
MapBuilder:
It's the faster solution (compared to the above one) for make_map(k1, v1, k2, v2 ...), but the codebase would become complex and large. In my testing code, I hardcoded the types of keys and values to get the value and create the builder. In a real scenario, we would need to match all types and builders to use MapBuilder. I'm not sure if it's a good approach.
For performance purposes: map(['a', 'b', 'c'], [1, 2, 3])
For user-friendly purposes: make_map(k1, v1, k2, v2 ...)
For the user-friendly one, I'll choose the first solution to keep the codebase simpler. I found that named_struct also uses ColumnarValue::values_to_arrays to do something similar.
let arrays = ColumnarValue::values_to_arrays(&values)?;
We could suggest that users who intend to get better performance or create a large map use the map() function.
Some appendixs for the benchmark result:
// first run for warm-up
Generating 1 random key-value pairs
Time elapsed in make_map() is: 391.618µs
Time elapsed in make_map_from() is: 224.568µs
Time elapsed in make_map_batch() is: 18.173µs
Generating 2 random key-value pairs
Time elapsed in make_map() is: 19.848µs
Time elapsed in make_map_from() is: 27.726µs
Time elapsed in make_map_batch() is: 32.628µs
Generating 4 random key-value pairs
Time elapsed in make_map() is: 18.495µs
Time elapsed in make_map_from() is: 37.102µs
Time elapsed in make_map_batch() is: 16.399µs
Generating 8 random key-value pairs
Time elapsed in make_map() is: 20.785µs
Time elapsed in make_map_from() is: 48.596µs
Time elapsed in make_map_batch() is: 42.98µs
Generating 10 random key-value pairs
Time elapsed in make_map() is: 26.3µs
Time elapsed in make_map_from() is: 84.601µs
Time elapsed in make_map_batch() is: 15.55µs
// it's not so common for a map use case
Generating 50 random key-value pairs
Time elapsed in make_map() is: 35.751µs
Time elapsed in make_map_from() is: 332.336µs
Time elapsed in make_map_batch() is: 52.425µs
Generating 100 random key-value pairs
Time elapsed in make_map() is: 178.775µs
Time elapsed in make_map_from() is: 508.017µs
Time elapsed in make_map_batch() is: 184.452µs
Generating 1000 random key-value pairs
Time elapsed in make_map() is: 386.535µs
Time elapsed in make_map_from() is: 3.125679ms
Time elapsed in make_map_batch() is: 45.549µs
Given the code, it seems you clone ColumnValue and push to VecDeque, we can avoid the clone and use Vec instead of VecDeque, since we don't need push_front.
Given the code, it seems you clone ColumnValue and push to VecDeque, we can avoid the clone and use Vec instead of VecDeque, since we don't need push_front.
Sounds great. I will try it.
You only take the first element, but shouldn't we process all the kv?
Activity
goldmedal commentedon Jul 4, 2024
take
jayzhan211 commentedon Jul 5, 2024
https://duckdb.org/docs/sql/data_types/map.html
we can follow the syntax from DuckDB,
map_from_entries, and convert the syntaxMAP {k: v, ...}to functionmap_from_entrieslateralamb commentedon Jul 5, 2024
I like this idea very mch
This is also consistent with how
struct/named_structworkHere is
named_structfunction: https://github.com/apache/datafusion/blob/main/datafusion/functions/src/core/named_struct.rsHere is the syntax support
datafusion/datafusion/functions/src/core/planner.rs
Lines 27 to 40 in b46d5b7
goldmedal commentedon Jul 5, 2024
Thanks @alamb @jayzhan211
It's a good idea. I also considered how to implement SQL syntax to support this scalar function, but I had no idea how to do it. It seems
UserDefinedSQLPlanneris what I need. It's helpful for me.I will try to implement the syntax for
MAP { x : 1 }. I guess I can follow how theRawDictionaryExprworks.Besides the SQL syntax, I'm considering what the scalar function would look like. Should we consider the data layout of
arrow::MapArray? If needed, I think its parameters could be different from DuckDB'smap_from_entriesbecause the data layout ofarrow::MapArrayallows many slots for a map value, like:The function would be used like this:
If we don't need to consider it, we could only allow one element for
MapArray. It would be similar to hownamed_struct()is used:The layout would be
What do you think?
alamb commentedon Jul 5, 2024
👍 ❤️
I think following the model for
named_structmakes the most sense hereFrom my reading of https://docs.rs/arrow/latest/arrow/array/struct.MapArray.html the idea is that each element of a
MapArrayis itself a map with some arbitrary number of key/value pairsI think we could use https://docs.rs/arrow/latest/arrow/array/builder/struct.MapBuilder.html to create the actual array/value
Syntax Proposal
Maybe we could use a function like
make_mapto follow the naming convention ofmake_array: https://datafusion.apache.org/user-guide/sql/scalar_functions.html#make-arrayImplementation suggestion
I recommend doing this in 2 PRs:
make_map)make_mapNotes / caveats
I am not sure how complete the MapArray support in arrow-rs is, so it wouldn't surprise me if we hit various unimplemented errors when we tried to do basic operations like compare maps or sort them. I don't think that is a reason not to proceed here, but I wanted to point it out
goldmedal commentedon Jul 5, 2024
I have finished a draft function that followed my original design yesterday. I follow how arrow-rs to create a map from strings.
It could be used like DuckDB syntax that accepts two lists to construct a map. (In my design, it could accept many key-value array pairs)
It may not fit the syntax you propose. I will follow the
named_structmodel to implement another one.The reason I didn't use
MapBuilderis that I can't find a smart way to get the corresponding ArrayBuilder for the arguments ( MapBuilder requires two initial builders for the key and value ). If we have some existing solutions for this issue, I think usingMapBuilderis better.I agree with you. I will implement the function in this issue and have a follow-up one for the map literal.
Thanks for reminding this problem. I think if I occurred this kind of problem, I might implement the required functions on the DataFusion side first.
jayzhan211 commentedon Jul 6, 2024
The datatype of key and value should be known before
invoke, so we can get the corresponding Builder based on the data typegoldmedal commentedon Jul 6, 2024
Indeed. I'll try to use MapBuilder. Thanks :)
jayzhan211 commentedon Jul 6, 2024
btw, there is probably a downside for using MapBuilder, since we need to create different builder for different types (therefore many macros) which easily causes code bloat. We have similar issue while building array function before #7988. I'm not sure what is the best approach yet (probably current draft function is the best), worth to explore it.
goldmedal commentedon Jul 6, 2024
Besides the code bloat, I'm also concerned about the performance. Because
MapBuilderdoesn't provide batch append (I think it's not its main purpose), we can only append data entry by entry. I might expect it to have O(n) complexity.I did some benchmarks to compare the
MapBuilderversion and theMapArray::from()version.https://github.com/goldmedal/datafusion/blob/95198c17eddd0a708e61e66076a383d5671646b7/datafusion/functions/src/core/map.rs#L132
I haven't tried any optimizations for them. The results are as follows:
The scenario for this function may not be for large maps, but I think the
MapArray::from()version always has better performance and won't cause code bloat. I prefer this version.What do you think? @alamb @jayzhan211
jayzhan211 commentedon Jul 6, 2024
make_map_batchversion is way faster, we should go with that one.Upd: I tried to remove clone in MapBuilder version, it still seems slower than manually constructed one
goldmedal commentedon Jul 6, 2024
Thanks for your effort. I think
make_map_batchis faster because it accepts two known-type arrays. We only need to rearrange the layout for theMapArray, which requires someArccloning. It's a good way to implement the DuckDB syntax:However, I found it hard to provide a function like
make_map(k1, v1, k2, v2 ...)that is both efficient and simple. I tried two solutions for it:Aggregate the keys and values, then pass them to
make_map_batch:The codebase is simple, but the performance is terrible. The bottleneck is
ColumnarValue::values_to_arrays. This function is convenient but slow. Actually, this version is slower than theMapBuilderone. :(MapBuilder:
It's the faster solution (compared to the above one) for
make_map(k1, v1, k2, v2 ...), but the codebase would become complex and large. In my testing code, I hardcoded the types of keys and values to get the value and create the builder. In a real scenario, we would need to match all types and builders to use MapBuilder. I'm not sure if it's a good approach.In conclusion, I will provide two functions:
map(['a', 'b', 'c'], [1, 2, 3])make_map(k1, v1, k2, v2 ...)For the user-friendly one, I'll choose the first solution to keep the codebase simpler. I found that
named_structalso usesColumnarValue::values_to_arraysto do something similar.datafusion/datafusion/functions/src/core/named_struct.rs
Line 60 in 08c5345
We could suggest that users who intend to get better performance or create a large map use the
map()function.Some appendixs for the benchmark result:
jayzhan211 commentedon Jul 7, 2024
what is your code for benchmarking?
Given the code, it seems you clone ColumnValue and push to VecDeque, we can avoid the clone and use Vec instead of VecDeque, since we don't need
push_front.You only take the first element, but shouldn't we process all the kv?
goldmedal commentedon Jul 7, 2024
Here's my testing code.
https://github.com/goldmedal/datafusion/blob/feature/11268-scalar-funciton-map-v2/datafusion/functions/src/core/map.rs
Sounds great. I will try it.
oops. It's an unfinished work. I forgot it. Yes, you're right.
We should take all. The key and value vec would be
I think I need to contact them to be one key array and one value array.
jayzhan211 commentedon Jul 7, 2024
You can also do the benchmarking in
datafusion/functions/benches/map.rs.Add this to cargo.toml, and run with cargo bench --bench map