返回首页

ClickHouse中的分区:策略与操作

本文解释了ClickHouse中的分区作为在文件夹级别进行物理数据分离的机制。涵盖了PARTITION BY函数(toYYYYMM、toYYYYMMDD)、通过system.parts查看、操作DROP/DETACH/ATTACH/FREEZE/MOVE PARTITION、分区大小选择策略(每台服务器100–1000个)以及通过脚本自动删除旧数据。

ClickHouse中的分区:完整指南
Advertisement 728x90

ClickHouse 中的分区:如何在文件夹级别管理数据

1. 为什么需要分区——按时间段隔离数据

想象一下,你存储了在线赌场过去三年的所有投注记录。那是数十亿行数据。业务负责人突然说:“我们只需要最近 6 个月的数据,删除所有更早的数据。”

在普通数据库中,你会写 DELETE FROM bets WHERE created_at < '2025-01-01'。在 ClickHouse 中,这样的查询会花费……非常长的时间。因为 ClickHouse 中的 DELETE 不是即时删除,而是异步重写数据部分,跳过标记的行。

但有一种方法可以实现即时删除——分区。如果数据被拆分成多个分区(逻辑块,每个块物理存储在磁盘上的独立文件夹中),那么 DROP PARTITION 会在毫秒内删除整个文件夹。无需扫描行,无需重写——只是在文件系统层面执行 rm -rf

Google AdInline article slot

现实类比: 想象你有跨越数年的纸质档案。它们被存放在盒子里,每个盒子对应一个月。如果你需要删除 2024 年 1 月的数据,你只需将标有“2024 年 1 月”的盒子扔进垃圾箱。无需逐页检查。ClickHouse 中的分区就是这些盒子。

为什么还需要分区:

  • 备份——你可以只冻结(FREEZE)所需的分区。
  • 数据移动——热数据(频繁查询)放在快速 SSD 上,冷数据(很少查询)放在慢速 HDD 或云存储(S3)上。
  • SELECT 加速——如果 WHERE 子句包含分区列,ClickHouse 会立即知道要读取哪些文件夹,忽略哪些文件夹。

2. PARTITION BY——语法和示例

分区在创建表时通过 PARTITION BY 定义。最常见的模式是按日期拆分。

Google AdInline article slot
-- 创建按月分区的投注表
CREATE TABLE bets
(
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)   -- 分区 = 年 + 月,例如 202512
ORDER BY (user_id, created_at);

这里发生了什么:

  • toYYYYMM(created_at)——一个 ClickHouse 函数,将日期时间 2025-12-15 14:30:00 转换为数字 202512(年份 2025,月份 12)。所有具有相同 202512 的行进入同一个分区。
  • 分区名称只是一个字符串。ClickHouse 会在磁盘上创建一个文件夹,名称类似于 202512_1_1_0(细节不重要,但内部与 202512 关联)。

其他基于时间的分区选项:

-- 按天(注意!可能会创建太多分区)
PARTITION BY toYYYYMMDD(created_at)   -- 20251215

-- 按小时(粒度非常细,几乎不需要)
PARTITION BY toStartOfHour(created_at)   -- 2025-12-15 14:00:00

-- 按周
PARTITION BY toWeek(created_at)   -- 年份中的周数

-- 按年
PARTITION BY toYear(created_at)   -- 2025

如果不指定 PARTITION BY 会发生什么? ClickHouse 会创建一个名为 all 的单一分区。所有数据都位于一个文件夹中。你只能通过 DELETE(慢)或 TRUNCATE(一次性全部删除)来删除数据。对于大多数时间序列表,这是一个坏主意。

Google AdInline article slot

3. 分区选择策略——最佳平衡点

为什么分区不能太小?

ClickHouse 不喜欢太多分区(建议不超过 1000–2000 个)。因为:

  • 每个分区都是一个独立的文件夹,包含元数据。
  • 在数据插入时,ClickHouse 可能会在不同分区中创建数据部分。
  • 后台合并操作在分区内进行,而不是跨分区。
  • system.parts 表(关于数据部分的元数据)会变得非常庞大。

如果按小时分区,且每小时有 100 万行数据,会发生什么? 一年内,你会得到 365 * 24 = 8760 个分区。这已经很糟糕了。每次插入时,ClickHouse 都会为数千个文件夹打开文件描述符。带有日期过滤的 SELECT 会很快,但系统操作(备份、删除、列出分区)会变慢。

为什么分区不能太大?

