Pinot中的Dictionary Index源码分析

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等类型。

  1. 收集各个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];
        }
    }

    对值进行排序,同时还记录最大值和最小值。

  2. 生成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)。

你可能感兴趣的:(Pinot中的Dictionary Index源码分析)