返回博客

表存在性校验:COUNT(*) 对比 array.length,重复项导致永久误报「缺表」

某个自动化后台任务上线后,始终卡在初始状态,日志抛出一条错误,列出了一长串「缺失的表名」。第一直觉是:表没建好?去数据库一查,每一张表都在,数据也正常写入。报错信息和现实完全对不上——这时候,就该怀疑是守卫逻辑本身的 bug。

现象

任务触发后,状态一直停在 waiting,不推进也不失败。查日志,发现类似:

schema_check_failed: missing wf_tasks, wf_companies, wf_archive_records, ...

去数据库执行 SHOW TABLES LIKE 'wf_%',列出的表名和报错列表完全重合——每一张都存在。更奇怪的是,连「已有数据」的核心表也出现在「缺失」列表里。

根因

守卫函数的实现大概长这样:

async function hasRequiredTables(conn, names) {
  const [rows] = await conn.query(
    `SELECT COUNT(*) AS count FROM information_schema.tables
     WHERE table_schema = DATABASE()
     AND table_name IN (${names.map(() => '?').join(',')})`,
    names,
  )
  return Number(rows[0].count) === names.length  // ← 这里是 bug
}

IN (...) 在 SQL 里是集合成员判断,不是多值计数。无论同一个表名在参数数组里出现几次,物理上只有一张表匹配,COUNT(*) 返回的是去重后的存在数量

names.length 计算的是数组的长度,含重复项

names 里存在重复时:COUNT(*) = 去重后的表数 < names.length → 永远返回 false → 守卫永远拦截 → 任务永远不跑。

重复项从哪来?典型场景是用展开运算符拼合多个分组:

const REQUIRED = [
  'wf_tasks', 'wf_companies',
  ...PERSON_TABLES,        // 里面已经包含 wf_tasks、wf_companies
  ...BIG_GROUP,            // 再次包含上面所有的
]
// 结果:89 项,去重后 52 个唯一表名
// COUNT(*) 返回 52,和 89 比较 → 永远 false
坑:错误报文把整个「必须列表」当作「缺失列表」打印

很多实现会把整个 names 数组输出到错误信息里,而不是只输出「实际缺失的表名」。这会导致报错把所有必须存在的表都列出来,让你以为它们都缺失,实际上一张都没缺——只是计数出了偏差。不要顺着报文逐一核查每张表,先验证 names 里有没有重复项。

正解

在守卫函数内部先去重,再做 IN 查询和长度对比:

async function hasRequiredTables(conn, names) {
  const unique = [...new Set(names)]            // 去重
  const [rows] = await conn.query(
    `SELECT COUNT(*) AS count FROM information_schema.tables
     WHERE table_schema = DATABASE()
     AND table_name IN (${unique.map(() => '?').join(',')})`,
    unique,
  )
  return Number(rows[0].count) === unique.length
}

顺手优化报错信息,只暴露真正缺失的表名:

if (Number(rows[0].count) !== unique.length) {
  const existing = rows.map(r => r.table_name)
  const missing = unique.filter(n => !existing.includes(n))
  throw new Error(`missing tables: ${missing.join(', ')}`)
}
正解:守卫内部去重,让 COUNT(*) 和 length 语义对齐

修复点集中在守卫函数一处,所有调用方不需要改动。去重一个已经无重复的数组是幂等操作,不影响正常路径;只有传入含重复项的调用才会从「永远 false」变为「正确通过」。

快速验证脚本

修复前先跑这几行确认根因,比逐表排查快得多:

console.log('array length:', names.length)       // e.g. 89
console.log('unique count:', new Set(names).size) // e.g. 52
// 如果两者不等,重复项就是罪魁祸首
// COUNT(*) 一定等于 unique count,永远 !== array length

一句话外卖

SQL IN (...) 是集合判断,COUNT(*) 只数物理对象,拿它对比含重复项的数组长度——守卫永远失败,任务永远不跑,报错永远说「缺表」。去重放在守卫里,一次修好所有调用方。