如果一个分区是一年,那么 DROP PARTITION 会立即删除该年的所有数据。这很方便,但是:

  • 即使只查询一天的数据,也必须扫描整个年分区。
  • 你无法快速删除单个月份——要么删除整年,要么使用 DELETE(慢)。
  • 在月度级别上,无法移动“热”数据和“冷”数据(例如,上个月在 SSD 上,其余在 HDD 上)。

经验法则

负载 分区大小 示例
每月数十亿行 天 (toYYYYMMDD) IoT 传感器数据、CDN 日志
每月数亿行 月 (toYYYYMM) 赌场投注、交易
每月数千万行 月或季度 销售分析
每月数千行 缓慢变化的参考数据

主要标准: 分区后,每台服务器上应有 100 到 1000 个分区。如果你有 10,000 个分区,那就过度了。

4. 查看分区——system.parts

要查看表中存在哪些分区以及它们包含多少数据,请使用系统表 system.parts

-- 查看 bets 表的所有分区
SELECT 
    partition,                          -- 分区名称(例如 202512)
    name,                               -- 具体数据部分名称
    rows,                               -- 该部分的行数
    bytes_on_disk,                      -- 磁盘上的字节大小
    modification_time,                  -- 最后修改时间
    active                              -- 该部分是否活跃(1)或已删除(0)
FROM system.parts
WHERE table = 'bets' AND active = 1    -- 仅活跃部分
ORDER BY partition DESC;

解释:

  • partition——你在 PARTITION BY 中指定的内容。用于分组。
  • name——数据部分的内部名称,包含分区编号和版本。
  • active = 1——该部分用于读取。如果为 0,则该部分标记为删除,但物理上仍然存在。
  • bytes_on_disk——显示分区占用的空间大小。

为什么一个分区内可能有多个数据部分? 因为你批量插入数据。ClickHouse 不会立即合并它们。在单个分区(202512)内,可能有 5–10 个小部分,最终会合并成一个大块。

聚合查询——按分区汇总:

SELECT 
    partition,
    sum(rows) AS total_rows,
    sum(bytes_on_disk) AS total_bytes,
    formatReadableSize(sum(bytes_on_disk)) AS human_size
FROM system.parts
WHERE table = 'bets' AND active = 1
GROUP BY partition
ORDER BY partition DESC;

5. 分区操作——DROP、DETACH、ATTACH、FREEZE

这是分区如此受欢迎的主要原因。你可以对分区执行行级别无法实现的操作。

DROP PARTITION——即时删除

-- 删除 2025 年 12 月的所有数据
ALTER TABLE bets DROP PARTITION '202512';

-- 动态删除 90 天前的数据
-- 首先计算 90 天前的分区名称
ALTER TABLE bets DROP PARTITION toYYYYMM(today() - interval 90 day);

执行此命令后,分区文件夹会从磁盘上消失。操作耗时毫秒级,无论里面有多少行——一百万或十亿。

为什么这很安全? 因为分区是隔离的。删除一个分区不会影响其他分区。

DETACH PARTITION——临时禁用

-- 分离分区(数据保留在磁盘上,但不可用于 SELECT)
ALTER TABLE bets DETACH PARTITION '202512';

分区移动到 detached 子文件夹。它不参与查询,但你可以恢复它。

何时需要:

  • 你怀疑数据损坏,想暂时隐藏它。
  • 你想将分区复制到另一台服务器(然后在新服务器上 ATTACH)。

ATTACH PARTITION——恢复分离的分区

-- 恢复分区(如果它在 detached 文件夹中)
ALTER TABLE bets ATTACH PARTITION '202512';

FREEZE PARTITION——即时备份

-- 创建 2025 年 12 月分区的备份
ALTER TABLE bets FREEZE PARTITION '202512';

ClickHouse 在 shadow/ 文件夹中创建分区文件的硬链接。这不是复制数据(很快),只是为磁盘上的相同块创建额外的指针。然后你可以将 shadow 复制到另一台服务器。

为什么这比常规备份更好? 因为你不需要停止写入,而且不会浪费空间进行数据重复。

MOVE PARTITION——用于分层存储

更多细节见第 6 节。

6. MOVE PARTITION——分层存储(SSD → HDD → S3)

在 ClickHouse 中,你可以配置多个具有不同成本和速度的存储层。例如:

  • 热数据(最近一个月)——放在快速 NVMe SSD 上
  • 温数据(2–6 个月)——放在普通 HDD 上
  • 冷数据(超过 6 个月)——放在云 S3 上(便宜但慢)

分区允许你通过一条命令在这些层之间移动数据。

