Pinot中的Dictionary Index
Pinot有几种index,包括dictionary index,forward index,和inverted index。这几种index的联合使用可以实现快速OLAP查询。Dictionary index是最简单的index,并且也为其他两种index提供基础数据。下面以Quickstart中的代码为例简单描述一下dictionary index的创建。
Quickstart从一个csv文件导入数据。这个文件有接近10万条数据。每行有25个列,其中有int,String等类型。
收集各个column的统计数据
遍历数据,对每一行都调用statsCollector.collectRow(row);
while (recordReader.hasNext()) { totalDocs++; long start = System.currentTimeMillis(); GenericRow row = recordReader.next(); long stop = System.currentTimeMillis(); statsCollector.collectRow(row); long stop1 = System.currentTimeMillis(); totalRecordReadTime += (stop - start); totalStatsCollectorTime += (stop1 - stop); }
collectRow的实现:
@Override public void collectRow(GenericRow row) throws Exception { for (final String column : row.getFieldNames()) { columnStatsCollectorMap.get(column).collect(row.getValue(column)); } }
可以看出对一个row的每个column分别调用AbstractColumnStatisticsCollector的collect。
AbstractColumnStatisticsCollector是一个抽象类,有5个子类,分别是Double/Float/Int/Long/String-ColumnPreIndexStatsCollector,自然分别对应double/float/int/long/String五种类型的column。
IntColumnPreIndexStatsCollector的collect:@Override public void collect(Object entry) { if (entry instanceof Object[]) { for (Object e : (Object[]) entry) { intAVLTreeSet.add(((Number) e).intValue()); } if (maxNumberOfMultiValues < ((Object[]) entry).length) { maxNumberOfMultiValues = ((Object[]) entry).length; } updateTotalNumberOfEntries((Object[]) entry); return; } int value = ((Number) entry).intValue(); addressSorted(value); intAVLTreeSet.add(value); }
在这里,intAVLTreeSet还是HashSet,而不是fastutil包里的IntAVLTreeSet。估计Pinot最开始的时候用的TreeSet,后来类型改成HashSet但是变量名没有再改。
addressSorted记录原始数据是否有序:
public void addressSorted(Object entry) { if (isSorted) { if (previousValue != null) { if (((Comparable) entry).compareTo(previousValue) != 0) { numberOfChanges++; } if (((Comparable) entry).compareTo(previousValue) < 0) { prevBiggerThanNextCount++; } if (!entry.equals(previousValue) && previousValue != null) { final Comparable prevValue = (Comparable) previousValue; final Comparable origin = (Comparable) entry; if (origin.compareTo(prevValue) < 0) { isSorted = false; } } } previousValue = entry; } }
seal:
@Override public void seal() { sealed = true; sortedIntList = new Integer[intAVLTreeSet.size()]; intAVLTreeSet.toArray(sortedIntList); Arrays.sort(sortedIntList); if (sortedIntList.length == 0) { min = null; max = null; return; } min = sortedIntList[0]; if (sortedIntList.length == 0) { max = sortedIntList[0]; } else { max = sortedIntList[sortedIntList.length - 1]; } }
对值进行排序,同时还记录最大值和最小值。
生成dictionary index
此时所需要的数据就是包含所有distinct value的数组。对于int/double/float/Long来说,每个数值占用的字节数是已知的。此时调用FixedByteWidthRowColDataFileWriter来把排好序的数组中的每个元素依次写入文件。
switch (spec.getDataType()) { case INT: final FixedByteWidthRowColDataFileWriter intDictionaryWrite = new FixedByteWidthRowColDataFileWriter(dictionaryFile, sortedList.length, 1, V1Constants.Dict.INT_DICTIONARY_COL_SIZE); for (int i = 0; i < sortedList.length; i++) { final int entry = ((Number) sortedList[i]).intValue(); intDictionaryWrite.setInt(i, 0, entry); } intDictionaryWrite.close(); dataReader = FixedByteWidthRowColDataFileReader.forMmap(dictionaryFile, sortedList.length, 1, V1Constants.Dict.INT_DICTIONARY_COL_SIZE); break; ... }
FixedByteWidthRowColDataFileWriter里用到了DirectByteBuffer。
对于String类型来说,因为各个value的长度是不一样的,不能提前判断占用字节数,还需要多一个处理步骤。
case STRING: case BOOLEAN: for (final Object e : sortedList) { String val = e.toString(); int length = val.getBytes(Charset.forName("UTF-8")).length; if (stringColumnMaxLength < length) { stringColumnMaxLength = length; } } final FixedByteWidthRowColDataFileWriter stringDictionaryWrite = new FixedByteWidthRowColDataFileWriter(dictionaryFile, sortedList.length, 1, new int[] { stringColumnMaxLength }); final String[] revised = new String[sortedList.length]; for (int i = 0; i < sortedList.length; i++) { final String toWrite = sortedList[i].toString(); final int padding = stringColumnMaxLength - toWrite.getBytes(Charset.forName("UTF-8")).length; final StringBuilder bld = new StringBuilder(); bld.append(toWrite); for (int j = 0; j < padding; j++) { bld.append(V1Constants.Str.STRING_PAD_CHAR); } revised[i] = bld.toString(); assert (revised[i].getBytes(Charset.forName("UTF-8")).length == stringColumnMaxLength); } Arrays.sort(revised); for (int i = 0; i < revised.length; i++) { stringDictionaryWrite.setString(i, 0, revised[i]); } stringDictionaryWrite.close(); dataReader = FixedByteWidthRowColDataFileReader.forMmap(dictionaryFile, sortedList.length, 1, new int[] { stringColumnMaxLength }); break;
遍历,找出最长的String,然后用STRING_PAD_CHAR(%)来补齐所有其他的字符串。这样,所有的value就都是等长度了。很浪费,但是用空间换性能,还是值得的。
到这里,dictionary index就已经build好了。此时用Sublime打开,可以看到二进制文件里已经有内容了。在dictionary index里查找某个value是否存在的时间复杂度是O(lgn),直接找到第N个位置的值的时间复杂度是O(1)。