在单个SQL查询中使用不同值更新多行

42
我有一个名为myTable的SQLite数据库,包含列idposXposY。行数不断变化(可能增加或减少)。如果我知道每行的id值和行数,我能否执行单个SQL查询来更新所有posXposY字段的值,使它们根据id具有不同的值?
例如:
---------------------
myTable:

id   posX    posY

1      35     565
3      89     224
6      11     456
14     87     475
---------------------

SQL 查询的伪代码:

UPDATE myTable SET posX[id] = @arrayX[id], posY[id] = @arrayY[id] "

@arrayX@arrayY 是存储 posXposY 字段的新值的数组。

例如,如果 arrayXarrayY 包含以下值:

arrayX = { 20, 30, 40, 50 }
arrayY = { 100, 200, 300, 400 }

...然后查询后的数据库应该长这样:

---------------------
myTable:

id   posX    posY

1      20     100
3      30     200
6      40     300
14     50     400
---------------------

这可行吗?我现在每次查询只更新一行,但随着行数的增加,这将需要进行数百次查询。顺便说一下,我是在AIR中完成所有操作的。


也许这可以帮助 http://dba.stackexchange.com/questions/17590/find-last-max-value-according-to-timestamp-using-update-method - adopilot
与MySQL相关:https://dba.stackexchange.com/questions/69269/updating-multiple-rows-with-different-values-in-one-query - Ciro Santilli OurBigBook.com
8个回答

44

有几种方法可以相当有效地完成这个任务。

首先 -
如果可能的话,您可以将一些数据批量插入到临时表中。 这在某种程度上取决于您的RDBMS/主机语言,但最坏的情况下可以使用简单的动态SQL(使用VALUES()子句),然后使用标准的update-from-another-table指令。 大多数系统都提供了用于批量加载的实用程序。

其次 -
这也与RDBMS有关,您可以构建一个动态更新语句。 在这种情况下,CTE内部的VALUES(...)子句是即时创建的:

WITH Tmp(id, px, py) AS (VALUES(id1, newsPosX1, newPosY1), 
                               (id2, newsPosX2, newPosY2),
                               ......................... ,
                               (idN, newsPosXN, newPosYN))

UPDATE TableToUpdate SET posX = (SELECT px
                                 FROM Tmp
                                 WHERE TableToUpdate.id = Tmp.id),
                         posY = (SELECT py
                                 FROM Tmp
                                 WHERE TableToUpdate.id = Tmp.id)


WHERE id IN (SELECT id
             FROM Tmp)

(根据文档,这个语句应该是有效的SQLite语法,但我无法在一个“fiddle”中使其工作)


