ClickHouse系列之查询优化

本文详细讲解了如何通过EXPLAIN查询计划、语法优化、建表策略来提升ClickHouse的查询性能。涉及数据类型选择、分区与索引设置、 COUNT优化、谓词下推等关键优化技术,以及单表查询、IN代替JOIN和分布式表使用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. Explain查询计划查看

// 查看执行计划,默认值
EXPLAIN PLAN SELECT arrayJoin([6,6,7])
// AST语法树查看
EXPLAIN AST SELECT numbers FROM system.numbers LIMIT 10;
// 用于优化语法
EXPLAIN SYNTAX SELECT arrayJoin([6,6,7])
// 查看PIPELINE计划
EXPLAIN PIPELINE SELECT arrayJoin([6,6,7])

2. 建表优化

2.1 数据类型

  • 时间字段类型:建表时能用数值型或日期时间表示的字段就不要用字符串。虽然ClickHouse底层将DateTime存储为时间戳Long类型,但不建议存储Long类型,因为DateTime不需要经过函数转换处理,执行效率高,可读性好
  • 空值存储类型:官方指出Nullable类型几乎总是会拖累性能。因为存储Nullable列时需要创建一个额外的文件来存储NULL的标识(具体var/lib/clickhouse/data/table/column.null.bin),并且Nullable列无法被索引。应直接使用字段默认值表示空,或者自行指定一个在业务中无意义的值

2.2 分区与索引

分区粒度根据根据业务特点决定,不宜过粗过细。一般选择按天分区。必须指定索引列,ClickHouse中索引列即排序列,通过order by指定。组合索引需满足查询频率大在前。基数特别大的列不适宜做索引列

3 ClickHouse内部语法优化规则

3.1 Count优化

如果使用的是count()或count(*),只要没指定具体字段,且没有where条件,则会直接使用system.tables的total_rows:

SELECT COUNT() FROM datasets.hits_vl;

3.2 子查询\ORDER BY\LIMIT BY\USING KEY重复字段会自动消除

3.3 谓词下推

EXPLAIN SYNTAX SELECT UserID FROM hits_v1 GROUP BY UserID Having UserID='111';
会被自动优化为
EXPLAIN SYNTAX SELECT UserID FROM hits_v1 WHERE UserID='111' GROUP BY UserID;

3.4 聚合函数外推

EXPLAIN SYNTAX SELECT SUM(UserID*2) FROM visits_v1;
会被自动优化为
SELECT SUM(UserID) * 2 FROM visits_v1;

3.5 聚合函数消除

EXPLAIN SYNTAX SELECT SUM(UserID*2),max(VisitID), max(UserID) FROM visits_v1 GROUP BY UserID;
会被自动优化为
EXPLAIN SYNTAX SELECT SUM(UserID) * 2, max(VisitID), UserID FROM visits_v1 GROUP BY UserID;

3.6 三元运算优化

EXPLAIN SYNTAX SELECT number=1?'hello':(number=2?'world':'china') FROM numbers(10) SETTINGS optimize_if_chain_to_multiif=1;
返回优化后的语句:
─explain───────────────────────────────────────────────────────────┐
│ SELECT multiIf(number = 1, 'hello', number = 2, 'world', 'china') │
│ FROM numbers(10)                                                  │
│ SETTINGS optimize_if_chain_to_multiif = 1                         │
└───────────────────────────────────────────────────────────────────┘

4 单表查询优化

4.1 prewhere代替where

prewhere和where语句的作用相同,用来过滤数据。不同之处在于prewhere只支持*MergeTree族系列引擎的表,首先会读取指定的列数据,来判断数据过滤,等待数据过滤之后在读取select声明的列字段来补全其余属性。默认情况下,where条件会自动优化成prewhere。

4.2 数据采样

通过采样运算可极大提升数据分析的性能

SELECT Title, count(*) AS PageViews FROM hits_v1 SAMPLE 0.1 WHERE CounterID=57 GROUP BY Title;

4.3 列裁剪与分区裁剪

列裁剪即指定所需要的列,而非全量*,分区裁剪就是只读取需要的分区,在过滤条件中指定

4.4 order by结合where limit

千万以上数据集进行order by查询时需要搭配where条件和limit语句一起使用

4.5 避免构建虚拟列

不要在结果集上构建虚拟列,非常消耗资源降低性能。

