Starrocks 主键查询和分区裁剪/bucket裁剪

背景

在之前的Starrocks ShortCircuit短路径的调度,我们只是提到经过了SQL优化模块,就直接进入到了发送BE阶段,其实在这里还有一个很重要的部分,就是分区裁剪和tablet裁剪,经过这两个步骤,才会发送到对应的BE节点上,也从侧面解释了StarRocks 各类索引以及存储位置详解中的主键为啥会在tablet层级(经过了tablet裁剪后,就可以直接通过主键索引获取到对应的数据),这里也顺便了解一下 分区裁剪和tablet裁剪 的实现。

分区裁剪PartitionPruneRule

  • 作用:在"分区(Partition)"维度剔除不相关的粗粒度数据集合。
  • 原理: 该 Rule 提取查询中的过滤条件(例如 WHERE pk = 123 或者 date = '2023-01-01'),并将这些条件(Predicate)与表的分区信息(Range 或 List)进行求交集运算。
  • 在主键查询中的表现:
  1. 如果你的表是非分区表(即整表只有一个默认分区),或者查询条件中未带上分区列,这一步会原封不动,保留全部分区或者这单一分区。
  2. 如果查询条件中包含了分区列(例如通常按时间范围分区,查询包含时间条件),PartitionPruneRule会计算出特定的值落在哪个分区,剔除(Prune)掉所有不符合条件的 Partition。
  • 收益:避免扫描几百到上千个无关的分区元数据和底层文件。
    这里的关键代码是在OptOlapPartitionPruner.prunePartitions:

    public static LogicalOlapScanOperator prunePartitions(LogicalOlapScanOperator logicalOlapScanOperator) {
    List selectedPartitionIds = null;
    OlapTable table = (OlapTable) logicalOlapScanOperator.getTable();

    复制代码
          PartitionInfo partitionInfo = table.getPartitionInfo();
    
          if (partitionInfo.isRangePartition()) {
              selectedPartitionIds = rangePartitionPrune(table, (RangePartitionInfo) partitionInfo, logicalOlapScanOperator);
          } else if (partitionInfo.getType() == PartitionType.LIST) {
              selectedPartitionIds = listPartitionPrune(table, (ListPartitionInfo) partitionInfo, logicalOlapScanOperator);
          }

这里会根据分区类型不同,选择不同的处理路径:

  • Range 分区裁剪 (rangePartitionPrune):内部利用查询中解析出的 where col1 >= 'X' and col1 <= 'Y',构建出一个 Range 过滤器,遍历所有 Partition 的区间 [low, high),找交集,交集不为空则保留这个分区。
  • List 分区裁剪 (listPartitionPrune):如果是 city in ('beijing', 'shanghai') 这种分区,它会直接找到这几个离散的分区 ID
    这里的代码和其他引擎的大同小异,读者自行查看即可

tablet裁剪DistributionPruneRule

  • 作用:在裁剪后剩余的分区中,进一步在"分桶 / 分片(Tablet)"维度精确定位数据。这个规则必须在 PartitionPruneRule 执行完成之后进行。

  • 原理: StarRocks 的分布式存储架构中,每个分区会被切分成多个分桶(Tablet),通常是通过主键 Hash 的方式来切分(即主键表的 hash distribution 配置)。
    DistributionPruneRule根据查询的分布列(Distribution columns,主键表里即对应主键字段)条件进行哈希计算。如果查询谓词给出的是主键的精确匹配等值条件(如 id = 1):规则类底层的 HashDistributionPruner 会对查询值 "1" 运行与导入时一样的数据 Hash 函数,直接计算出该数据必然落在该分区内的**某 1 个特定的 Tablet(Bucket)**上。

  • 在主键查询中的表现: 经过这一步,原本需要在选定分区下扫描的所有 Tablet (通常会有几到几百个),会被硬核裁剪到只剩 1 个指定的 Tablet ID。

  • 收益:彻底消除无需读取的分桶 I/O 开销和分布式执行资源调度的开销。

这里只说核心部分:

以 Hash 分桶为例,关键的代码在于HashDistributionPruner.prune

复制代码
public Collection<Long> prune(int columnId, HashDistributionKey hashKey, int complex) {
        if (columnId == distributionColumns.size()) {
            // compute Hash Key
            long hashValue = hashKey.getHashValue();
            return Lists.newArrayList(tabletIdsInOrder.get((int) ((hashValue & 0xffffffff) % tabletIdsInOrder.size())));
        }
        Column keyColumn = distributionColumns.get(columnId);
...
  public long getHashValue() {
        CRC32 hashValue = new CRC32();
        int i = 0;
        for (LiteralExpr expr : keys) {
            ByteBuffer buffer = expr.getHashValue(types.get(i));
            hashValue.update(buffer.array(), 0, buffer.limit());
            i++;
        }
        return hashValue.getValue();
    }

bucketId是通过如下方法获取的bucketId = (hashValue & 0xffffffff) % bucketNum ,用这个 hashValue 对 bucket 数取模,得到 bucket 序号,然后用该序号去 tabletIdsInOrder 里取对应的 tabletId

而这里的分桶算法和当初写入数据时计算的bucketId是一致的,核心代码在OlapTablePartitionParam::find_tablets():

C++ 复制代码
Status OlapTablePartitionParam::find_tablets(Chunk* chunk, std::vector<OlapTablePartition*>* partitions,
                                             std::vector<uint32_t>* hashes, std::vector<uint8_t>* selection,
                                             std::vector<int>* invalid_row_indexs, int64_t txn_id,
                                             std::vector<std::vector<std::string>>* partition_not_exist_row_values) {
    size_t num_rows = chunk->num_rows();
    partitions->resize(num_rows);

    _compute_hashes(chunk, hashes);
...

void OlapTablePartitionParam::_compute_hashes(const Chunk* chunk, std::vector<uint32_t>* hashes) {
    size_t num_rows = chunk->num_rows();
    hashes->assign(num_rows, 0);

    if (is_hash_distribution()) {
        for (size_t i = 0; i < _distributed_slot_descs.size(); ++i) {
            _distributed_columns[i] = chunk->get_column_by_slot_id(_distributed_slot_descs[i]->id()).get();
            _distributed_columns[i]->crc32_hash(&(*hashes)[0], 0, num_rows);
        }
    } else if (is_random_distribution()) {
        uint32_t r = _rand.Next();
        for (auto i = 0; i < num_rows; ++i) {
            (*hashes)[i] = r++;
        }
    }
}

对于这里的Hash算法,采用的是CRC32,和tablet裁剪的时候是一致的:

  • 单列分桶:hash = CRC32(col)
  • 多列分桶:hash = CRC32( CRC32(0, col1), col2, ... )(按列顺序累积)
相关推荐
小羊没烦恼!2 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
一隅论数智2 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
尧炎科技2 天前
防潮抗变形,就选纯品梅花全桉多层板
大数据
程序员大阳2 天前
副队长大数据教程(5)--集群情况下虚拟机网络配置
大数据·集群·nat·网路
自由能燃气设备3 天前
商用全预混低氮冷凝锅炉免费方案vs付费方案对比+选型避坑指南
大数据·数据库·人工智能
科创致远3 天前
科创致远 ESOP 系统核心效能与实战价值展示
大数据·数据库·人工智能·精益工程
嘉立创FPC苗工3 天前
FPC与机器人的双向赋能,解锁智能装备进化新势能
大数据·人工智能·制造·fpc·电路板
AI职业加油站3 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
龙亘川3 天前
一网统管AI平台民生业务实践:基于城市数字底座赋能公积金业务服务升级
大数据·安全·智慧城市·开源软件·数据可视化·政务
码流子3 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构