你能把你的代码放到答案里吗?现在如果那个链接失效了,你的答案就变得几乎没用了。 - George Stocker
@GeorgeStocker - 啊,谢谢。不知怎么的,我错过了原始标签,我的示例语句在目标RDBMS中无效。希望现在能够正常工作... - Clockwork-Muse
看起来不错!但是需要进行一处小的修正,因为在问题中列的名字是“id”,所以它也应该出现在答案中。请在所有地方将“i”替换为“id”。 - HaimS
1
我也会有效地使用JOIN,更新TableToUpdate和Tmp表格中相应id的posX和posY字段,LEFT JOIN使用(TableToUpdate.id = Tmp.id)连接。 - Naga
@Clockwork-Muse 我想在iOS上使用它,但是我遇到了以下错误...库例程调用顺序不正确:(有什么想法吗? - Jules
显示剩余2条评论

24

一种方法:使用SET x=CASE..END(任何SQL)

是的,您可以这样做,但我怀疑除非您的查询具有真正的大延迟,否则它不会提高性能。

如果查询在搜索值上建立索引(例如,如果id是主键),则定位所需的元组非常快,而且在第一次查询后,表将保存在内存中。

因此,在这种情况下,多个UPDATE并不那么糟糕。

另一方面,如果条件需要对整个表进行全表扫描,并且更糟糕的是,表的内存影响很大,那么即使评估更新比简单更新更昂贵(会在内部优化),使用单个复杂查询也会更好。

在后一种情况下,您可以这样做:

 UPDATE table SET posX=CASE
      WHEN id=id[1] THEN posX[1]
      WHEN id=id[2] THEN posX[2]
      ...
      ELSE posX END [, posY = CASE ... END]
 WHERE id IN (id[1], id[2], id[3]...);

总成本或多或少由以下公式给出:NUM_QUERIES * (COST_QUERY_SETUP + COST_QUERY_PERFORMANCE)。 这样,您可以通过减少 NUM_QUERIES(从 N 个单独的 id 到 1 个)来降低成本,但是 COST_QUERY_PERFORMANCE 将会增加(MySQL 5.28 大约增加了 3 倍;尚未在 MySQL 8 中测试)。

否则,我建议对 id 进行索引或修改架构。

这是一个 PHP 的示例,其中我假设我们已经有一个需要进行完整表扫描的条件,并且可以将其用作键:

// Multiple update rules 
$updates = [
   "fldA='01' AND fldB='X'" => [ 'fldC' => 12, 'fldD' => 15 ],
   "fldA='02' AND fldB='X'" => [ 'fldC' => 60, 'fldD' => 15 ],
   ...
];
右手表达式中更新的字段可以是一个或多个,但必须始终相同(在此情况下始终为fldC和fldD)。可以消除此限制,但需要修改算法。 然后我可以通过循环构建单个查询:
$where = [ ];
$set   = [ ];
foreach ($updates as $when => $then) {
    $where[] = "({$when})";
    foreach ($then as $fld => $value) {
       if (!array_key_exists($fld, $set)) {
           $set[$fld] = [ ];
       }
       $set[$fld][] = $value;
    }
}

$set1 = [ ];
foreach ($set as $fld => $values) {
    $set2 = "{$fld} = CASE";
    foreach ($values as $i => $value) {
        $set2 .= " WHEN {$where[$i]} THEN {$value}";
    }
    $set2 .= ' END';
    $set1[] = $set2;
}

// Single query
$sql  = 'UPDATE table SET '
      . implode(', ', $set1)
      . ' WHERE '
      . implode(' OR ', $where);

另一种方法:使用ON DUPLICATE KEY UPDATE(MySQL)

在MySQL中,如果id是主键,则可以更轻松地通过多个INSERT ON DUPLICATE KEY UPDATE来完成此操作。需要注意的是,不存在的条件(例如“id = 777”但表中没有777)将被插入到表中,如果查询中未指定其他必需列(声明为NOT NULL),可能会导致错误:

INSERT INTO tbl (id, posx, posy, bazinga)
     VALUES (id1, posY1, posY1, 'DELETE'),
     ...
ON DUPLICATE KEY SET posx=VALUES(posx), posy=VALUES(posy);

DELETE FROM tbl WHERE bazinga='DELETE';

以上的“bazinga”技巧允许删除任何可能因其ID不存在而无意中插入的行(在其他情况下,您可能希望插入的行保留下来)。

例如,从一组收集的传感器进行定期更新,但某些传感器可能没有被传输:

INSERT INTO monitor (id, value)
VALUES (sensor1, value1), (sensor2, 'N/A'), ...
ON DUPLICATE KEY UPDATE value=VALUES(value), reading=NOW();

(这是一个人为制造的情况,更合理的做法可能是锁定表格,将所有传感器更新为N/A和NOW(),然后只插入我们确实拥有的值)。

第三种方法:CTE(适用于PostgreSQL,不确定SQLite3是否适用)

从概念上讲,这几乎与INSERT MySQL技巧相同。 如所写,它适用于PostgreSQL 9.6:

WITH updated(id, posX, posY) AS (VALUES
    (id1, posX1, posY1), 
    (id2, posX2, posY2),
    ...
)
UPDATE myTable
    SET 
    posX = updated.posY,
    posY = updated.posY
FROM updated
WHERE (myTable.id = updated.id);

不错的例子!不幸的是,正如我所说的,行数将会改变,因此“WHEN”运算符的数量需要不断修改。 - astralmaster
如果你真的很想这样做,你可以动态地构建查询。将查询分成五到六个部分,然后重新组合,其中三个部分(第一个和第二个CASE以及IN子句)在单个WHILE循环内构建。如果值发生变化,你无论如何都应该这样做... - LSerni
1
你能详细说明一下在SET语句中使用CASE语句是如何导致COST_QUERY_PERFORMANCE上升的吗? - Michael

6
你可能会对这样的东西感兴趣:
"UPDATE myTable SET ... ;
 UPDATE myTable SET ... ;
 UPDATE myTable SET ... ;
 UPDATE myTable SET ... ;"

如果posX或posY值中有任何一个相同,那么它们可以合并为一个查询。
UPDATE myTable SET posX='39' WHERE id IN('2','3','40');

你可以在文本编辑器中准备这样的语句,然后直接粘贴到命令行中。如果你没有太多的更新需要进行,这种方法是非常有效的。 - pavitran

2
在最近的SQLite版本中(从2018年的3.24.0开始),您可以使用UPSERT子句。假设只有唯一的id列被更新,您可以使用这种方法来更新现有数据集,该方法类似于@LSerni的ON DUPLICATE建议:
INSERT INTO myTable (id, posX, posY) VALUES
  ( 1, 35, 565),
  ( 3, 89, 224),
  ( 6, 11, 456),
  (14, 87, 475)
ON CONFLICT (id) DO UPDATE SET
  posX = excluded.posX, posY = excluded.posY

ON CONFLICT 的一个问题是,如果您想省略不需要更新的 NOT NULL 列,则无法正常工作:https://dev59.com/8qnka4cB1Zd3GeqPIhh6 - Ciro Santilli OurBigBook.com

1

实际上,我无法让@Clockwork-Muse工作。但我可以让这个变体起作用:

WITH Tmp AS (SELECT * FROM (VALUES (id1, newsPosX1, newPosY1), 
                                   (id2, newsPosX2, newPosY2),
                                   ......................... ,
                                   (idN, newsPosXN, newPosYN)) d(id, px, py))

UPDATE t

SET posX = (SELECT px FROM Tmp WHERE t.id = Tmp.id),
    posY = (SELECT py FROM Tmp WHERE t.id = Tmp.id)

FROM TableToUpdate t

我希望这对你也有用!


0

使用逗号 ","

eg: 
UPDATE my_table SET rowOneValue = rowOneValue + 1, rowTwoValue  = rowTwoValue + ( (rowTwoValue / (rowTwoValue) ) + ?) * (v + 1) WHERE value = ?

我觉得这没什么意义,因为你需要多个where条件来匹配每一行。您能否详细说明一下? - Marian Klühspies

0

如果要使用不同的值更新表中的列1,并给出列2上的值,可以按照以下SQLite方式进行:

"UPDATE table SET column1=CASE WHEN column2<>'something' THEN 'val1' ELSE 'val2' END"

-12

尝试使用“update tablet set (row='value' where id=0001'), (row='value2' where id=0002'), ...”


你真的试过这个吗? - icedwater
这很棒,也是我正在寻找的。 - ii iml0sto1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接