config.xml 中的配置(简化):

<storage_configuration>
    <disks>
        <ssd>
            <path>/mnt/ssd/clickhouse/</path>
        </ssd>
        <hdd>
            <path>/mnt/hdd/clickhouse/</path>
        </hdd>
    </disks>
    <policies>
        <hot_to_cold>
            <volumes>
                <hot>
                    <disk>ssd</disk>
                </hot>
                <cold>
                    <disk>hdd</disk>
                </cold>
            </volumes>
        </hot_to_cold>
    </policies>
</storage_configuration>

将分区从 SSD 移动到 HDD:

-- 2025 年 1 月的数据(不再需要快速访问)移动到 HDD
ALTER TABLE bets MOVE PARTITION '202501' TO VOLUME 'cold';

发生了什么: ClickHouse 将分区文件夹从 SSD 物理移动到 HDD。SELECT 仍然可以工作,但会稍慢(由于 HDD)。DROP PARTITION 仍然很快。

类比: 你将旧文件夹从身边的快速柜子移到远处的归档柜子。仍然可以访问,但需要更长时间才能到达。

7. 按多列分区

有时你需要不仅按时间,还按另一个维度拆分数据。例如,你有一个多品牌平台(一个数据库中有多个赌场)。你想为每个品牌单独删除旧数据,并可能将它们存储在不同的磁盘上。

