还在为 ClickHouse 迁移到 Apache Doris 发愁?本文带你从零搭建 DDL 转换脚本、配置双跑导入链路、实现存量数据迁移与四阶段校验,代码可直接跑通。
关键词:Apache Doris · ClickHouse 迁移 · DDL 转换 · Stream Load · 数据校验 · Colocate Join · 全栈实战 · 存算分离
阅读前提
本教程假设你已具备:
- 基本的 ClickHouse 使用经验(了解 MergeTree、分布式表概念)
- 已部署 Apache Doris 集群(单节点或集群均可)
- 会使用基本的 SQL 和 Shell 命令
第一步:环境准备 & 连接 Doris
首先确认 Doris 集群可连接,并创建目标 Database:
sql
-- 连接 Doris(默认 MySQL 协议,端口 9030)
-- mysql -h 127.0.0.1 -P 9030 -u root
-- 创建目标数据库
CREATE DATABASE IF NOT EXISTS migration_demo;
-- 查看集群状态
SHOW BACKENDS;
SHOW FRONTENDS;
第二步:DDL 自动转换------从 CK 到 Doris
2.1 核心映射规则
ClickHouse 和 Apache Doris 在数据类型、表引擎、分区策略上存在差异,需要做以下映射:
python
# ck_to_doris_ddl.py ------ DDL 类型映射核心代码
# ClickHouse → Doris 类型的映射表(覆盖快手支持的 22 种)
TYPE_MAP = {
"UInt8": "TINYINT",
"UInt16": "SMALLINT",
"UInt32": "INT",
"UInt64": "BIGINT",
"Int8": "TINYINT",
"Int16": "SMALLINT",
"Int32": "INT",
"Int64": "BIGINT",
"Float32": "FLOAT",
"Float64": "DOUBLE",
"String": "VARCHAR(65533)",
"FixedString": "CHAR",
"Date": "DATE",
"DateTime": "DATETIME",
"DateTime64": "DATETIME",
"Array": "ARRAY",
"Map": "MAP",
"LowCardinality": "VARCHAR", # LowCardinality 展开为 VARCHAR
"Enum8": "TINYINT", # Enum 通常转为基础类型
"Enum16": "SMALLINT",
}
def convert_type(ck_type):
"""转换 ClickHouse 类型到 Doris 类型"""
# 处理 Nullable(Type)
if ck_type.startswith("Nullable("):
inner = ck_type[9:-1]
return convert_type(inner) # Doris 默认支持 NULL,去除 Nullable 包装
# 处理 Array(Type)
if ck_type.startswith("Array("):
inner = ck_type[6:-1]
return f"ARRAY<{convert_type(inner)}>"
# 基本类型映射
return TYPE_MAP.get(ck_type, "VARCHAR(65533)") # 未知类型兜底为 VARCHAR
2.2 生成完整的 Doris DDL
python
# 接上文件 ck_to_doris_ddl.py
import re
def generate_doris_ddl(ck_table_name, ck_columns, ck_partition_by, ck_order_by):
"""
根据 ClickHouse 表信息生成 Doris DDL
参数:
ck_table_name: ClickHouse 表名
ck_columns: [(col_name, col_type), ...]
ck_partition_by: ClickHouse 分区表达式
ck_order_by: ClickHouse 排序键列名列表
"""
doris_table = ck_table_name.replace(".", "_")
# 1. 生成列定义
col_defs = []
for col_name, col_type in ck_columns:
doris_type = convert_type(col_type)
col_defs.append(f" {col_name} {doris_type}")
# 2. 从 ORDER BY 推导 DUPLICATE KEY(前两列作为 Key)
key_cols = ck_order_by[:2] if len(ck_order_by) >= 2 else ck_order_by
# 3. 分区策略:取第一个 Date/DateTime 列做 RANGE 分区
partition_col = ck_partition_by if ck_partition_by else "event_date"
# 4. 分桶:按第一列 HASH,默认 32 桶
bucket_col = ck_order_by[0] if ck_order_by else ck_columns[0][0]
bucket_num = 32 # 可根据分区数据量调整
ddl = f"""
CREATE TABLE IF NOT EXISTS {doris_table} (
{','.join(col_defs)}
) ENGINE = OLAP
DUPLICATE KEY({','.join(key_cols)})
PARTITION BY RANGE({partition_col}) ()
DISTRIBUTED BY HASH({bucket_col}) BUCKETS {bucket_num}
PROPERTIES (
"replication_num" = "3"
);
"""
return ddl.strip()
# ---------- 使用示例 ----------
if __name__ == "__main__":
# 模拟一个 ClickHouse 表结构
ck_columns = [
("event_date", "Date"),
("user_id", "UInt64"),
("event_type", "String"),
("event_value", "Float64"),
("event_time", "DateTime"),
("tags", "Array(String)"),
("ext_info", "Map(String, String)"),
]
ck_order_by = ["user_id", "event_time"]
ddl = generate_doris_ddl(
ck_table_name="db.ck_user_behavior",
ck_columns=ck_columns,
ck_partition_by="event_date",
ck_order_by=ck_order_by
)
print("-- 生成的 Doris DDL --")
print(ddl)
执行结果将生成类似以下 DDL:
sql
-- 生成的 Doris DDL --
CREATE TABLE IF NOT EXISTS db_ck_user_behavior (
event_date DATE,
user_id BIGINT,
event_type VARCHAR(65533),
event_value DOUBLE,
event_time DATETIME,
tags ARRAY<VARCHAR(65533)>,
ext_info MAP<VARCHAR(65533),VARCHAR(65533)>
) ENGINE = OLAP
DUPLICATE KEY(user_id, event_time)
PARTITION BY RANGE(event_date) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3"
);
第三步:配置双跑------数据同时写入 CK 和 Doris
迁移期间,新增数据需要同时写入 ClickHouse 和 Apache Doris:
bash
#!/bin/bash
# dual_write.sh ------ 离线数据双写脚本
DORIS_HOST="127.0.0.1"
DORIS_PORT="8030" # Stream Load HTTP 端口
DORIS_USER="root"
DORIS_DB="migration_demo"
DORIS_TABLE="user_behavior"
# 假设已有 Hive → CK 的导出文件 data_export.csv
DATA_FILE="/data/export/data_export.csv"
# Stream Load 写入 Doris
curl --location-trusted -u ${DORIS_USER}: \
-H "label:dual_write_$(date +%s)" \
-H "column_separator:," \
-H "format:csv" \
-T ${DATA_FILE} \
http://${DORIS_HOST}:${DORIS_PORT}/api/${DORIS_DB}/${DORIS_TABLE}/_stream_load
echo "Dual write completed: $(date)"
第四步:存量数据迁移------Hive 到 Doris
对于上游 Hive 数据完整的情况,直接复用已有 ETL 链路:
sql
-- 通过 Doris Multi-Catalog 直接查询 Hive 并 INSERT INTO 目标表
-- 适用于 Hive 数据保留周期够长的场景
-- 1. 创建 Hive Catalog
CREATE CATALOG hive_catalog PROPERTIES (
"type" = "hms",
"hive.metastore.uris" = "thrift://hive-metastore:9083"
);
-- 2. 将 Hive 数据批量写入 Doris 内表
INSERT INTO migration_demo.user_behavior
SELECT
event_date,
user_id,
event_type,
event_value,
event_time
FROM hive_catalog.ods.user_behavior_hive
WHERE event_date >= '2025-01-01';
-- 3. 查看导入状态
SHOW LOAD FROM migration_demo;
第五步:数据校验------确保迁移结果可靠
5.1 基础数据量校验
sql
-- 第一层校验:行数和基础统计
-- 在 Doris 中执行
SELECT
'Doris' AS source,
COUNT(*) AS row_count,
SUM(event_value) AS total_value,
COUNT(DISTINCT user_id) AS unique_users
FROM migration_demo.user_behavior
UNION ALL
-- 对比 ClickHouse(需单独查询后贴入)
-- 预期结果:row_count 和 total_value 应一致
SELECT 'ClickHouse' AS source, 1000000, 12345678.90, 50000;
5.2 维度聚合校验
sql
-- 第二层:按核心维度 GROUP BY 对比
SELECT
event_date,
event_type,
COUNT(*) AS cnt,
SUM(event_value) AS sum_val,
AVG(event_value) AS avg_val
FROM migration_demo.user_behavior
WHERE event_date >= '2025-06-01'
GROUP BY event_date, event_type
ORDER BY event_date, event_type
LIMIT 100;
-- 将此结果与 ClickHouse 中相同查询结果逐行对比
5.3 Float 精度校验
python
# float_check.py ------ 精度容忍校验
import math
def check_float_precision(ck_value, doris_value, tolerance=0.001):
"""
检查 Float 类型精度偏差
参数:
ck_value: ClickHouse 侧数值
doris_value: Doris 侧数值
tolerance: 相对容忍阈值(默认 0.1%)
"""
if ck_value == 0 and doris_value == 0:
return True
relative_error = abs(ck_value - doris_value) / max(abs(ck_value), abs(doris_value))
if relative_error > tolerance:
print(f"⚠ 精度超标: CK={ck_value}, Doris={doris_value}, error={relative_error:.6f}")
return False
return True
# 使用示例
assert check_float_precision(3.14159265, 3.14159) # True(偏差约 0.00008%)
assert not check_float_precision(3.14, 3.25) # False(偏差约 3.5%,超标)
第六步:灰度切换
bash
#!/bin/bash
# gray_switch.sh ------ 通过 OneSQL 或其他路由层逐步切换
# 1. 10% 流量打向 Doris
echo "Gray release: 10% traffic"
# onesql-cli set-route --table user_behavior --target doris --weight 10
# 2. 观察 24 小时,确认无异常
# 3. 50% 流量
echo "Gray release: 50% traffic"
# onesql-cli set-route --table user_behavior --target doris --weight 50
# 4. 观察 24 小时
# 5. 100% 流量
echo "Full switch to Doris"
# onesql-cli set-route --table user_behavior --target doris --weight 100
常见问题
Q:DDL 转换后查询变慢了? A:检查三点:① Bucket 数是否根据数据量合理设置(太小导致并发不足,太大增加元数据压力);② 分桶 Key 是否与查询模式匹配;③ 需要 Join 的两张表是否配置了 Colocation Group。
Q:Stream Load 导入报错 "too many open files"? A:Doris BE 进程需要调整 ulimit -n,建议设为 65536 以上。
Q:补数期间 Doris 查询性能受影响? A:建议将补数任务放在业务低峰期执行,或在导入时设置 "strict_mode" = "false" 降低导入对查询的影响。
扩展阅读
关于 Apache Doris :Apache Doris 是高性能实时分析数据库,支持 PB 级数据亚秒级查询,广泛应用于报表分析、Ad-hoc 查询、统一数仓等场景。SelectDB 是 Apache Doris 的商业化公司,提供企业级支持和云服务。欢迎加入 Doris 社区 交流更多实践。