由 "select *" 引发的“惨案”
今天凌晨做发布, 要合并多个分数据库的表数据到主数据库中, 有 30+ 分数据库。 前面都比较顺利, 在临近结束时,突然发现一个字段的值插入错误。 有一个表 T,字段分别为 (f1, f2, f3, gmt_create, gmt_modify, name) 。 假设分数据库为 a1, a2, ..., a30 , 主数据库为 A 。 合并的逻辑是: 从 a1, a2, ..., a30 取出对应的字段, 依次插入到 主数据库中。
aRet = adb.query("select * from T") allTuples = [] for (f1, f2, f3, gmt_create , gmt_modify, name) in aRet: allTuples.append((f1, f2, f3, gmt_create, gmt_modify, name)) Adb.executeMany("insert into T(f1, f2, f3, gmt_create, gmt_modify, name) values(%s, %s, %s, %s, %s, %s)", allTuples)
log.info(allTuples) Adb.commit()
NOTE: 这里会将 allTuples 分成 1000 个元组一片进行提交执行。 不过不影响此处的理解。
那么, 这会有什么问题呢? 初看上去似乎没什么, 但是“惨案” 就这样发生了。
有一个集群 ai 的表 T 的字段顺序跟其他集群的略有不同。其表字段顺序是 (f1, f2, f3, name , gmt_create, gmt_modify) 。 这样从 ai 取出的数据 (f1, f2, f3, name, gmt_create, gmt_modify) 将插入到 A 的 (f1, f2, f3, gmt_create, gmt_modify, name) , 也就是说, a1.name 插入到 A.gmt_create, a1.gmt_create 插入到 A.gmt_modify, a1.gmt_modify 插入了 A.name , 由于 gmt_create , gmt_modify, name 均为字符串, 因此没有报错。 验证的时候, 由于另外一个地方因其他原因报了大量错误,掩盖了这个问题。
解决的办法很简单: 将 "select * from T" 改为 "select f1, f2, f3, gmt_create , gmt_modify, name from T"
教训: 在做“逐字段取出-插入” 的数据库操作时, 切忌使用 “select * ”
发现数据插入错误之后, 马上进行清空和重新执行。 这时, 更糟糕的事情发生了。 由于清空操作要考虑将对 T.A 的新改动(考虑到发布过程中会有外部调用修改T.A的数据)同步到 T.ai 。 而上述已经对 T.A 进行了大量改动, 因此会以 T.A 的数据为准, 对 T.ai 的相应记录进行回写, 结果将 ai 的原数据覆盖了, 且没有预先做表备份。 ai 的数据就这样丢失了!
教训: 合并过程中, 最好不要回写源数据库, 降低复杂性; 如果一定要回写源数据库, 要单独做一个脚本, 取名更明显, 且要做表备份操作。
现在必须马上恢复数据! 当时差点忘了, 由于 db.executeMany 接口没办法获取到直接执行的SQL, 因此昨天早上思虑再三,新添了一行代码使用了 log.info(allTuples) , 记录下了所要插入的源数据。 万一出问题, 避免从 DB 中取执行SQL的麻烦。 不过, 由于偷了一点懒, 打印出的 allTuples 的格式相当难以解析, 费了不少劲才将 <primarykey, name> 的关系取出来, 重新做了订正。 昨天早上的那行代码成了今天早上的救命稻草之一。
教训: 对数据库的insert, update, delete 操作一定要加日志。 如果数据库接口不方便直接打印SQL的日志, 就要单独打印出源数据以备后用。 此外, 最好不要偷懒, 因为每一点偷懒都会对后面某个时候造成障碍, 而稍微做的便利一点, 就会对后续产生有益的用处。 这都是活生生的教训。