练习表 3:Schema 转换
把 TRIPS_RAW 和 FACT_TRIPS 的每一列映射到对应的 ClickHouse 类型,并翻译七个 Snowflake 表达式,每个答案都有即时反馈。
预计耗时: 20–25 分钟 参考资料: Snowflake 与 ClickHouse 对照,第 2 节(SQL 方言差异)
概念
类型映射和函数翻译是迁移中最机械的部分,但如果做得草率, 也是最容易出错的部分。Snowflake 和 ClickHouse 的类型 系统不同,语义也不同,用错类型会导致无声的精度 丢失、存储浪费或查询逻辑错误。
关键原则:
-
对精度要显式。 Snowflake 的
TIMESTAMP_NTZ(9)具有纳秒 精度。ClickHouse 的DateTime只有秒级精度,不要把它用于 版本列。用DateTime64(3, 'UTC')表示毫秒精度(符合大多数 现实需求),或用DateTime64(9, 'UTC')表示纳秒。这关乎 正确性: 如果ReplacingMergeTree的版本列只有秒级精度,同一秒内 到达的两次更新就是不确定的,ClickHouse 无法 判断哪一个更新。 -
使用最小的正确整数类型。 Snowflake 的
INTEGER就是NUMBER(38, 0), 以 128 位值存储的 38 位定点精度。ClickHouse 有定宽 整数:Int8、Int16、Int32、Int64、UInt8、UInt16、UInt32、UInt64。 为vendor_id(取值 1–3)选择UInt8,相比Int64每行节省 7 字节。在 5000 万 行上,就是 350MB。 -
VARIANT → String。 ClickHouse 有原生的
JSON类型(v25.3+ 起 生产可用),但它是为真正动态的 schema 设计的,即建表时字段名 和结构都未知的情况。对本实验中的trip_metadata而言, 结构是已知的(driver.rating、app.surge_multiplier等),更好的 做法是在迁移过程中预先拍平成带类型的列,或者存为String并在查询时使用JSONExtract*。当你确实无法 预测 schema 时才用JSON类型:例如摄取任意客户事件负载,其中每个事件 都有不同字段。("预先拍平成带类型的列"指的是在 ETL 过程中把字段抽取到 独立的顶层列中,就像FACT_TRIPS.driver_rating从trip_metadata产出的方式,而不是把 JSON 块本身包进一个Tuple。Tuple列 仍然锁定在一组固定字段上,因此只要某次行程的元数据 不符合那个形状,它就会失效。) -
浮点精度。 Snowflake 的
FLOAT在 ClickHouse 中映射为Float64。对于需要 精确十进制运算的金额,使用Decimal(18, 2),但对 本实验来说,Float64足以与源端保持一致。(这是默认做法,不是说 每个FLOAT列都不论取值范围一律取Float64:像driver_rating这样取值在 1.0–5.0、只保留一位小数的列,完全可以放进Float32约 7 位有效数字的范围,那里的选择取决于可空性,而不是 规则 4 为金额所保护的精度。) -
LowCardinality(),ClickHouse 独有的优化。 把一个类型包进LowCardinality(String)(或LowCardinality(UInt8)等)会告诉 ClickHouse 对该列使用 字典编码,值以指向字典的整数引用形式存储,而不是重复的 字符串。对不同值少于约 10,000 个的字符串列,这通常能带来 2–5 倍的压缩 提升和更快的GROUP BY。Snowflake 没有等价物;它自动处理这件事。本实验中的好 候选:pickup_borough(6 个值)、payment_type(6 个值)、vehicle_type、vendor_name。
练习:TRIPS_RAW 的类型映射
把 NYC_TAXI_DB.RAW.TRIPS_RAW 的每一列映射到对应的 ClickHouse 类型。TRIP_ID 已经
作为示例填好:从 VARCHAR(36) 迁移过来时,String 是惯用做法,它
不需要任何转换,支持所有字符串函数,并且避免了插入时的 UUID 解析开销,
即便 ClickHouse 同样有原生的 UUID 类型。
练习:FACT_TRIPS 的类型映射
FACT_TRIPS 增加了由 dbt 管道添加的计算列/派生列。大多数
列沿用 TRIPS_RAW 的决策;DRIVER_RATING 和 UPDATED_AT 是新增的。
DRIVER_RATING 经常为 NULL(未给出评分)。在 ClickHouse 中,Nullable(Float64)
相比非空列有轻微的性能开销,数据旁会额外存储一个位图,
用于追踪哪些行为 null。这一列的选择在
Nullable(Float32)(显式的 null 语义)和使用哨兵
值(如 -1.0)的裸 Float32(更快,但不那么常规)之间。本实验为了
正确性选用 Nullable(Float32)。
练习:函数翻译
把每个 Snowflake 表达式翻译成对应的 ClickHouse 等价写法。它们直接
取自 01-setup-snowflake/queries/ 中的 Q1–Q7。八个当中有三个,QUALIFY、MERGE INTO
以及 CDC 流读取,答案无法用一行表达式给出,所以它们改为
在表格下方以问题形式作答。
反思问题
上面的表格填完之后,再来做这些。三个来自练习 3 中
需要多行才能回答的翻译;三个来自源练习表的
"Non-Obvious Translation Decisions",即上面 TRIP_METADATA、FARE_AMOUNT
和 PICKUP_LOCATION_ID 类型选择背后的推理。
Loading worksheet...
转录到 migration-plan.md
把你的类型决策以及任何不显而易见的翻译说明抄到
migration-plan.md 的第 5 节,并勾选:
- [ ] Schema translation: completed