// 反例
SELECT Income, Age, Income/Age AS Ia FROM datasets.hits_v1;

4.6 uniqCombined替代distinct

性能可提升10倍以上,uniqCombined底层采用类似HyperLogLog算法实现。不建议在千万级数据上执行distinct去重查询,改为近似去重uniqCombined。

4.7 考虑使用物化视图

ClickHouse的物化视图是一种查询结果的持久化。物化视图不会随着基础表的变换而变化,被称为’快照’,
如下表而言,当发布日期大于10月1日,会在物化视图的存储表.inner_id.xxxx中插入数据,而当清空基础表house时,.inner_id.xxxx中的数据并不会丢失。

// 创建物化视图
CREATE MATERIALIZED VIEW study.house_new_mv ENGINE ReplacingMergeTree PARTITION BY toYYYYMMDD(publish_date) ORDER BY(id, city, region, name)
AS SELECT id, city, region, name, publish_date FROM study.house WHERE publish_date > toDate('2022-10-01');
// 插入数据
INSERT INTO study.house VALUES (2, '上海', '静安', '彭一小区', 70000, toDateTime('2022-05-06'));
INSERT INTO study.house VALUES (2, '上海', '静安', '彭一小区', 70000, toDateTime('2022-10-06'));
TRUNCATE TABLE study.house;
SELECT * FROM study.house_new_mv;

3

5. 用IN代替JOIN

当多表查询时,查询的数据仅从一张表出时,可考虑用IN操作而不是JOIN

SELECT a.* FROM hits_v1 a WHERE a.CounterID in (SELECT CounterID FROM visits_v1);

6. 多表JOIN小表在右

右表关联时被加载到内存与左表进行对比,所以多表JOIN时右表必须是小表。

7. 注意谓词下推

ClickHouse在join查询时不会主动发起谓词下推的操作,需要每个子查询提前完成过滤操作。新版本不存在此问题,可以在查询时EXPLAIN SYNTAX

8. 分布式表使用gobal

两张分布式表上的IN和JOIN之前必须加上GLOBAL关键字,右表只会在接收查询请求的那个节点查询一次,并将其分发到其他节点上。如果不加GLOBAL关键字的话,每个节点都会单独发起一次对右表的查询,而右表又是分布式表,就导致右表一共会被查询N^2次(N是该分布式表的分片数量),这就是查询放大,会导致很大开销

欢迎关注公众号算法小生或沈健的技术博客shenjian.online

