SELECT ip_address, groupArray(id) AS id, arrayDistinct(groupArray(account_name)) AS account_name, from_hostname, groupArray(event_time) AS event_time_array
FROM odd_logs_linux_distributed
WHERE ((message LIKE ‘wget%http%’) AND (message LIKE ‘%base64%’)) AND (event_time BETWEEN toDateTime(‘2025-10-28 17:00:51’) AND toDateTime(‘2025-10-28 18:00:51’))
GROUP BY ip_address, from_hostname --65 Linux_可疑命令活动 这个内置规则在8点多重复执行,后面我自己单独拎出来跑也是看着cpu飙升
在这里插入图片描述

日志是从9月21号开始,扫描周期一个小时;
在这里插入图片描述
在这里插入图片描述

时间 查询类型 执行时间 数据量
20:22:49 groupArray查询 34,792ms 9.4M行
20:23:30 groupArray查询 40,591ms 9.5M行
20:29:08 groupArray查询 15,533ms 10.7M行
20:36:34 groupArray查询 40,351ms 10.7M行
20:37:10 groupArray查询 36,136ms 10.7M行 相同查询的执行时间在波动中增长,说明系统负载在累积。
在这里插入图片描述

– 查询执行逻辑: 1. 分布式表向"所有分片"发送查询 2. 不存在的分片返回空或超时 3. 实际只有localhost:9000返回161万行数据 ,但耗时统计如上图是1000w+行,是全表扫描,这是分布式表的设计机制导致的:分布式表先触发各分片的本地查询,再汇总结果,因此 system.query_log中记录的 read_rows 是各分片 “原始扫描行数” 的总和,而非最终过滤后的行数(不为0)当时产生告警了。我们的linux表结构是按receive_time每小时分区;而查询是按event_time分区;所以是全表扫描1000w+行;观察到的 “执行时间波动增长”,说明系统在多轮查询后出现了资源争抢(CPU、内存未及时释放),导致后续查询耗时增加。
ClickHouse的LIKE查询对于前缀匹配(如LIKE ‘wget%’)效率尚可,但对通配符(如LIKE ‘%base64%’)效率极低
内存压力:随着查询次数增加,ClickHouse的内存使用量增加,可能导致部分操作转为磁盘I/O;但当系统负载高时,查询时间会显著增加。
ClickHouse在并发查询时容易导致资源竞争,CPU可能被打满,当时我们8点多是一直都有并发查询,一直卡住;
在这里插入图片描述

第一次执行:需要扫描数据、构建数组、去重等,CPU使用率高
第二次执行:可能由于第一次执行后缓存了部分数据,但系统负载增加,导致时间略长
第三次执行:缓存命中率高,执行时间大幅下降
第四次和第五次执行:缓存可能被清空,或系统负载增加,导致执行时间又变长

SELECT groupArray(id) AS id, ip_address, groupArray(event_time) AS event_time_array, groupArray(account_name) AS account_name, from_hostname
FROM htyh_bd.odd_logs_linux_distributed
WHERE ((message LIKE ‘wget%http%’) AND (message LIKE ‘%base64%’)) and (event_time BETWEEN toDateTime(‘2025-10-29 20:50:31’) AND toDateTime(‘2025-10-29 21:50:31’))
GROUP BY ip_address, from_hostname —100528行 500+ms执行完了

SELECT
query_id,
query,
event_time,
query_duration_ms AS duration_ms,–查询执行总时间(毫秒)
read_rows,–读取的行数
read_bytes,–读取的数据量(字节)
written_rows,–写入的行数
written_bytes,–写入的数据量(字节)
memory_usage,–内存使用量(字节)
– CPU使用估算(毫秒)
round(query_duration_ms * 0.8, 2) AS estimated_cpu_ms
FROM system.query_log
WHERE event_time > now() - INTERVAL 24 HOUR
AND query_duration_ms > 3000 – 超过10秒的查询
AND type = ‘QueryFinish’
ORDER BY query_duration_ms DESC

