You need to enable JavaScript to run this app.
导航
ByteHouse 高并发点查询调优最佳实践
最近更新时间:2025.09.05 11:11:41首次发布时间:2025.08.21 21:09:59
复制全文
我的收藏
有用
有用
无用
无用

适用场景简介

在诸多在线服务的数据分析场景中,例如游戏日志检索、实时用户信息查询等,业务方需要为多个分析服务提供基于相似 SQL 模板(SQL Pattern)的高性能查询接口。这类场景的共同特点是对 QPS(每秒查询率)和查询延迟(Latency)有着极为严苛的要求,并且期望系统具备卓越的线性扩展能力,即查询性能能够随计算资源的增加而同步增长。

ByteHouse 面向高并发点查询的关键特性

从理论的层面剖析,一款能够卓越应对高并发点查询场景的分析型数据库,其核心能力必须建立在以下六大技术基石之上:

  1. 查询优化器估的准
    基于 RBO/CBO 与实时统计信息,为每一个查询请求稳定地生成最高效、最简短的执行计划,从源头上杜绝任何冗余的数据扫描与计算。
  2. 计算引擎足够轻量
    点查询的 SQL 一般不会特别复杂,就要求计算引擎必须能以较小开销构建并调度执行执行流水线(Pipeline),保证 On-CPU 的性能热点在真正的计算逻辑上。
  3. 元数据的并发控制足够轻量
    高并发点查询场景会也会对元数据系统(常见为 Context 或者 Catalog) 造成较大并发访问压力。因此就需要访问元数据时采用无锁或轻量级锁的设计,消除高并发下的锁争用与等待瓶颈,避免一些 Off-CPU 的性能热点。
  4. 存储引擎读的数据足够小
    通过数据聚簇、索引等手段,使物理上真实读取的数据量趋近于计算逻辑上所需的数据量,减少“读放大”效应。
  5. 核心数据与索引常驻内存
    核心数据与索引结构应常驻于内存缓存(如 PageCache/BufferPool)中,将耗时的磁盘 I/O 影响降至最低。
  6. 可预测的线性扩展能力
    在高并发场景下,查询吞吐能力(QPS)必须能随着计算副本(Replica)数量的增加而同步线性增长。

针对这些核心特性,ByteHouse 团队设计并实现了下面的几点优化手段。

优化点

说明

预先注册查询模(Prepared Statement)

支持预先注册查询模板,避免对模版 SQL 的分析和优化的开销。

上下文锁

优化引擎的 context lock(上下文锁),可提高 CPU 利用率。

Sync pulling pipeline

可通过在查询中设置 Settings use_sync_pipeline_executor = 1 开启,控制引擎的执行模式。启用后,使用 sync 模式可提升 CPU 利用率。

Table scan pipeline

可通过在查询中设置 Settings enable_table_scan_build_pipeline_optimization = 1 启用,用于优化 tablescan 的 pipeline 构建开销,可提高 CPU 利用率。

调整 index_granularity

可在查询中设置 Settings index_granularity = 1024。该参数调整了一个 mark 的行数,点查场景将命中更少的数据,可提高 QPS。

使用限制

使用前,请确认您的引擎大于 v2.6。您可以在 ByteHouse 企业版控制台中的集群管理页面,单击集群名称,查看引擎版本。
Image

存储引擎与索引优化

建表最佳实践

在采用 MergeTree 系列表引擎建表时,可通过调节 MergeTree-level settings 来显著优化高并发点查性能,推荐的参数如下,供参考:

CREATE TABLE t1
(
    `id`         UInt64,
    `event_time` DateTime,
    `city`       String,
    `category`   String,
    `amount`     UInt32
)
ENGINE = HaUniqueMergeTree('/clickhouse/default/t1/{shard}', '{replica}')
PARTITION BY toDate(event_time)
ORDER BY (city, category)
UNIQUE KEY id
SETTINGS
    index_granularity                   = 128,
    index_granularity_bytes             = 0,
    enable_disk_based_unique_key_index  = 0,
    load_index_granule_caches           = 1,
    merge_selector_config               = '{
        "name": "dance",
        "max_parts_to_merge_base": 25,
        "min_parts_to_merge_base": 3,
        "min_parts_to_enable_multi_selection": 10
    }';

优化项说明

采用 HaUniqueMergeTree 表引擎

说明

  • 核心优势:支持基于唯一键(UNIQUE KEY union_id)的实时数据更新与删除,解决了社区版的痛点,极大简化了实时分析应用的开发。
  • 性能表现:查询性能与标准 MergeTree 几乎无损,同时单分片写入吞吐可达 10万行/秒以上。

