Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/design/broker.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ To determine which nodes to forward queries to, the Broker node first builds a v
Caching
-------

Broker nodes employ a cache with a LRU cache invalidation strategy. The broker cache stores per segment results. The cache can be local to each broker node or shared across multiple nodes using an external distributed cache such as [memcached](http://memcached.org/). Each time a broker node receives a query, it first maps the query to a set of segments. A subset of these segment results may already exist in the cache and the results can be directly pulled from the cache. For any segment results that do not exist in the cache, the broker node will forward the query to the
Broker nodes employ a cache with a LRU cache invalidation strategy. The broker cache stores per-segment results. The cache can be local to each broker node or shared across multiple nodes using an external distributed cache such as [memcached](http://memcached.org/). Each time a broker node receives a query, it first maps the query to a set of segments. A subset of these segment results may already exist in the cache and the results can be directly pulled from the cache. For any segment results that do not exist in the cache, the broker node will forward the query to the
historical nodes. Once the historical nodes return their results, the broker will store those results in the cache. Real-time segments are never cached and hence requests for real-time data will always be forwarded to real-time nodes. Real-time data is perpetually changing and caching the results would be unreliable.

HTTP Endpoints
Expand Down
2 changes: 1 addition & 1 deletion docs/content/design/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Druid is a column store, which means each individual column is stored separately
in that query, and Druid is pretty good about only scanning exactly what it needs for a query.
Different columns can also employ different compression methods. Different columns can also have different indexes associated with them.

Druid indexes data on a per shard (segment) level.
Druid indexes data on a per-shard (segment) level.

## Loading the Data

Expand Down
9 changes: 7 additions & 2 deletions docs/content/querying/segmentmetadataquery.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
layout: doc_page
---
# Segment Metadata Queries
Segment metadata queries return per segment information about:
Segment metadata queries return per-segment information about:

* Cardinality of all columns in the segment
* Min/max values of string type columns in the segment
* Estimated byte size for the segment columns if they were stored in a flat format
* Number of rows stored inside the segment
* Interval the segment covers
Expand Down Expand Up @@ -103,13 +104,17 @@ This is a list of properties that determines the amount of information returned

By default, all analysis types will be used. If a property is not needed, omitting it from this list will result in a more efficient query.

There are four types of column analyses:
There are five types of column analyses:

#### cardinality

* `cardinality` in the result will return the estimated floor of cardinality for each column. Only relevant for
dimension columns.

#### minmax

* Estimated min/max values for each column. Only relevant for dimension columns.

#### size

* `size` in the result will contain the estimated total segment byte size as if the data were stored in text format
Expand Down
126 changes: 114 additions & 12 deletions processing/src/main/java/io/druid/query/metadata/SegmentAnalyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.primitives.Longs;
import com.metamx.common.StringUtils;
import com.metamx.common.guava.Accumulator;
import com.metamx.common.guava.Sequence;
import com.metamx.common.logger.Logger;
import io.druid.granularity.QueryGranularity;
import io.druid.query.dimension.DefaultDimensionSpec;
import io.druid.query.metadata.metadata.ColumnAnalysis;
import io.druid.query.metadata.metadata.SegmentMetadataQuery;
import io.druid.segment.Cursor;
import io.druid.segment.DimensionSelector;
import io.druid.segment.QueryableIndex;
import io.druid.segment.Segment;
import io.druid.segment.StorageAdapter;
Expand All @@ -38,8 +45,10 @@
import io.druid.segment.column.ColumnCapabilitiesImpl;
import io.druid.segment.column.ComplexColumn;
import io.druid.segment.column.ValueType;
import io.druid.segment.data.IndexedInts;
import io.druid.segment.serde.ComplexMetricSerde;
import io.druid.segment.serde.ComplexMetrics;
import org.joda.time.Interval;

import javax.annotation.Nullable;
import java.util.EnumSet;
Expand Down Expand Up @@ -104,7 +113,11 @@ public Map<String, ColumnAnalysis> analyze(Segment segment)
analysis = analyzeNumericColumn(capabilities, length, NUM_BYTES_IN_TEXT_FLOAT);
break;
case STRING:
analysis = analyzeStringColumn(capabilities, column, storageAdapter.getDimensionCardinality(columnName));
if (index != null) {
analysis = analyzeStringColumn(capabilities, column);
} else {
analysis = analyzeStringColumn(capabilities, storageAdapter, columnName);
}
break;
case COMPLEX:
analysis = analyzeComplexColumn(capabilities, column, storageAdapter.getColumnTypeName(columnName));
Expand Down Expand Up @@ -140,6 +153,11 @@ public boolean analyzingCardinality()
return analysisTypes.contains(SegmentMetadataQuery.AnalysisType.CARDINALITY);
}

public boolean analyzingMinMax()
{
return analysisTypes.contains(SegmentMetadataQuery.AnalysisType.MINMAX);
}

private ColumnAnalysis analyzeNumericColumn(
final ColumnCapabilities capabilities,
final int length,
Expand All @@ -161,28 +179,30 @@ private ColumnAnalysis analyzeNumericColumn(
capabilities.hasMultipleValues(),
size,
null,
null,
null,
null
);
}

private ColumnAnalysis analyzeStringColumn(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this not just call the other impl? why 2 impls?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

column is null for incremental index, which make impossible to use other impl.

final ColumnCapabilities capabilities,
@Nullable final Column column,
final int cardinality
final Column column
)
{
long size = 0;

if (column != null && analyzingSize()) {
if (!capabilities.hasBitmapIndexes()) {
return ColumnAnalysis.error("string_no_bitmap");
}
Comparable min = null;
Comparable max = null;

final BitmapIndex bitmapIndex = column.getBitmapIndex();
if (cardinality != bitmapIndex.getCardinality()) {
return ColumnAnalysis.error("bitmap_wrong_cardinality");
}
if (!capabilities.hasBitmapIndexes()) {
return ColumnAnalysis.error("string_no_bitmap");
}

final BitmapIndex bitmapIndex = column.getBitmapIndex();
final int cardinality = bitmapIndex.getCardinality();

if (analyzingSize()) {
for (int i = 0; i < cardinality; ++i) {
String value = bitmapIndex.getValue(i);
if (value != null) {
Expand All @@ -191,11 +211,91 @@ private ColumnAnalysis analyzeStringColumn(
}
}

if (analyzingMinMax() && cardinality > 0) {
min = Strings.nullToEmpty(bitmapIndex.getValue(0));
max = Strings.nullToEmpty(bitmapIndex.getValue(cardinality - 1));
}

return new ColumnAnalysis(
capabilities.getType().name(),
capabilities.hasMultipleValues(),
size,
analyzingCardinality() ? cardinality : 0,
min,
max,
null
);
}

private ColumnAnalysis analyzeStringColumn(
final ColumnCapabilities capabilities,
final StorageAdapter storageAdapter,
final String columnName
)
{
int cardinality = 0;
long size = 0;

Comparable min = null;
Comparable max = null;

if (analyzingCardinality()) {
cardinality = storageAdapter.getDimensionCardinality(columnName);
}

if (analyzingSize()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im not sure i understand why this needs to be reimplemented

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot understand why those kind of patches, returning zero for size for incremental index, allowed to be committed but it's already done and I've just wanted it to return some value which is more useful than zero.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@navis mostly because "size" is assumed by many committers to not be very useful, as the docs on it are vague enough that it is not really clear what it's supposed to be doing. Are you getting value from it? If so maybe we should also tighten up the docs & behavior & tests to make it a really useful thing.

or, we could also deprecate it if we are not aware of anyone getting value from it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@navis also, incremental improvement- at first introduction, segmentMetadata did not return any info at all, for any analysis type, for incremental index. that has been improved over time for various analysis types.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gianm Thanks for the detailed explanation. I just surprised to see the semantic of query can be changed so easily. It would be better to return -1 or something if we cannot calculate it, not omitting some part of the value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we replace the previous impl? I don' think anyone is getting value from the "size" field right now. I agree the previous "size" is not useful at all

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@navis I agree in general for general queries, the "size" field here is just a little "special" due to its history and generally being under-specified as to what it actually should be doing. I think in the long run we should either fully specify what it does, or remove it if nobody cares enough to do that.

(my vote is for removing it although that would probably have to wait to 0.10 at this point)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, I have nothing against adding this "size" functionality in this PR. It's at least better than what we're doing now.

Although the under-specification remains a problem imo.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for discussion, I think this PR Is fine:

How useful is the current method of size calculation?

If we keep the "size" functionality, is the current "flat file" size estimate more or less useful than returning the actual on-disk size of the segments (or expected size, for IncrementalIndexes)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think the actual on disk size is more useful.

FYI the original use case was for a somewhat esoteric metric that was needed at Metamarkets a long time ago, but as far as I know is no longer needed there. So maybe one of the Metamarketers can chime in on that.

final long start = storageAdapter.getMinTime().getMillis();
final long end = storageAdapter.getMaxTime().getMillis();

final Sequence<Cursor> cursors =
storageAdapter.makeCursors(null, new Interval(start, end), QueryGranularity.ALL, false);

size = cursors.accumulate(
0L,
new Accumulator<Long, Cursor>()
{
@Override
public Long accumulate(Long accumulated, Cursor cursor)
{
DimensionSelector selector = cursor.makeDimensionSelector(
new DefaultDimensionSpec(
columnName,
columnName
)
);
if (selector == null) {
return accumulated;
}
long current = accumulated;
while (!cursor.isDone()) {
final IndexedInts vals = selector.getRow();
for (int i = 0; i < vals.size(); ++i) {
final String dimVal = selector.lookupName(vals.get(i));
if (dimVal != null && !dimVal.isEmpty()) {
current += StringUtils.toUtf8(dimVal).length;
}
}
cursor.advance();
}

return current;
}
}
);
}

if (analyzingMinMax()) {
min = storageAdapter.getMinValue(columnName);
max = storageAdapter.getMaxValue(columnName);
}

return new ColumnAnalysis(
capabilities.getType().name(),
capabilities.hasMultipleValues(),
size,
cardinality,
min,
max,
null
);
}
Expand All @@ -218,7 +318,7 @@ private ColumnAnalysis analyzeComplexColumn(

final Function<Object, Long> inputSizeFn = serde.inputSizeFn();
if (inputSizeFn == null) {
return new ColumnAnalysis(typeName, hasMultipleValues, 0, null, null);
return new ColumnAnalysis(typeName, hasMultipleValues, 0, null, null, null, null);
}

final int length = column.getLength();
Expand All @@ -232,6 +332,8 @@ private ColumnAnalysis analyzeComplexColumn(
hasMultipleValues,
size,
null,
null,
null,
null
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

import java.util.Objects;

Expand All @@ -32,13 +33,15 @@ public class ColumnAnalysis

public static ColumnAnalysis error(String reason)
{
return new ColumnAnalysis("STRING", false, -1, null, ERROR_PREFIX + reason);
return new ColumnAnalysis("STRING", false, -1, null, null, null, ERROR_PREFIX + reason);
}

private final String type;
private final boolean hasMultipleValues;
private final long size;
private final Integer cardinality;
private final Comparable minValue;
private final Comparable maxValue;
private final String errorMessage;

@JsonCreator
Expand All @@ -47,13 +50,17 @@ public ColumnAnalysis(
@JsonProperty("hasMultipleValues") boolean hasMultipleValues,
@JsonProperty("size") long size,
@JsonProperty("cardinality") Integer cardinality,
@JsonProperty("minValue") Comparable minValue,
@JsonProperty("maxValue") Comparable maxValue,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a serde test checking for this?

@JsonProperty("errorMessage") String errorMessage
)
{
this.type = type;
this.hasMultipleValues = hasMultipleValues;
this.size = size;
this.cardinality = cardinality;
this.minValue = minValue;
this.maxValue = maxValue;
this.errorMessage = errorMessage;
}

Expand Down Expand Up @@ -81,6 +88,20 @@ public Integer getCardinality()
return cardinality;
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonProperty
public Comparable getMinValue()
{
return minValue;
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonProperty
public Comparable getMaxValue()
{
return maxValue;
}

@JsonProperty
public String getErrorMessage()
{
Expand Down Expand Up @@ -113,21 +134,29 @@ public ColumnAnalysis fold(ColumnAnalysis rhs)
Integer cardinality = getCardinality();
final Integer rhsCardinality = rhs.getCardinality();
if (cardinality == null) {

cardinality = rhsCardinality;
} else {
if (rhsCardinality != null) {
cardinality = Math.max(cardinality, rhsCardinality);
}
} else if (rhsCardinality != null) {
cardinality = Math.max(cardinality, rhsCardinality);
}

return new ColumnAnalysis(
type,
hasMultipleValues || rhs.isHasMultipleValues(),
size + rhs.getSize(),
cardinality,
null
);
final boolean multipleValues = hasMultipleValues || rhs.isHasMultipleValues();

Comparable newMin = choose(minValue, rhs.minValue, false);
Comparable newMax = choose(maxValue, rhs.maxValue, true);

return new ColumnAnalysis(type, multipleValues, size + rhs.getSize(), cardinality, newMin, newMax, null);
}

private <T extends Comparable> T choose(T obj1, T obj2, boolean max)
{
if (obj1 == null) {
return max ? obj2 : null;
}
if (obj2 == null) {
return max ? obj1 : null;
}
int compare = max ? obj1.compareTo(obj2) : obj2.compareTo(obj1);
return compare > 0 ? obj1 : obj2;
}

@Override
Expand All @@ -138,6 +167,8 @@ public String toString()
", hasMultipleValues=" + hasMultipleValues +
", size=" + size +
", cardinality=" + cardinality +
", minValue=" + minValue +
", maxValue=" + maxValue +
", errorMessage='" + errorMessage + '\'' +
'}';
}
Expand All @@ -156,12 +187,14 @@ public boolean equals(Object o)
size == that.size &&
Objects.equals(type, that.type) &&
Objects.equals(cardinality, that.cardinality) &&
Objects.equals(minValue, that.minValue) &&
Objects.equals(maxValue, that.maxValue) &&
Objects.equals(errorMessage, that.errorMessage);
}

@Override
public int hashCode()
{
return Objects.hash(type, hasMultipleValues, size, cardinality, errorMessage);
return Objects.hash(type, hasMultipleValues, size, cardinality, minValue, maxValue, errorMessage);
}
}
Loading