– 然后查看查询统计信息(使用英文别名)
SELECT
query,
query_duration_ms AS duration_ms,
query_duration_ms / 1000 AS duration_seconds,
memory_usage AS memory_used,
read_rows AS rows_read,
read_bytes AS bytes_read,
result_rows AS result_rows,
result_bytes AS result_bytes
FROM system.query_log
WHERE query LIKE ‘%wget%http%’
AND event_time > now() - interval 20 minute
ORDER BY event_time DESC
LIMIT 100;
根据提供的执行计划,确实表示全表扫描。
执行计划分析
从图片中的执行步骤来看:
Expression ((Projection + Before ORDER BY))
Aggregating
Expression (Before GROUP BY)
Expression
ReadFromMergeTree (htyh_bd.odd_logs_linux) ← 关键信息在这里
为什么是全表扫描?

  1. ReadFromMergeTree的含义

    这表示从 MergeTree 表引擎读取数据

    但没有显示任何索引使用信息(如 Index: event_time)

    在 ClickHouse 中,如果使用了主键索引,执行计划会明确显示

EXPLAIN indexes = 1
SELECT
ip_address,
groupArray(id) AS id,
arrayDistinct(groupArray(account_name)) AS account_name,
from_hostname,
groupArray(event_time) AS event_time_array
FROM htyh_bd.odd_logs_linux_distributed
WHERE
message LIKE ‘wget%http%’
AND message LIKE ‘%base64%’
AND event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’)
GROUP BY ip_address, from_hostname; Expression ((Projection + Before ORDER BY))
Aggregating
Expression (Before GROUP BY)
Expression
ReadFromMergeTree (htyh_bd.odd_logs_linux)
Indexes:
MinMax
Condition: true
Parts: 27/27
Granules: 2066/2066
Partition
Condition: true
Parts: 27/27
Granules: 2066/2066
PrimaryKey
Condition: true
Parts: 27/27
Granules: 2066/2066
Indexes:
MinMax
Condition: true ← 索引条件始终为true
Parts: 27/27 ← 扫描了所有27个数据部分
Granules: 2066/2066 ← 扫描了所有2066个数据颗粒
Partition
Condition: true ← 同上
Parts: 27/27
Granules: 2066/2066
PrimaryKey
Condition: true ← 主键索引未被使用
Parts: 27/27
Granules: 2066/2066
为什么是全表扫描?

  1. 索引条件分析
    Condition: true表示没有有效的索引过滤
    所有三个索引(MinMax、Partition、PrimaryKey)的条件都是true,说明它们都没有起到过滤作用
  2. 数据扫描范围
    Parts: 27/27- 扫描了表中所有的27个数据部分
    Granules: 2066/2066- 扫描了所有的2066个数据颗粒
    这明确表明是100%的全表扫描
    说明即使是1500w的全表扫描。clickhouse的执行效果是很快的,主要之前的任务堆积导致的恶性循环;改进:把event_time作为分区时间,ip_address加上索引

EXPLAIN SELECT ip_address, groupArray(id) AS id, arrayDistinct(groupArray(account_name)) AS account_name, from_hostname, groupArray(event_time) AS event_time_array FROM htyh_bd.odd_logs_windows_distributed WHERE message LIKE ‘wget%http%’ AND message LIKE ‘%base64%’ AND event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’) GROUP BY ip_address, from_hostname;
得出如下:
Expression ((Projection + Before ORDER BY)) Aggregating Expression (Before GROUP BY) Expression ReadFromMergeTree (htyh_bd.odd_logs_windows)
我就疑惑 为啥windows表是按event_time分区的,还是全表扫描呢?
问通义千问深度思考版本答案,给出的是它的答案—是错误的

  1. ClickHouse支持的索引类型
    minmax- 适合范围查询
    set(max_rows)- 适合枚举值
    ngrambf_v1(n, size, hashes, seed)- 适合字符串
    tokenbf_v1(size, hashes, seed)- 适合分词
    bloom_filter([false_positive])- 适合等值查询
    在这里插入图片描述

疑惑:message LIKE ‘wget%http%’ AND message LIKE ‘%base64%’ AND event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’) 我是在这个小时分区内限制,message 全模糊匹配应该在这个小时分区内全模糊匹配,为啥还要全表扫描?
在这里插入图片描述

  • 关键问题:LIKE ‘%base64%’ 无法使用任何索引
    – 即使分区剪枝工作,每个分区内仍需全扫描
    EXPLAIN indexes = 1
    SELECT count(*)
    FROM htyh_bd.odd_logs_windows
    WHERE event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’)
    AND message LIKE ‘%base64%’;
    问deepseek,还是顺着你的思路去回答,不是很专业?
    在这里插入图片描述

