天天看点

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

本节书摘来自异步社区出版社《mysql排错指南》一书中的第1章,第1.3节,作者:【美】sveta smirnova(斯维特 斯米尔诺娃),更多章节内容可以访问云栖社区“异步社区”公众号查看。

如果select查询返回了非预期的结果集,这并不总是意味着查询语句本身有错误,也有可能是因为你以为已经进行了插入、更新或者删除等操作,而事实上它们并未生效。

在你调查这种可能之前,你应该先完全仔细检查前一节讨论的select语句编写错误的问题。在select语句编写正确并且能够返回你想要的值的情况下,现在我开始调查由数据本身的问题导致错误的可能性。为了确认问题是由数据本身而非select语句产生的,我尝试精简语句,使其变成某个独立表的简单查询。如果是小表,那么移除所有的where条件和group by语句,然后通过“野蛮”的select from table - name检查所有的数据。对于大表来说,用where条件来筛选出你想要的值是明智的选择。如果你仅关心查询结果集的条数是否和预期的一致,也可以考虑用count()来显示条数。

一旦你确定select查询工作正常,那就意味着是数据不一致产生的问题,你就需要定位哪里出了问题。有很多可能的原因:使用了错误的备份、错误的update语句,或者从节点与主节点之间同步异常(这里先仅列出这些最常见的可能)。在这一节中,我们会看到一些关于delete或update操作在随后的select查询中没有生效的示例。下一节会介绍一些令人困惑的情况,在这些情况中,问题会在被触发很久后才出现,当然我们也会告诉你如何反向定位这类错误。本章并不涉及事务中的问题,这类问题将在第2章中讨论。这里展示的场景都是基于数据库中的数据已经稳定的前提,也就是说,所有使用到的事务都已经完成了。我将继续使用从现实场景中简化过的示例。

我们从可能的最佳情形开始,即错误发生后立即提示数据不一致的问题。我们将使用下面的初始数据集:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

在应用程序中,临时表包含从主日志表中查询出来的部分结果集。这是一个保存日常常用数据经常使用的技术手段,当你只需要用到主表中的一小部分数据并且用户不想改变主表中的数据或者锁定主表的时候,可以使用临时表。

所以在这个示例中,当使用完结果集后,用户想要同时删除两个表中的相应行。通常人们很难想象用一个查询语句去做多件事情。不过现实可以与你的设想不同,并且你还会得到非预期的结果或负面影响:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

如果用户注意观察输出的delete语句的相应结果,就会立即发现出了问题。delete操作没有影响到任何行意味着它什么都没做。然而,一条语句的输出通常不是这么显而易见,有时候它并不可见,因为sql语句是在程序或者脚本内部执行的,并且没有人会去监控执行结果。通常情况下,你应该始终检查语句执行的返回信息,从而了解有多少行数据受影响且它们的值是否与你预期的一致。在应用程序中,你必须明确检查信息功能。

继续下面的讨论。如果你立即执行select查询,你可能会很惊讶,以为查询语句出现了错误或者查询缓存没有清除:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

如果把select语句改为查询行的数量,就可以确认这不是缓存或者其他相关的问题。这个小例子也告诉我们可以通过对同一张表进行不同的查询方式来确认数据的一致性:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

这里count(*)仍然返回一个正数,这表明表是非空的。细心的用户应该已经注意到,delete操作实际上没有删除任何行。为了找出原因,我们可以将delete语句改为相应的select语句。这样做可以告诉我们哪些行满足了删除条件。

尽管这个简单的示例中没有where语句,但是这个技巧对于包含where语句的删除和更新操作同样有效。select语句返回的行即为delete操作将要删除的行或者update操作将要更新的行:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

与之前的结果一致,这里也返回空集合。这就是为什么没有删除任何行!然而,现在仍不清楚产生这个现象的具体原因,但是既然我们有一个select查询,就可以利用第一节提到的相关技术。在这个场景中,最佳选择就是用explain命令执行select语句然后分析输出结果:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

输出中最后的信息表明查询语句被修饰成了内部连接(inner join),该内部连接仅当另一张表也有满足条件的行时才会同时返回两张表的行。对于t1表中的每一行,在t2表中应至少有一行的值匹配。在这个示例中,因为t2表是空的,自然连接操作返回空集合。

我们刚刚学习了另一个有助于找出update或delete语句错误原因的重要技巧:把语句转换成具有相同join和where条件的select语句。针对select查询,可以使用explain extended[1]命令去获取实际的执行计划,同时也可以避免直接操作结果集带来的危险或者修改了错误的行。

这里有一个的使用update的更复杂示例。我们仍使用items表:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

description和additional字段是text类型的。在这个示例中,我们将使用一个错误的语句,该语句想要把表中的中null值替换成更有语义的文本(一个替换成“no description”,另一个替换成“no additional comments”):

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

该语句会更新一些数据(“影响到3行”),让我们检查一下现在表中数据是否合理:

 

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

正如我们所见,有3行记录的description字段的值被修改,不过值是0而不是我们预期的“no description”。并且,additional字段的值根本没有改变。为了定位该问题发生的原因,我们应该检查警告。注意服务器返回的这些语句,我们看到有共3个警告:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

这条消息看起来很奇怪。为什么上述语句执行后,这里会报告关于double的警告,而description和additional字段的类型都是text的。

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

我们还想知道为什么additional字段完全没有变化,并且我们也没有得到任何警告。

我们把该语句拆分成小段,然后分别检查每段都做了什么:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

这是update语句惯用的开头,没有什么问题:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

该段使用set语句。我们来检查一下它实际做了什么。and关键字在这里究竟意味什么?我们在语句中加上圆括号来突出一下运算符号优先级:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

所以,实际上这个语句计算了下列表达式:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

然后将值赋给description字段。计算等式会产生一个布尔类型的结果,表示为longlong类型的值。为了证明这点,以--column-type-info选项打开mysql命令行客户端,然后再次运行select查询:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

我们可以清楚地看到表达式的结果是0,这个值随后被插入了description字段。并且因为我们对additional字段的更新已被这个奇怪的表达式所覆盖了,所以没有值插入该字段中,也就看不到服务器端给出任何关于该字段的信息。

现在可以修改上述语句中的逻辑错误了:

《MySQL排错指南》——1.3 当错误可能由之前的更新引起时

如果需要你也可以检查where语句,不过在这个示例里它没有错误。

这个示例表明返回值和查询执行信息的重要性。我们来进一步讨论它们。

继续阅读