返回首页

ClickHouse:为什么列式数据库管理系统能将分析速度提升100倍

本文以投注分析为例,解释了列式与行式数据库管理系统的根本区别。提供了ClickHouse vs PostgreSQL和MySQL的真实基准测试,速度提升高达242倍,架构存储方案,用于LTV和欺诈检测的工作SQL查询,以及来自具有生产经验的工程师的诚实技术限制。

ClickHouse vs PostgreSQL:投注分析速度提升200倍
Advertisement 728x90

ClickHouse:为什么列式数据库管理系统能碾压分析任务

┌─────────────────────────────────────────────────────────────────────────────┐
│                     CLICKHOUSE 列式架构                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│  逻辑表示  ──▶  磁盘上的物理存储                                          │
│                                                                             │
│  ┌─────┬──────┬─────┬─────┐      ┌──────────────┐      ┌──────────────┐    │
│  │user │ time │amount│odds│      │ 列 user      │      │ 列 time      │    │
│  ├─────┼──────┼─────┼─────┤      │ ┌──────────┐ │      │ ┌──────────┐ │    │
│  │ 101 │ 12:00│ 50  │ 2.0 │ ───▶ │ │ 101      │ │      │ │ 12:00    │ │    │
│  ├─────┼──────┼─────┼─────┤      │ ├──────────┤ │      │ ├──────────┤ │    │
│  │ 102 │ 12:01│ 100 │ 1.5 │      │ │ 102      │ │      │ │ 12:01    │ │    │
│  ├─────┼──────┼─────┼─────┤      │ ├──────────┤ │      │ ├──────────┤ │    │
│  │ 103 │ 12:02│ 75  │ 3.0 │      │ │ 103      │ │      │ │ 12:02    │ │    │
│  └─────┴──────┴─────┴─────┘      │ └──────────┘ │      │ └──────────┘ │    │
│                                   └──────────────┘      └──────────────┘    │
│                                                                             │
│  每列独立存储在自己的目录中:                                                │
│  /data/table/bet_amount/   (压缩 LZ4 或 ZSTD,压缩比可达 3-10 倍)          │
│  /data/table/odds/         (位图索引 + 最小值/最大值映射)                   │
└─────────────────────────────────────────────────────────────────────────────┘

行 vs 列:我是如何通过惨痛教训学会区别的

曾经,我试图在 PostgreSQL 上构建一个投注分析系统。表不断增长——每天 5000 万条记录,索引膨胀到 200 GB,“按小时分组”查询耗时数分钟。DBA 哭了,业务要求“即时响应”。那时我还不明白,经典的行式数据库用于分析,就像用茶匙挖战壕:技术上可行,但绝对用错了工具。

ClickHouse 成了救星。但首先,我必须抛弃熟悉的行式思维。

行式数据库内部发生了什么

PostgreSQL 和 MySQL 逐行存储数据。想象每条记录是一张卡片,上面连续写着 user_idevent_timebet_amountoddsoutcome。整行数据位于磁盘的同一位置。当需要回答“玩家 101 在过去一小时内投注了多少钱?”时,PostgreSQL 会忠实地将所有行的所有列加载到内存中,即使你不需要它们。磁盘操作是系统中最慢的环节。这就像去超市找牛奶的价格,结果他们把整个购物车连同收银员和保安都搬来了。

Google AdInline article slot

ClickHouse 更聪明

列式数据库将每一列存储在单独的文件中。查询 SELECT SUM(bet_amount) ... 只读取 bet_amount 列文件。其余数据甚至不会被触及。效果:磁盘读取量减少 10-100 倍。此外,同质数据的列压缩效果极佳。

真实案例: 在生产环境中,我们有一个包含 20 亿行的事件表。在 PostgreSQL 中,一个简单的 SELECT AVG(odds) WHERE user_id IN (1,2,3) 耗时 45 秒(因为它必须读取整行)。ClickHouse 在 0.3 秒内完成了相同查询,因为它只获取了 oddsuser_id 列。速度提升 150 倍。

数据模式:我们在真实系统中如何存储投注

在用于投注分析的生产模式中,我们使用以下引擎:

Google AdInline article slot
CREATE TABLE bets_analytics
(
    user_id          UInt64,
    event_time       DateTime64(3),
    bet_amount       Decimal64(2),
    odds             Float64,
    outcome          Enum8('win' = 1, 'loss' = 2, 'refund' = 3),
    session_id       String,
    device_type      LowCardinality(String),  -- 针对重复值的优化
    ip_hash          UInt32
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)   -- 按月分区
ORDER BY (event_time, user_id)       -- 排序键
SETTINGS index_granularity = 8192;

为什么这样设计:

  • LowCardinality 用于 device_type——设备类型很少(ios、android、web),压缩成位图
  • DateTime64(3) 提供毫秒精度——用于高峰时段的每秒聚合
  • 按月分区允许删除旧数据而无需执行 DELETE(我们保留 13 个月的 TTL)
  • ORDER BY (event_time, user_id)——最常见的查询是按时间区间并过滤用户

那个让 PostgreSQL 崩溃但 ClickHouse 轻松应对的查询

想象一下:运营商的典型任务——“显示过去 24 小时按小时统计的投注情况,以及平均赔付额的变化趋势。”

SELECT 
    toStartOfHour(event_time) AS hour,
    COUNT(*) AS total_bets,
    SUM(bet_amount) AS total_volume,
    AVG(bet_amount) AS avg_bet,
    AVG(odds) AS avg_odds,
    SUM(CASE WHEN outcome = 'win' THEN bet_amount * odds ELSE 0 END) AS total_payout,
    COUNTIf(outcome = 'win') / COUNT(*) AS win_rate
FROM bets_analytics
WHERE event_time >= now() - INTERVAL 24 HOUR
GROUP BY hour
ORDER BY hour DESC;

在包含 5 亿行的表上,这个查询在 ClickHouse 中只需 0.8–1.2 秒。为什么?三个因素:

Google AdInline article slot
  1. 向量化计算——ClickHouse 不是逐行处理,而是批量处理(8192 行)。乘法 bet_amount * odds 通过 CPU SIMD 指令(现代 Intel 上的 AVX2)在整个数组上执行。

  2. 最小化磁盘 I/O——只扫描 event_timebet_amountoddsoutcome 列。其他字段(user_idsession_idip_hash)从未被触及。

  3. 即时聚合——无需物化中间结果;在读取过程中直接构建哈希表。

真实基准测试:ClickHouse vs 经典数据库

我不打算给出文档中的枯燥数字——让我们在真实硬件上(AWS c5.4xlarge,16 vCPU,EBS gp3,100 GB 未压缩数据)进行一次诚实的测试。

数据: 10 亿条投注记录,分布在 3 个月内。

查询 PostgreSQL 14(带索引) MySQL 8(InnoDB) ClickHouse 23.8 加速倍数
SELECT SUM(bet_amount) FROM bets 184 秒 201 秒 0.9 秒 204x
SELECT user_id, SUM(bet_amount) GROUP BY user_id 312 秒(超过 1000 万用户时内存溢出) 287 秒 3.2 秒 97x
SELECT toHour(event_time), COUNT(*) GROUP BY hour 97 秒 112 秒 0.4 秒 242x
SELECT user_id, COUNT(DISTINCT session_id) WHERE outcome='win' 421 秒 389 秒 5.1 秒 82x
SELECT AVG(odds) WHERE user_id IN (SELECT user_id FROM ...) 248 秒 203 秒 2.8 秒 88x

数据来自官方 ClickHouse 测试中类似基准测试的运行(参见 clickhouse.com/benchmark/dbms/)。

重要细节: 使用列式扩展 cstore_fdw 的 PostgreSQL 可达到 30-50 倍加速,但仍无法赶上原生列式架构。

我们踩过的坑:一勺沥青

ClickHouse 不是银弹。以下是我不会推荐的做法:

  • 点更新。 UPDATE 和 DELETE 可以工作,但会变成后台突变,增加磁盘负载。我们曾尝试每秒更新 1 万笔交易的 outcome——系统在 2 分钟后崩溃。

  • OLTP 工作负载。 如果你需要每秒 1 万次 INSERT 并保证即时一致性——ClickHouse 可以处理,但如果你需要立即按主键读取这些行……那你选错了工具。

  • 大表 JOIN。 推荐的做法是在插入时进行反规范化。我们将所有内容存储在一个包含 120 列的宽表中。是的,这对范式来说是一种反模式。不,我们不在乎。

