diff --git a/benchmarks/src/main/java/io/druid/benchmark/ExpressionSelectorBenchmark.java b/benchmarks/src/main/java/io/druid/benchmark/ExpressionSelectorBenchmark.java index f46b56b4eff4..e7ada7a7516c 100644 --- a/benchmarks/src/main/java/io/druid/benchmark/ExpressionSelectorBenchmark.java +++ b/benchmarks/src/main/java/io/druid/benchmark/ExpressionSelectorBenchmark.java @@ -124,6 +124,80 @@ public void tearDown() throws Exception } } + @Benchmark + public void timeFormatUsingExpression(Blackhole blackhole) throws Exception + { + final Sequence cursors = new QueryableIndexStorageAdapter(index).makeCursors( + null, + index.getDataInterval(), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "v", + "timestamp_format(__time, 'yyyy-MM-dd')", + ValueType.LONG, + TestExprMacroTable.INSTANCE + ) + ) + ), + Granularities.ALL, + false, + null + ); + + final List results = Sequences.toList( + Sequences.map( + cursors, + cursor -> { + final DimensionSelector selector = cursor.getColumnSelectorFactory().makeDimensionSelector( + new DefaultDimensionSpec("v", "v", ValueType.STRING) + ); + consumeDimension(cursor, selector, blackhole); + return null; + } + ), + new ArrayList<>() + ); + + blackhole.consume(results); + } + + @Benchmark + public void timeFormatUsingExtractionFn(Blackhole blackhole) throws Exception + { + final Sequence cursors = new QueryableIndexStorageAdapter(index).makeCursors( + null, + index.getDataInterval(), + VirtualColumns.EMPTY, + Granularities.ALL, + false, + null + ); + + final List results = Sequences.toList( + Sequences.map( + cursors, + cursor -> { + final DimensionSelector selector = cursor + .getColumnSelectorFactory() + .makeDimensionSelector( + new ExtractionDimensionSpec( + Column.TIME_COLUMN_NAME, + "v", + new TimeFormatExtractionFn("yyyy-MM-dd", null, null, null, false) + ) + ); + + consumeDimension(cursor, selector, blackhole); + return null; + } + ), + new ArrayList<>() + ); + + blackhole.consume(results); + } + @Benchmark public void timeFloorUsingExpression(Blackhole blackhole) throws Exception { diff --git a/processing/src/main/java/io/druid/segment/virtual/ExpressionSelectors.java b/processing/src/main/java/io/druid/segment/virtual/ExpressionSelectors.java index 2c2a3a80a93f..77966bcf906c 100644 --- a/processing/src/main/java/io/druid/segment/virtual/ExpressionSelectors.java +++ b/processing/src/main/java/io/druid/segment/virtual/ExpressionSelectors.java @@ -176,9 +176,16 @@ public static DimensionSelector makeDimensionSelector( final String column = Iterables.getOnlyElement(columns); final ColumnCapabilities capabilities = columnSelectorFactory.getColumnCapabilities(column); - if (capabilities != null - && capabilities.getType() == ValueType.STRING - && capabilities.isDictionaryEncoded()) { + if (column.equals(Column.TIME_COLUMN_NAME)) { + // Optimization for expressions that hit the __time column and nothing else. + // May be worth applying this optimization to all long columns? + return new SingleLongInputCachingExpressionDimensionSelector( + columnSelectorFactory.makeColumnValueSelector(Column.TIME_COLUMN_NAME), + expression + ); + } else if (capabilities != null + && capabilities.getType() == ValueType.STRING + && capabilities.isDictionaryEncoded()) { // Optimization for dimension selectors that wrap a single underlying string column. return new SingleStringInputDimensionSelector( columnSelectorFactory.makeDimensionSelector(new DefaultDimensionSpec(column, column, ValueType.STRING)), diff --git a/processing/src/main/java/io/druid/segment/virtual/SingleLongInputCachingExpressionDimensionSelector.java b/processing/src/main/java/io/druid/segment/virtual/SingleLongInputCachingExpressionDimensionSelector.java new file mode 100644 index 000000000000..85d4bd65c031 --- /dev/null +++ b/processing/src/main/java/io/druid/segment/virtual/SingleLongInputCachingExpressionDimensionSelector.java @@ -0,0 +1,85 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.segment.virtual; + +import com.google.common.base.Preconditions; +import io.druid.java.util.common.ISE; +import io.druid.math.expr.Expr; +import io.druid.math.expr.Parser; +import io.druid.query.monomorphicprocessing.RuntimeShapeInspector; +import io.druid.segment.BaseLongColumnValueSelector; + +public class SingleLongInputCachingExpressionDimensionSelector extends BaseSingleValueDimensionSelector +{ + private final BaseLongColumnValueSelector selector; + private final Expr expression; + private final SingleInputBindings bindings = new SingleInputBindings(); + + // Is this the first row? + private boolean first = true; + + // Last read input value + private long lastInput; + + // Last computed output value + private String lastOutput; + + public SingleLongInputCachingExpressionDimensionSelector( + final BaseLongColumnValueSelector selector, + final Expr expression + ) + { + // Verify expression has just one binding. + if (Parser.findRequiredBindings(expression).size() != 1) { + throw new ISE("WTF?! Expected expression with just one binding"); + } + + this.selector = Preconditions.checkNotNull(selector, "selector"); + this.expression = Preconditions.checkNotNull(expression, "expression"); + } + + @Override + public void inspectRuntimeShape(final RuntimeShapeInspector inspector) + { + inspector.visit("selector", selector); + inspector.visit("expression", expression); + } + + @Override + public String getValue() + { + final long currentInput = selector.getLong(); + + if (first || lastInput != currentInput) { + final String output = eval(currentInput); + lastInput = currentInput; + lastOutput = output; + first = false; + } + + return lastOutput; + } + + private String eval(final long value) + { + bindings.set(value); + return expression.eval(bindings).asString(); + } +}