10_dump_legacy.sh 20 KB
#!/usr/bin/env bash
#
# 六库合一:把六个源库的"保留侧"灌进 parking_cloud
#
# 判定依据见 ../merge-conflict-plan.md;不搬的表由 exclude_tables.txt 给出
# (由 ../tools/gen_migration_sql.py 从 check_subset.py 的 CHECKS 生成,别手改)。
#
# 全程**只读源库**:只 mysqldump,不写源库。源库保持可回滚。
#
# 两种模式,**校验方式完全不同,别无脑混用**:
#
#   · 默认(全量迁移):709 张表 / ~19 亿行 / ~747 GB。主张是"零丢失",
#     配套校验 99_verify.sql + 98_count_reconcile.sql(那边断言"行数相等")。
#     磁盘按 1.3× 预留;按库分段,可中断续跑(见 ONLY=)。
#
#   · SAMPLE=N(抽样开发库):每张表最多 N 行,给开发用的镜像。主张是
#     "每张表都在、列结构一致、每张表都落了样例行",配套校验 97_verify_sample.sql。
#     ⚠️ 抽样镜像上跑 98/99 §3§4 会**必然全红**——那两个断言的是行数相等。
#     另外抽样模式**默认不做一致性快照**(SNAPSHOT=0):抽几百行不在乎撕裂,
#     而不做快照就不需要 RELOAD 权限(全量迁移正是卡在这上面)。
#
# ⚠️ 抽样模式会把**源库的真实数据**写到开发机的 dump 文件里。含密钥/口令的表
#    由 struct_only_tables.txt 给出(由 gen_migration_sql.py 从列名判据生成),
#    这些表**只搬结构**——生产的支付/短信/云账号凭据不进开发环境。
#
# ⚠️ 字符集是本脚本最需要小心的部分,见 §字符集。

set -euo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ENV_FILE:-$HERE/../../私密/legacy-ro.env}"
WORK="${WORK:-$HERE/../.dump}"          # 中间产物目录(.gitignore,勿提交)
TARGET_DB="${TARGET_DB:-parking_cloud}"
ONLY="${ONLY:-}"                        # 逗号分隔,只跑这些库,如 ONLY=zt_park
SAMPLE="${SAMPLE:-}"                    # 抽样模式每张表最多几行;空/0 = 全量迁移

# 六个源库(库名与业务域错位是历史事实,见 ../legacy-db-actual.md §1)
DBS=(zt_sys zt_park zt_order zt_order_n zt_bill zt_charge)