不过下述思路可以借鉴:
在这里插入图片描述

但我执行以下分析计划
EXPLAIN indexes = 1
SELECT count(*)
FROM htyh_bd.odd_logs_windows
WHERE event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’)
AND message LIKE ‘%base64%’; Expression ((Projection + Before ORDER BY))
Aggregating
Expression (Before GROUP BY)
Expression
ReadFromMergeTree (htyh_bd.odd_logs_windows)
Indexes:
MinMax
Keys:
event_time
Condition: and((event_time in (-Inf, 1761745851]), (event_time in [1761742251, +Inf)))
Parts: 2/24
Granules: 2/24
Partition
Condition: true
Parts: 2/2
Granules: 2/2
PrimaryKey
Keys:
event_time
Condition: and((event_time in (-Inf, 1761745851]), (event_time in [1761742251, +Inf)))
Parts: 2/2
Granules: 2/2

执行计划分析:分区剪裁确实在工作!
关键发现解读:
MinMax Index:
Condition: and((event_time in (-Inf, 1761745851]), (event_time in [1761742251, +Inf)))
Parts: 2/24 ← 关键信息:扫描了24个分区中的2个
Granules: 2/24 ← 扫描了24个数据颗粒中的2个

Partition:
Parts: 2/2 ← 扫描了2个分区
Granules: 2/2

PrimaryKey:
Parts: 2/2 ← 扫描了2个分区
Granules: 2/2

  • 查询确实只会在指定的小时分区内扫描
    WHERE event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’)

但ClickHouse执行计划中的"全表扫描"指的是:
“在目标分区内进行全分区扫描”,而不是"扫描整张表的所有分区"
– 查看查询实际涉及的分区
EXPLAIN PIPELINE
SELECT
ip_address,
groupArray(id) AS id,
arrayDistinct(groupArray(account_name)) AS account_name,
from_hostname,
groupArray(event_time) AS event_time_array
FROM htyh_bd.odd_logs_linux_distributed
WHERE
message LIKE ‘wget%http%’
AND message LIKE ‘%base64%’
AND event_time BETWEEN toDateTime(‘2025-10-30 08:12:31’) AND toDateTime(‘2025-10-30 09:12:31’)
GROUP BY ip_address, from_hostname; (Expression)
ExpressionTransform × 4
(Aggregating)
Resize 4 → 4
AggregatingTransform × 4
StrictResize 4 → 4
(Expression)
ExpressionTransform × 4
(Expression)
ExpressionTransform × 4
(ReadFromMergeTree)
MergeTreeSelect(pool: ReadPool, algorithm: Thread) × 4 0 → 1

EXPLAIN PIPELINE SELECT ip_address, groupArray(id) AS id, arrayDistinct(groupArray(account_name)) AS account_name, from_hostname, groupArray(event_time) AS event_time_array FROM htyh_bd.odd_logs_linux_distributed WHERE message LIKE ‘wget%http%’ AND message LIKE ‘%base64%’ AND event_time BETWEEN toDateTime(‘2025-10-30 08:12:31’) AND toDateTime(‘2025-10-30 09:12:31’) GROUP BY ip_address, from_hostname; (Expression) ExpressionTransform × 4 (Aggregating) Resize 4 → 4 AggregatingTransform × 4 StrictResize 4 → 4 (Expression) ExpressionTransform × 4 (Expression) ExpressionTransform × 4 (ReadFromMergeTree) MergeTreeSelect(pool: ReadPool, algorithm: Thread) × 4 0 → 1
根据您提供的 EXPLAIN PIPELINE执行计划,我可以详细分析这个查询的执行流程和性能特征:
执行计划分析
流水线执行步骤:
(Expression)
ExpressionTransform × 4
(Aggregating)
Resize 4 → 4
AggregatingTransform × 4
StrictResize 4 → 4
(Expression)
ExpressionTransform × 4
(Expression)
ExpressionTransform × 4
(ReadFromMergeTree)
MergeTreeSelect(pool: ReadPool, algorithm: Thread) × 4 0 → 1
关键性能指标解读

  1. ​​并行度分析​​
    × 4表示每个阶段有 ​​4个并行线程​​ 同时执行
    Resize 4 → 4和 StrictResize 4 → 4表示数据在并行流水线间重新分配
    ​​优点​​:查询充分利用了多核CPU的并行处理能力
  2. ​​数据流分析​​
    MergeTreeSelect × 4 0 → 1:4个读取线程,但输出数据量很少(0→1)
    这表明虽然并行读取,但​​实际匹配的数据行数很少​​
  3. ​​执行阶段分解​​
    数据读取 → 表达式计算 → 聚合 → 最终表达式计算
    ↓ ↓ ↓ ↓
    MergeTreeSelect → ExpressionTransform → AggregatingTransform → ExpressionTransform
    (×4) (×4) (×4) (×4)
    性能优化建议
    当前性能特征:
    ✅ ​​良好的并行化​​:4线程并行,充分利用多核
    ⚠️ ​​全表扫描​​:MergeTreeSelect没有显示索引使用
    ⚠️ ​​数据过滤效率低​​:需要在读取后通过表达式进行LIKE过滤
    优化建议:
  4. ​​索引优化​​(最高优先级)
    – 检查当前主键
    SHOW CREATE TABLE htyh_bd.odd_logs_linux_distributed;

