尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MySQL到金仓:零日期与宽松模式数据清洗——历史系统非法日期迁移的检测、修复与回退

MySQL到金仓:零日期与宽松模式数据清洗——历史系统非法日期迁移的检测、修复与回退 文章目录每日一句正能量1. 背景与问题真正难迁的不是 0000-00-00而是它背后的业务含义2. 环境与数据先盘点 sql_mode再盘点日期列2.1 第一步不是扫数据而是记录 SQL mode2.2 重点关注这些模式2.3 KingbaseES兼容模式也需要确认3. 复现过程为什么“一刀切转 NULL”不够专业3.1 零日期可能是未知值3.2 零时间可能代表“尚未发生”3.3 部分零日期不能猜3.4 非法自然日更不能自动纠偏3.5 合法日期不等于真实业务日期4. 方案实施建立“原值—规则—目标值”三段式清洗链路4.1 第一步把日期列分类4.2 第二步不要直接在源生产表原地 UPDATE4.3 第三步每条自动清洗必须可审计4.4 第四步模糊数据进入隔离表4.5 第五步零日期转换成 NULL 时同步修改约束4.6 第六步把“未发生”从日期值迁到状态字段4.7 第七步全量和CDC必须用同一套清洗库4.8 第八步新系统要收紧输入不要把历史兼容问题继续带过去5. 结果对比清洗验收必须做到“数量闭合”5.1 最低校验指标零日期数量目标 NULL 数量隔离数量修复数量规则分布5.2 按业务维度分桶5.3 业务计算回归5.4 示例结果模板6. 风险与复盘最危险的不是清不掉而是清错了6.1 风险一把合法哨兵值误删6.2 风险二把零日期和未知状态混为一谈6.3 风险三源库不同Session的sql_mode不同6.4 风险四在线直接UPDATE导致CDC风暴6.5 风险五日期字符串解析受格式影响6.6 风险六MySQL兼容模式不是继续保留脏数据的理由回退方案一定保留“原始值证据”双列过渡回退触发条件回退动作最终复盘附录 A源库检测 SQL附录 B建议清洗映射附录 C审计表附录 D最低验收清单每日一句正能量“有一种欣喜叫触底反弹有一种快乐叫柳暗花明。”最深的谷底往往也是转折的开始。真正的欣喜若狂不是来自顺境的锦上添花而是来自绝境后的绝地反击。主题非法日期与 SQL 模式 / MySQL → KingbaseES / 历史系统迁移重点零日期、部分零日期、宽松sql_mode、检测 SQL、清洗规则、数据校验、增量迁移与回退适用场景老 CRM、ERP、会员系统、订单平台、历史数据仓库等长期使用 MySQL 宽松模式的系统。1. 背景与问题真正难迁的不是0000-00-00而是它背后的业务含义老 MySQL 系统中经常可以看到0000-00-00 0000-00-00 00:00:00 2019-00-15 2019-02-31 1970-01-01 9999-12-31这些值看上去都“可疑”但它们不是同一种问题。MySQL 官方文档长期支持一种相对宽松的日期处理方式在特定sql_mode下可以允许零月、零日甚至把0000-00-00作为“dummy date”启用ALLOW_INVALID_DATES后日期只做有限检查。NO_ZERO_DATE和NO_ZERO_IN_DATE则用于约束这类数据。citeturn862513search8turn862513search4MySQL 8.4 默认 SQL mode 已包含STRICT_TRANS_TABLES NO_ZERO_IN_DATE NO_ZERO_DATE但历史系统不一定一直使用默认配置。很多十年前上线的系统可能曾经关闭严格模式或者应用连接初始化时覆盖了 Sessionsql_mode。citeturn862513search2turn862513search4因此一个老表里出现birthday0000-00-00可能代表不知道生日也可能代表前端没有填写旧代码自动塞了默认值还可能代表数据导入脚本失败后被MySQL宽松模式兜成零日期如果迁移时统一0000-00-00→NULL技术上看似合理业务上却未必正确。所以本文的核心原则是先识别“异常日期的业务语义”再决定如何清洗。2. 环境与数据先盘点 sql_mode再盘点日期列示例环境源库MySQL 5.7/8.0 历史混合环境 目标KingbaseES V9 MySQL兼容模式/标准日期类型 系统十年以上历史 CRM 订单平台 数据量约12亿行 异常日期分布多个业务库 迁移方式全量 CDC源表CREATETABLElegacy_customer(idBIGINTPRIMARYKEY,birthdayDATENOTNULLDEFAULT0000-00-00,register_timeDATETIMENOTNULL,last_login_timeDATETIME);另一个老表CREATETABLElegacy_order(order_idBIGINTPRIMARYKEY,paid_atDATETIMENOTNULLDEFAULT0000-00-00 00:00:00);2.1 第一步不是扫数据而是记录 SQL mode先执行SELECTGLOBAL.sql_mode,SESSION.sql_mode;如果系统有多主 多实例 读写分离 连接池初始化SQL每个写入节点都要记录。MySQL 的sql_mode是 Global/Session 可配置项因此“数据库全局模式”不一定等于某个应用连接真实使用的模式。citeturn862513search42.2 重点关注这些模式STRICT_TRANS_TABLES STRICT_ALL_TABLES NO_ZERO_DATE NO_ZERO_IN_DATE ALLOW_INVALID_DATESMySQL 官方 FAQ 把启用STRICT_TRANS_TABLES、STRICT_ALL_TABLES或TRADITIONAL视为严格模式关闭严格模式时某些不合法或缺失值可能按隐式默认值处理而不是直接报错。这意味着历史脏数据往往不是偶然而是数据库配置和应用写入方式共同形成的。2.3 KingbaseES兼容模式也需要确认KingbaseES 提供 MySQL 兼容模式并有sql_mode等兼容参数官方兼容文档说明 MySQL 模式支持大量 MySQL 数据类型和 SQL 语法。同时 KingbaseES 的标准DATE类型要求输入可被解释为合法日期值官方文档推荐无歧义 ISO 格式YYYY-MM-DD并明确说明日期解析还可能受DateStyle影响。迁移设计不应该依赖“目标兼容模式也许能接住零日期”更可靠的是在迁移层把非法日期清理成合法、可解释的数据再进入目标业务表。3. 复现过程为什么“一刀切转 NULL”不够专业3.1 零日期可能是未知值例如birthday0000-00-00如果业务确认为用户未填写生日最合理目标通常是birthday NULL同时如果业务必须区分未填写和已填写但系统丢失还需要birthday_unknowntrue不能只靠 NULL 承载所有语义。3.2 零时间可能代表“尚未发生”例如paid_at0000-00-00 00:00:00很多旧订单系统用它表示尚未支付如果改成paid_atNULL通常是合理的但最好同时让payment_statusUNPAID成为真正业务状态。这样以后查询WHEREpaid_at0000-00-00 00:00:00就可以逐步替换为WHEREpayment_statusUNPAID这是一次数据语义修复而不仅是数据库兼容。3.3 部分零日期不能猜MySQL 文档明确提到历史上可以允许2010-00-01 2010-01-00这样的零月或零日形式NO_ZERO_IN_DATE用来限制它们。citeturn862513search8假设contract_date2019-00-15你不能自动改成2019-01-15因为没有任何证据表明 0 月代表 1 月。正确做法隔离 人工或业务规则复核必要时拆成known_year2019 known_day15 month_unknowntrue而不是伪造完整日期。3.4 非法自然日更不能自动纠偏例如2019-02-31如果启用了ALLOW_INVALID_DATESMySQL 只做有限日期检查因此历史系统可能存下这类值。迁移时自动2019-02-31 → 2019-02-28是非常危险的。除非有原始业务单据 或 应用代码明确的纠正规则否则应该进入隔离表。3.5 合法日期不等于真实业务日期例如1970-01-01 1900-01-01 9999-12-31它们都是合法日期。但历史系统常拿它们当未设置 最小值 永久有效 无截止日期所以检测 SQL 不能只找“语法非法日期”。还要找异常高频合法日期然后回查代码 默认值 产品规则 历史文档再决定是否清洗。4. 方案实施建立“原值—规则—目标值”三段式清洗链路4.1 第一步把日期列分类建议做一张清单schema table column type nullable default zero_date_count partial_zero_count sentinel_count business_owner cleanup_rule风险等级L1明确零日期未知 L2明确零日期未发生 L3合法哨兵日期 L4部分零日期 L5非法自然日优先自动处理L1/L2人工复核L3/L4/L54.2 第二步不要直接在源生产表原地 UPDATE错误方式UPDATElegacy_customerSETbirthdayNULLWHEREbirthday0000-00-00;这种做法的问题无法恢复原值 无法证明清了多少 CDC会产生大量更新 可能影响线上业务逻辑更推荐迁移 staging 层清洗例如原始字段先作为文本birthday_raw进入中间层。然后valid date → 转DATE 0000-00-00 → 根据规则NULL 非法日期 → quarantine这样源库不动风险最低。4.3 第三步每条自动清洗必须可审计建立date_cleanup_audit字段source_table source_pk source_column source_value_raw target_value rule_id batch_id created_at例如source0000-00-00 ruleZERO_DATE_TO_NULL_UNKNOWN targetNULL以后业务问“这个生日为什么变成 NULL”可以追踪到具体规则而不是回答迁移脚本统一改的4.4 第四步模糊数据进入隔离表migration_invalid_date_quarantine保存batch_id source_table source_pk source_column source_value_raw reason_code review_status原因码ZERO_MONTH ZERO_DAY INVALID_CALENDAR_DATE SENTINEL_REVIEW AMBIGUOUS_BUSINESS_MEANING目标业务主表只装合法 或 经过明确规则清洗的数据。这样不会为了“迁移完成率100%”把错误值硬塞到新库。4.5 第五步零日期转换成 NULL 时同步修改约束源birthdayDATENOTNULLDEFAULT0000-00-00如果业务已经决定未知生日 → NULL目标必须允许birthdayDATENULL否则清洗规则和 DDL 冲突。所以迁移不是只改数据而是数据语义 列约束 应用代码一起改。4.6 第六步把“未发生”从日期值迁到状态字段例如paid_at0000-00-00建议paid_atNULL payment_statusUNPAID查询从WHEREpaid_at0000-00-00 00:00:00改成WHEREpayment_statusUNPAID这会明显降低未来数据库迁移和数据分析歧义。4.7 第七步全量和CDC必须用同一套清洗库这是非常关键的一点。全量脚本0000-00-00 → NULL但 CDC 实时同步如果还是原值直接写目标切流前就会再次出现不一致。所以应该把normalize_date(source_value, rule_id)做成统一转换库。全量调用它CDC也调用它目标应用新写入直接禁止非法日期三条链路必须一致。4.8 第八步新系统要收紧输入不要把历史兼容问题继续带过去MySQL 8.4 默认已启用严格和零日期限制相关模式。迁移后的系统应该做到应用层参数校验 数据库合法日期类型 禁止零日期约定否则你今天清完1000万条明天新业务又继续写0000-00-00迁移治理等于白做。5. 结果对比清洗验收必须做到“数量闭合”清洗前zero_date800万 partial_zero20万 invalid_calendar5万 sentinel_review100万清洗后不能只说目标导入成功而应该做到数学闭合源异常总量 自动置NULL 规则修复 隔离待审 明确保留例如825万异常 780万置NULL 5万修复 40万隔离每一条都能解释。5.1 最低校验指标零日期数量source zero count目标 NULL 数量target null count隔离数量quarantine count修复数量repaired count规则分布rule_id → count5.2 按业务维度分桶例如生日按用户注册年份 地区 渠道统计零日期比例。如果某一年90%都是0000-00-00可能说明那一时期产品根本没采集生日而不是数据坏了。这类分析可以帮助确定NULL才是最合理语义。5.3 业务计算回归重点验证年龄 账龄 保修期 过期判断 合同有效期 日/月报表例如旧逻辑DATEDIFF(CURDATE(),birthday)遇到零日期可能产生特殊行为。清洗成 NULL 后结果可能变成NULL应用报表必须相应调整。5.4 示例结果模板指标清洗前清洗后零日期800万0部分零日期20万0进入业务主表非法自然日5万0进入业务主表NULL200万980万隔离记录025万可追溯清洗率0%100%以上是验收模板示例不是本文声称的生产数据。6. 风险与复盘最危险的不是清不掉而是清错了6.1 风险一把合法哨兵值误删例如9999-12-31有些系统明确表示永久有效如果统一改 NULL查询WHEREexpiry_dateCURRENT_DATE语义会变化。因此合法哨兵日期必须先问业务。6.2 风险二把零日期和未知状态混为一谈unknown not happened not collected not applicable四种状态都可能被历史系统塞成0000-00-00迁移时如果全部转 NULL至少要评估是否需要额外状态字段。6.3 风险三源库不同Session的sql_mode不同应用 ASTRICT应用 BALLOW_INVALID_DATES会造成同一张表写入质量不同。所以只看GLOBAL.sql_mode不够。要查应用连接初始化 连接池配置 数据库代理6.4 风险四在线直接UPDATE导致CDC风暴千万级零日期UPDATE...会带来redo/binlog 锁 复制延迟 CDC洪峰因此更推荐迁移 staging 层清洗而不是源生产表一次性原地修。6.5 风险五日期字符串解析受格式影响KingbaseES 官方 DATE 文档说明日期输入可能受DateStyle影响因此推荐YYYY-MM-DD这类无歧义 ISO 格式。迁移文件不要使用03/04/2020这种格式。6.6 风险六MySQL兼容模式不是继续保留脏数据的理由KingbaseES MySQL 兼容模式的目标是降低迁移成本官方文档也明确强调兼容数据类型、SQL 语法和常见生态能力。但兼容不应该被理解成继续保留历史上所有不合理数据习惯对于零日期这类典型技术债迁移窗口反而是最适合治理的时候。回退方案一定保留“原始值证据”最推荐raw value target value rule_id batch_id一起保存。如果发现某规则错误可以按rule_idbatch_id找出全部受影响记录回退。双列过渡例如birthday_raw birthday灰度期应用读birthday 迁移审计保留birthday_raw如果问题切回旧字段/旧库回退触发条件异常数量不闭合 业务报表差异 0 生日/账龄等计算错误 错误规则命中量异常 隔离数据超预期 CDC出现新零日期回退动作1. 停止当前清洗批次 2. 固化batch_id 3. 找出该批次所有audit记录 4. 恢复raw值或切回旧读取路径 5. 修正规则 6. 小批次重跑 7. 重新完成数量闭合验证最终复盘MySQL 到 KingbaseES 的零日期迁移本质上是一次数据质量治理。完整流程应该是识别sql_mode → 检测异常日期 → 识别业务语义 → 规则清洗 → 模糊数据隔离 → 目标严格落库 → 数量闭合 → 应用回归 → 可追溯回退如果只记住一句话0000-00-00不是一个日期问题而是一个“历史系统曾经不知道该填什么”的业务语义问题。真正专业的迁移不是把它换成另一个合法日期而是把“未知、未发生、无效、待确认”这些含义重新表达清楚。附录 A源库检测 SQLSELECTGLOBAL.sql_mode,SESSION.sql_mode;SELECTCOUNT(*)FROMlegacy_customerWHEREbirthday0000-00-00;SELECTCOUNT(*)FROMlegacy_orderWHEREpaid_at0000-00-00 00:00:00;附录 B建议清洗映射0000-00-00 业务未知 → NULL 0000-00-00 业务未发生 → NULL status YYYY-00-DD → quarantine 非法自然日 → quarantine 合法哨兵日期 → business review附录 C审计表CREATETABLEdate_cleanup_audit(table_nameVARCHAR(128),pk_valueVARCHAR(128),column_nameVARCHAR(128),source_value_rawVARCHAR(64),target_valueDATE,rule_idVARCHAR(64),batch_idVARCHAR(64),created_atTIMESTAMP);附录 D最低验收清单[ ] GLOBAL sql_mode已记录 [ ] SESSION sql_mode已核对 [ ] DATE/DATETIME/TIMESTAMP列已盘点 [ ] zero date已统计 [ ] partial-zero已统计 [ ] invalid calendar date已统计 [ ] sentinel date已统计 [ ] 每条规则已有业务Owner确认 [ ] raw值已保留 [ ] quarantine表已建立 [ ] 全量和CDC使用同一清洗规则 [ ] 异常数量已闭合 [ ] 业务报表回归通过 [ ] 应用已禁止新写零日期 [ ] 回退脚本已演练转载自https://blog.csdn.net/u014727709/article/details/163728745欢迎 点赞✍评论⭐收藏欢迎指正
返回列表