Skip to content

Conversation

gianm
Copy link
Contributor

@gianm gianm commented Jun 2, 2017

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:

  • Grouping by functions of multiple columns (e.g. group by concat(lastName, ", ", firstName))
  • Filtering on functions of multiple columns (e.g. filter where x > y)
  • Post-aggregation projections on dimensions (e.g. group by mydim but include strlen(mydim) as a post-aggregation)
  • Ordering by post-aggregation projections (e.g. group by mydim but order by lower(mydim))

Main changes:

  • 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 gianm added this to the 0.10.2 milestone Jun 2, 2017
@gianm gianm force-pushed the sql-expression branch 2 times, most recently from 4afdb10 to 005accc Compare June 2, 2017 20:36
@fjy
Copy link
Contributor

fjy commented Jun 2, 2017

👍 from my side for design, i didn't look at implementation but trust that it works

@gianm gianm force-pushed the sql-expression branch from 005accc to cbf15d1 Compare June 2, 2017 20:44
@gianm
Copy link
Contributor Author

gianm commented Jun 2, 2017

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:

  • adjusting expression null handling
  • adding the ExprMacroTable concept
  • adding string return for "expression" post-aggregator and virtual column
  • adding the "expression" filter
  • adding the new expression functions and macros
  • each of the two query changes
  • all the SQL changes together

If people feel that is necessary then I'd at least like to have the overall design reviewed first before breaking up the code.

@leventov
Copy link
Member

leventov commented Jun 2, 2017

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.

@jihoonson
Copy link
Contributor

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.

@b-slim
Copy link
Contributor

b-slim commented Jun 5, 2017

Great proposal and +1 on splitting this !

@gianm
Copy link
Contributor Author

gianm commented Jun 5, 2017

Thanks @fjy @leventov @jihoonson @b-slim for reviewing the overall proposal. I will split it into smaller PRs.

@gianm gianm added the WIP label Jun 5, 2017
@gianm
Copy link
Contributor Author

gianm commented Jun 5, 2017

Broke out one PR into #4365.

@gianm gianm force-pushed the sql-expression branch from f9f9a19 to a512ae1 Compare June 6, 2017 00:37
@gianm
Copy link
Contributor Author

gianm commented Jun 6, 2017

Broke out #4366 and #4367 too.

@gianm gianm force-pushed the sql-expression branch 7 times, most recently from d2d0d48 to ea7566f Compare June 9, 2017 23:25
@gianm gianm mentioned this pull request Jun 14, 2017
@gianm
Copy link
Contributor Author

gianm commented Jun 14, 2017

Broke out #4405.

@gianm
Copy link
Contributor Author

gianm commented Jun 15, 2017

Broke out #4406.

@gianm
Copy link
Contributor Author

gianm commented Jun 22, 2017

Broke out #4442.

@leventov leventov modified the milestones: 0.11.0, 0.10.2 Jun 26, 2017
@gianm gianm removed the WIP label Jun 28, 2017
@gianm
Copy link
Contributor Author

gianm commented Jun 28, 2017

@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.

@gianm
Copy link
Contributor Author

gianm commented Jun 29, 2017

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.
@jihoonson
Copy link
Contributor

@gianm thanks. I'll review today.

Copy link
Contributor

@jihoonson jihoonson left a 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 though select 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?

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@gianm gianm Jun 30, 2017

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?

Copy link
Contributor

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.

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.
Copy link
Contributor

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?

Copy link
Contributor Author

@gianm gianm Jun 30, 2017

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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(
Copy link
Contributor

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()?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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()
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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()
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@gianm
Copy link
Contributor Author

gianm commented Jun 30, 2017

@jihoonson, thanks for your review! About your top level comments:

I tested a simple query like select sum(cast(l_linenumber, integer)) from druid.lineitem but the result was 0 even though select 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?

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.

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?

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)

@gianm
Copy link
Contributor Author

gianm commented Jul 5, 2017

@jihoonson, I pushed an updated patch, including a fix and test case for the problem with casting that you noticed.

@jihoonson
Copy link
Contributor

@gianm thanks for the update. Changes look good, but would you check the inspection failure?

@gianm
Copy link
Contributor Author

gianm commented Jul 6, 2017

They seem spurious, as they're in sections of the code that I haven't changed. I tried restarting the teamcity build.

@jihoonson
Copy link
Contributor

@gianm yeah, it's fine now. The latest patch looks good to me.

@gianm
Copy link
Contributor Author

gianm commented Jul 6, 2017

Thanks for the review @jihoonson!

@fjy @leventov @b-slim, any further comments?

@leventov
Copy link
Member

leventov commented Jul 6, 2017

I'm not planning to review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants