ClickHouse 完全指南

ClickHouse 是列式存储数据库。与行式存储(MySQL、PostgreSQL)不同,列式存储将同一列的所有数据连续存放在磁盘上。 行式存储 vs 列式存储: ClickHouse 适用场景:日志分析、用户行为统计、监控指标聚合、实时报表。 MergeTree 是 ClickHouse 最重要的存储引擎族,所有生产场景几乎都用此系列。 最基础的引擎,数据按 ORDER BY 键排序存储,后台定期合并数据片段(parts)。 在后台合并时,对相同 ORDER BY 键的行进行去重,保留最新版本。用于模拟 UPSERT 语义。 注意:去重仅在后台合并时

分享

官方文档:https://clickhouse.com/docs/zh
适用版本:ClickHouse 24.x(2026-05-07 整理)


目录


核心概念

列式存储原理

ClickHouse 是列式存储数据库。与行式存储(MySQL、PostgreSQL)不同,列式存储将同一列的所有数据连续存放在磁盘上。

行式存储 vs 列式存储

维度 行式存储(MySQL) 列式存储(ClickHouse)
读取方式 按行读取,每次读整行 按列读取,只读需要的列
写入性能 高(适合单行写入) 一般(适合批量写入)
聚合查询 慢(需扫描所有列) 快(只扫描聚合列)
压缩效率 低(同列数据类型相同但不连续) 高(同列数据相似,压缩比高)
适用场景 OLTP(事务处理) OLAP(分析查询)

OLAP vs OLTP

特性 OLAP OLTP
全称 Online Analytical Processing Online Transaction Processing
典型操作 聚合、扫描大量行 点查、单行增删改
数据量 数亿到数百亿行 通常数千万行以内
写入模式 批量写入 单行写入、更新、删除
代表数据库 ClickHouse、Doris、Hive MySQL、PostgreSQL
查询延迟 秒级(但扫描量大) 毫秒级

ClickHouse 适用场景:日志分析、用户行为统计、监控指标聚合、实时报表。


MergeTree 系列引擎

MergeTree 是 ClickHouse 最重要的存储引擎族,所有生产场景几乎都用此系列。

MergeTree

最基础的引擎,数据按 ORDER BY 键排序存储,后台定期合并数据片段(parts)。

CREATE TABLE events
(
    event_date Date,
    user_id    UInt32,
    event_type String,
    value      Float64
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id);

ReplacingMergeTree

在后台合并时,对相同 ORDER BY 键的行进行去重,保留最新版本。用于模拟 UPSERT 语义。