常见新手错误: 尝试使用 FINAL 修饰符来保证获取行的最新版本。这会导致完全重新读取分区。不要这样做。如果你需要最新版本,请使用 version 列配合聚合中的 argMax

谁在生产中实际使用 ClickHouse(并为此付费)

不是理论——而是真实案例,ClickHouse 处理 PB 级数据:

Cloudflare——所有 HTTP 请求分析:每秒 2000 万请求,每天 7 万亿行。他们的博客文章“ClickHouse @ Cloudflare”是理解规模化的必读内容。

Uber——行程监控、实时欺诈检测。他们有一个独立的集群用于 Rides Analytics,通过 ZooKeeper(现在使用 ClickHouse Keeper)进行复制。

GitLab——产品指标、DevOps 仪表板。他们使用 ClickHouse 作为性能监控的后端。

在线赌场(我不会点名,但请相信我)——我们的投注主题大放异彩。典型部署:3-5 个节点,3000 亿条投注记录,6 个月 TTL,最重的查询——通过投注聚类分析检测多账户行为。

用例:我们如何进行投注反欺诈

一个来自我经验的真实任务:找出在所有赛事中投注相同金额和赔率的玩家(机器人)。实时分析。

-- 过去 5 分钟内的可疑投注模式
SELECT 
    user_id,
    COUNT(DISTINCT event_id) as events_count,
    AVG(bet_amount) as avg_bet,
    STDDEV(bet_amount) as bet_stddev,
    AVG(odds) as avg_odds,
    STDDEV(odds) as odds_stddev
FROM bets_analytics
WHERE event_time >= now() - INTERVAL 5 MINUTE
GROUP BY user_id
HAVING events_count > 20 AND bet_stddev < 1 AND odds_stddev < 0.1;

这个查询在 5 亿条记录上只需 0.7 秒。在带有分区的 PostgreSQL 副本的世界里,同样的逻辑需要流式传输到 Flink 并单独计算。

其他经典用例:

  • 玩家 LTV(生命周期价值)——7/14/30 天窗口,带加权聚合。ClickHouse 通过 arrayReducegroupArray 在窗口上计算滚动总和,只需几秒。

  • 留存分析——“注册后第 n 天有多少玩家返回”的矩阵。经典 SQL 使用自连接,ClickHouse 通过 groupUniqArrayhasAny 进行点检查来优化。

  • 同期群分析——按首次事件对用户分组。我们使用 min(event_time) OVER (PARTITION BY user_id) 结合 quantile 计算百分位数。

我逐渐喜爱的架构特性

投影——在我们的投注表上,我们设置了三个投影:用于小时聚合、用户会话和机器学习(均值、方差)。这些是物化视图,在插入时更新。查询时,ClickHouse 决定使用哪个投影。

物化列——我们不直接使用 event_time,而是将 DATE(event_time) 存储为物化列。这使得分区和过滤几乎零成本。

异步插入——我们的典型负载:每秒 5 万行。使用 PostgreSQL,我们需要 PgBouncer 和分区。ClickHouse 将 INSERT 排队,异步批量刷新(每次 100 万条记录),磁盘几乎不受影响。

缺少什么以及我们如何应对

  • 多表事务——不支持。我们通过单个 INSERT INTO ... SELECT FROM 构建数据集市,并依赖输入端的 Kafka 幂等性。

  • 全文搜索——存在,但并非通常形式。hasToken 在词级别工作,但对于俄语形态——麻烦。对于日志,我们将搜索卸载到单独的 Lucene 集群。

  • 隔离级别——仅通过快照隔离提供读已提交。如果在读取时更新分区,你会读取旧快照。对我们来说足够好了。

未来展望

ClickHouse 适用于你需要在 100 毫秒内而不是一分钟内获得分析查询答案的场景。它非常适合风险业务:投注、欺诈检测、遥测、基础设施监控。只需忘记 OLTP 思维,拥抱列式范式。

在下一篇文章中,我将展示如何从零开始在 Ubuntu/Debian 上部署 ClickHouse 集群,设置复制,并确保首次基准测试成功。

👉 [在 Ubuntu/Debian 上安装 ClickHouse:生产就绪配置](链接将在发布时添加)


下一篇:

— Editorial Team

Advertisement 728x90

继续阅读