1.前言最近经常遇到开发者不小心误删、误更新数据。这不,他们又找我麻烦了。让我们来看看整个过程。2.流程因为开发需要修复生产环节的数据,需要执行120条SQL语句,需要更新数据,所以开发连接生产数据库,第一个SQL更新表名setsource_name="bj1062-Changying,ChaoyangDistrict,Beichenfudi"wheresource_name="-ChangyingBeichenfudi,ChaoyangDistrict,Beijing"我们仔细看了这条SQL,确实没有问题,where条件也正常.大意是在这个地址前面加上字符串bj1062,真的没有错误吗?是的,没有错误。开发执行完成后,结果确实符合预期。然后开发执行剩下的SQL,和上面的SQL一样,更新地址。执行完成后,开发一头雾水,发现source_name变成了0,开发赶紧打电话给我说:我执行了update,where条件正确,set的值也正确,但是设置后的字段全部都变成0了,请速速帮我看看能不能恢复数据。赶紧上了服务器,这期间查看了binlog,发现有大量updatetablenamesetsource_name=0语句,使用binlog2sql进行分析。项目地址:binlog2sql(https://github.com/danfengcao/binlog2sql)赶紧开发,确定运行的时间点,生成闪回SQL,进行数据恢复,同时保留现场证据。然后查看开发执行的SQL,发现有几个很奇怪的SQL:这些SQL的引号跑在where字段名后面,简化后的SQL变成:updatebl_namesetstr_col="xxx"="yyy"那么这个SQL是怎么执行的MySQL中的语义转换?会不会是这样的?updatebl_nameset(str_col="xxx")="yyy"语法错误,所以只会是下面的形式,updatebl_namesetstr_col=("xxx"="yyy")select"xxx"="yyy"的值为0,所以updatebl_namesetstr_col="xxx"="yyy"相当于updatetbl_namesetstr_col=0,导致source_name字段全部更新为0。我们研究一下select表单中的语句会发生什么情况。mysql[localhost]{msandbox}(test)>selectid,str_colfromtbl_namewherestr_col="xxx"="yyy";+----+-----+|id|str_col|+----+---------+|1|aaa||2|aaa||3|aaa||4|aaa|+----+--------+我们发现这条SQL也找到了str_col='aaa'的记录,为什么?mysql[localhost]{msandbox}(test)>warningsShowwarningsenabled.mysql[localhost]{msandbox}(test)>explainextendedselectid,str_colfromtbl_namewherestr_col="xxx"="yyy"\G*****************************1.row******************************id:1select_type:SIMPLEtable:tbl_nametype:indexpossible_keys:NULLkey:idx_strkey_len:33ref:NULLrows:4filtered:100.00Extra:Usingwhere;Usingindex1rowinset,1warning(0.00sec)Note(Code1003):/*select#1*/select`test`.`tbl_name`.`id`AS`id`,`test`.`tbl_name`.`str_col`AS`str_col`from`test`.`tbl_name`where((`test`.`tbl_name`.`str_col`='xxx')='yyy')这里他把where条件转化为((`test`.`tbl_name`.`str_col`='xxx')='yyy')这个条件先判断str_col和'xxx'是否相等,如果相等,则括号内的值为1,不相等则为0,则为0或1然后用'yyy'判断,因为等号的那一边是int,另一边是字符串,两边都转为float进行比较。可以看我之前的文章MySQL隐式转换导致查询结果错误案例分析(https://www.fordba.com/mysql-type-convert-analysis.html)'yyy'转为浮点型为0,0和0比较等于1mysql[localhost]{msandbox}(test)>select'yyy'+0.0;+------------+|'yyy'+0.0|+----------+|0|+------------+1rowinset,1warning(0.00sec)mysql[localhost]{msandbox}(test)>select00=0;+-----+|00=0|+-----+|1|+-----+1rowinset(0.00sec)结果常成立,即select语句等价于以下SQLselectid,str_colfromtbl_namewhere11=1;将查询所有记录。很正常,但是会导致所有的执行结果都是错误的。执行前必须在测试环境中执行测试,结合IDE的语法高亮可以发现相应的问题。
