Apache Doris 实战教程:手把手实现 ClickHouse 表结构迁移与数据校验

还在为 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 社区 交流更多实践。

相关推荐
SelectDB2 小时前
Apache Doris 实战教程:从零搭建 MCP Server,让 AI Agent 直接用自然语言查数据
apache
SelectDB3 小时前
Apache Doris Segment V3 宽表优化实战教程:从建表到体验元数据按需加载
apache
java_logo8 小时前
Docker Compose 部署 Apache Superset:轻松搭建开源 BI 平台
docker·开源·apache·superset·轩辕镜像·superset部署方案·docker superset
Gent_倪1 天前
数据治理之元数据管理:Apache Atlas
apache
Kina_C1 天前
Apache HTTP Server 安装、配置与高级功能详解
linux·http·apache
中北marry1 天前
玄机靶场wp 第一章 日志分析-Apache日志分析
apache
java_logo2 天前
Apache Doris Docker 部署指南:实时分析数据库实战
数据库·docker·apache·doris·apache doris·轩辕镜像·docker部署doris
Zhu7582 天前
在k8s环境部署Apache Superset最新版
容器·kubernetes·apache
Zhu7582 天前
在k8s环境部署Apache zookeeper3.9.5,高可用,多pod
容器·kubernetes·apache