– 确保主键包含 event_time 且顺序靠前
– ORDER BY (event_time, message, …)
2. ​​跳数索引优化​​
– 添加布隆过滤器索引加速LIKE查询
ALTER TABLE htyh_bd.odd_logs_linux_distributed
ADD INDEX message_bf message TYPE ngrambf_v1(4, 256, 2, 0) GRANULARITY 2;
3. ​​查询重写​​
– 优化LIKE条件
WHERE
(message LIKE ‘wget%http%’ AND message LIKE ‘%base64%’)
AND event_time BETWEEN …

– 或者使用正则表达式
WHERE
match(message, ‘wget.*http.*base64’)
AND event_time BETWEEN …
4. ​​调整并行度​​(如需要)
– 如果数据量小,可以减少并行度避免开销
SET max_threads = 2;
– 执行查询…
预期优化效果
优化后理想的执行计划应该是:
MergeTreeSelect × 4 45 → 1 – 扫描颗粒数大幅减少

…(后续步骤不变)
​​关键指标改善​​:
MergeTreeSelect的输入颗粒数从接近全表大幅减少
查询响应时间显著提升
CPU和内存使用效率优化
当前执行计划显示查询虽然并行化良好,但由于缺乏索引支持,仍然需要进行全表扫描。建议优先优化表索引结构

EXPLAIN indexes = 1—这个函数代表啥含义?
SELECT
ip_address,
groupArray(id) AS id,
arrayDistinct(groupArray(account_name)) AS account_name,
from_hostname,
groupArray(event_time) AS event_time_array
FROM htyh_bd.odd_logs_windows_distributed
WHERE
message LIKE ‘wget%http%’
AND message LIKE ‘%base64%’
AND event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’)
GROUP BY ip_address, from_hostname;
Condition: and((event_time in (-Inf, 1761745851]), (event_time in [1761742251, +Inf)))
Parts: 2/24
Granules: 2/24
Partition
Condition: true
Parts: 2/2
Granules: 2/2
PrimaryKey
Keys:
event_time
Condition: and((event_time in (-Inf, 1761745851]), (event_time in [1761742251, +Inf)))
Parts: 2/2
Granules: 2/2

  1. 分区剪裁100%有效
    总分区数:24个
    实际扫描:2个分区(20点和21点)
    剪裁效果:91.7%的分区被跳过!

EXPLAIN indexes = 1—这个函数代表啥含义? 和EXPLAIN有啥区别,为什么我之前理解会误解为全表扫表而不是但ClickHouse执行计划中的"全表扫描"指的是: “在目标分区内进行全分区扫描”,而不是"扫描整张表的所有分区" 这个理解?