HaUniqueMergeTree 是 ByteHouse 自研的表引擎,既保留了 ClickHouse 高效的查询性能,又支持主键更新。它解决了社区版 ClickHouse 不能支持高效更新操作的痛点,帮助业务更简单地开发实时分析应用。
HaUniqueMergeTree 引擎具有以下特点:

  • 用户可配置唯一键(UNIQUE KEY),提供 upsert 更新写语义,查询时自动返回每个唯一键的最新值;
  • 性能:单 shard 写入吞吐一般可以达到 100k+ rows/s,查询性能与 HaMergeTree 表几乎相同;
  • 唯一键支持设置多字段和表达式,目前支持最多三个字段;
  • 支持分区级别唯一键和表级别唯一键两种模式;
  • 支持自定义版本字段,写入低版本数据时自动忽略;
  • 支持多副本部署,通过主备异步复制保障数据可靠性;
  • 支持根据 UNIQUE KEY 实时删除数据。

如需进一步了解 HaUniqueMergeTree 及使用详情,可参考HaUniqueMergeTree

索引粒度调优

index_granularity(索引粒度)是 ClickHouse 中 MergeTree 表引擎的一个重要概念,它定义了数据被标记成索引的间隔,表数据以 index_granularity 的粒度(默认 8192)被标记成多个小区间,其中每个区间最多 8192 行数据,每个区间标记后形成一个Mark(MarkRange),通过 start 和 end 表示 MarkRange 的具体范围,数据文件也会按照 index_granularity 的间隔粒度生成压缩数据块。
如果检测到明显读放大现象,可以尝试将该参数从基准值下调至 1024 及以下(建议修改到 128),通过增加了索引标记的密度,使得引擎能够更精确地定位数据。
降低 index_granularity 可能带来一定损耗,因为更精细的索引意味着:

  • 更大的索引体积:索引文件会因包含更多标记而增大。
  • 更慢的数据合并速度:后台合并(Merge)过程需要处理更多的索引标记,会消耗更多资源。

然而,在高并发、读密集型的点查询场景下,查询延迟是首要优化目标。因此,牺牲部分写入和合并性能以换取读取性能的巨大提升,是完全合理且高效的优化选择。
同时需要注意:表的 index_granularity 参数一经确定,便无法再次修改
同时,建议您设置index_granularity_bytes=0,禁用 Adaptive Mark,固定 Mark 大小,减少 Mark 定位计算开销。

在内存中维护和构建索引缓存

  • HaUniqueMergeTree的唯一键维护一个 in-memory key index
    设置 enable_disk_based_unique_key_index = 0

    参数

    默认值

    配置说明

    enable_disk_based_unique_key_index

    默认值:1

    0:in-memory mode。在此模式下,系统会在每张唯一键表维护一个 in-memory key index,因此能支撑的数据量受限于内存。
    1:disk-based mode。在此模式下,不限制数据量,但是性能比 in-memory 方式低 10%~30%(插入数据越频繁,导入速度损失越大)。
    推荐选择:建议整体数据量 < 1亿条*集群 Shard 数时,选择 in-memory 模式,其他场景选择 disk-based 模式。

  • 建立内存级别索引缓存
    在数据导入和节点重载时,设置 load_index_granule_caches = 1,主动为每个数据分区(Part)构建索引缓存并加载到内存中,确保查询时索引无需从磁盘读取。
    在执行查询时,需要添加 query-level-setting : enable_skip_index_cache 以使用构建的缓存。

    参数

    推荐值

    配置说明

    load_index_granule_caches

    true

    表在导入 / reload 时是否构建 cache,默认为 false。

针对常用查询键建立单独的索引

  • 针对数值与日期列:利用 minmax 索引进行范围剪裁
    当查询的 WHERE 条件涉及到该列时(例如 WHERE price > 100WHERE event_date = '2023-10-26'),ByteHouse 引擎会首先检查这个 minmax 索引。如果一个颗粒的 [min, max] 范围与查询条件完全不相交,引擎就可以安全地、完全地跳过读取这个数据颗粒的所有数据。

    INDEX number_minmax_idx number TYPE minmax GRANULARITY 1
    

    主键和排序键默认包含此特性,但可以为其他任何查询常用的数值、日期或日期时间列手动添加 minmax 索引,以增强其在过滤条件下的性能。

  • 针对高基数字符串列:采用 bloom_filter 索引进行存在性判断
    Bloom filter 允许对集合成员进行高效的存在性测试,但存在轻微的误报风险。而在索引的使用场景,假阳性并非关键问题,其唯一影响仅是会额外读取少量非必要的数据块。
    针对字符串类型的查询键,我们可在其上建立 bloomfilter 索引,使其更适配高基数字符串列的场景。

    INDEX string_bf_idx string_idx TYPE bloom_filter(0.0001) GRANULARITY 1
    

    bloom_filter(0.0001);这里的参数 0.0001 (即 0.01%) 代表了期望的误报率,值越小,索引越精确,但占用的空间也越大。

如需了解索引的更多细节,请参考HaUniqueMergeTree

Part 合并策略优化

在 ClickHouse 中,每行写入操作都会按照 part 命名规则生成新的分区目录(称为 part)。这意味着即使数据所属的 partition 相同,也不会向已有的分区目录直接添加数据。
MergeTree 表参数 merge_selector_config 能够优化数据合并策略,控制分区数量和合并规模,减少查询时扫描的分区数。