CREATE TABLE user_profiles
(
    user_id    UInt32,
    name       String,
    updated_at DateTime
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY user_id;

注意:去重仅在后台合并时发生,查询时可能仍有重复行,需配合 FINAL 关键字或 GROUP BY 手动去重。

-- 强制去重(性能较差,谨慎使用)
SELECT * FROM user_profiles FINAL;

SummingMergeTree

合并时对相同 ORDER BY 键的行进行数值列求和,适合预聚合场景。

CREATE TABLE page_views_agg
(
    date      Date,
    page_id   UInt32,
    views     UInt64,
    duration  Float64
)
ENGINE = SummingMergeTree((views, duration))
ORDER BY (date, page_id);

括号内指定需要求和的列,未指定的数值列也会被求和,非数值列保留第一行的值。

AggregatingMergeTree

比 SummingMergeTree 更通用,支持存储聚合函数的中间状态(使用 AggregateFunction 类型)。通常配合物化视图使用。

CREATE TABLE visits_agg
(
    date       Date,
    page_id    UInt32,
    uniq_users AggregateFunction(uniq, UInt32)
)
ENGINE = AggregatingMergeTree()
ORDER BY (date, page_id);

写入时需使用 -State 后缀函数,查询时使用 -Merge 后缀函数。

-- 写入
INSERT INTO visits_agg
SELECT date, page_id, uniqState(user_id)
FROM raw_visits
GROUP BY date, page_id;

-- 查询
SELECT date, page_id, uniqMerge(uniq_users)
FROM visits_agg
GROUP BY date, page_id;

引擎对比

引擎 合并时行为 适用场景
MergeTree 仅排序合并,不去重不聚合 通用原始数据存储
ReplacingMergeTree 去重,保留最新行 状态表、UPSERT 语义
SummingMergeTree 对数值列求和 计数器、简单预聚合
AggregatingMergeTree 合并聚合函数中间状态 复杂预聚合、物化视图

PARTITION BY 与 ORDER BY 的关系

  • PARTITION BY:决定数据如何分区存储在磁盘上,查询时可跳过不相关分区(分区裁剪)
  • ORDER BY:决定分区内数据的物理排序,同时决定 MergeTree 系列的去重/聚合键,查询时支持稀疏索引加速

两者关系:

  • ORDER BY 的第一列通常与 PARTITION BY 相关,保证同一分区内数据局部性好
  • PRIMARY KEY 若未指定,默认等于 ORDER BY;可单独设置 PRIMARY KEYORDER BY 的前缀(仅用于索引,不影响排序)
  • 分区数量建议控制在数千以内,过多分区严重影响性能(见踩坑部分)

数据库与表操作

创建数据库

CREATE DATABASE IF NOT EXISTS analytics;
USE analytics;

CREATE TABLE 完整语法

CREATE TABLE [IF NOT EXISTS] [db.]table_name
(
    column1 Type1 [DEFAULT expr1] [COMMENT 'comment'],
    column2 Type2 [CODEC(compression_codec)],
    ...
    INDEX index_name expr TYPE type GRANULARITY n
)
ENGINE = MergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[TTL expr [DELETE|TO DISK 'disk'|TO VOLUME 'vol']]
[SETTINGS setting = value, ...];

常用子句说明

子句 是否必须 说明
ENGINE 必须 指定存储引擎
ORDER BY MergeTree 必须 排序键,决定主键索引
PARTITION BY 可选 分区表达式,建议按时间分区
PRIMARY KEY 可选 默认等于 ORDER BY,可设为其前缀
TTL 可选 数据过期时间,支持删除或迁移到冷存储
SETTINGS 可选 引擎级别参数,如 index_granularity=8192

TTL 示例

CREATE TABLE logs
(
    ts      DateTime,
    level   String,
    message String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(ts)
ORDER BY ts
TTL ts + INTERVAL 30 DAY DELETE;

ALTER TABLE

-- 新增列
ALTER TABLE events ADD COLUMN os String DEFAULT '' AFTER event_type;

-- 删除列
ALTER TABLE events DROP COLUMN os;

-- 修改列类型
ALTER TABLE events MODIFY COLUMN value Float32;

-- 修改 TTL
ALTER TABLE logs MODIFY TTL ts + INTERVAL 7 DAY;

-- 删除分区
ALTER TABLE events DROP PARTITION '202401';

-- 清空表
TRUNCATE TABLE events;

注意:ClickHouse 的 ALTER TABLE 是异步的,ADD COLUMN / DROP COLUMN 操作立即返回,实际变更在后台执行,但不影响查询。

INSERT INTO

ClickHouse 写入设计为批量操作,每次 INSERT 产生一个新的 part,后台异步合并。

-- 基础写入
INSERT INTO events (event_date, user_id, event_type, value)
VALUES ('2024-01-01', 1001, 'click', 1.0);

-- 从另一张表写入(批量)
INSERT INTO events_archive
SELECT * FROM events WHERE event_date < '2024-01-01';

-- 指定格式写入(HTTP 接口常用)
INSERT INTO events FORMAT CSV
2024-01-01,1001,click,1.0
2024-01-01,1002,view,1.0

字段类型

数值类型

类型 字节 范围 说明
UInt8 1 0 ~ 255 无符号整数
UInt16 2 0 ~ 65535 无符号整数
UInt32 4 0 ~ 4294967295 无符号整数
UInt64 8 0 ~ 1.8×10¹⁹ 无符号整数
Int8 1 -128 ~ 127 有符号整数
Int16 2 -32768 ~ 32767 有符号整数
Int32 4 -2³¹ ~ 2³¹-1 有符号整数
Int64 8 -2⁶³ ~ 2⁶³-1 有符号整数
Float32 4 IEEE 754 单精度 注意精度损失
Float64 8 IEEE 754 双精度 注意精度损失
Decimal(P, S) 变长 精确小数 P=总位数,S=小数位数,金融场景使用

字符串类型

类型 说明
String 变长字符串,无长度限制,UTF-8
FixedString(N) 固定长度 N 字节,不足则补零,适合 MD5/UUID 等定长值
UUID 128 位 UUID,等价于 FixedString(16),有专用函数
Enum8('a'=1, 'b'=2) 枚举,最多 256 个值,节省存储
Enum16(...) 枚举,最多 65536 个值

时间类型

类型 精度 范围 说明
Date 1970-01-01 ~ 2149-06-06 2 字节存储
Date32 1900-01-01 ~ 2299-12-31 4 字节存储
DateTime 1970-01-01 ~ 2106-02-07 4 字节,可附带时区
DateTime64(precision, timezone) 亚秒 精度 0~9(毫秒=3,微秒=6,纳秒=9) 8 字节
-- 带时区的 DateTime
CREATE TABLE t (ts DateTime('Asia/Shanghai'));

复合类型

类型 示例 说明
Array(T) Array(UInt32) 变长数组,元素类型必须一致
Tuple(T1, T2, ...) Tuple(String, UInt32) 固定长度异构元组
Map(K, V) Map(String, UInt64) 键值对,K 必须是基础类型
Nullable(T) Nullable(String) 允许 NULL,有额外存储开销,尽量避免
LowCardinality(T) LowCardinality(String) 低基数列字典编码,适合枚举值少的 String 列

数组函数

ClickHouse 对数组操作有丰富的内置支持。

arrayJoin

将数组展开为多行,类似 SQL 的 UNNEST。

SELECT arrayJoin([1, 2, 3]) AS val;
-- 返回三行: 1, 2, 3

SELECT user_id, arrayJoin(tags) AS tag
FROM articles;
-- 将每篇文章的 tags 数组展开,每个 tag 一行

arrayMap

对数组每个元素应用 lambda 函数,返回新数组。

SELECT arrayMap(x -> x * 2, [1, 2, 3]);
-- [2, 4, 6]

SELECT arrayMap(x -> x + 1, prices) AS adjusted_prices
FROM products;

arrayFilter

过滤数组中满足条件的元素。

SELECT arrayFilter(x -> x > 2, [1, 2, 3, 4]);
-- [3, 4]

-- 过滤掉 0 值
SELECT arrayFilter(x -> x != 0, values) AS non_zero
FROM metrics;

arraySum

对数组元素求和。

SELECT arraySum([1, 2, 3, 4]);
-- 10

SELECT user_id, arraySum(daily_scores) AS total_score
FROM user_stats;

其他常用数组函数

函数 说明
length(arr) 数组长度
arrayFirst(f, arr) 返回第一个满足条件的元素
arrayExists(f, arr) 是否存在满足条件的元素
arrayAll(f, arr) 是否所有元素都满足条件
indexOf(arr, x) 返回元素 x 的下标(从 1 开始),不存在返回 0
has(arr, x) 数组是否包含元素 x
arrayUniq(arr) 数组唯一值数量
arrayDistinct(arr) 去重后的数组
arraySort(arr) 升序排序
arrayReverse(arr) 反转数组
arraySlice(arr, offset, len) 切片
array_concat(arr1, arr2) 拼接两个数组

查询语法

SELECT 基础与 MySQL 的差异

-- 基础查询结构(与 MySQL 基本相同)
SELECT
    toDate(ts) AS date,
    user_id,
    count()    AS cnt
FROM events
WHERE ts >= '2024-01-01'
  AND event_type = 'click'
GROUP BY date, user_id
ORDER BY date DESC, cnt DESC
LIMIT 100;

ClickHouse 与 MySQL SELECT 的主要差异

差异点 MySQL ClickHouse
count() 需写 count(*) count() 即可(等价 count(*)
GROUP BY 必须列出所有非聚合列 同 MySQL,但支持列编号 GROUP BY 1, 2
NULL 处理 标准 SQL 大量函数对 NULL 行为不同,尽量避免 Nullable
子查询 灵活 支持但性能不如 JOIN,复杂子查询建议改写
大小写 关键字不区分大小写 同 MySQL
字符串引号 单引号为字符串,双引号为标识符 同 MySQL
反引号 用于标识符 用于标识符(同 MySQL)

聚合函数

函数 说明
count() 行数
count(col) 非 NULL 值的行数
sum(col) 求和
avg(col) 平均值
min(col) 最小值
max(col) 最大值
uniq(col) 去重计数(近似,HyperLogLog)
uniqExact(col) 精确去重计数(内存消耗大)
quantile(level)(col) 近似分位数,level 为 0~1
quantileExact(level)(col) 精确分位数
groupArray(col) 将列值聚合成数组
groupUniqArray(col) 去重后聚合成数组
argMax(val, ts) 返回 ts 最大时对应的 val
argMin(val, ts) 返回 ts 最小时对应的 val
-- 分位数示例
SELECT
    quantile(0.5)(response_time)  AS p50,
    quantile(0.95)(response_time) AS p95,
    quantile(0.99)(response_time) AS p99
FROM requests
WHERE date = today();

GROUP BY WITH ROLLUP / CUBE

-- ROLLUP: 从右到左逐级汇总
SELECT
    toYear(event_date) AS year,
    toMonth(event_date) AS month,
    sum(value) AS total
FROM events
GROUP BY year, month WITH ROLLUP
ORDER BY year, month;

-- CUBE: 所有维度组合的汇总
SELECT
    region,
    product,
    sum(revenue) AS total
FROM sales
GROUP BY region, product WITH CUBE;

-- TOTALS: 额外返回总计行
SELECT
    event_type,
    count() AS cnt
FROM events
GROUP BY event_type WITH TOTALS;

窗口函数

ClickHouse 从 21.6 开始正式支持窗口函数,语法与标准 SQL 兼容。

-- 排名
SELECT
    user_id,
    score,
    row_number() OVER (ORDER BY score DESC) AS rn,
    rank()       OVER (ORDER BY score DESC) AS rnk,
    dense_rank() OVER (ORDER BY score DESC) AS drnk
FROM leaderboard;

-- 分组内排名
SELECT
    user_id,
    category,
    revenue,
    row_number() OVER (
        PARTITION BY category
        ORDER BY revenue DESC
    ) AS category_rank
FROM sales;

-- 滑动窗口聚合
SELECT
    ts,
    value,
    avg(value) OVER (
        ORDER BY ts
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_avg_7
FROM metrics;

PREWHERE vs WHERE

这是 ClickHouse 特有的优化机制。

特性 WHERE PREWHERE
执行时机 读取所有指定列后过滤 先读过滤列,过滤后再读其余列
适用场景 通用 过滤性强的条件 + 查询列较多时
数据读取量 全部查询列都读取 过滤列先读,被过滤掉的行的其他列不读
自动优化 - ClickHouse 会自动将部分 WHERE 条件转为 PREWHERE
-- 手动使用 PREWHERE(通常不需要,引擎自动优化)
SELECT user_id, name, email, address, phone
FROM users
PREWHERE is_active = 1
WHERE created_at > '2024-01-01';
-- is_active 先过滤,只有满足条件的行才读取 name/email/address/phone

多数情况下让 ClickHouse 自动决定即可,不必手动写 PREWHERE

WITH CTE

-- 基础 CTE
WITH
    active_users AS (
        SELECT DISTINCT user_id
        FROM events
        WHERE event_date >= today() - 30
    )
SELECT u.user_id, u.name
FROM users u
WHERE u.user_id IN (SELECT user_id FROM active_users);

-- 多个 CTE
WITH
    daily_stats AS (
        SELECT
            toDate(ts) AS date,
            count() AS cnt
        FROM events
        GROUP BY date
    ),
    avg_stat AS (
        SELECT avg(cnt) AS avg_cnt FROM daily_stats
    )
SELECT date, cnt, avg_cnt
FROM daily_stats, avg_stat
ORDER BY date;

JOIN 类型

ClickHouse 的 JOIN 与 MySQL 有重要差异:右表(小表)必须能放入内存,不支持 hash join 溢出到磁盘(默认配置)。

-- 基础 JOIN 语法
SELECT a.user_id, a.event_type, b.name
FROM events AS a
INNER JOIN users AS b ON a.user_id = b.user_id;

JOIN 类型

JOIN 类型 说明
INNER JOIN 取交集,与 MySQL 相同
LEFT OUTER JOIN 左表所有行,右表无匹配为 NULL
RIGHT OUTER JOIN 右表所有行,左表无匹配为 NULL
FULL OUTER JOIN 两表并集
CROSS JOIN 笛卡尔积
LEFT SEMI JOIN 左表中在右表有匹配的行(不展开右表)
LEFT ANTI JOIN 左表中在右表无匹配的行
LEFT ANY JOIN 右表只取第一条匹配(不重复)
ASOF JOIN 时间序列近似匹配 JOIN(ClickHouse 特有)

ClickHouse JOIN 注意事项

  • 大表 JOIN 大表性能差,尽量用右表为小表(维度表)
  • 推荐使用字典(Dictionary)代替频繁的维度表 JOIN
  • 分布式查询中 JOIN 需要注意数据分布,可能产生大量网络传输

Python 客户端

clickhouse-driver(同步,原生 TCP 协议)

pip install clickhouse-driver
from clickhouse_driver import Client

# 创建连接
client = Client(
    host='localhost',
    port=9000,
    database='analytics',
    user='default',
    password='',
    settings={'use_numpy': True}
)

# 查询,返回列表
rows = client.execute('SELECT user_id, count() FROM events GROUP BY user_id LIMIT 10')
# [(1001, 500), (1002, 300), ...]

# 查询带列名
rows, columns = client.execute(
    'SELECT user_id, count() AS cnt FROM events GROUP BY user_id LIMIT 10',
    with_column_types=True
)
# columns: [('user_id', 'UInt32'), ('cnt', 'UInt64')]

# 参数绑定(防注入)
rows = client.execute(
    'SELECT * FROM events WHERE event_type = %(event_type)s AND event_date = %(date)s',
    {'event_type': 'click', 'date': '2024-01-01'}
)

# 批量插入(推荐方式)
data = [
    ('2024-01-01', 1001, 'click', 1.0),
    ('2024-01-01', 1002, 'view', 1.0),
]
client.execute(
    'INSERT INTO events (event_date, user_id, event_type, value) VALUES',
    data
)

# 流式查询(大数据量)
settings = {'max_block_size': 100000}
for block in client.execute_iter('SELECT * FROM big_table', settings=settings):
    process(block)

Client 构造参数:

参数 类型 默认值 说明
host str 'localhost' 服务器地址
port int 9000 原生 TCP 端口
database str 'default' 默认数据库
user str 'default' 用户名
password str '' 密码
connect_timeout int 10 连接超时(秒)
send_receive_timeout int 300 读写超时(秒)
compression bool/str False 传输压缩(lz4/zstd
settings dict {} ClickHouse 查询设置

asynch(异步,原生 TCP 协议)

pip install asynch
import asyncio
from asynch import connect

async def main():
    conn = await connect(
        host='localhost',
        port=9000,
        database='analytics',
        user='default',
        password=''
    )
    cursor = await conn.cursor()

    # 查询
    await cursor.execute('SELECT count() FROM events')
    result = await cursor.fetchall()

    # 批量插入
    await cursor.execute(
        'INSERT INTO events (event_date, user_id, event_type, value) VALUES',
        [
            ('2024-01-01', 1001, 'click', 1.0),
            ('2024-01-01', 1002, 'view', 1.0),
        ]
    )
    await conn.commit()
    await conn.close()

asyncio.run(main())

clickhouse-connect(HTTP 协议)

适合通过 HTTP/HTTPS 连接,支持 pandas/arrow 格式输出,适合数据科学场景。

pip install clickhouse-connect
import clickhouse_connect

# 创建客户端
client = clickhouse_connect.get_client(
    host='localhost',
    port=8123,
    username='default',
    password='',
    database='analytics'
)

# 查询为 QueryResult
result = client.query('SELECT * FROM events LIMIT 10')
print(result.result_rows)   # 列表形式
print(result.column_names)  # 列名

# 查询为 pandas DataFrame
df = client.query_df('SELECT * FROM events LIMIT 10')

# 查询为 Apache Arrow
table = client.query_arrow('SELECT * FROM events LIMIT 10')

# 插入 pandas DataFrame
import pandas as pd
df = pd.DataFrame({
    'event_date': ['2024-01-01'],
    'user_id': [1001],
    'event_type': ['click'],
    'value': [1.0]
})
client.insert_df('events', df)

# 批量插入列表
client.insert(
    'events',
    data=[
        ['2024-01-01', 1001, 'click', 1.0],
        ['2024-01-01', 1002, 'view', 1.0],
    ],
    column_names=['event_date', 'user_id', 'event_type', 'value']
)

批量插入最佳实践

ClickHouse 每次 INSERT 都会产生一个新的 part,过于频繁的小批量 INSERT 会导致 part 数量爆炸,触发 Too many parts 错误,后台合并压力大。

# 错误做法:逐行插入
for row in rows:
    client.execute('INSERT INTO events VALUES', [row])  # 每行一个 part,严禁

# 正确做法一:累积后批量插入
BATCH_SIZE = 10000
buffer = []

for row in rows:
    buffer.append(row)
    if len(buffer) >= BATCH_SIZE:
        client.execute('INSERT INTO events VALUES', buffer)
        buffer.clear()

if buffer:
    client.execute('INSERT INTO events VALUES', buffer)

# 正确做法二:使用队列异步缓冲
import asyncio
from asyncio import Queue

async def insert_worker(queue: Queue, client):
    batch = []
    while True:
        try:
            row = await asyncio.wait_for(queue.get(), timeout=1.0)
            batch.append(row)
            if len(batch) >= 10000:
                await flush(client, batch)
                batch.clear()
        except asyncio.TimeoutError:
            if batch:
                await flush(client, batch)
                batch.clear()

批量插入建议

建议 说明
单次插入行数 1000 ~ 100000 行
插入频率 每秒不超过 1~2 次(同一张表)
使用缓冲区 应用层累积后批量提交
考虑 Buffer 引擎 ClickHouse Buffer 引擎在内存中缓冲写入,定期刷入目标表

踩坑与注意事项

不支持事务

ClickHouse 不支持跨行的 ACID 事务。每条 INSERT 语句是原子的(要么整批成功,要么失败),但不支持 BEGIN/COMMIT/ROLLBACK。

如果需要保证幂等写入,使用 ReplacingMergeTree 并在业务层保证 ORDER BY 键的唯一性。

UPDATE/DELETE 代价极高

ClickHouse 的 UPDATE/DELETE 是通过重写整个数据 part 实现的(Mutation),非常慢且消耗大量 IO。

-- 这两个操作都是异步重写 part,代价极高
ALTER TABLE events UPDATE value = 0 WHERE user_id = 1001;
ALTER TABLE events DELETE WHERE user_id = 1001;

-- 查看 mutation 状态
SELECT * FROM system.mutations WHERE is_done = 0;

替代方案

  • 不需要更新:设计时让数据只追加
  • 需要更新最新状态:使用 ReplacingMergeTree,INSERT 新版本,查询时用 FINALargMax
  • 逻辑删除:增加 is_deleted 标志列,查询时过滤

分区数量不宜过多

每个分区对应磁盘上的独立目录,每次 INSERT 在相关分区中创建新 part。分区数量过多导致:

  • Too many parts 错误(默认超过 300 个 active parts 报错)
  • 后台合并线程压力大
  • 内存占用增加

建议

  • 时间分区粒度不低于月(toYYYYMM(date)),高频写入场景用天(toYYYYMMDD(date))已是上限
  • 不要用高基数字段(如 user_id)做分区键
  • 单表分区总数控制在数百到数千以内

Nullable 类型的性能损耗

Nullable(T) 需要额外存储一个 null mask 文件,聚合函数对 Nullable 列的处理更慢。除非业务上确实存在 NULL,否则用默认值代替(空字符串、0、1970-01-01 等)。

JOIN 右表必须小

大表与大表 JOIN 在 ClickHouse 中性能极差,右表必须能放入内存。替代方案:

  • 使用字典(Dictionary)做维度查找,替代 JOIN
  • 提前在 ETL 阶段做宽表,避免查询时 JOIN

时区问题

ClickHouse 默认使用服务器时区存储 DateTime。跨时区部署时需要明确指定时区:

-- 显式指定时区
CREATE TABLE t (ts DateTime('Asia/Shanghai'));

-- 查询时转换
SELECT toDateTime(ts, 'Asia/Shanghai') FROM t;

数据采样(SAMPLE BY)

如果表定义了 SAMPLE BY,可以用采样查询快速得到近似结果:

-- 只扫描 10% 的数据,结果近似
SELECT count() * 10 AS approx_total
FROM events SAMPLE 0.1;

最佳实践

使用 MergeTree 系列引擎:普通 OLAP 场景首选 ReplacingMergeTree(幂等写入)或 AggregatingMergeTree(预聚合),避免使用 Memory 引擎存储持久数据。

按查询模式设计分区键PARTITION BY toYYYYMM(ts) 让按月过滤的查询只扫描相关分区。避免分区过细(超过 1000 个分区会拖慢后台合并)。

主键(ORDER BY)排在过滤字段前:把最高基数、最常 WHERE 的字段放前面。典型模式:ORDER BY (tenant_id, event_date, user_id)

批量写入代替逐行 INSERT:每次写入至少 1000 行,使用 clickhouse-client--format 批量导入或 HTTP batch API,单行 INSERT 会造成大量小文件影响合并性能。

-- 批量插入(推荐)
INSERT INTO events (ts, user_id, action)
VALUES
  ('2024-01-01 00:00:00', 1, 'click'),
  ('2024-01-01 00:00:01', 2, 'view'),
  -- ...
;

使用物化视图做实时预聚合:将高频聚合查询结果写入 AggregatingMergeTree,查询时用 *Merge 函数合并中间状态,避免每次全表扫描。

CREATE MATERIALIZED VIEW mv_hourly
ENGINE = AggregatingMergeTree()
ORDER BY (hour, user_id)
AS
SELECT
    toStartOfHour(ts) AS hour,
    user_id,
    countState() AS cnt
FROM events
GROUP BY hour, user_id;

-- 查询
SELECT hour, user_id, countMerge(cnt) FROM mv_hourly GROUP BY hour, user_id;

右表用字典替代 JOIN:维度表(城市、品类)转为 Dictionary,通过 dictGet() 点查,避免大表 JOIN 内存溢出。


常见陷阱

陷阱:ORDER BY 与主键混淆

现象: 以为 PRIMARY KEY 决定存储顺序,实际查询没有走稀疏索引。
原因: ClickHouse 的 PRIMARY KEY 仅定义稀疏索引范围,存储顺序由 ORDER BY 决定。若只写 ORDER BY 不写 PRIMARY KEY,则主键默认与 ORDER BY 相同。
解决: 明确只写 ORDER BY (col1, col2),不要与 PRIMARY KEY 混用,除非有意只对前几列建稀疏索引。

陷阱:高频小批量写入导致"too many parts"错误

现象: 写入一段时间后查询变慢,日志出现 Too many parts,最终抛出异常。
原因: 每次 INSERT 生成一个 Part,后台 Merge 线程来不及合并,Part 数超过 max_parts_in_total 限制(默认 100000)。
解决: 应用层做缓冲,每次写入 ≥ 1000 行;或使用 Buffer 引擎中间层聚合后批量落盘。

-- Buffer 引擎:先写 Buffer,满足条件后自动 flush 到目标表
CREATE TABLE events_buffer AS events
ENGINE = Buffer(default, events, 4, 10, 60, 1000, 100000, 10000000, 1000000000);

陷阱:DateTime 时区存储与查询不一致

现象: 插入数据后按时间范围查询结果偏移 8 小时。
原因: 表建时未指定时区,使用服务器默认 UTC 存储;应用层传入的是本地时间字符串,导致偏移。
解决: 建表时明确指定 DateTime('Asia/Shanghai'),同时确保写入端也传带时区的 ISO 8601 字符串。


参见

MySQL基础完全指南
PostgreSQL完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog