# SnowConvert 部署零报错，恰是迁移最脆弱的时刻

> 工具改得了语法，改不了语义：空字符串、类型转换和文本分组可能不报错却算错数，只能用数据证明一致。

原文: https://fdetimes.net/zh/guides/snowconvert-snowflake-migration-validation-checks/

你已经用 SnowConvert AI 把客户的全部 SQL 资产跑了一遍。脚本部署到 Snowflake 没报一个错，第一块仪表盘已经出数，客户正在问什么时候能关掉旧系统。项目恰恰在这个时候最容易出问题，因为剩下的每一个 bug 都不会触发任何告警。

自动转换工具很擅长处理语法，但无法保证语义，即同一条 SQL 在两个平台上返回相同的结果。在这个阶段，前线部署工程师（FDE）的任务是用数据证明结果确实一致，并在客户关掉旧系统之前把证据摆到客户面前。

## 工具的警告清单只是起点

对于两个平台之间无法自行解决的差异，SnowConvert AI 会记录为 EWI（错误、警告与问题）和 FDM。Snowflake 自己也建议在部署前审查转换后的代码。人们很容易以为，清空这份清单就算完工。

陷阱在于：有些差异是工具看懂了、却有意不改的；还有些差异只有在代码跑真实数据时才会暴露。把 EWI 清单当作该往哪里看的地图，而不是逐项打勾的待办清单。下面四个方面最值得优先关注。

| 陷阱 | 源系统 | Snowflake | 检查方法 |
|---|---|---|---|
| 空字符串 | Oracle 把 '' 当作 NULL | '' 和 NULL 是不同的值 | 搜索带 '' 的 REPLACE 和 CONCAT |
| 字符串转数字 | Teradata 能读取空字符串或全空白字符串 | 容易出现运行时错误 | 统计源列中的脏值数量 |
| 文本列上的 GROUP BY | Teradata 默认不区分大小写 | 'ABC' 和 'abc' 变成两组 | 比较两个系统的分组数量 |
| 定长 CHAR | Teradata 连尾部填充空格一起匹配 | CHAR 不补空格 | 重跑 LIKE 操作和字符串函数 |

## Oracle：空字符串没你想的那么“空”

Oracle 文档写得很明确：长度为零的字符值被视为 NULL。Snowflake 不这样处理，所以 SnowConvert 会把空字符串改写为 NULL，以保留原有行为。大多数情况下，这种改写是正确的。

例外是 REPLACE 和 CONCAT 的参数。SnowConvert 文档说明，这里不做该转换，因为在这些调用里把空字符串变成 NULL 会改变查询结果。看一个规范化客户电话号码的查询：

```sql
-- Oracle，转换后保持不变
SELECT customer_id,
       REPLACE(phone_number, '-', '') AS clean_phone
FROM customers
WHERE notes IS NULL;
```

REPLACE 这一行是有意保留不改的，但你仍然要在两个系统上各跑一遍，确认结果一致。更值得担心的是 WHERE 这一行。在 Oracle 上，`IS NULL` 也会命中“空白”备注，因为 Oracle 从不存储空字符串。如果数据加载到 Snowflake 后，`notes` 列里留下了 ''，这些行就会悄无声息地从结果中消失。

## 一条查询，两边对账

第一项检查很简单：对每个重要的文本列，分别统计 NULL、空字符串和只含空白字符的字符串。在 Snowflake 上，查询如下：

```sql
-- Snowflake（几乎原样可在 Teradata 上运行）
SELECT
  COUNT(*) AS total_rows,
  SUM(CASE WHEN notes IS NULL THEN 1 ELSE 0 END) AS null_count,
  SUM(CASE WHEN notes = '' THEN 1 ELSE 0 END) AS empty_count,
  SUM(CASE WHEN notes <> '' AND TRIM(notes) = ''
           THEN 1 ELSE 0 END) AS whitespace_only_count
FROM customers;
```

在 Oracle 上，比较 `= ''` 永远不成立，因为空字符串本来就是 NULL，所以 `empty_count` 始终为 0。要在 Oracle 上统计全空白字符串，把条件改为 `notes IS NOT NULL AND TRIM(notes) IS NULL`。

比对时，把 Snowflake 的 `null_count` 与 `empty_count` 相加，再与 Oracle 的 `null_count` 对照。如果数字不一致，就要回头检查那些按 `IS NULL` 过滤的查询。

同理，对任何即将用于 GROUP BY 的列，在 Snowflake 上比较 `COUNT(DISTINCT city)` 和 `COUNT(DISTINCT UPPER(city))`。两个数字不同，说明该列中有仅大小写不同的值，分组报表就会像下文所述那样与 Teradata 出现偏差。

## Teradata：在最意想不到的地方宽松

Teradata 的差异在于容忍度。把字符串转为数字时，Teradata 接受空字符串或全空白字符串，Snowflake 则更严格。一条多年来运行顺畅的 `CAST(postal_code AS INTEGER)`，第一次遇到这类行时就可能出错。错误只在运行时出现，转换阶段看不出来。

GROUP BY 的问题更难发现。Teradata 默认不区分大小写比较字符串（NOT CASESPECIFIC）。除非设置排序规则（collation），否则 Snowflake 会把仅大小写不同的值当作不同的值：

```sql
SELECT city, COUNT(*) AS order_count, SUM(total_amount) AS revenue
FROM orders
GROUP BY city;
```

假设 `city` 列里同时有 'Ha Noi' 和 'HA NOI'。在 Teradata 上它们是一组；在 Snowflake 上，上面的查询会返回两行，按城市统计的收入报表里突然多出一个“新城市”。全表总数依然一致，所以只核对总数抓不到这个 bug。

定长 CHAR 是第三个陷阱。Teradata 会在值的尾部补空格，有些函数会连同填充空格一起匹配整个值。Snowflake 的 CHAR 不补空格，所以 CHAR 列上的 LIKE 操作和字符串函数都需要重跑并比对。

**要点:** 脚本能跑，只证明语法没错。数字对不对，只有比对之后才知道。

## 数据验证：三个层级，由便宜到昂贵

代码审查告诉你该怀疑哪里，数据比对才是证据。Snowflake 的数据验证流程分三个层级：结构、指标和行级。行级验证最耗资源，因此默认关闭。

在结构层，比对数据类型、精度和标度。如果源端的精度或标度大于目标 Snowflake 列，流程会在对应层级报告差异。

指标层以默认容差 0.001 比对聚合度量。这个容差对金额列是否可接受，要去问客户，而不是自己假定。

行级检查会在源端为每个分区生成签名，由主键列加整行的 MD5 哈希组成。任何有差异的分区，都会进一步核查到单个单元格。这一层成本很高，因此只对供给财务报告或监管申报的表开启；日志表用指标层就够了。

迁移工具厂商 Datafold 认为，数据类型悄无声息地不匹配，是迁移后出现差异的常见原因，并建议逐列检查，而不只是统计行数。上面的 GROUP BY 例子说明了原因：基础表的行数完全一致，出错的只是分组结果。

## 在客户现场的工作顺序

先从 EWI 和 FDM 清单入手，按业务重要性而不是警告数量给文件排序。然后在全部转换后的代码里 grep 上表中的四种模式：带空字符串的 REPLACE 和 CONCAT、转为数字的类型转换、文本列上的 GROUP BY，以及 CHAR 列上的字符串函数。

接着，对所有表运行结构和指标检查，再对你和客户商定的一小批“绝不能出错”的表开启行级检查。比对期间冻结已迁移的数据：Snowflake 文档提醒，验证期间不要修改数据，否则验证结果会失真。

最常见的错误，是把“行数一致”当作完工的证明来汇报。其次是直接在目标表里修正差异却不做记录，导致下一轮验证没有可比对的基线。第三是只处理工具标出的问题，忽略工具有意保留不改的地方。

## 把这段经历变成简历上的亮点

如果你的目标是数据方向的 FDE 或解决方案工程师岗位，迁移经验只有在你讲得清验证工作时才算数。简历上不要写“参与 Snowflake 迁移”这类空泛的话。

写清楚你验证了哪些表、在哪个层级验证、发现了哪些类型的语义差异、又是怎么修复的。看简历的人一眼就能看出，你为数字负了责，而不只是按下了运行工具的按钮。

客户很少记得一次迁移搬了几千行 SQL，但他们会记得 Snowflake 上第一个月的收入报表和旧系统对不对得上。

**本周动手试试:**

- 在当前项目里挑一张有文本列的表，在源系统和 Snowflake 上分别运行统计 NULL、空字符串和全空白字符串的查询，并比对两边结果。
- 找出从 Oracle 转换过来的代码中所有带空字符串参数的 REPLACE 和 CONCAT 调用，在两个系统上并排运行并比对结果。
- 挑一份按文本列分组的 Teradata 报表，比对两个系统的分组数量和每组合计。

## 信源

- [SnowConvert AI - About](https://docs.snowflake.com/en/migrations/snowconvert-docs/general/about)

- [Oracle Database SQL Language Reference 19c: Nulls](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Nulls.html)

- [SnowConvert AI: Oracle Literals translation reference](https://docs.snowflake.com/en/migrations/snowconvert-docs/translation-references/oracle/basic-elements-of-oracle-sql/literals)

- [Code Conversion - Teradata Issues (SSC-EWI-TD0052)](https://docs.snowflake.com/en/migrations/aim-for-datawarehouses/code-conversion/issues-and-troubleshooting/conversion-issues/teradataEWI)

- [Data validation (Snowflake migration docs)](https://docs.snowflake.com/en/migrations/aim-for-datawarehouses/data-migration-validation/data-validation)

- [Teradata to Snowflake Migration: Challenges, Best Practices & Practical Guide](https://datafold.com/resources/teradata-to-snowflake-migration)