-- 5 个品牌的投注表,按月 AND 品牌分区
CREATE TABLE bets_multi_brand
(
    brand_id    UInt8,          -- 1 = casino_A, 2 = casino_B, ...
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = MergeTree()
PARTITION BY (toYYYYMM(created_at), brand_id)   -- 两列!
ORDER BY (brand_id, user_id, created_at);

工作原理:

  • (年+月, brand_id) 的每个唯一组合成为一个独立的分区。
  • 对于 2025 年 12 月和品牌 1——分区 ('202512', 1)
  • 对于 2025 年 12 月和品牌 2——分区 ('202512', 2)

为什么这很有用:

  • DROP PARTITION 可以删除特定品牌在特定月份的数据。
  • MOVE PARTITION 可以将品牌 A 的旧数据移动到 HDD,同时将品牌 B(高级)保留在 SSD 上。

如何删除特定品牌的分区:

-- 删除 casino_A 在 2025 年 12 月的数据
ALTER TABLE bets_multi_brand DROP PARTITION ('202512', 1);

但有一个细微差别: 分区名称现在是一个元组。在 system.parts 中查看时,它会显示为 ('202512', '1')。这很正常。

8. 分区如何影响性能

对 INSERT 的影响

插入数据时,ClickHouse 会查看 PARTITION BY 表达式,并将行路由到相应的分区。如果你有 1000 个分区,速度很快。如果你有 100,000 个,每次插入都必须确定正确的文件夹,从而减慢写入速度。

如果单个 INSERT 包含来自不同分区的行,会发生什么? ClickHouse 会将它们分发到不同的文件夹——这很正常,但比所有行都来自一个分区要稍慢。

提示: 如果你批量插入数据(例如,每分钟一次),尽量让批次中的行保持在较短的时间范围内。这样它们会落入一个或两个分区,使插入更快。

对 SELECT 的影响

分区只有在 WHERE 子句包含分区列的条件时才能加速 SELECT。ClickHouse 使用分区进行粗略修剪:它检查需要哪些分区,并只读取它们的文件夹。

-- 快速:分区修剪掉 2025 年 12 月之外的所有数据
SELECT count() FROM bets 
WHERE created_at BETWEEN '2025-12-01' AND '2025-12-31';

-- 慢速:未使用分区,扫描所有分区
SELECT count() FROM bets 
WHERE amount > 1000;   -- 没有对 created_at 的过滤

为什么不要总是进行非常细粒度的分区? 因为:

  • 从 1000 个小分区(例如,按天跨越 3 年)读取可能比从 36 个大分区(按月)读取更慢,如果查询覆盖了较宽的范围。ClickHouse 为每个分区打开一个文件描述符。
  • 索引(主键)通常比细粒度分区更有效。

经验法则: 首先优化 ORDER BY(索引),然后才是分区。分区用于数据管理(DROP、MOVE),而不仅仅是为了加速 SELECT。

9. 常见错误:过度细粒度的分区

这是 ClickHouse 初学者最常遇到的问题。

错误方法的示例:

-- 糟糕:每天 1000 万行数据的表按天分区
CREATE TABLE logs_bad
(
    created_at DateTime,
    message String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(created_at)   -- 注意!
ORDER BY created_at;

3 年内,你会得到 365 * 3 = 1095 个分区。这还可以忍受(但接近极限)。10 年内——3650 个分区,这很糟糕。系统会开始变慢。

更糟糕的是——按小时分区,每小时 100 万条记录: 一年内,8760 个分区。每次插入时,ClickHouse 都会查找或创建当前小时的文件夹。SELECT * FROM system.parts 会花费数秒。后台合并会阻塞。

分区过多的迹象:

  1. 查询 SELECT * FROM system.parts WHERE table = 'mytable' 耗时超过 1 秒。
  2. ClickHouse 日志显示警告:Too many parts (300+)
  3. ALTER TABLE ... DROP PARTITION 不是立即执行,而是需要几秒钟。

如果已经分区得太细,该怎么办? 你可以通过 ALTER TABLE ... MODIFY PARTITION BY ... 合并分区,但这很复杂。更简单的方法是创建一个具有正确分区的新表,迁移数据,然后重命名。

10. 自动删除旧分区的脚本

在实践中,你可能只需要保留最近 90 天的数据。每个月都提醒自己删除旧分区是不现实的。你需要自动化。

选项 1:通过 cron 或任务定期执行查询

-- 删除所有超过 90 天的分区
-- 每天凌晨 3:00 运行一次
ALTER TABLE bets DROP PARTITION WHERE partition < toYYYYMM(today() - interval 90 day);

工作原理:

  • toYYYYMM(today() - interval 90 day)——计算 90 天前的分区名称。例如,如果今天是 2026-06-11,那么 90 天前是 2026-03-13,toYYYYMM 给出 202603
  • partition < 202603——删除所有名称小于 202603 的分区,如 202602、202601、202512 等。

重要: 此查询仅在分区名称是数字(年+月)时有效。对于字符串名称(例如 '2025-12'),你需要不同的比较方式。

选项 2:使用存储列表的更安全脚本

-- 查找超过 N 天的分区列表
SELECT partition
FROM system.parts
WHERE table = 'bets' 
  AND active = 1
  AND toDate(parseDateTimeBestEffort(partition)) < today() - interval 90 day
GROUP BY partition;

-- 对于每个找到的分区,执行 DROP(从外部脚本,而不是直接在 SQL 中)

为什么不用动态计算的 DROP PARTITION WHERE partition < ... 因为如果表中有非标准名称的分区(例如 'all' 或手动创建的),条件可能会意外行为。最好先查看列表。

Bash / Python 的完整示例脚本:

#!/bin/bash
# 删除 bets 表中超过 90 天的分区

CLICKHOUSE_CLIENT="clickhouse-client --host localhost"
TABLE="bets"
DAYS_TO_KEEP=90

# 计算边界日期
BORDER_DATE=$(date -d "today - $DAYS_TO_KEEP days" +%Y%m)

# 获取早于边界的分区列表
PARTITIONS=$($CLICKHOUSE_CLIENT --query="
    SELECT DISTINCT partition 
    FROM system.parts 
    WHERE table = '$TABLE' 
      AND active = 1 
      AND partition < '$BORDER_DATE'
      AND partition != 'all'
    FORMAT TabSeparated
")

# 删除每个分区
for PARTITION in $PARTITIONS; do
    echo "正在从 $TABLE 删除分区 $PARTITION"
    $CLICKHOUSE_CLIENT --query="ALTER TABLE $TABLE DROP PARTITION '$PARTITION'"
done

通过 cron 自动化:

# 每天凌晨 3:00 运行
0 3 * * * /usr/local/bin/cleanup_clickhouse_partitions.sh >> /var/log/cleanup.log 2>&1

接下来是什么

现在你明白了分区如何将数据删除从痛苦的操作转变为即时操作。接下来的主题:

  • 高级 TTL(生存时间)策略——无需干预即可自动删除或移动行。
  • 配置存储策略——如何按计划自动将分区从 SSD 移动到 S3。
  • 使用分区优化 SELECT——如何编写查询,使 ClickHouse 修剪掉 99% 的分区。

总结: ClickHouse 中的分区是管理数据生命周期的工具,而不仅仅是为了加速查询。选择分区大小,使每台服务器上有 100 到 1000 个分区。不要过度细化,也不要过度聚合。始终检查 system.parts——它会告诉你真相。


上一篇:
下一篇: ClickHouse中的ORDER BY与PRIMARY KEY:如何正确设置索引

— Editorial Team

Advertisement 728x90

继续阅读