-
Notifications
You must be signed in to change notification settings - Fork 3.8k
SQL + Expressions = Best friends forever. #4360
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
Conversation
4afdb10
to
005accc
Compare
👍 from my side for design, i didn't look at implementation but trust that it works |
Note, this could be broken up into different patches, but I had written them together and it would take extra effort to pull them apart since they are somewhat inter-dependent and have a shared motivation. I hope reviewing them together isn't too bad, but if that's not possible, a break-up could be one for each of:
If people feel that is necessary then I'd at least like to have the overall design reviewed first before breaking up the code. |
I vote for breaking this PR into multiple smaller PRs, because I'm not going to review this PR as a whole, but I would review some of the smaller parts. The overall design looks good to me. |
I feel this design is much better than #4207. And I also vote for breaking this PR into smaller ones. It is easy to miss some details when reviewing a large patch as a whole. |
Great proposal and +1 on splitting this ! |
Thanks @fjy @leventov @jihoonson @b-slim for reviewing the overall proposal. I will split it into smaller PRs. |
Broke out one PR into #4365. |
d2d0d48
to
ea7566f
Compare
Broke out #4405. |
Broke out #4406. |
Broke out #4442. |
@fjy @jihoonson @leventov @b-slim this PR is finally down to one commit that only touches the SQL module. I removed the WIP label since it's ready for review again. |
Resolved conflicts by rebase since it looks like nobody has started reviewing yet. |
- Use expressions as a projection layer for anything that can't be expressed using traditional Druid extractionFns. Sometimes they're embedded directly (like "expression" filters, builtin aggregators, or "expression" post-aggregators). Sometimes they're referenced through virtual columns (like dimensionSpecs, which can't innately reference functions of more than one column without the virtual column layer). - Add many new functions and operators, taking advantage of the expression capability (see the querying/sql.md doc). - Improve consistency of constant reduction and of casting by using Druid expressions for this instead of Calcite's RexExecutor.
@gianm thanks. I'll review today. |
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.
@gianm great work! I have two more comments.
- I tested a simple query like
select sum(cast(l_linenumber, integer)) from druid.lineitem
but the result was 0 even thoughselect cast(l_linenumber, integer) from druid.lineitem
returned a valid result.l_linenumber
is defined as a string dimension. What is more weird is that I cannot reproduce this in unit tests. Do you have any idea? - All works around this patch is to use Druid's expression as an internal projection layer for sql. I guess this is to avoid adding huge amount of codes to cover SQL's flexible expressiveness. However, this means, sometimes a sql query is parsed into Calcite's AST, and then some parts of the AST are converted to druid expression strings again. This will cause additional parsing overhead, and if the query size is exceptionally large like a few KB to MB, this can be significant. There will be some applications which uses large queries if we support join. What do you think about this?
docs/content/querying/sql.md
Outdated
grouping expressions or aggregated values. It can only be used together with GROUP BY. | ||
|
||
The ORDER BY clause refers to columns that are present after execution of GROUP BY. It can be used to order the results | ||
based on either grouping expressions or aggregated values. It can only be used together with GROUP BY. |
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.
Interesting. Do we have to loose this restriction in the future?
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.
I'm not aware of any plans to support ORDER BY for non-aggregation queries, but it could be done in principle.
and both will evaluate to true if `col` contains an empty string. Similarly, the expression `COALESCE(col1, col2)` will | ||
return `col2` if `col1` is an empty string. While the `COUNT(*)` aggregator counts all rows, the `COUNT(expr)` | ||
aggregator will count the number of rows where expr is neither null nor the empty string. String columns in Druid are | ||
NULLable. Numeric columns are NOT NULL; if you query a numeric column that is not present in all segments of your Druid |
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.
Numeric columns are NOT NULL;
I'm not sure what this means. Does this mean that null numeric values are internally represented by 0 instead of null?
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.
It means that numeric columns in a Druid table are reported as BIGINT NOT NULL or FLOAT NOT NULL. Of course, some segments might not have the column, and for those segments it's treated as if the column was present and all zeroes.
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.
Hmm. It's still not clear to me. Numeric columns are NOT NULL because null numeric values are casted to 0? FLOAT NOT NULL
sounds like that it works like the NOT NULL constraint.
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.
I guess it's a matter of how you want to think about it.
What is really happening in the Druid runtime is that for columns that the SQL layer believes are numeric, a LongColumnSelector or FloatColumnSelector will get created. If the column doesn't actually exist in a particular segment, the StorageAdapter will generate a selector that returns all zeroes.
Maybe you could think of that as "casting null to zero". I'm not sure if that's the right way to think about it or not.
Does that make sense?
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.
Hmm yeah. I think nulls in sql mean missing values, so casting nulls to zero
makes sense.
docs/content/querying/sql.md
Outdated
The following table describes how SQL types map onto Druid types during query runtime. Casts between two SQL types | ||
that have the same Druid runtime type will have no effect, other than exceptions noted in the table. Casts between two | ||
SQL types that have different Druid runtime types will generate a runtime cast in Druid. If a value cannot be properly | ||
cast to another value, as in `CAST('foo' AS BIGINT)`, the runtime will substitute a default value. |
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.
Is this default value used for null values?
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.
Yes. I added a sentence to clarify this:
NULL values cast to non-nullable types will also be substitued with a default value (for example, nulls cast to numbers will be converted to zeroes).
} | ||
} | ||
|
||
// Verify that all names are properly namespaced. |
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.
Would you elaborate more on namespace? Is it related to the prefix check below?
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.
Yes, it is. I expanded the comment a bit.
return null; | ||
} | ||
|
||
return DruidExpression.fromFunctionCall( |
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.
Why is the simpleExtraction null here unlike in TimeFloorOperatorConversion.applyTimestampFloor()
?
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.
Since we have an extractionFn that can do floor, but not ceil. I added a comment about this.
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
public class TimeFloorOperatorConversion implements SqlOperatorConversion |
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.
Why are there two operatorConversions for the floor operator unlike ceil? Is it necessary to add another operatorConversion to handle dynamic granularity for ceil operator?
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.
It's a special case because for time_floor, we can use an extractionFn if the granularity is known up front (i.e. if it's a literal). There is no such extractionFn for time_ceil. I added a comment about this.
} | ||
|
||
MATH_TYPES = builder.build(); | ||
builder.put(SqlTypeName.BOOLEAN, ExprType.LONG); |
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.
I guess this is because we currently don't support three-valued logic and it will make the future conversion easier. Am I right? If so, please add some comments for this.
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.
Yeah, booleans are treated as two valued in Druid expressions. I'll add a comment.
} | ||
|
||
@Override | ||
public boolean isSortByOrdinal() |
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.
Is this described somewhere in documents? If not, please add it.
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.
It's described in Calcite's documentation. I'll add a comment anyway.
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.
Ah, I mean the document not java doc. Maybe sql.md?
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.
Oh, I see. Sure, I'll add that.
} | ||
|
||
@Override | ||
public boolean isSortByAlias() |
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.
Is this described somewhere in documents? If not, please add it.
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.
It's described in Calcite's documentation. I'll add a comment anyway.
import java.math.BigDecimal; | ||
import java.util.List; | ||
|
||
public class DruidRexExecutor implements RexExecutor |
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.
Would you add some description?
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.
Sure, I added a javadoc.
@jihoonson, thanks for your review! About your top level comments:
Good catch… I'll add a test case for this. My guess is it's because the SQL layer has some code to detect when a cast is "unnecessary" to pass down to Druid, since it will be coerced at runtime anyway. In that case the Druid query uses a direct field access, which is often faster. I believe this is actually not ok when casting string to number for an aggregator, and that's where the problem comes from.
I think it's probably ok, since if a query is very large due to lots of joins, then I bet none of that will make it into Druid expressions anyway. If it's very large due to lots of complex expressions, then yes that will have to be parsed twice, but it should be dwarfed by execution overhead. (complex expressions are probably not cheap to execute at runtime) |
@jihoonson, I pushed an updated patch, including a fix and test case for the problem with casting that you noticed. |
@gianm thanks for the update. Changes look good, but would you check the inspection failure? |
They seem spurious, as they're in sections of the code that I haven't changed. I tried restarting the teamcity build. |
@gianm yeah, it's fine now. The latest patch looks good to me. |
Thanks for the review @jihoonson! |
I'm not planning to review |
This is a follow-up to #4207, but taking a different approach. The line count is higher but that's mostly because this patch also adds important functionality (in particular, post-aggregation projections, and a lot of new functions) that the other patch didn't have.
The motivation is to make more kinds of queries possible. With the changes in this patch, these are all possible in both native queries and Druid SQL queries:
concat(lastName, ", ", firstName)
)x > y
)mydim
but includestrlen(mydim)
as a post-aggregation)mydim
but order bylower(mydim)
)Main changes:
expressed using traditional Druid extractionFns. Sometimes they're
embedded directly (like "expression" filters, builtin aggregators,
or "expression" post-aggregators). Sometimes they're referenced
through virtual columns (like dimensionSpecs, which can't innately
reference functions of more than one column without the virtual
column layer).
expression capability (see the querying/sql.md doc).
using Druid expressions for this instead of Calcite's RexExecutor.