子参数

作用

推荐值

name

选择合并算法,dance 适合向量数据分布,平衡写入与查询性能。

"dance"

max_parts_to_merge_base

单次合并的最大分区数,防止过度合并导致写入阻塞。

25

min_parts_to_merge_base

触发合并的最小分区数,避免小分区长期存在增加扫描开销。

1

max_total_rows_to_merge

单次合并的最大行数,避免构建过大的索引。

20000000

max_parts_to_break

当分区数量超过该阈值时,内置策略会尝试主动拆分大分区,以维持查询性能和合并效率的平衡。

10000

enable_heuristic_to_align_parts

禁用启发式对齐,简化合并逻辑,减少合并耗时。

0(禁用)

因此我们推荐添加如下参数。在数据导入完成后,通过积极的后台合并,确保数据最终能整合进一个单一、最优化的 Part ,减少因 Part 数量过多造成的读放大。

merge_selector_config = '{"name": "dance",  "max_parts_to_merge_base": 25,  "min_parts_to_merge_base": 1,  "min_parts_to_enable_multi_selection": 10}'

计算引擎优化

在完成表引擎的优化后,我们也对计算引擎进行精细化配置,以最大化 CPU 利用率和并发处理能力。

配置 Config.xml

对于高并发点查询,建议通过 config.xml 对服务端优化以下配置。修改配置参数的操作详情请参见修改配置参数

<enable_use_local_lock>1</enable_use_local_lock>
<max_concurrent_queries>20000</max_concurrent_queries> 

参数

作用

推荐值

enable_use_local_lock

将部分全局锁降级为本地锁,减少了高并发查询在访问元数据的竞争等待,直接提升了系统的并发吞吐量。

1(启用)

vector_index_cache_size

降低并发数量。

20000

uncompressed_cache_size

存储解压缩数据的缓存大小,尽量确保热点数据块驻留在内存中。

根据机器内存大小调整

mark_cache_size

mark索引所使用的Cache大小,尽量确保热点数据的索引驻留在内存中。

根据机器内存大小调整

配置 User.xml

在 User.xml 中创建一份用于高并发点查询的 Profile , 修改配置参数的操作详情请参见修改配置参数

<settings>
    <log_queries_min_type>QUERY_FINISH</log_queries_min_type>
    <max_threads>1</max_threads>
    <exchange_source_pipeline_threads>1</exchange_source_pipeline_threads>
    <enable_plan_cache>true</enable_plan_cache>
    <optimize_skip_unused_shards>true</optimize_skip_unused_shards>
    <enable_table_scan_build_pipeline_optimization>true</enable_table_scan_build_pipeline_optimization>
    <use_sync_pipeline_executor>true</use_sync_pipeline_executor>
    <enable_mark_bitmap_index>false</enable_mark_bitmap_index>
    <enable_short_circuit>true</enable_short_circuit>
</settings> 

配置项名称

作用

推荐值

log_queries_min_type

query_log 最小类型的日志,用来限制进入 query_log 的条目。

QUERY_FINISH

max_threads

高并发点查询场景下,单查询单线程运行,降低线程竞争带来的性能损耗。

1

exchange_source_pipeline_threads

配置 exchange 算子读取数据的线程数。

1

enable_plan_cache

开启 plan 缓存,降低执行引擎构建 plan 的代价。

true

optimize_skip_unused_shards

数据按 sharding_key 进行分片时开启。在 SELECT 查询时按 sharding_key 进行过滤时,优化跳过未使用的分片。

true

enable_table_scan_build_pipeline_optimization

降低 TableScan 算子的 pipeline 构建开销,进一步提高 CPU 利用率。

true

use_sync_pipeline_executor

点查询采用同步执行模式,减少异步调度与线程切换开销,提升 CPU 利用率。

true

enable_mark_bitmap_index

在覆盖率较高的情况时规避仅使用唯一键索引读取 mark 的误报率。

false

enable_short_circuit

TopN 最短路径评估,用于优化布尔表达式的执行,如果确定了结果,则不再计算剩下的表达式。

true

使用预处理查询 PREPARED STATEMENT

预处理查询(PREPARED STATEMENT)用于高效地重复执行相同(或相似)SQL 语句,将 SQL 模板(包含占位符 ?)发送给数据库进行一次性的预编译(解析和优化),消除了服务端重复解析和优化 SQL 的开销,功能详情请参见预处理查询 PREPARED STATEMENT

注意

目前预处理查询(PREPARED STATEMENT) 仅支持固定参数数量的 SQL。

使用示例如下:

CREATE PERMANENT PREPARED STATEMENT prep1
AS
SELECT count() 
FROM (SELECT number FROM system.numbers LIMIT 10)
WHERE number < [literal_name: DataType]; 

CREATE PERMANENT PREPARED STATEMENT prep1 AS
SELECT count()
FROM (SELECT number FROM system.numbers LIMIT 10)
WHERE number < [i: UInt32];