使用EXPLAIN indexes = 1来查看查询时索引的使用情况。它会显示查询执行过程中哪些索引被使用以及如何被使用。
而普通的EXPLAIN(不带indexes参数)只显示查询的执行计划,而不显示索引的详细使用情况。

  1. 基础 EXPLAIN
    – 普通EXPLAIN只显示查询的执行流程
    EXPLAIN
    SELECT count(*) FROM table WHERE condition;
    输出:查询的逻辑执行步骤(投影、过滤、聚合等)
  2. EXPLAIN indexes = 1
    sql
    – 显示索引使用详情
    EXPLAIN indexes = 1
    SELECT count() FROM table WHERE condition;
    额外显示:
    分区剪枝情况(哪些分区被选中/排除)
    主键索引使用情况
    跳数索引使用情况
    数据颗粒扫描范围
    为什么会产生误解
    执行计划输出的误导性
    普通EXPLAIN输出:
    text
    ReadFromMergeTree (htyh_bd.odd_logs_windows)
    这个表述很模糊
    看起来像是"读取整个表"
    EXPLAIN indexes = 1 输出:
    text
    ReadFromMergeTree (htyh_bd.odd_logs_windows)
    Indexes:
    MinMax
    Condition: (event_time >= ‘2025-10-29 20:50:51’) AND (event_time <= ‘2025-10-29 21:50:51’)
    Parts: 2/27 ← 关键信息:27个分区中只扫描2个
    Granules: 156/2066 ← 2066个数据颗粒中只扫描156个
    验证示例
    让我们实际对比一下
    sql
    – 普通EXPLAIN(容易误解)
    EXPLAIN
    SELECT count(
    )
    FROM htyh_bd.odd_logs_windows
    WHERE event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’);

– EXPLAIN indexes = 1(显示详细信息)
EXPLAIN indexes = 1
SELECT count()
FROM htyh_bd.odd_logs_windows
WHERE event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’);
预期的输出差异
普通EXPLAIN可能显示:
text
Expression
ReadFromMergeTree (htyh_bd.odd_logs_windows)
→ 看起来像是全表扫描
EXPLAIN indexes = 1 显示:
text
Expression
ReadFromMergeTree (htyh_bd.odd_logs_windows)
Indexes:
MinMax
Condition: (event_time >= ‘2025-10-29 20:50:51’) AND (event_time <= ‘2025-10-29 21:50:51’)
Parts: 2/27
Granules: 156/2066
PrimaryKey
Condition: (event_time >= ‘2025-10-29 20:50:51’) AND (event_time <= ‘2025-10-29 21:50:51’)
Parts: 2/27
Granules: 156/2066
→ 明确显示只扫描了2个分区中的156个数据颗粒
关键指标解释
Parts: 2/27
27: 表总共有27个数据部分(对应分区)
2: 查询只涉及其中2个数据部分
分区剪枝效果: 排除了25个不相关的分区
Granules: 156/2066
2066: 表总共有2066个数据颗粒
156: 查询只扫描其中156个数据颗粒
数据颗粒: ClickHouse存储的最小数据单元(默认8192行)
为什么会产生"全表扫描"的误解
ClickHouse术语的模糊性
"全表扫描" 在传统数据库中意味着扫描整张表的所有数据
ClickHouse中的"全表扫描" 可能指"在当前查询涉及的数据范围内全扫描"
执行计划输出没有明确区分这两种情况
验证真正的扫描范围
sql
– 检查表的总数据量
SELECT count(
) as total_rows FROM htyh_bd.odd_logs_windows;

– 检查查询实际扫描的数据量
SELECT count(*) as scanned_rows
FROM htyh_bd.odd_logs_windows
WHERE event_time BETWEEN toDateTime(‘2025-10-29 20:50:51’) AND toDateTime(‘2025-10-29 21:50:51’);
学习建议
总是使用 EXPLAIN indexes = 1
– 最佳实践:总是查看索引使用情况
EXPLAIN indexes = 1
SELECT your_query;

– 对于复杂查询,还可以结合其他选项
EXPLAIN indexes = 1, header = 1, description = 1
SELECT your_query;
关注关键指标
Parts: 扫描数/总数 - 分区剪枝效果
Granules: 扫描数/总数 - 数据颗粒过滤效果
Condition: 具体的索引条件
结论
您的误解是合理的,因为:
1.普通 EXPLAIN 输出信息有限且表述模糊
2.ClickHouse术语"全表扫描"有歧义
3.没有显示具体的分区剪枝和数据颗粒过滤信息
EXPLAIN indexes = 1 才是查看查询执行细节的正确工具,它能明确显示:
实际扫描的分区数量
数据颗粒过滤情况
各种索引的使用效果
现在应该能准确理解查询的真实扫描范围了!