log() { printf '\033[36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
die() { printf '\033[31m错误:\033[0m %s\n' "$*" >&2; exit 1; }

[[ -f "$ENV_FILE" ]] || die "找不到 env 文件:$ENV_FILE"
# 只读凭据:本脚本用它 dump,不用它写任何源库
set -a; . "$ENV_FILE"; set +a
: "${LEGACY_DB_HOST:?}"; : "${LEGACY_DB_USER:?}"; : "${LEGACY_DB_PASSWORD:?}"
PORT="${LEGACY_DB_PORT:-3306}"

# 目标库的连接方式由调用者给(本脚本不假定目标与源同实例)
: "${TARGET_HOST:=$LEGACY_DB_HOST}"
: "${TARGET_USER:=$LEGACY_DB_USER}"
# ⚠️ 这里用 `-` 而不是 `:-`:`:-` 把"显式传了空口令"也当成没传,
# 于是本地无口令的目标库(root 无密码)会被悄悄替换成**源库口令**,
# 连不上还算好的——它同时意味着"只要 TARGET_HOST 指到别处,
# 源库口令就被发到那台机器上"。`-` 只在**没设过**时才取默认值:
# 想用源库口令就别设 TARGET_PASSWORD;想用空口令就显式 `TARGET_PASSWORD=`。
: "${TARGET_PASSWORD-$LEGACY_DB_PASSWORD}"
: "${TARGET_PORT:=$PORT}"

# 客户端可覆盖:本机 Homebrew 的 mysql/mysqldump 26.x 连不上老库(见 preflight_dump),
# 指到能用的客户端即可,不必改脚本:
#     export DUMP_BIN=/opt/homebrew/opt/mysql-client/bin/mysqldump
#     export MYSQL_BIN=/opt/homebrew/opt/mysql-client/bin/mysql
# ⚠️ 用 MariaDB 客户端要留意:`--set-gtid-purged` 是 MySQL 特有的开关,
#    mariadb-dump 未必认(本机没装 MariaDB,未实测)。真要用就先单独试一条
#    `mariadb-dump --set-gtid-purged=OFF --no-data zt_sys sys_org`;认不了就换 mysql-client。
MYSQL_BIN="${MYSQL_BIN:-mysql}"
DUMP_BIN="${DUMP_BIN:-mysqldump}"
command -v "$MYSQL_BIN" >/dev/null || die "缺少 mysql 客户端(${MYSQL_BIN})"
command -v "$DUMP_BIN"  >/dev/null || die "缺少 mysqldump(${DUMP_BIN})"

EXCLUDE_FILE="$HERE/exclude_tables.txt"
[[ -f "$EXCLUDE_FILE" ]] || die "缺少 ${EXCLUDE_FILE}(先跑 tools/gen_migration_sql.py)"

# 抽样模式:含密钥/口令的表清单(只搬结构)。缺了**必须停下来**——
# 没有它就会把生产的支付/短信/云账号凭据 dump 到开发机上(见 §抽样模式)。
STRUCT_ONLY_FILE="$HERE/struct_only_tables.txt"
IS_SAMPLE=0
if [[ -n "$SAMPLE" && "$SAMPLE" != "0" ]]; then
  IS_SAMPLE=1
  [[ "$SAMPLE" =~ ^[0-9]+$ ]] || die "SAMPLE 必须是整数(当前:${SAMPLE})"
  [[ -f "$STRUCT_ONLY_FILE" ]] \
    || die "抽样模式缺 ${STRUCT_ONLY_FILE}(先跑 tools/gen_migration_sql.py)
    —— 这份清单决定哪些表**只搬结构不搬数据**(含密钥/口令的表)。
    没有它,生产凭据会被 dump 到开发机上,所以宁可不跑。"
fi

# ⚠️ 口令**不放进 argv**:`--password=xxx` 会让同机任何进程从 `ps` 读到生产口令,
# 客户端也会打 "Using a password on the command line interface can be insecure"。
# 改走 MYSQL_PWD,且**按命令前缀**给(dump 用源口令、load 用目标口令,不能混)。
SRC=(--host="$LEGACY_DB_HOST" --port="$PORT" --user="$LEGACY_DB_USER")
DST=(--host="$TARGET_HOST" --port="$TARGET_PORT" --user="$TARGET_USER")

mkdir -p "$WORK"

# 一致性快照:`--single-transaction` 让 709 张表落在**同一个一致性读视图**里。
# ⚠️ 它需要 **RELOAD** 权限——客户端在 dump 前会先发一条
#    `FLUSH /*!40101 LOCAL */ TABLES`。本项目的源库只读账号**没有**它,
#    实测:`--single-transaction` 无论怎么组合都是 rc=2
#        ERROR 1227: Access denied; you need the RELOAD privilege(s)
#    (`--no-data`、`--skip-lock-tables` 都救不了;不加它则 dump 正常。)
# 两条出路,**都要显式**,不能默认偷偷降级:
#    · DBA 给只读账号加 RELOAD —— 推荐。RELOAD 是管理权限、**不是数据写权限**,
#      加了它账号依然只读(本项目 dump 全程不发写语句)
#    · SNAPSHOT=0 接受「每张表各自一个时间点」的非一致 dump——
#      库在线写入时可能撕裂(订单表与明细表对不上)。本次的硬要求是零数据丢失,
#      所以只在你确认源库已静默时才这么用
# 抽样模式默认**不开**快照:只抽几百行,撕裂与否不影响开发用途;而不做快照
# 就不需要 RELOAD 权限——那正是全量迁移卡住的地方(见上)。显式 SNAPSHOT=1 仍可覆盖。
if [[ "$IS_SAMPLE" == "1" ]]; then
  SNAPSHOT="${SNAPSHOT:-0}"
else
  SNAPSHOT="${SNAPSHOT:-1}"
fi

# 客户端**真能连**吗?只 `command -v` 不够——Homebrew 的 MySQL 26.x 移除了
# mysql_native_password,而老库账号用的正是它,于是连第一条 SQL 都发不出去
# (实测:exit 2、`ERROR 2059`)。若不预检,会在 dump 到一半(可能几十分钟后)才炸。
#
# 顺带在这一步就探出 RELOAD 权限问题:`--single-transaction` 的失败在**动第一张表之前**
# 就能看见(它是 dump 的第一条语句),别等搬了半小时才发现要改权限。
preflight_dump() {
  local err
  err=$(MYSQL_PWD="$LEGACY_DB_PASSWORD" "$DUMP_BIN" "${SRC[@]}" \
        --no-tablespaces --skip-comments \
        --default-character-set=utf8mb4 --no-data zt_sys sys_org 2>&1 >/dev/null) && {
    if [[ "$SNAPSHOT" == "1" ]]; then
      err=$(MYSQL_PWD="$LEGACY_DB_PASSWORD" "$DUMP_BIN" "${SRC[@]}" \
            --single-transaction --no-tablespaces --skip-comments \
            --default-character-set=utf8mb4 --no-data zt_sys sys_org 2>&1 >/dev/null) && return 0
      if [[ "$err" == *RELOAD* || "$err" == *"1227"* ]]; then
        die "源库账号没有 RELOAD 权限,做不了一致性快照
    $err
    —— \`--single-transaction\` 在 dump 前要先发 \`FLUSH TABLES\`,它要 RELOAD 权限。
    两条出路(**别静默降级**,非一致快照在写库上会撕裂数据):
      ① 让 DBA 给只读账号加 RELOAD —— 推荐。RELOAD 是管理权限、不是数据写权限,
         加了账号依然只读(本脚本 dump 全程不发写语句)
      ② 确认源库已静默后,显式接受非一致 dump:SNAPSHOT=0 bash $0 dump"
      fi
      die "mysqldump 一致性快照预检失败:$err"
    fi
    printf '\033[33m⚠️  SNAPSHOT=0:本次 dump 不是一致性快照,库在写入时可能撕裂\033[0m\n' >&2
    return 0
  }
  if [[ "$err" == *mysql_native_password* ]]; then
    die "mysqldump 连不上老库:认证插件缺失
    $err
    —— 当前客户端($(basename "$DUMP_BIN"))无法识别老库账号用的 mysql_native_password。
    ⚠️ Homebrew 的 mysql / mysql@26.7(26.7.0)**不行**,实测退出码 2。
    本机实测**可用**的是 mysql-client 9.6:
        export DUMP_BIN=/opt/homebrew/opt/mysql-client/bin/mysqldump
        export MYSQL_BIN=/opt/homebrew/opt/mysql-client/bin/mysql
    (MariaDB 客户端也能连,但 `--set-gtid-purged` 是 MySQL 特有开关、未必支持;
      Python 侧的 tools/*.py 走 PyMySQL,不受影响)"
  fi
  die "mysqldump 预检失败:$err"
}

# ---------------------------------------------------------------------------
# 字符集:709 张表里非 utf8mb4 的有 670 张(实测)
#   表级:utf8_general_ci 635 / utf8_bin 30 / latin1_swedish_ci 1 / utf8mb4_* 43
#   列级:utf8_bin 324 列 / latin1_swedish_ci 23 列
#
# ⚠️ utf8_bin 必须映射到 **utf8mb4_bin**,不能一律拍成 utf8mb4_general_ci:
#    _bin 是**大小写敏感**、_ci 是**大小写不敏感**,换了以后
#    `pl_no='P15040225z'` 这类查询会开始命中它以前命中不到的行。
#    实测 _bin 列上**没有**唯一索引(所以不会撞键),但比较语义会变——
#    这是"静默改变业务结果"的那一类,不能图省事。
#
# utf8mb3 → utf8mb4 是安全的(utf8mb3 存不下 4 字节字符,无损)。
# 唯一的 latin1 表 zt_order_n.td_b_pay_blncac_wx 的 23 列装的都是
# 金额/订单号/交易状态(纯 ASCII),latin1→utf8mb4 同样无损。
# ---------------------------------------------------------------------------
rewrite_charset() {
  # 写法要点(踩过坑,别"简化"回去):
  #  ① **所有** utf8mb4 都得先变成 @@MB4@@ 占位——不只是"已经存在的",
  #     连后面**即将写进去的**也是。第一版没做这一点,于是
  #     `utf8_bin → utf8mb4_bin` 写进去的字面量又被最后那条
  #     `s/utf8/utf8mb4/g` 二次替换成 `utf8mb4mb4_bin`(建表直接报错)。
  #     一句话:**同一个 token 不能被两条规则都碰到**。
  #  ② 占位之后,`s/utf8/@@MB4@@/g` 这种朴素全量替换才是安全的。
  #  ③ **`utf8mb3` 必须单独一条,且排在朴素 `s/utf8/…/g` 前面**:
  #     MySQL 8.0.29+ 的 mysqldump 会把 `utf8` 输出成 `utf8mb3`。
  #     不先处理它,`s/utf8/@@MB4@@/g` 会把它变成 `utf8mb4mb3`(建表报错)。
  #     本机实测的客户端(mysql-client 9.6 对 5.7 服务端)**不会**输出 utf8mb3,
  #     但操作者可能换客户端——所以按"会输出"来防,不赌。
  #  ④ **不要顺手补 COLLATE**。utf8mb4 的默认排序规则本来就是 utf8mb4_general_ci,
  #     补了会和原本带 COLLATE 的表撞成
  #     `COLLATE=utf8mb4_general_ci COLLATE=utf8mb4_bin`(重复 COLLATE,建表报错)。
  #     实测踩中:`DEFAULT CHARSET=utf8 COLLATE=utf8_bin`(正是那 30 张 utf8_bin 表)
  sed -E \
    -e 's/utf8mb4/@@MB4@@/g' \
    -e 's/utf8mb3/@@MB4@@/g' \
    -e 's/utf8_bin/@@MB4@@_bin/g' \
    -e 's/utf8_general_ci/@@MB4@@_general_ci/g' \
    -e 's/utf8_unicode_ci/@@MB4@@_unicode_ci/g' \
    -e 's/latin1_swedish_ci/@@MB4@@_general_ci/g' \
    -e 's/utf8/@@MB4@@/g' \
    -e 's/latin1/@@MB4@@/g' \
    -e 's/@@MB4@@/utf8mb4/g'
}

# 改完必须自查:残留的 utf8_ / charset=utf8 / latin1 / **mb4mb4** / **mb4mb3** 都算失败。
# `mb4mb4` 是第一版漏判的写法——当时 guard 放行了坏输出("审计说 OK,东西是坏的",
# AGENTS.md §1.1 记的正是这一类);`mb4mb3` 是同一个错法的另一半(utf8mb3 版本)。
# 两者都别删:**守卫的价值全在它拦得住什么**。
guard_charset() {
  local f="$1" bad
  # utf8mb4_general_ci 不会误匹配 utf8_general_ci(前者含 "4",后者不是它的子串)
  bad=$(grep -cE 'mb4mb4|mb4mb3|utf8mb3|utf8_general_ci|utf8_bin|utf8_unicode_ci|charset=utf8([^m]|$)|CHARSET=utf8([^m]|$)|COLLATE=utf8 |latin1' "$f" || true)
  [[ "$bad" == "0" ]] || die "$f 里还有 $bad 处未转换/被重复替换的字符集声明,请检查 rewrite_charset 的规则"
}

build_excludes() {
  # exclude_tables.txt 每行 `<db>.<table>` → 生成该库的 --ignore-table 参数
  local db="$1"
  grep -E "^${db}\." "$EXCLUDE_FILE" | sed 's/^/--ignore-table=/' || true
}

build_struct_only() {
  # struct_only_tables.txt 每行 `<db>.<table>` → 数据那一遍要 --ignore-table 的表
  local db="$1"
  grep -E "^${db}\." "$STRUCT_ONLY_FILE" | sed 's/^/--ignore-table=/' || true
}

dump_one() {
  local db="$1" sql="$WORK/$1.sql"
  # ⚠️ 别用 `mapfile`:macOS 自带的 bash 是 **3.2**(无 mapfile/readarray),
  #    原版这行直接 `mapfile: command not found` 中止——彩排时第一个踩到的就是它。
  #    `ign[${#ign[@]}]=` 是 bash 3.2 也能用的追加写法。
  #    ⚠️ 空数组也要能展开:bash 3.2 下 `set -u` 让 `"${ign[@]}"` 在数组为空时
  #    报 unbound variable,所以下面用的是 `${ign[@]+"${ign[@]}"}`。
  #    zt_sys / zt_charge 就是 0 张排除表,走的就是这条空分支。
  local -a ign=()
  while IFS= read -r t; do
    ign[${#ign[@]}]="$t"
  done < <(build_excludes "$db")

  # 一致性快照开关(默认开;只读账号缺 RELOAD 时由 preflight_dump 拦下,见上)
  local -a snap=()
  [[ "$SNAPSHOT" == "1" ]] && snap=(--single-transaction)

  # ⚠️ 不要为"省一次 mysqldump 调用"把这两遍并成一遍:`--where` 是**整个 dump 一把梭**
  #    的开关,没法"这几张表取 N 行、那几张表一行不取"。能按表区分的只有 --ignore-table,
  #    所以只能:① 全部表的结构 ② 除结构表外的数据。
  local -a only=()
  if [[ "$IS_SAMPLE" == "1" ]]; then
    while IFS= read -r t; do
      only[${#only[@]}]="$t"
    done < <(build_struct_only "$db")
  fi

  log "dump $db → $sql (忽略 ${#ign[@]} 张表,SNAPSHOT=${SNAPSHOT},SAMPLE=${SAMPLE:-全量},只搬结构 ${#only[@]} 张)"
  # --single-transaction:InnoDB 一致性快照,不锁表(源库在线,不能用 --lock-all-tables)
  # --set-gtid-purged=OFF:⚠️ **必须有**。源库开了 GTID(实测),不加这个开关,
  #     dump 头里会写一大段 `SET @@GLOBAL.GTID_PURGED='<几十个 uuid:区间>'`,
  #     而目标库若 gtid_mode=OFF(MySQL 8 的默认值)就整段被拒——load 从第一屏就失败。
  #     GTID_PURGED 只在"拿这份 dump 搭从库"时有意义,本次是并入独立目标库,不需要。
  #     ⚠️ 本机目标库恰好 gtid_mode=ON,**不加也能过**——"我这儿能跑"最会掩盖这类问题。
  # --default-character-set=utf8mb4:以 utf8mb4 出线,服务端会把 utf8mb3/latin1 列转过来
  # --no-tablespaces / --skip-add-locks / --skip-comments:省得需要额外权限、减小体积
  local -a nodata=()
  [[ "$IS_SAMPLE" == "1" ]] && nodata=(--no-data)   # 抽样:第一遍只出结构
  MYSQL_PWD="$LEGACY_DB_PASSWORD" "$DUMP_BIN" "${SRC[@]}" \
    ${snap[@]+"${snap[@]}"} ${nodata[@]+"${nodata[@]}"} --quick --skip-lock-tables \
    --set-gtid-purged=OFF \
    --default-character-set=utf8mb4 \
    --no-tablespaces --skip-add-locks --skip-comments \
    --hex-blob --routines=false --events=false --triggers=true \
    ${ign[@]+"${ign[@]}"} \
    "$db" > "$sql.raw"

  # 抽样:第二遍只出数据,`--where="1=1 LIMIT N"` 按表截断(实测有效:
  # 同一张 242 行的表,带它出 3 行)。含密钥/口令的表在这遍被排除掉,
  # 于是它们**有结构、0 行**——这正是 97_verify_sample.sql §3 断言的东西。
  if [[ "$IS_SAMPLE" == "1" ]]; then
    log "抽样数据(每张表 ≤ ${SAMPLE} 行,跳过 ${#only[@]} 张含密钥/口令的表)"
    MYSQL_PWD="$LEGACY_DB_PASSWORD" "$DUMP_BIN" "${SRC[@]}" \
      --no-create-info --triggers=false --where="1=1 LIMIT ${SAMPLE}" \
      --quick --skip-lock-tables --set-gtid-purged=OFF \
      --default-character-set=utf8mb4 \
      --no-tablespaces --skip-add-locks --skip-comments --hex-blob \
      --routines=false --events=false \
      ${ign[@]+"${ign[@]}"} ${only[@]+"${only[@]}"} \
      "$db" >> "$sql.raw"
  fi

  log "改写字符集声明(utf8_bin→utf8mb4_bin)"
  rewrite_charset < "$sql.raw" > "$sql"
  guard_charset "$sql"
  local raw_mb; raw_mb=$(du -m "$sql.raw" | cut -f1)
  rm -f "$sql.raw"
  log "$db 完成:$sql (${raw_mb} MB)"
}

load_one() {
  local db="$1" sql="$WORK/$1.sql"
  [[ -f "$sql" ]] || die "$sql 不存在,先 dump"
  log "load $sql → $TARGET_DB"
  # 源库里带 `CREATE DATABASE`/`USE` 时,统一落到目标库
  sed -e "s/^USE \`.*\`;/USE \`$TARGET_DB\`;/" "$sql" \
    | MYSQL_PWD="$TARGET_PASSWORD" "$MYSQL_BIN" "${DST[@]}" \
        --default-character-set=utf8mb4 "$TARGET_DB"
  log "$db 灌入完成"
}

case "${1:-}" in
  dump)   # dump 前三道预检,都要在**动第一张表之前**给出结论:
          #   ① 本脚本在 bash 3.2 上跑得起来吗(离线、秒级;macOS 自带的就是 3.2)
          #   ② 字符集改写自测(离线、秒级;含"guard 必须拦下坏输出"的反向用例)
          #   ③ 客户端真能连老库吗(26.x 的 mysql_native_password;只读账号的 RELOAD)
          # 前两道是离线自测,放在联网预检之前:没有可用客户端的人也该能验证脚本逻辑。
          # 三件都别拖到搬了半小时才发现。
          bash "$HERE/tests/shell_compat_test.sh" \
            || die "bash 兼容性检查未通过,已中止(见 sql/migration/tests/)"
          bash "$HERE/tests/charset_rewrite_test.sh" \
            || die "字符集改写自测未通过,已中止(见 sql/migration/tests/)"
          preflight_dump
          for db in "${DBS[@]}"; do
            [[ -z "$ONLY" || ",$ONLY," == *",$db,"* ]] && dump_one "$db"
          done ;;
  load)   for db in "${DBS[@]}"; do
            [[ -z "$ONLY" || ",$ONLY," == *",$db,"* ]] && load_one "$db"
          done ;;
  all)    "$0" dump; "$0" load ;;
  *)      die "用法:$0 {dump|load|all}   [ONLY=zt_park] [WORK=dir] [SNAPSHOT=0] [SAMPLE=500]" ;;
esac

if [[ "$IS_SAMPLE" == "1" ]]; then
cat <<EOF

下一步(**抽样开发库**;判据与全量迁移相反,别跑 98/99 §3§4):
  1. mysql --force -N -h\$TARGET_HOST -u\$TARGET_USER -p < 97_verify_sample.sql
     —— §1~§5 期望**全部 0 行**。它会查:表都在、列结构一致、
       含密钥/口令的表 0 行、其余每张表都落了样例行。
       ⚠️ `--force` 别省:§4 逐表 COUNT(*) 撞到不存在的表会报 ERROR 1146,
       不加它 mysql 就**从这里退出、§5 不会执行**(实测踩到,见文件里 §跑法)。
       想证明这五段真会报红(不是空转),跑 tests/97_negative_test.sh。
  2. ⚠️ 不要跑 98_count_reconcile.sql / 99_verify.sql §3§4:那两份断言
     "目标行数 == 源库行数",在只抽了 ${SAMPLE} 行的镜像上**必然全红**。
  3. 源库全程只读,保持可回滚,别删。
EOF
else
cat <<EOF

下一步(**全量迁移**,顺序不能变):
  1. mysql -h\$TARGET_HOST -u\$TARGET_USER -p $TARGET_DB < 20_union_lost_rows.sql
     —— 补进只存在于副本的 32 行(29 客户 + 3 微信支付配置)
  2. mysql -h\$TARGET_HOST -u\$TARGET_USER -p $TARGET_DB < 99_verify.sql
  3. mysql -h\$TARGET_HOST -u\$TARGET_USER -p $TARGET_DB < 98_count_reconcile.sql
  4. 校验通过后,源库仍保持只读保留(回滚路径),别删。
EOF
fi

# 注:不要在 load 阶段开 FOREIGN_KEY_CHECKS=0 —— 存量库没有外键约束(实测
# information_schema.REFERENTIAL_CONSTRAINTS 为空),开了只会掩盖问题。