主题
第 18 章 生态与扩展
学习目标:跳出「ClickHouse 只是一个 SQL 数据库」的盒子,理解它在数据世界里的「外延」 —— 单进程版(
clickhouse-local)、嵌入式版(chdb)、UDF 扩展点、与 BI / 数据湖 / 流计算的集成、各语言客户端、周边运维工具。学完之后你会发现:把 ClickHouse 当一种「可被到处嵌入的列式 SQL 引擎」来用,比当数据库用更香。
0. 开场白:ClickHouse 不是一个产品,是一个「形态家族」
┌────────────── ClickHouse 形态家族 ───────────────┐
│ │
│ clickhouse-local ← 单进程 / 即跑即走 │
│ chdb (Python/Go/...) ← 库形态 / 嵌入应用进程 │
│ clickhouse-server ← 服务形态 / 真正的「数据库」│
│ Distributed cluster ← 分布式 / 跨机器 │
│ 云托管 (Altinity / Tinybird / ClickHouse Cloud) │
│ │
│ 共享同一份「列式 SQL 引擎 + MergeTree」核心 │
└───────────────────────────────────────────────────┘外延工具与生态围绕这个核心生长:
BI 看板 (Superset/Metabase/Redash/Grafana/DataLens)
▲ JDBC/ODBC/HTTP
│
客户端 (Python/Go/JS/Java) ←─── chdb 同核心引擎
│
──── ClickHouse 引擎 (server / local / chdb) ────
│
流计算 (Flink CDC) ─→ Kafka 引擎 ─→ MV ─→ MergeTree
数据湖 (S3/Parquet) ←─→ S3 表函数 / Delta / Iceberg
维度库 (MySQL/PG) ←─→ Dictionary / 表函数
ETL / 调度 (DBT-clickhouse, Airbyte, Debezium)
UDF (SQL UDF / Executable UDF / 自定义聚合函数)本章把这些外延一件一件讲清楚。
18.1 clickhouse-local:「SQL 版 awk」
18.1.1 它是什么
clickhouse-local 是一个独立可执行文件,把 ClickHouse 的 SQL 引擎完整内嵌进来,不需要任何服务端。它把磁盘 / 网络 / 标准输入流上的文件直接当作表来查。
bash
# 安装(如果没装服务端,单独拿)
curl https://clickhouse.com/ | sh
ls clickhouse # 单文件,二选一即可:./clickhouse local 或 ./clickhouse-local
# 体验:把 csv 当表查
$ cat sales.csv
date,country,amount
2025-04-15,CN,120
2025-04-15,US,80
2025-04-16,CN,200
$ clickhouse local --query "
SELECT country, sum(amount) AS total
FROM file('sales.csv', CSVWithNames,
'date Date, country String, amount UInt32')
GROUP BY country ORDER BY total DESC
"
CN 320
US 80📌 首次术语解释 · 表函数(Table Function):与「表」可互换的 SQL 构造,最典型就是
file('xxx.csv', Format)、url('https://...', Format)、s3('s3://...', Format),让你临时把外部资源当表查,无需 CREATE TABLE。
18.1.2 7 个最常用姿势
bash
# 1) 标准输入 → 一行 SQL
$ cat access.log | clickhouse local --query "
SELECT count() FROM table" --input-format LineAsString
# 2) Parquet 直接查(无 schema 也行,CH 自己推断)
$ clickhouse local -q "SELECT count(), sum(price) FROM file('orders.parquet')"
# 3) 多文件 + 通配符
$ clickhouse local -q "
SELECT toStartOfDay(ts), count() FROM file('logs/2025-04-*.json.gz', JSONEachRow)
GROUP BY 1 ORDER BY 1"
# 4) 输出格式自由切换
$ clickhouse local -q "SELECT * FROM file('a.csv') FORMAT JSONEachRow" > a.json
$ clickhouse local -q "SELECT * FROM file('a.csv') FORMAT PrettyCompact"
# 5) 文件互转(CSV → Parquet)
$ clickhouse local -q "
SELECT * FROM file('big.csv') FORMAT Parquet" > big.parquet
# 6) 读 S3 / HTTP
$ clickhouse local -q "
SELECT count() FROM s3(
'https://bkt.s3.amazonaws.com/2025/*.parquet','AKID','SK','Parquet')"
# 7) 直接连远端 ClickHouse 查(变成一个轻量客户端)
$ clickhouse local -q "
SELECT * FROM remote('ck-prod:9000','db.t','user','pwd') LIMIT 10"18.1.3 用 clickhouse-local 一行命令分析 nginx access log
下面这段是真实生产场景。access.log 一行长这样:
192.168.1.1 - - [17/Apr/2025:10:23:01 +0800] "GET /api/order HTTP/1.1" 200 1287 "-" "curl/7.68"目标:找出过去 1 小时内 5xx 状态码 Top 10 的接口路径,按出现次数倒序。
bash
clickhouse local --query "
SELECT
extract(line, '\"(?:GET|POST|PUT|DELETE) ([^ ?]+)') AS path,
extract(line, ' (\\d{3}) ') AS status,
count() AS errors
FROM file('/var/log/nginx/access.log', LineAsString, 'line String')
WHERE toUInt16OrZero(status) >= 500
AND parseDateTimeBestEffort(extract(line, '\\[([^\\]]+)\\]')) > now() - INTERVAL 1 HOUR
GROUP BY path, status
ORDER BY errors DESC
LIMIT 10
FORMAT PrettyCompact
"grep + awk + sort + uniq -c 几十行 shell 脚本能干的事,被 1 条 SQL 替代了,而且自动多线程、自动列存压缩中间状态。这就是为什么社区把 clickhouse-local 叫「SQL 版的 awk / sed」。
18.1.4 与 DuckDB 的对比
📌 横向对比小框
维度 clickhouse-local DuckDB 形态 单二进制 / Python (chdb) 单二进制 / Python pip 安装 引擎 ClickHouse 列式 + MergeTree 自研列式 + 向量化 SQL 方言 ClickHouse 方言(函数极多,2000+) PostgreSQL 方言 持久化 临时;可写本地 MergeTree 单文件 .duckdb 与 Pandas / Polars 集成 chdb / pyarrow 一等公民,零拷贝 分布式 同核心可扩展到 server / cluster 偏单机 典型场景 「平时用 CH server,临时分析掏出 local」 「一直就是单机/嵌入式」 一句话:DuckDB 是「设计就是嵌入式的」,clickhouse-local 是「server 引擎的小弟分身」;二者都很优秀,用熟一个即可,但选 ClickHouse 系的好处是:写过的 SQL、用过的函数、调过的优化经验,从 local 到 server 到分布式集群完全无缝。
18.2 chdb:嵌入式 ClickHouse
18.2.1 它是什么
chdb 把 ClickHouse 引擎打成 进程内库,不起 server,直接在 Python / Go / Rust 应用里调用。可以理解为「Python 里也有一个 SQLite,但它是 OLAP 列式 + 向量化的」。
18.2.2 安装与第一段 SQL
bash
pip install chdbpython
import chdb
# 一行 SQL
res = chdb.query("SELECT 1+1, version()")
print(res) # b"2\t24.x.x.x\n"
print(res.bytes()) # 同上 raw bytes18.2.3 直接对 Pandas DataFrame 跑 SQL
chdb 会把 df 通过零拷贝(基于 Arrow)注册成 ClickHouse 引擎里的「文件表」,然后你就能 SQL 它。
python
import pandas as pd
import chdb
df = pd.DataFrame({
"country": ["CN", "US", "CN", "JP", "US"],
"amount": [120, 80, 200, 90, 60],
})
# 老式:每次都把 df 序列化成 parquet 临时文件
import io, pyarrow as pa, pyarrow.parquet as pq
buf = io.BytesIO()
pq.write_table(pa.Table.from_pandas(df), buf)
open("/tmp/d.parquet","wb").write(buf.getvalue())
print(chdb.query(
"SELECT country, sum(amount) AS s FROM file('/tmp/d.parquet') GROUP BY country ORDER BY s DESC",
"PrettyCompact"
).bytes().decode())
# 新式:用 chdb.dataframe 模块(24.x 起)
from chdb.dataframe import query as df_query
out = df_query("SELECT country, sum(amount) AS s FROM df GROUP BY country", df=df)
print(out)18.2.4 持久化:给 chdb 一个 MergeTree 数据库目录
chdb 也支持把数据写到本地路径,下次进程启动数据还在:
python
sess = chdb.session.Session("/tmp/my_chdb_db")
sess.query("CREATE DATABASE IF NOT EXISTS app")
sess.query("""
CREATE TABLE IF NOT EXISTS app.events (
ts DateTime, user_id UInt64, value Float32
) ENGINE = MergeTree ORDER BY (user_id, ts)
""")
sess.query("INSERT INTO app.events VALUES (now(), 1, 3.14)")
print(sess.query("SELECT * FROM app.events").bytes().decode())18.2.5 适用场景
- 数据分析脚本 / Notebook:DataFrame 太大、Pandas 慢,又不想起 server。
- 机器学习预处理:把 Spark / Hive 抽出来的 Parquet 在本地跑 ETL。
- Edge / Lambda:函数计算容器内只跑几秒,不可能起 server。
- 测试 / CI:CI 里跑 SQL 单测,不想拉 docker。
📌 三种形态对比:
server是「数据库」、local是「命令行工具」、chdb是「进程内库」。三者引擎完全相同,所以学的 SQL、踩的坑、调的参数都通用。
18.3 UDF(用户自定义函数)
ClickHouse 的 UDF 分两类:SQL UDF 和 Executable UDF。
18.3.1 SQL UDF:纯 SQL 表达式
最轻量,等价于「带名字的表达式宏」,写完即用。
sql
CREATE FUNCTION fee_after_tax AS (price, tax_rate) -> price * (1 + tax_rate);
SELECT fee_after_tax(100, 0.13);
-- → 113
CREATE FUNCTION user_age_from_id AS (uid) ->
floor((toUInt64(now()) - bitAnd(uid, 0xFFFFFFFFFF)) / (3600 * 24 * 365));
DROP FUNCTION IF EXISTS fee_after_tax;特点:
- 只能是单条表达式,不能多语句、不能 IF;
- 定义存在
system.functions里; - 在 SQL 解析阶段直接展开,零开销。
适合「业务里反复出现的复杂表达式」,比如脱敏、特殊业务计算。
18.3.2 Executable UDF:调用任意外部脚本
更强大但也更危险:让 ClickHouse 把每行(或每批)数据通过 STDIN 传给外部进程,再读 STDOUT 拿结果。
第一步:写脚本(Python / Bash / 任何能读写 stdin/stdout 的语言)
/var/lib/clickhouse/user_scripts/upper.py
python
#!/usr/bin/env python3
import sys
for line in sys.stdin:
sys.stdout.write(line.strip().upper() + "\n")
sys.stdout.flush()bash
chmod +x /var/lib/clickhouse/user_scripts/upper.py第二步:在 <user_defined_executable_functions_config> 注册
config.d/udf.xml:
xml
<clickhouse>
<user_defined_executable_functions_config>/etc/clickhouse-server/udf_*.xml</user_defined_executable_functions_config>
</clickhouse>/etc/clickhouse-server/udf_upper.xml:
xml
<functions>
<function>
<type>executable</type>
<name>my_upper</name>
<return_type>String</return_type>
<argument><type>String</type></argument>
<format>TabSeparated</format>
<command>upper.py</command>
</function>
</functions>第三步:用
sql
SYSTEM RELOAD FUNCTIONS;
SELECT my_upper('hello clickhouse');
-- → HELLO CLICKHOUSE
SELECT user_id, my_upper(country) FROM events LIMIT 5;进阶:把 <type> 改成 executable_pool 可以复用进程(避免每次 fork),适合频繁调用的场景。
18.3.3 限制与安全
- 脚本必须放在
user_scripts目录里,且 ClickHouse 进程对其有执行权限; - 每行启动一个进程开销很大,必须用
executable_pool或一次处理一批; - 不要在 UDF 里做网络调用 / 写数据库:每行都做一次会拖死整个查询;
- 多租户场景下要开
<execute_direct>false</execute_direct>等限制;UDF 是 「绝对信任脚本」 的,能跑任何代码。
18.3.4 自定义聚合函数 / 窗口函数?
ClickHouse 不支持像 PG 那样自己写 C 注册聚合函数(社区有 clickhouse-cpp-udf 实验项目,未成主流)。需要自定义聚合时,常用做法是:
- 组合内置
quantile* / topK / argMax / sumMap等已经很丰富的函数; - 用
groupArray + arrayMaplambda 自己组装; - 实在不行,导出到
chdb在 Python 里算。
18.4 BI 集成
ClickHouse 几乎是所有主流 BI 工具的「一等公民数据源」。下面是最常用的连接姿势:
18.4.1 Apache Superset
text
Database URL:
clickhouse+native://default:@127.0.0.1:9000/learn_ck
Driver: clickhouse-connect / clickhouse-sqlalchemy或者 HTTP:
text
clickhousedb+connect://default:@127.0.0.1:8123/learn_ck实操注意:Superset 默认用 LIMIT 1000 探查表 schema,对超大表要在 Database 设置里开「Allow CREATE TABLE AS」、「Async Query」等,并把 query timeout 调大。
18.4.2 Metabase
社区版自 0.45 起内置 ClickHouse driver。Connection 字符串:
Host: 127.0.0.1 Port: 8123
DB: learn_ck User: default
SSL: 关18.4.3 Redash
text
DSN: clickhouse://default@127.0.0.1:8123/learn_ck18.4.4 Grafana
Grafana 9+ 官方插件 grafana-clickhouse-datasource:
yaml
# datasource.yml
apiVersion: 1
datasources:
- name: ClickHouse
type: grafana-clickhouse-datasource
jsonData:
host: 127.0.0.1
port: 9000
protocol: native
username: default适合做:监控面板、时序大屏、即席探索。
18.4.5 DataLens(Yandex 自家)
ClickHouse 是 Yandex 出品,DataLens 是同门 BI,集成度最深,可视化体验在 OLAP 时序分析上很顺手。SaaS 形态可直连云上 ClickHouse。
18.4.6 选型建议
| 场景 | 推荐 |
|---|---|
| 业务自助分析 / 公司报表 | Superset / Metabase |
| 监控大屏 / 时序面板 | Grafana |
| Notebook 式探索 | Redash / chdb + Jupyter |
| 全套 Yandex 生态 | DataLens |
18.5 数据湖与流计算集成
18.5.1 Spark:clickhouse-spark 与 JDBC
社区维护的 clickhouse-spark-connector 支持 DataSource V2,分区读写、谓词下推一应俱全。
scala
spark.read
.format("clickhouse")
.option("host", "ck-prod")
.option("database", "learn_ck")
.option("table", "events_v2")
.load()
.filter($"country" === "CN")
.groupBy($"event_date")
.agg(count("*"))或简单走 JDBC:
scala
spark.read.format("jdbc")
.option("url", "jdbc:clickhouse://ck-prod:8123/learn_ck")
.option("dbtable", "events_v2")
.load()📌 常见模式:Spark 跑长批 ETL(Hive / S3 → Parquet),最后一段
INSERT OVERWRITE把结果写入 ClickHouse 当查询层。Spark 干「重型搬运」,CH 干「即席查询」,分工明确。
18.5.2 Flink CDC → ClickHouse
MySQL/PG (源) ─┐ ┌─→ ClickHouse Kafka 引擎
│ Flink CDC │ ↓ Materialized View
▼ ▼
捕获 binlog/wal ─→ Kafka ─→ MergeTree两段式架构最稳:CDC 进 Kafka,CH 用 Kafka 引擎 + 物化视图消费。一旦下游 CH 抖动,Kafka 兜底。
也可以直接 flink-connector-clickhouse 一步到位写入。
18.5.3 DBT-ClickHouse
dbt-clickhouse 是官方 dbt 适配器。把 SQL 变更纳入 git + CI / CD:
yaml
# profiles.yml
my_project:
outputs:
prod:
type: clickhouse
host: ck-prod
port: 9000
schema: learn_ck
user: default
target: prodsql
-- models/daily_uv.sql
{{ config(materialized='materialized_view',
engine='AggregatingMergeTree() ORDER BY day',
to_table='daily_uv_target') }}
SELECT toDate(event_time) AS day,
uniqState(user_id) AS uv_state
FROM {{ ref('events_v2') }}
GROUP BY day;dbt run 会自动建/重建物化视图,并把版本和血缘记录在 dbt-docs 里。
18.5.4 Airbyte / Debezium
Airbyte 有 ClickHouse 的 source 与 destination connector,很适合做「Postgres 全表同步到 CH」「业务数据库 → 数仓」一类标准搬运任务。Debezium 直出 Kafka,配合 18.5.2 的两段式架构。
18.6 客户端生态(按语言)
| 语言 | 推荐库 | 协议 | 备注 |
|---|---|---|---|
| Python | clickhouse-connect(官方) | HTTP / 8123 | 全教程统一用它 |
| Python | clickhouse-driver | TCP / 9000 | 历史悠久;性能更接近 native |
| Python | chdb | 进程内 | 嵌入式,无需 server |
| Java / JVM | clickhouse-jdbc(官方) | HTTP/TCP | Spring Boot 直接接 |
| Go | clickhouse-go(官方 v2) | TCP | 高性能,支持 batch insert |
| Node.js | @clickhouse/client(官方) | HTTP | 流式读写都很自然 |
| C++ | clickhouse-cpp | TCP | 嵌入式应用 |
| .NET | ClickHouse.Client | HTTP | NuGet 装 |
| Rust | clickhouse.rs(官方) | HTTP | 异步 |
| ODBC | clickhouse-odbc | HTTP | Excel / Tableau / Power BI 走它 |
Python 写入示例(统一全教程风格):
python
import clickhouse_connect
client = clickhouse_connect.get_client(
host="127.0.0.1", port=8123, username="default", database="learn_ck"
)
# 批量写入
rows = [(1, "view"), (2, "click")]
client.insert("events", rows, column_names=["user_id", "event_type"])
# 查询
rs = client.query("SELECT count() FROM events").result_rows
print(rs[0][0])18.7 周边运维 / 数据工具
| 工具 | 一句话 | 典型场景 |
|---|---|---|
clickhouse-backup | 备份恢复工程化封装 | 第 17 章 |
clickhouse-benchmark | 内置压测客户端 | 灰度上线 / 容量评估 |
clickhouse-copier | 跨集群表数据搬运(已退出主推,新版用 INSERT FROM remote) | 老集群 → 新集群迁移 |
clickhouse-obfuscator | 数据脱敏:保留分布、随机替换值 | 给开发拿生产样本练 SQL |
clickhouse-diagnostics | 一键收集运维诊断包 | 提工单 |
clickhouse-keeper | ZK 替代品,CH 自研协调器 | 副本协调,更轻量 |
clickhouse-format | SQL 格式化 / 美化 | CI 校验 SQL 风格 |
18.7.1 clickhouse-benchmark 速用
bash
echo "SELECT count() FROM learn_ck.events_v2 WHERE country='CN'" |
clickhouse benchmark --host=127.0.0.1 --port=9000 \
--concurrency=8 --iterations=100
# 输出会给 P50 / P90 / P99 延迟、QPS、行/秒、字节/秒18.7.2 clickhouse-obfuscator 做生产样本
bash
clickhouse-client --query="SELECT * FROM events FORMAT Native" |
clickhouse obfuscator \
--structure="user_id UInt64, country String, event_type String" \
--input-format=Native --output-format=CSV --seed=42 > sample.csv随机化但保留分布特征(高基数列不变成 1 个值),开发拿到这份 CSV 在 chdb 或 local 里练 SQL,绝对不泄漏隐私。
18.8 真实案例串讲:用 clickhouse-local + chdb 做「一次性数据盘点」
背景:产品同学发了一份「过去 30 天用户事件 csv(200 GB,gzip)」,让你回答 5 个问题。 你既不想把它灌到 server(污染生产),也不想搬到 Python 一行一行处理(慢)。
bash
# Step 1: clickhouse-local 看 schema
clickhouse local -q "
DESCRIBE file('events_*.csv.gz', CSVWithNames)
SETTINGS schema_inference_make_columns_nullable=0"
# Step 2: 算一个全局 PV / UV
clickhouse local -q "
SELECT count(), uniqExact(user_id)
FROM file('events_*.csv.gz', CSVWithNames)"
# Step 3: 把粗算后的中间结果写成 parquet 落盘 (压缩后只有 200 MB)
clickhouse local -q "
SELECT toDate(ts) AS d, country, count() AS pv, uniqExact(user_id) AS uv
FROM file('events_*.csv.gz', CSVWithNames)
GROUP BY d, country
FORMAT Parquet" > daily.parquetpython
# Step 4: 在 Jupyter 里用 chdb + Pandas 继续探索
import chdb, pandas as pd
df = pd.read_parquet("daily.parquet")
print(chdb.dataframe.query("""
SELECT country, avg(uv) AS avg_uv, max(pv) AS peak_pv
FROM df GROUP BY country ORDER BY avg_uv DESC LIMIT 10
""", df=df))整个流程没有起服务端、没有动生产、SQL 体验完全一致。这就是 ClickHouse 生态的杀手级体验:SQL 写一遍,从命令行到嵌入式到分布式都能跑。
18.9 本章小结
┌──────────────────────────────────────────────────────────┐
│ 本章核心要点 │
├──────────────────────────────────────────────────────────┤
│ ① 三形态: │
│ server - 真正的数据库 │
│ local - SQL 版 awk, 单进程即跑即走 │
│ chdb - 嵌入式库, 在 Python/Go 进程里跑 SQL │
│ 三者引擎完全相同, SQL/调优经验通用 │
│ │
│ ② UDF 两类: │
│ SQL UDF - 表达式宏, 零开销 │
│ Executable UDF - 外挂脚本, 强大但要 pool & 防滥用 │
│ │
│ ③ BI 全家桶: Superset / Metabase / Redash / Grafana / │
│ DataLens, 都有现成 driver │
│ │
│ ④ 数据湖 / 流计算: │
│ Spark + clickhouse-spark / JDBC │
│ Flink CDC + Kafka 两段式 │
│ dbt-clickhouse 把 SQL 改动纳入 CI/CD │
│ Airbyte / Debezium 兜底 │
│ │
│ ⑤ 客户端: Python(connect/driver/chdb) / JDBC / Go / │
│ Node / C++ / Rust / ODBC, 全语言通行 │
│ │
│ ⑥ 运维工具: clickhouse-backup / -benchmark / │
│ -obfuscator / -diagnostics / -keeper / -format │
└──────────────────────────────────────────────────────────┘18.10 面试高频题
Q1:clickhouse-server、clickhouse-local、chdb 三者的关系与区别?
考察点:对 ClickHouse 形态全景的理解。
标准答案:
- 同一个引擎:三者都使用相同的 ClickHouse 列式 + 向量化执行 + MergeTree 存储核心,SQL 方言完全一致。
- 形态不同:
clickhouse-server是「服务端」:常驻进程,通过 TCP/9000、HTTP/8123 提供网络服务,数据持久化在/var/lib/clickhouse,是真正的「数据库」。clickhouse-local是「命令行工具」:单二进制,不监听端口,把 STDIN / 本地文件 / S3 / HTTP / 远端表当成临时表查;进程退出数据消失(除非显式写到 MergeTree 路径)。chdb是「进程内库」:把同一引擎打成 .so / .py 包,在 Python / Go 应用进程里直接调用,零网络开销,可读写 Pandas DataFrame。
- 取舍:生产数据库用 server;命令行临时分析 / CSV 处理用 local;嵌入数据脚本 / Notebook / Edge 函数用 chdb。
加分项:能补一句「DuckDB 的形态对应是 duckdb CLI 与 duckdb python 包,但 DuckDB 没有 server 形态;ClickHouse 系的优势是 server / local / chdb / Distributed cluster 一以贯之」。
易错点:以为 clickhouse-local 是「轻量版 server」,可以多人连接 —— 它根本不监听网络。
Q2:SQL UDF 和 Executable UDF 的区别?分别什么时候用?
考察点:UDF 体系。
标准答案:
- SQL UDF:
CREATE FUNCTION fn AS (a, b) -> a + b * 2。本质是「带名字的表达式宏」,在 SQL 解析阶段直接展开,零开销;只能写单条表达式,不能多语句、不能 IF。适合「业务里反复出现的复杂计算式」。 - Executable UDF:通过 XML 注册一个外部脚本(Python / Bash / Go),ClickHouse 把行通过 STDIN 给它,从 STDOUT 拿结果;功能强(任意逻辑),但代价高(每次 fork 进程)。必须用
executable_pool复用进程,绝对不要在 UDF 里做网络调用。
加分项:能提「ClickHouse 不支持 PG 那种自定义聚合 / 窗口函数;需要时一般组合内置 topK / quantile / argMax,或 export 到 chdb 在 Python 里完成」。
易错点:在 SQL UDF 里写多语句逻辑(语法错误);在 Executable UDF 里逐行 fork 进程(性能塌方)。
Q3:ClickHouse 适合用什么 BI 工具?接入要注意什么?
考察点:BI 集成实战。
标准答案:
- 主流 BI(Superset / Metabase / Redash / Grafana / DataLens)都有官方或社区 driver,连接串通常是
clickhouse://default:@host:8123/db(HTTP)或clickhouse+native://default:@host:9000/db(TCP)。 - 接入注意点:
- Schema 探查:很多 BI 默认
LIMIT 1000拉样本,对超大表 OK;但SELECT *探 schema 时记得加FORMAT Null或LIMIT 0,避免拉数据。 - Query timeout / async:BI 自身有超时,对长聚合查询要在 BI 与 CH user profile 双侧调大
max_execution_time。 - 并发限制:用
quotas给 BI 用户限并发,防止「一个看板拖死全集群」。 - 缓存:能在 BI 层缓存就在 BI 层缓存(Superset cache、Grafana panel 缓存),CH 端再开 query result cache 进一步优化。
- 权限:建专用 readonly 用户给 BI 用,加行级 / 列级权限。
- Schema 探查:很多 BI 默认
加分项:能提「时序大屏首选 Grafana + grafana-clickhouse-datasource,自助分析首选 Superset」。
易错点:用 default 账号 + 无 quota,结果 BI 误发一个全表扫的查询打挂集群。
Q4:你怎么把 MySQL / PostgreSQL 的实时变更同步到 ClickHouse?
考察点:流批集成架构。
标准答案:两段式 CDC 是行业标准:
源 MySQL/PG
↓ (Flink CDC / Debezium / 阿里 Canal)
Kafka topic (按 binlog/wal 顺序投递)
↓ (ClickHouse Kafka 引擎表)
↓ (Materialized View 把消息变换/过滤后)
ReplicatedMergeTree (业务最终表)为什么两段式:
- Kafka 起到「削峰 + 兜底」作用,下游 CH 抖动不丢数据;
- 多个下游消费者可以并行消费(CH、ES、HDFS、监控),不耦合;
- 调试和回放方便。
也可一步到位:用 flink-connector-clickhouse 直写 CH,或 ClickHouse 自带的 MaterializedPostgreSQL 引擎做 PG 整库 CDC(实验性)。但生产推荐 Kafka 中介。
注意点:
- CDC 投到 Kafka 时通常有「Insert / Update / Delete」三种事件,CH 落地用
ReplacingMergeTree(version)+ 软删,或CollapsingMergeTree(sign)做行折叠。 - 一定要给 Kafka topic 设合理 retention,避免重启时 offset 丢失要从源库重头来一次。
加分项:能提「dbt-clickhouse 在 CDC 之后做汇总层 / 维度层,把数据建模纳入 git」。
易错点:直接 Flink → CH,CH 一抖整条链路停摆;或者把 CDC 当成「INSERT only」忽略 update 事件,结果 CH 里数据永不更新。
Q5:clickhouse-local / chdb 与 DuckDB 怎么选?
考察点:行业视野。
标准答案:「取决于你的整体技术栈」。
- 如果你的生产环境本来就跑着 ClickHouse server,那么用
clickhouse-local/chdb让本地 SQL、CI 单测、Notebook 与生产完全一致:函数库一致、SQL 方言一致、调优经验通用,零迁移成本。 - 如果你只是单机 / 嵌入式分析,没有 OLAP 集群,DuckDB 在「与 Pandas/Polars/Arrow 的零拷贝集成」「单文件 .duckdb 形式持久化」上更顺手,且 SQL 方言贴近 PG 标准。
- 性能方面:在「单机 + 中等数据量(< 100 GB)」上两者各有胜负,差距不大;超过 1 TB 或需要分布式 / 高并发服务,ClickHouse 系优势明显(因为它有 server 形态)。
- 趋势:两者都在快速演进;选熟悉的一个深用就好,不必互斥(
chdb与 DuckDB 都可以同时装在 Python 里)。
加分项:能提「DuckDB 主导『嵌入式 OLAP』范式,ClickHouse 反过来用 clickhouse-local / chdb 拥抱了这个范式 —— 行业共识:OLAP 不再只是大集群,而是从嵌入式到分布式连续光谱」。
易错点:把它们说成「替代品」 —— 在很多团队里它们并存,分别服务不同人群。
📌 本章总结:到这里,我们已经从「ClickHouse 是什么」「怎么用」「怎么调」「怎么运维」,走到了「它在整个数据世界中的位置」。下一章(第 19 章)会用一个完整项目把前 18 章串起来 —— 从建模 → 摄入 → 物化视图 → 查询 → 看板,让你真正具备「拿 ClickHouse 做一套实时数据系统」的能力。
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
python
#!/usr/bin/env python3
"""
第 18 章 · chdb 嵌入式 ClickHouse 演示
=========================================
干什么:
1) 用 chdb 直接在 Python 进程里跑 ClickHouse SQL, 无需 server.
2) 演示三种典型形态:
(a) 纯 SQL 计算 (numbers/range)
(b) 直接读取本地 CSV / Parquet 文件
(c) Pandas DataFrame ↔ chdb 互通 (DataFrame as table)
3) 演示 Session 模式: 在内存里建表, 多次查询复用 + 持久化到本地目录.
依赖:
pip install chdb pandas pyarrow
运行:
python chdb_demo.py
"""
from __future__ import annotations
import os
import sys
import tempfile
import textwrap
from pathlib import Path
try:
import chdb
from chdb import session as chs
except ImportError:
print("[ERROR] 请先 pip install chdb", file=sys.stderr)
sys.exit(1)
try:
import pandas as pd
except ImportError:
print("[ERROR] 请先 pip install pandas pyarrow", file=sys.stderr)
sys.exit(1)
def banner(title: str) -> None:
print()
print("=" * 60)
print(f" {title}")
print("=" * 60)
# ------------------------------------------------------------
# Demo 1: 一行 SQL 算 1 ~ 1e6 的和 (纯 stateless)
# ------------------------------------------------------------
def demo_stateless() -> None:
banner("Demo 1 · stateless: chdb.query 直接出结果")
res = chdb.query(
"SELECT count() AS cnt, sum(number) AS total FROM numbers(1000000)",
"PrettyCompact",
)
print(res)
# ------------------------------------------------------------
# Demo 2: 用 chdb 读 CSV / 写 Parquet
# ------------------------------------------------------------
def demo_files(workdir: Path) -> Path:
banner("Demo 2 · file(): 把本地 CSV 当成表")
csv_path = workdir / "sales.csv"
csv_path.write_text(textwrap.dedent("""\
order_id,country,amount,ts
1,CN,120.50,2026-04-01 10:00:00
2,US, 88.00,2026-04-01 10:05:00
3,CN,330.10,2026-04-01 10:30:00
4,JP, 50.00,2026-04-01 11:00:00
5,CN,210.00,2026-04-01 11:30:00
6,US,400.00,2026-04-01 12:00:00
7,JP,160.00,2026-04-01 12:10:00
8,DE, 75.00,2026-04-01 12:30:00
"""), encoding="utf-8")
sql = f"""
SELECT country,
count() AS orders,
round(sum(amount), 2) AS gmv
FROM file('{csv_path}', CSVWithNames)
GROUP BY country
ORDER BY gmv DESC
FORMAT PrettyCompact
"""
print(chdb.query(sql))
parquet_path = workdir / "sales.parquet"
chdb.query(f"""
SELECT * FROM file('{csv_path}', CSVWithNames)
INTO OUTFILE '{parquet_path}'
FORMAT Parquet
""")
print(f"[OK] 已写出 Parquet: {parquet_path} "
f"({parquet_path.stat().st_size} bytes)")
return parquet_path
# ------------------------------------------------------------
# Demo 3: Pandas DataFrame 互通
# chdb 0.x 起支持: 把 DataFrame 当成虚拟表, 用 SQL 直接查
# ------------------------------------------------------------
def demo_dataframe() -> None:
banner("Demo 3 · DataFrame 互通: SQL on Pandas")
df = pd.DataFrame({
"user_id": [1, 1, 2, 2, 3, 3, 3],
"event": ["view", "buy", "view", "view", "view", "buy", "buy"],
"amount": [0, 99.0, 0, 0, 0, 12.5, 8.0],
})
print("[原始 DataFrame]")
print(df)
print()
sql = """
SELECT user_id,
countIf(event = 'view') AS views,
countIf(event = 'buy') AS buys,
round(sumIf(amount, event = 'buy'), 2) AS spend
FROM Python(df)
GROUP BY user_id
ORDER BY user_id
FORMAT PrettyCompact
"""
try:
print(chdb.query(sql))
except Exception as e: # 老版本 chdb 可能不支持 Python() 表函数
print(f"[WARN] 当前 chdb 版本不支持 Python() 表函数: {e}")
print("[FALLBACK] 用 csv 中转")
with tempfile.NamedTemporaryFile(
mode="w", suffix=".csv", delete=False
) as f:
df.to_csv(f.name, index=False)
tmp = f.name
sql2 = f"""
SELECT user_id,
countIf(event = 'view') AS views,
countIf(event = 'buy') AS buys,
round(sumIf(amount, event = 'buy'), 2) AS spend
FROM file('{tmp}', CSVWithNames)
GROUP BY user_id
ORDER BY user_id
FORMAT PrettyCompact
"""
print(chdb.query(sql2))
os.unlink(tmp)
# ------------------------------------------------------------
# Demo 4: Session 模式 + 持久化 (本地目录就是数据库)
# ------------------------------------------------------------
def demo_session(workdir: Path, parquet: Path) -> None:
banner("Demo 4 · Session 模式: 本地目录 = 数据库, 多次查询复用")
db_dir = workdir / "chdb_data"
db_dir.mkdir(exist_ok=True)
sess = chs.Session(str(db_dir))
sess.query("CREATE DATABASE IF NOT EXISTS demo")
sess.query("""
CREATE TABLE IF NOT EXISTS demo.sales
ENGINE = MergeTree
ORDER BY ts
AS SELECT * FROM file('{path}', Parquet)
""".format(path=parquet))
print("[demo.sales 行数]")
print(sess.query("SELECT count() FROM demo.sales", "PrettyCompact"))
print("[按国家汇总]")
print(sess.query("""
SELECT country, count() AS orders, round(sum(amount), 2) AS gmv
FROM demo.sales
GROUP BY country
ORDER BY gmv DESC
FORMAT PrettyCompact
"""))
sess.close()
print(f"[OK] 关闭 session. 数据落地于 {db_dir} (下次直接 reopen 即可)")
def main() -> None:
print("chdb version:", getattr(chdb, "__version__", "unknown"))
with tempfile.TemporaryDirectory(prefix="chdb_demo_") as tmp:
workdir = Path(tmp)
demo_stateless()
parquet = demo_files(workdir)
demo_dataframe()
demo_session(workdir, parquet)
banner("DONE")
print("以上 4 个 demo 均不需要 ClickHouse server, 全部跑在 Python 进程内.")
if __name__ == "__main__":
main()bash
#!/usr/bin/env bash
# ============================================================
# 第 18 章 · clickhouse-local 演示脚本
# ------------------------------------------------------------
# 干什么:
# 1) 生成一份「nginx access log 样例」(100 行)
# 2) 用 clickhouse-local 跑 6 段不同形态的 SQL 演示, 完全无需 server
# 3) 输出转 Parquet, 演示「文件即表 + 一行命令转格式」
#
# 前置:
# - 已安装 clickhouse-local 或 clickhouse 多功能二进制
# curl https://clickhouse.com/ | sh # 在当前目录得到 ./clickhouse
# export CHL="./clickhouse local" 或 alias CHL="clickhouse local"
#
# 用法:
# bash local_demo.sh
# ============================================================
set -euo pipefail
# 优先用 PATH 上的 clickhouse-local; 没有就用 ./clickhouse local
if command -v clickhouse-local >/dev/null 2>&1; then
CHL="clickhouse-local"
elif command -v clickhouse >/dev/null 2>&1; then
CHL="clickhouse local"
elif [[ -x "./clickhouse" ]]; then
CHL="./clickhouse local"
else
echo "[ERROR] 找不到 clickhouse-local. 请先 'curl https://clickhouse.com/ | sh'" >&2
exit 1
fi
WORKDIR="$(mktemp -d -t ch_local_demo_XXXX)"
LOG="${WORKDIR}/access.log"
PARQUET="${WORKDIR}/access.parquet"
echo "==================================================="
echo "工作目录: ${WORKDIR}"
echo "==================================================="
# ------------------------------------------------------------
# 1) 生成 100 行 nginx 风格的 access.log
# ------------------------------------------------------------
echo
echo "[Step 1] 生成 100 行 nginx access.log → ${LOG}"
cat > "${LOG}" <<'EOF_HEAD'
EOF_HEAD
PATHS=("/api/order" "/api/order" "/api/order" "/api/user" "/api/cart" "/api/pay" "/static/js/app.js" "/health")
STATUSES=(200 200 200 200 200 200 304 404 500 502 503)
METHODS=(GET GET GET POST POST DELETE)
for i in $(seq 1 100); do
ip="$((RANDOM % 200 + 1)).$((RANDOM % 255)).$((RANDOM % 255)).$((RANDOM % 255))"
ts_offset=$((i * 30))
ts="$(date -u -d "${ts_offset} seconds ago" +'%d/%b/%Y:%H:%M:%S +0000' 2>/dev/null \
|| date -u -v "-${ts_offset}S" +'%d/%b/%Y:%H:%M:%S +0000')"
m="${METHODS[$((RANDOM % ${#METHODS[@]}))]}"
p="${PATHS[$((RANDOM % ${#PATHS[@]}))]}"
s="${STATUSES[$((RANDOM % ${#STATUSES[@]}))]}"
size=$((RANDOM % 8000 + 100))
echo "${ip} - - [${ts}] \"${m} ${p} HTTP/1.1\" ${s} ${size} \"-\" \"curl/7.68\"" >> "${LOG}"
done
wc -l "${LOG}"
head -3 "${LOG}"
# ------------------------------------------------------------
# 2) 演示 SQL ① 总行数
# ------------------------------------------------------------
echo
echo "[Step 2] 总行数"
$CHL --query "
SELECT count() AS total
FROM file('${LOG}', LineAsString, 'line String')
"
# ------------------------------------------------------------
# 3) 演示 SQL ② 状态码分布 Top
# ------------------------------------------------------------
echo
echo "[Step 3] HTTP 状态码分布 Top"
$CHL --query "
WITH file('${LOG}', LineAsString, 'line String') AS T
SELECT
extract(line, ' (\\d{3}) ') AS status,
count() AS cnt
FROM T
GROUP BY status
ORDER BY cnt DESC
FORMAT PrettyCompact
"
# ------------------------------------------------------------
# 4) 演示 SQL ③ 5xx 路径 Top 10 (生产场景)
# ------------------------------------------------------------
echo
echo "[Step 4] 5xx 失败接口 Top 10"
$CHL --query "
WITH parsed AS (
SELECT
extract(line, ' \"(?:GET|POST|PUT|DELETE) ([^ ?]+)') AS path,
toUInt16OrZero(extract(line, ' (\\d{3}) ')) AS status
FROM file('${LOG}', LineAsString, 'line String')
)
SELECT path, count() AS errors
FROM parsed
WHERE status >= 500
GROUP BY path
ORDER BY errors DESC
LIMIT 10
FORMAT PrettyCompact
"
# ------------------------------------------------------------
# 5) 演示 SQL ④ 按方法统计平均响应字节
# ------------------------------------------------------------
echo
echo "[Step 5] 按 HTTP 方法的平均响应字节"
$CHL --query "
SELECT
extract(line, ' \"([A-Z]+) ') AS method,
round(avg(toUInt32OrZero(extract(line, ' (\\d{3}) (\\d+) ')))) AS avg_bytes,
count() AS cnt
FROM file('${LOG}', LineAsString, 'line String')
GROUP BY method
ORDER BY cnt DESC
FORMAT PrettyCompact
"
# ------------------------------------------------------------
# 6) 演示 SQL ⑤ 转 Parquet 落盘 (一行命令)
# ------------------------------------------------------------
echo
echo "[Step 6] 把 log 解析成结构化 Parquet → ${PARQUET}"
$CHL --query "
SELECT
extract(line, '^([^ ]+) ') AS ip,
parseDateTimeBestEffortOrNull(extract(line, '\\[([^\\]]+)\\]')) AS ts,
extract(line, ' \"([A-Z]+) ') AS method,
extract(line, ' \"(?:GET|POST|PUT|DELETE) ([^ ?]+)') AS path,
toUInt16OrZero(extract(line, ' (\\d{3}) ')) AS status,
toUInt32OrZero(extract(line, ' \\d{3} (\\d+) ')) AS bytes
FROM file('${LOG}', LineAsString, 'line String')
FORMAT Parquet
" > "${PARQUET}"
ls -lh "${PARQUET}"
# ------------------------------------------------------------
# 7) 演示 SQL ⑥ 把 Parquet 当表反查
# ------------------------------------------------------------
echo
echo "[Step 7] 反查 Parquet, 验证落盘后还能跑同样的 SQL"
$CHL --query "
SELECT toStartOfMinute(ts) AS minute, count() AS cnt
FROM file('${PARQUET}', Parquet)
WHERE ts IS NOT NULL
GROUP BY minute
ORDER BY minute DESC
LIMIT 5
FORMAT PrettyCompact
"
echo
echo "==================================================="
echo "全部演示完成. 中间文件保留在: ${WORKDIR}"
echo " rm -rf ${WORKDIR} # 清理"
echo "==================================================="