1、ClickHouse单机JOIN实现
ClickHouse 单机JOIN操作默认采用HASH JOIN算法,可选MERGE JOIN算法。其中,MERGE JOIN算法数据会溢出到磁盘,性能相比前者较差。介绍基于HASH JOIN算法的实现JOIN操作
从right_table 读取该表全量数据,在内存中构建HASH MAP;
从left_table 分批读取数据,根据JOIN KEY到HASH MAP中进行查找,如果命中,则该数据作为JOIN的输出;
在这里插入图片描述

从这个实现中可以看出,如果right_table的数据量超过单机可用内存空间的限制,则JOIN操作无法完成。通常,两表JOIN时,将较小表作为right_table.

2、ClickHouse分布式JOIN实现
ClickHouse 是去中心化架构,非常容易水平扩展集群。当以集群模式提供服务时候,分布式JOIN查询就无法避免。这里的分布式JOIN通常指,JOIN查询中涉及到的left_table 与 right_table 是分布式表。通常,分布式JOIN实现机制无非如下几种:
Broadcast JOIN
Shuffle Join
Colocate JOIN

表实际存储关系
– 查看表的引擎和存储路径
SELECT
name as table_name,
engine,
data_paths,
metadata_path,
partition_key,
sorting_key
FROM system.tables
WHERE database = ‘htyh_bd’ AND name = ‘odd_logs_ips’;
在这里插入图片描述

又是docker映射的docker run --restart always -d
–name ck19
–ulimit nofile=262144:262144
-e TZ=Asia/Shanghai
-v /new_disk/data/clickhouse/data/:/new_disk/data/clickhouse/data/
-v /new_disk/data/clickhouse/clickhouse-server/:/etc/clickhouse-server/
-v /new_disk/data/clickhouse/logs/:/var/log/clickhouse-server/
-p 9000:9000
-p 8123:8123
-p 9009:9009
clickhouse/clickhouse-server:24.8.6.70
var/lib/clickhouse/对应宿主机new_disk/data/clickhouse/data/
图片显示的{‘/new_disk/data/clickhouse/data/store/b2d/b2d2574b-277f-42fc-8fcd-c61f406eae4e/’}就是对应这个路径: /new_disk/data/clickhouse/data/store/b2d/b2d2574b-277f-42fc-8fcd-c61f406eae4e

ClickHouse数据目录结构

new_disk/data/clickhouse/data/htyh_bd/odd_logs_ips/
├── 202401_1_10_1/ # Part 1
│ ├── checksums.txt # 校验文件
│ ├── columns.txt # 列信息
│ ├── count.txt # 行数
│ ├── primary.idx # 主键索引
│ ├── event_time.bin # 列数据文件
│ ├── event_time.mrk2 # 列标记文件
│ ├── src_ip.bin # 列数据文件
│ ├── src_ip.mrk2 # 列标记文件
│ └── …
├── 202401_11_20_1/ # Part 2
├── 202402_1_15_1/ # 另一个分区的Part
└── detached/ # 分离的parts
使用的是ClickHouse的ReplicatedMergeTree引擎,它会在part名称前加上一个哈希值;
这些UUID名称就是ClickHouse的Part标识:

1ac51b924cadbbb72f0f2a45b6elec81_0_0_0
48449577a23a756183ef22c3dac20b5e_0_0_0
7fe843eaa707bb32958adafe826bb44b_0_0_0

Part名称格式:
{UUID}_最小块号_最大块号_合并层级
UUID: 每个Part的唯一标识符
最小块号: 该Part包含的最小数据块号
最大块号: 该Part包含的最大数据块号
合并层级: Part经历的合并次数(0表示未合并过
– 查看每个Part的详细信息,包括所属分区
SELECT
name as part_name,
partition, – 这是分区信息!
rows,
formatReadableSize(bytes) as size,
min_time,
max_time,
level,
path
FROM system.parts
WHERE database = ‘htyh_bd’ AND table = ‘odd_logs_ips’
ORDER BY partition, name;
在这里插入图片描述
在这里插入图片描述

核心数据文件:
data.bin (39.4 KB) - 压缩的列数据文件
primary.cidx (42 B) - 主键索引文件
data.cmrk3 (147 B) - 列标记文件,用于定位数据位置
元数据文件:
partition.dat (11 B) - 分区信息文件(2023111705)
minmax_event_time.idx (8 B) - 时间范围索引
columns.txt (906 B) - 列定义信息
count.txt (4 B) - 该Part的总行数
系统文件:
checksums.txt (322 B) - 文件校验和
serialization.json (235 B) - 序列化配置
default_compression_codec.txt (10 B) - 压缩算法
metadata_version.txt (1 B) - 元数据版本
在这里插入图片描述

– 查看特定Part的详细信息
SELECT
name as part_name,
partition,
rows,
formatReadableSize(bytes) as size,
min_time,
max_time,
– 解析Part名称的各个部分
splitByChar(‘‘, name)[1] as uuid,
splitByChar(’
’, name)[2] as min_block,
splitByChar(‘‘, name)[3] as max_block,
splitByChar(’
’, name)[4] as merge_level
FROM system.parts
WHERE database = ‘htyh_bd’ AND table = ‘odd_logs_ips’
AND name LIKE ‘%1ac51b924cadbbb72f0f2a45b6e1ec81_0_0_0%’; – 替换为Part名称

– 建立Part名称到分区的完整映射
SELECT
'Part: ’ || name as part_identifier,
'→ 分区: ’ || partition as partition_info,
'→ 行数: ’ || rows as row_count,
'→ 时间范围: ’ || toString(min_time) || ’ 到 ’ || toString(max_time) as time_range,
'→ 路径: ’ || path as disk_location
FROM system.parts
WHERE database = ‘htyh_bd’ AND table = ‘odd_logs_ips’
ORDER BY partition, name;
在这里插入图片描述

当查询数据时:

确定分区 → 通过partition.dat和分区键

定位Part → 通过minmax_event_time.idx确定包含所需时间的Parts

读取索引 → 使用primary.cidx定位数据块

读取数据 → 通过data.cmrk3标记定位data.bin中的具体数据
在这里插入图片描述
在这里插入图片描述

这个小时是3000条数据,是验证了

索引存储区分方式

数据目录结构

/new_disk/data/clickhouse/data /database/table/
├── primary.idx # 主键索引
├── [column].mrk2 # 列标记文件
├── [column].bin # 列数据文件
└── skp_idx_[index_name].idx # 跳数索引文件
在这里插入图片描述

– 监控索引命中率
SELECT
query,
read_rows,
total_rows_to_read,
(read_rows / total_rows_to_read) as hit_ratio
FROM system.query_log
WHERE event_date = today()
AND query LIKE ‘%your_table%’
ORDER BY query_start_time DESC
LIMIT 10;
索引工作原理:
o稀疏索引:一级索引是稀疏的,每index_granularity(默认8192行)生成一个索引标记

o二级索引:是跳数索引,基于数据块聚合信息,粒度由GRANULARITY定义,表示跨越多少个一级索引粒度
。例如,GRANULARITY 1表示每个一级索引粒度对应一个二级索引条目。
o检索过程:查询时,先使用一级索引定位到可能的数据块,然后用二级索引进一步过滤
。例如,bloom_filter索引可以快速判断值是否可能存在,避免扫描整个数据块。
跳数索引的粒度(GRANULARITY)是指每隔多少个数据块(granule)构建一个索引条目。粒度越大,索引条目越少,索引本身越小,但跳过的精度会降低(可能跳过更多的数据块,但误跳的风险增加)。通常,跳数索引的粒度与主键索引的粒度相同(默认为8192行一个数据块),但可以调整。例如,GRANULARITY 1表示每个数据块一个索引条目,GRANULARITY 2表示每2个数据块一个索引条目(即索引条目数量减半)
1.替代分区的索引?
o跳数索引不能完全替代分区,因为分区还有数据管理的功能(如删除旧数据只需删除分区)。但是,如果分区键选择不当导致分区效果不好,可以通过跳数索引来优化查询性能。
o例如,如果数据按天分区,但每天的数据量仍然很大,且查询经常按小时范围,那么可以考虑为时间字段(比如event_time)创建一个minmax索引,这样在查询某个小时的数据时,可以在每个分区内跳过不包含该小时的数据块。

索引存储查看:从

可以找到,索引大小可以通过系统表查询,如system.parts表查看分区信息,或system.data_skipping_indices查看索引使用情况。具体查询语句如:
SELECT table, name AS index_name, formatReadableSize(bytes) AS size
FROM system.parts
WHERE database = ‘your_db’ AND table = ‘your_table’

分析历史查询读取数据的效率(例如,检查是否有多读、少读的情况),可以使用 read_rows(实际读取行数)和 result_rows(返回行数)来计算一个“读取效率”。这常用于评估WHERE条件过滤是否有效。
SELECT
query,
read_rows,
result_rows,
if (read_rows > 0, result_rows / read_rows, 0) AS read_efficiency_ratio – 计算读取效率
FROM system.query_log
WHERE (event_date = today())
AND (query LIKE ‘%odd_logs_windows%’)
AND (read_rows > 0) – 避免除零错误,并过滤掉无读操作的查询
ORDER BY query_start_time DESC
LIMIT 10;

在这里插入图片描述

1.主键索引(Primary Key):ClickHouse的主键索引是稀疏索引,它不会为每一行创建索引项,而是为每个索引粒度(默认8192行)创建一个索引项。因此,主键索引的大小相对较小,但只能用于快速定位到数据块,然后在数据块内进行扫描。
2.跳数索引(Data Skipping Indices):跳数索引是在主键索引的基础上,为每个数据块(granule)存储一些额外的元信息,以便在查询时跳过不满足条件的数据块。
如果查询条件中经常包含某个字段,并且该字段具有较高的区分度(即不同值较多),可以考虑为该字段创建跳数索引。(id)
表结构如下:
CREATE TABLE my_table
(
event_time DateTime,
user_id UInt64,
product_id UInt32,
action_type String,
… 其他字段
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (event_time, user_id, product_id)
假设我们经常需要查询某一天的数据中,特定user_id的记录。
由于我们按天分区,每个分区1亿行,并且主键是(event_time, user_id, product_id),那么对于查询条件中同时包含event_time和user_id的查询,主键索引可以有效地定位到数据。但是,如果查询条件只包含user_id,那么主键索引可能无法有效使用,因为主键的第一列是event_time。
为了优化这种情况,我们可以为user_id创建一个跳数索引。
例如,为user_id创建一个布隆过滤器索引:
ALTER TABLE my_table ADD INDEX idx_user_id user_id TYPE bloom_filter(0.025) GRANULARITY 3;
然后,进行测试:
查询1:查询某一天中,特定user_id的记录
SELECT * FROM my_table
WHERE event_time = ‘2024-01-01’ AND user_id = 123456
这个查询会先通过分区键定位到2024-01-01的分区,然后通过主键索引(因为event_time是主键第一列)快速定位到数据块,然后使用布隆过滤器索引进一步过滤user_id。
查询2:查询特定user_id在所有日期中的记录(可能会扫描多个分区)
SELECT * FROM my_table
WHERE user_id = 123456
这个查询会扫描所有分区,但使用布隆过滤器索引可以跳过不包含user_id=123456的数据块。
我们可以通过EXPLAIN语句来查看索引的使用情况。

如果字段是数值类型且经常进行范围查询,可以使用minmax索引。

如果字段是字符串类型或高基数的数值类型,并且经常进行等值查询,可以使用布隆过滤器索引。(ip_address,user_id)
性能预期:
布隆过滤器索引:等值查询性能提升 10-100倍
minmax索引:范围查询性能提升 5-50倍
set索引:枚举值查询性能提升 20-100倍
ngram索引:模糊查询性能提升 3-10倍
在这里插入图片描述

– 最终优化表结构
CREATE TABLE perf_test.user_behavior_final
(
– 字段定义同上…
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_date, category_id, user_id, action_type, revenue)
PRIMARY KEY (event_date, category_id)
SETTINGS
index_granularity = 8192,
min_bytes_for_wide_part = 1073741824, – 1GB以上使用wide格式
min_rows_for_wide_part = 10000000; – 1000万行以上使用wide格式

– 跳数索引配置
ALTER TABLE perf_test.user_behavior_final
ADD INDEX idx_user_product (user_id, product_id) TYPE bloom_filter(0.01) GRANULARITY 2,
ADD INDEX idx_high_value (revenue) TYPE minmax GRANULARITY 10 WHERE revenue > 100,
ADD INDEX idx_complex_cond (device_type, region_id) TYPE set(100) GRANULARITY 1,
ADD INDEX idx_session (session_id) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 4;

更多推荐