<think>我们正在讨论如何优化ClickHouse中INSERT INTO操作的内存使用。根据提供的引用和ClickHouse的特性,以下是一些优化方法: 1. **批量插入**:ClickHouse处理批量插入比单条插入更高效。单条插入会导致大量的小块数据写入,增加内存开销。建议每次插入至少1000行以上,理想情况下是数万行或更多[^1]。 2. **使用合适的格式**:插入数据时,使用高效的格式(如TabSeparated、Native)而不是JSONEachRow等文本格式,可以减少解析开销和内存占用。 3. **调整设置**: - `max_memory_usage`:设置单次查询的最大内存使用量,避免插入大表时占用过多内存。 - `max_insert_block_size`:控制每次插入块的大小。适当调整这个值(例如从默认的1048576调小)可以避免一次性加载过多数据到内存中。 - 设置`async_insert`为1(开启异步插入),这样多个插入请求会被合并,减少内存压力。 4. **避免大事务**:虽然ClickHouse不支持传统事务,但插入操作本身会生成数据块。如果插入的数据量非常大,可以拆分成多个批次插入,避免单个插入操作过大。 5. **使用物化视图**:如果插入的数据需要实时转换,考虑使用物化视图(Materialized View)[^1]。物化视图可以将插入操作转化为后台的异步操作,并且可以预先聚合数据,减少最终存储的数据量。但注意,物化视图本身也会占用存储和计算资源。 6. **考虑表引擎选择**:使用MergeTree系列引擎(如ReplacingMergeTree、SummingMergeTree)可以自动合并数据块,减少最终存储的数据量,从而间接降低插入时的内存压力。 7. **使用Buffer引擎**:对于高频率的小批量插入,可以先插入到Buffer表中,再由Buffer表异步写入目标表。这样可以平滑写入压力,避免内存峰值。 8. **优化数据结构**:设计表结构时,使用合适的数据类型(如使用`LowCardinality`类型代替字符串)可以减少内存占用。 9. **监控和调整**:使用ClickHouse的系统表(如`system.metrics`、`system.events`)监控内存使用情况,根据实际情况调整配置。 10. **利用外部工具**:如果数据源来自其他数据库(如MySQL),可以考虑使用ClickHouse的MaterializedMySQL引擎[^3],它将MySQL的数据变更实时同步到ClickHouse,避免了手动插入。 示例:调整插入块大小 ```sql SET max_insert_block_size = 100000; INSERT INTO table_name ... ``` 注意:这些优化方法需要根据具体场景调整。例如,在内存受限的环境中,降低`max_insert_block_size`和开启`async_insert`可能效果显著。 §§相关问题§§ 1. ClickHouse中如何监控INSERT操作的内存使用? 2. 如何调整ClickHouse的配置参数以优化写入性能? 3. ClickHouse的Buffer表引擎是如何工作的?</think>### ClickHouse INSERT INTO 内存优化方法 根据ClickHouse的特性[^2][^3]和优化实践,以下是优化INSERT操作内存使用的方法: #### 1. **批量插入优化** - **原理**:ClickHouse对批量写入更高效,小批量插入会产生大量临时文件 - **建议**: $$批量大小 ≥ 10,000行$$ ```sql -- 从文件批量导入(内存效率最高) INSERT INTO table_name FROM INFILE 'data.csv' FORMAT CSV ``` #### 2. **异步插入模式** - **启用方法**: ```sql SET async_insert = 1; -- 开启异步插入 SET wait_for_async_insert = 0; -- 非阻塞写入 ``` - **优势**:合并写入请求,减少内存峰值[^3] - **适用场景**:高频小批量写入(如IoT场景) #### 3. **调整内存参数** | 参数名 | 推荐值 | 作用说明 | |-----------------------|---------------|----------------------------| | `max_memory_usage` | 物理内存的50-70% | 限制单次操作最大内存 | | `max_insert_block_size` | 100,000-500,000 | 控制单批次写入行数 | | `min_insert_block_size_rows` | 10,000+ | 避免过小写入块 | ```sql -- 设置示例 SET max_insert_block_size = 100000; ``` #### 4. **写入目标优化** - **使用Buffer表引擎**: ```sql CREATE TABLE buffer_table AS target_table ENGINE = Buffer(default, target_table, 16, 10, 100, 10000, 1000000) ``` - **优势**:累积小批量写入,集中刷新到目标表 - **参数说明**:16个内存分块 | 10秒刷新间隔 | 100K行或1M数据刷新 - **物化视图预聚合**[^1]: ```sql CREATE MATERIALIZED VIEW mv_agg ENGINE = SummingMergeTree AS SELECT key, sum(value) FROM source_table GROUP BY key ``` - **效果**:减少最终表的数据量 #### 5. **数据格式优化** - **优先选择列式格式**: ```bash # 使用Native格式替代文本格式 clickhouse-client --query "INSERT INTO table FORMAT Native" < data.native ``` - **压缩效率对比**: | 格式 | 压缩比 | 内存开销 | |------------|-------|---------| | Native | ★★★★☆ | ★★☆☆☆ | | Parquet | ★★★★☆ | ★★★☆☆ | | CSV | ★★☆☆☆ | ★★★★☆ | #### 6. **分区与排序键优化** ```sql CREATE TABLE optimized_table ( ... ) ENGINE = MergeTree PARTITION BY toYYYYMM(date) -- 按月份分区 ORDER BY (device_id, timestamp) -- 排序键匹配查询模式 ``` - **效果**:减少内存中数据排序的开销 #### 7. **外部工具集成** - 使用**MaterializedMySQL**[^3]同步数据: ```sql CREATE DATABASE mysql_db ENGINE = MaterializedMySQL('mysql:3306', 'source_db', 'user', 'pass') ``` - **优势**:避免手动INSERT,直接从MySQL同步 #### 监控诊断方法 ```sql -- 查看内存使用 SELECT * FROM system.metrics WHERE metric LIKE '%Memory%' -- 分析写入瓶颈 SELECT event, value FROM system.events WHERE event IN ('InsertQuery', 'InsertedRows') ``` > **关键建议**:对于持续写入场景,优先采用`异步写入+Buffer表`组合方案,结合批量大小调整,可降低内存峰值40-70%[^3]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

算法小生Đ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值