MySQL更新更改多列是非原子的?

Posted

技术标签:

【中文标题】MySQL更新更改多列是非原子的?【英文标题】:MySQL update changing multiple columns is non-atomic? 【发布时间】:2012-05-28 09:41:56 【问题描述】:

我在使用带有 mysql 5.5.22 的 Django 时遇到以下问题。

给定一个包含列 id、level 和存储为 a11、a12、a21、a22 的 2x2 矩阵的表,我有这一行:

id   a11   a12   a21   a22   level
324  3     2     5     3     2

给定一个查询集 qs,我进行以下更新:

qs.update(
    a11=(b12 * a21 - b11 * a22) * F('a11') + (b11 * a12 - b12 * a11) * F('a21'),
    a12=(b12 * a21 - b11 * a22) * F('a12') + (b11 * a12 - b12 * a11) * F('a22'),
    a21=(b22 * a21 - b21 * a22) * F('a11') + (b21 * a12 - b22 * a11) * F('a21'),
    a22=(b22 * a21 - b21 * a22) * F('a12') + (b21 * a12 - b22 * a11) * F('a22'),
    level=(F('level') - 1)
    )

django 为其生成以下查询(从 db.connection.queries 获取,为简洁起见,删除 where 子句):

UPDATE `storage` 
SET 
`a21` = (3 * `storage`.`a11`) + (-1 * `storage`.`a21`), 
`a22` = (3 * `storage`.`a12`) + (-1 * `storage`.`a22`), 
`level` = `storage`.`level` - -1, 
`a11` = (2 * `storage`.`a11`) + (-1 * `storage`.`a21`), 
`a12` = (2 * `storage`.`a12`) + (-1 * `storage`.`a22`) 

然后我的行看起来像这样:

id   a11   a12   a21   a22   level
324  2     1     4     3     1

对于任何一行,a12*a21 - a11*a22 = 1 应该是 True,据此,该行应该是:

id   a11   a12   a21   a22   level
324  1     1     4     3     1

这是我在 SQLite 上得到的结果,Django 生成相同的查询,我花了很多时间才发现 MySQL 做了一些不同的事情。从查询来看,似乎在更新相互依赖的多行时,MySQL 不会将其视为单个原子操作,并且随着列的更新,它们会影响依赖于它们的值。我确认这似乎是 Python 提示符下的以下代码发生的情况:

>>> a11, a12, a21, a22 = (3, 2, 5, 3)
>>> (2 * a11) + (-1 * a21),\
... (2 * a12) + (-1 * a22),\
... (3 * a11) + (-1 * a21),\
... (3 * a12) + (-1 * a22)
(1, 1, 4, 3)

如果列一次更新一个,按照查询给出的相同顺序:

>>> a11, a12, a21, a22 = (3, 2, 5, 3)
>>> a21 = (3*a11) + (-1*a21)
>>> a22 = (3*a12) + (-1*a22)
>>> a11 = (2*a11) + (-1*a21)
>>> a12 = (2*a12) + (-1*a22)
>>> (a11, a12, a21, a22)
(2, 1, 4, 3)

这是一个非常可怕的行为,因为这是一个旨在跨平台使用的库。我的问题是:

    哪个做错了,MySQL 还是 SQLite?这可以被认为是一个错误吗? 我可以从其他主要数据库(Oracle、PostgreSQL 和 SQLServer)中获得什么? 我可以用 Django ORM(无原始查询)做什么来规范这种行为?

编辑

问题很明确,但我仍在寻找解决方案。对于这个特定的应用程序来说,拉取所有值并将它们推回是不可接受的解决方案。

【问题讨论】:

这是一个有趣的问题。我在sqlfiddle 上玩过它,似乎 MySQL 是唯一一个这样做的。 相关/重复:***.com/questions/2203202/… 【参考方案1】:

PostgreSQL、Oracle 和 SQL Server 都将其视为原子操作。 See the following SQL Fiddle, and switch the server to see the behavior of the following SQL:

CREATE TABLE Swap (
  a CHAR(1),
  b CHAR(1)
);

INSERT INTO Swap (a, b) VALUES ('a', 'b');

UPDATE Swap SET a = b, b = a;

SELECT * FROM Swap;

MySQL 是唯一实现此功能的 RBDMS,更新后两列都包含相同的值。

至于您将如何解决此问题,我将改为从数据库中提取值,在您的应用程序内部进行计算(而不是您的更新语句),然后使用计算出的值更新数据库。这样您就可以保证以一致的方式执行计算。

【讨论】:

谢谢。 SQL Fiddle 对我来说是新的并且非常有用。不幸的是,从数据库中提取所有数据并将其推回会破坏该库的全部目的。如果我愿意,有更好的方法来做它所做的事情。如果没有其他办法,很好,但我希望有更好的解决方案,即使有点 hackish。【参考方案2】:

如MySQL manual中所述:

以下语句中的第二个赋值将col2 设置为当前(更新的)col1 值,而不是原始col1 值。结果是col1col2 具有相同的值。此行为不同于标准 SQL。

更新 t1 SET col1 = col1 + 1, col2 = col1;

因此,在您的情况下,评估表达式 `a11` = (2 * `storage`.`a11`) + (-1 * `storage`.`a21`) 时用于 a21 的值是新的、更新的值 4,而不是原始值 5。正如手册所说,这个行为不同于标准 SQL

您可以改为使用多表 UPDATE 语法的自联接,但是我不知道是否可以使用 Django ORM 实现这样的事情:

UPDATE storage AS old
  JOIN storage AS new USING (id)
SET
  new.a21   = (3 * old.a11) + (-1 * old.a21),
  new.a22   = (3 * old.a12) + (-1 * old.a22),
  new.level = old.level - -1,
  new.a11   = (2 * old.a11) + (-1 * old.a21),
  new.a12   = (2 * old.a12) + (-1 * old.a22);

在sqlfiddle 上查看。

我唯一的另一个想法(这肯定可以在 Django 中实现)是将更新拆分为单独的部分,定义在后面部分更新的字段与那些已被更新的字段的新值(而不是旧值)相关。在前面部分更新:

UPDATE storage
SET    a21   = (3 * a11) + (-1 * a21),
       a22   = (3 * a12) + (-1 * a22),
       level = level - -1;

UPDATE storage
SET    a11   = (2 * a11) + (-1 * (3*a11 - a21)),
       a12   = (2 * a12) + (-1 * (3*a12 - a22));

为防止并发问题,您应该在事务中执行这两个更新(如果 RDBMS 支持)。

【讨论】:

感谢您的参考。这很清楚。我希望也许有一个设置可以改变这种行为。 很好的解决方案!例如,SQL Server 与 inserteddeleted 伪表的工作方式相同。

以上是关于MySQL更新更改多列是非原子的?的主要内容,如果未能解决你的问题,请参考以下文章

MYSQL 使用变量更新多列

MySQL中带有'SET'的两个表之间的多列更新

php mysql批量或批量更新多列和多行但如果列不为空则不更新

Oracle:在更新具有多列的表的一个字段时复制行

聊聊mysql的多列组合查询

MySQL关联表多行转多列?