【问题标题】:Multiple Updates in MySQLMySQL 中的多个更新
【发布时间】:2010-09-05 10:18:31
【问题描述】:

我知道您可以一次插入多行,有没有办法在 MySQL 中一次更新多行(如在一个查询中)?

编辑: 例如我有以下

Name   id  Col1  Col2
Row1   1    6     1
Row2   2    2     3
Row3   3    9     5
Row4   4    16    8

我想将以下所有更新合并到一个查询中

UPDATE table SET Col1 = 1 WHERE id = 1;
UPDATE table SET Col1 = 2 WHERE id = 2;
UPDATE table SET Col2 = 3 WHERE id = 3;
UPDATE table SET Col1 = 10 WHERE id = 4;
UPDATE table SET Col2 = 12 WHERE id = 4;

【问题讨论】:

    标签: mysql sql sql-update


    【解决方案1】:

    是的,这是可能的 - 您可以使用 INSERT ... ON DUPLICATE KEY UPDATE。

    使用您的示例:

    INSERT INTO table (id,Col1,Col2) VALUES (1,1,1),(2,2,3),(3,9,3),(4,10,12)
    ON DUPLICATE KEY UPDATE Col1=VALUES(Col1),Col2=VALUES(Col2);
    

    【讨论】:

    • 如果没有重复,那么我不希望插入该行。 id应该怎么做?因为我正在从另一个维护带有 id 的表的站点获取信息。我正在插入与该 ID 相关的值。如果该站点有新记录,那么我最终将只插入 ID 和计数,除了所有其他信息。当且仅当 id 有一个条目时,它应该更新,否则它应该跳过。我该怎么办?
    • 注意:这个答案还假设 ID 是主键
    • @JayapalChandran 您应该将 INSERT IGNORE 与 ON DUPLICATE KEY UPDATE 一起使用。 dev.mysql.com/doc/refman/5.5/en/insert.html
    • @HaralanDobrev 使用 INSERT IGNORE 仍会插入非重复记录。 Jayapal 想要避免的。 INSERT IGNORE 只是将任何错误变成警告:( stackoverflow.com/questions/548541/…
    • 您还应该关心您的增量(如果存在),如果您插入 O.D.K.U. 它也会增加。即使没有插入新记录!如果你用这种方式做很多更新,你的自动增量很快就会溢出!
    【解决方案2】:

    由于您有动态值,因此您需要使用 IF 或 CASE 来更新要更新的列。它有点难看,但它应该可以工作。

    使用您的示例,您可以这样做:

    更新表 SET Col1 = CASE id 当 1 时 1 当 2 然后 2 当 4 然后 10 其他 Col1 结尾, Col2 = 案例编号 当 3 那么 3 当 4 然后 12 ELSE Col2 结尾 WHERE id IN (1, 2, 3, 4);

    【讨论】:

    • 对于动态更新来说写起来可能不太漂亮,但看看套管的功能很有趣......
    • @user2536953 ,它也可以很好地用于动态更新。例如,我在 php 中循环使用了该解决方案:$commandTxt = 'UPDATE operations SET chunk_finished = CASE id '; foreach ($blockOperationChecked as $operationID => $operationChecked) $commandTxt .= " WHEN $operationID THEN $operationChecked "; $commandTxt .= 'ELSE id END WHERE id IN ('.implode(', ', array_keys(blockOperationChecked )).');';
    【解决方案3】:

    这个问题很老了,但我想用另一个答案来扩展这个话题。

    我的观点是,实现它的最简单方法就是用事务包装多个查询。接受的答案 INSERT ... ON DUPLICATE KEY UPDATE 是一个不错的 hack,但应该意识到它的缺点和局限性:

    • 如前所述,如果您碰巧使用表中不存在主键的行启动查询,则查询会插入新的“半生不熟”记录。可能不是你想要的
    • 如果你有一个非空字段且没有默认值的表,并且不想在查询中触及该字段,即使你根本不插入一行,你也会收到"Field 'fieldname' doesn't have a default value" MySQL 警告.如果您决定严格并在您的应用中将 mysql 警告转变为运行时异常,这会给您带来麻烦。

    我对三个建议的变体进行了一些性能测试,包括INSERT ... ON DUPLICATE KEY UPDATE 变体、带有“case / when / then”子句的变体和带有事务的幼稚方法。您可能会得到 python 代码和结果here。总的结论是,带有 case 语句的变体的速度是其他两个变体的两倍,但是很难为其编写正确且注入安全的代码,因此我个人坚持使用最简单的方法:使用事务。

    编辑:Dakusan 的发现证明我的性能估计并不完全有效。请参阅this answer 了解另一项更详细的研究。

    【讨论】:

    • 使用事务,非常好的(简单)提示!
    • 如果我的表不是 InnoDB 类型怎么办?
    • 有人可以提供一个链接,指向哪些交易看起来像这样吗?和/或带有 case 语句的变体的注入安全代码的代码?
    • 我发现这篇文章中关于速度的信息是错误的。我在下面的帖子中写到了它。 stackoverflow.com/questions/3432/multiple-updates-in-mysql/…
    • @Dakusan,很好的答案。非常感谢您扩展、评论和修复我的结果。
    【解决方案4】:

    不知道为什么还没有提到另一个有用的选项:

    UPDATE my_table m
    JOIN (
        SELECT 1 as id, 10 as _col1, 20 as _col2
        UNION ALL
        SELECT 2, 5, 10
        UNION ALL
        SELECT 3, 15, 30
    ) vals ON m.id = vals.id
    SET col1 = _col1, col2 = _col2;
    

    【讨论】:

    • 这是最好的。特别是如果您像我一样从另一个 SQL 查询中提取更新值。
    • 这对于更新具有大量列的表非常有用。我将来可能会经常使用这个查询。谢谢!
    • 我已经尝试过这种类型的查询。但是当记录达到 30k 边界服务器停止。还有其他解决办法吗?
    • 这看起来很棒。我将尝试将其与未更新主键但用于标识要更改的列的 WHERE 子句结合使用。
    • @BhavinChauhan 您是否尝试过使用临时表而不是连接选择来规避问题?
    【解决方案5】:

    以下所有内容均适用于 InnoDB。

    我觉得了解这 3 种不同方法的速度很重要。

    有3种方法:

    1. 插入:使用重复键更新插入
    2. TRANSACTION:您对事务中的每条记录进行更新的位置
    3. CASE:在这种情况下/何时为 UPDATE 中的每个不同记录提供一个案例

    我刚刚对此进行了测试,对我而言,INSERT 方法比 TRANSACTION 方法快 6.7 倍。我尝试了一组 3,000 和 30,000 行。

    TRANSACTION 方法仍然必须运行每个单独的查询,这需要时间,尽管它在执行时将结果批处理到内存或其他东西中。 TRANSACTION 方法在复制和查询日志中的开销也相当大。

    更糟糕的是,CASE 方法比具有 30,000 条记录的 INSERT 方法慢 41.1 倍(比 TRANSACTION 慢 6.1 倍)。在 MyISAM 中速度慢 75 倍。 INSERT 和 CASE 方法在大约 1,000 条记录时实现了收支平衡。即使有 100 条记录,CASE 方法也快了很多。

    所以总的来说,我觉得 INSERT 方法是最好的和最容易使用的。查询更小更易于阅读,并且只占用 1 个操作查询。这适用于 InnoDB 和 MyISAM。

    奖金:

    INSERT非默认字段问题的解决方法是暂时关闭相关SQL模式:SET SESSION sql_mode=REPLACE(REPLACE(@@SESSION.sql_mode,"STRICT_TRANS_TABLES",""),"STRICT_ALL_TABLES","")。如果您打算恢复它,请务必先保存 sql_mode

    至于我见过的其他 cmets 说 auto_increment 使用 INSERT 方法上升,这在 InnoDB 中似乎是这种情况,但不是 MyISAM。

    运行测试的代码如下。它还输出 .SQL 文件以消除 php 解释器开销

    <?php
    //Variables
    $NumRows=30000;
    
    //These 2 functions need to be filled in
    function InitSQL()
    {
    
    }
    function RunSQLQuery($Q)
    {
    
    }
    
    //Run the 3 tests
    InitSQL();
    for($i=0;$i<3;$i++)
        RunTest($i, $NumRows);
    
    function RunTest($TestNum, $NumRows)
    {
        $TheQueries=Array();
        $DoQuery=function($Query) use (&$TheQueries)
        {
            RunSQLQuery($Query);
            $TheQueries[]=$Query;
        };
    
        $TableName='Test';
        $DoQuery('DROP TABLE IF EXISTS '.$TableName);
        $DoQuery('CREATE TABLE '.$TableName.' (i1 int NOT NULL AUTO_INCREMENT, i2 int NOT NULL, primary key (i1)) ENGINE=InnoDB');
        $DoQuery('INSERT INTO '.$TableName.' (i2) VALUES ('.implode('), (', range(2, $NumRows+1)).')');
    
        if($TestNum==0)
        {
            $TestName='Transaction';
            $Start=microtime(true);
            $DoQuery('START TRANSACTION');
            for($i=1;$i<=$NumRows;$i++)
                $DoQuery('UPDATE '.$TableName.' SET i2='.(($i+5)*1000).' WHERE i1='.$i);
            $DoQuery('COMMIT');
        }
        
        if($TestNum==1)
        {
            $TestName='Insert';
            $Query=Array();
            for($i=1;$i<=$NumRows;$i++)
                $Query[]=sprintf("(%d,%d)", $i, (($i+5)*1000));
            $Start=microtime(true);
            $DoQuery('INSERT INTO '.$TableName.' VALUES '.implode(', ', $Query).' ON DUPLICATE KEY UPDATE i2=VALUES(i2)');
        }
        
        if($TestNum==2)
        {
            $TestName='Case';
            $Query=Array();
            for($i=1;$i<=$NumRows;$i++)
                $Query[]=sprintf('WHEN %d THEN %d', $i, (($i+5)*1000));
            $Start=microtime(true);
            $DoQuery("UPDATE $TableName SET i2=CASE i1\n".implode("\n", $Query)."\nEND\nWHERE i1 IN (".implode(',', range(1, $NumRows)).')');
        }
        
        print "$TestName: ".(microtime(true)-$Start)."<br>\n";
    
        file_put_contents("./$TestName.sql", implode(";\n", $TheQueries).';');
    }
    

    【讨论】:

    • 你在这里做主的工作;)非常感谢。
    • 测试 GoLang 和 PHP 之间的一些性能,在 MariaDB 上使用 40k 行,我在 PHP 上获得了 2 秒,在 golang 上获得了超过 6 秒......好吧,我总是被告知 GoLang 会运行比 PHP 快!!!所以,我开始想知道如何提高性能...使用 INSERT ... ON DUPLICATE KEY UPDATE ...我在 Golang 上得到 0.74 秒,在 PHP 上得到 0.86 秒!!!!
    • 我的代码的重点是将计时结果限制为严格的 SQL 语句,而不是语言或库的代码。 GoLang 和 PHP 是两种完全独立的语言,用于完全不同的事物。 PHP 适用于单线程上的单次运行脚本环境,主要是有限和被动的垃圾收集。 GoLang 适用于长时间运行的编译应用程序,具有积极的垃圾收集和多线程作为主要语言功能之一。它们在语言功能和推理方面几乎没有什么不同。 [继续]
    • 因此,在运行测试时,请确保将速度测量严格限制为 SQL 语句的“查询”函数调用。比较和优化源代码中不严格查询调用的其他部分就像比较苹果和橘子一样。如果您将结果限制在此范围内(预编译字符串并准备就绪),那么结果应该非常相似。在那一点上的任何差异都是语言的 SQL 库的错误,而不一定是语言本身的错误。在我看来,INSERT ON DUPLICATE 解决方案曾经并将永远是最佳选择。[续]
    • 至于您对 GoLang 更快的评论,这是一个非常广泛的声明,没有考虑到这些语言及其设计的众多警告或细微差别。 Java 是一种解释型语言,但我在 15 年前发现它在某些情况下的速度实际上几乎可以匹敌(甚至有时甚至超过)C。 C 是一种编译语言,是除汇编语言之外最常见的最低级系统语言。我真的很喜欢 GoLang 正在做的事情,它绝对有能力和流动性成为最常见的系统之一并经过优化 [续]
    【解决方案6】:
    UPDATE table1, table2 SET table1.col1='value', table2.col1='value' WHERE table1.col3='567' AND table2.col6='567'
    

    这应该对你有用。

    the MySQL manual 中有多个表的引用。

    【讨论】:

      【解决方案7】:

      使用临时表

      // Reorder items
      function update_items_tempdb(&$items)
      {
          shuffle($items);
          $table_name = uniqid('tmp_test_');
          $sql = "CREATE TEMPORARY TABLE `$table_name` ("
              ."  `id` int(10) unsigned NOT NULL AUTO_INCREMENT"
              .", `position` int(10) unsigned NOT NULL"
              .", PRIMARY KEY (`id`)"
              .") ENGINE = MEMORY";
          query($sql);
          $i = 0;
          $sql = '';
          foreach ($items as &$item)
          {
              $item->position = $i++;
              $sql .= ($sql ? ', ' : '')."({$item->id}, {$item->position})";
          }
          if ($sql)
          {
              query("INSERT INTO `$table_name` (id, position) VALUES $sql");
              $sql = "UPDATE `test`, `$table_name` SET `test`.position = `$table_name`.position"
                  ." WHERE `$table_name`.id = `test`.id";
              query($sql);
          }
          query("DROP TABLE `$table_name`");
      }
      

      【讨论】:

        【解决方案8】:

        为什么没有人提到一个查询中有多个语句

        在php中,你使用mysqli实例的multi_query方法。

        来自php manual

        MySQL 可选择允许在一个语句字符串中包含多个语句。一次发送多个语句可减少客户端-服务器往返,但需要特殊处理。

        这是更新 30,000 raw 中与其他 3 种方法相比的结果。代码可以在here 找到,它基于@Dakusan 的回答

        交易:5.5194580554962
        插入:0.20669293403625
        案例:16.474853992462
        多:0.0412278175354

        如您所见,多语句查询比最高答案更有效。

        如果您收到这样的错误消息:

        PHP Warning:  Error while sending SET_OPTION packet
        

        你可能需要增加mysql配置文件中的max_allowed_packet,在我的机器上是/etc/mysql/my.cnf,然后重启mysqld。

        【讨论】:

        • 以下所有比较均针对 INSERT 测试运行。我只是在相同条件下运行测试,没有事务,它在 300 行上慢 145x,在 3000 行上慢 753x。我最初是从 30,000 行开始的,但我去给自己做午饭,然后回来,它还在继续。这是有道理的,因为运行单个查询并将每个查询单独刷新到数据库将非常昂贵。尤其是复制。不过,开启交易会有很大的不同。在 3,000 行时,它需要 1.5x 多,在 30,000 行时需要 2.34x。 [继续]
        • 但你说得对,它很快(有交易)。在 3,000 和 30,000 行中,它比 INSERT 方法之外的所有方法都快。运行 1 个查询绝对不可能获得比 30,000 个查询更好的结果,即使它们是在特殊的 MySQL API 调用中批处理的。仅运行 300 行,它比所有其他方法快得多(令我惊讶的是),它遵循与 CASE 方法大致相同的图形曲线。这可以通过两种方式来解释。第一个是由于“ON DUPLICATE KEY [cont],INSERT 方法基本上总是插入 2 行
        • UPDATE" 导致“INSERT”和“UPDATE”。另一个是由于索引查找,一次只编辑 1 行在 SQL 处理器中的工作较少。我不确定您如何获得与我不同的结果,但您的附加测试看起来很可靠。我什至不确定复制将如何处理此调用。这也仅适用于执行 UPDATE 调用。使用单个 INSERT 查询插入调用总是最快的.
        • 我在一个表上一次执行 300 次更新,以修正 for 循环中的错误,耗时 41 秒。将相同的 UPDATE 查询放入一个 $mysqli-&gt;multi_query($sql) 需要“0”秒。但是,随后的查询失败了,导致我将其设为单独的“程序”。
        • 谢谢。能够使用多查询在一分钟内更新大约 5k 行(没有测试更多)。如果有人正在寻找 PDO 解决方案:stackoverflow.com/questions/6346674/…
        【解决方案9】:

        您可以更改一个名为“multi statement”的设置,它禁用 MySQL 的“安全机制”以防止(多个)注入命令。典型的 MySQL 的“出色”实现,它还阻止用户进行有效的查询。

        这里 (http://dev.mysql.com/doc/refman/5.1/en/mysql-set-server-option.html) 是有关设置的 C 实现的一些信息。

        如果你用的是PHP,可以用mysqli做多语句(我觉得php已经自带mysqli一段时间了)

        $con = new mysqli('localhost','user1','password','my_database');
        $query = "Update MyTable SET col1='some value' WHERE id=1 LIMIT 1;";
        $query .= "UPDATE MyTable SET col1='other value' WHERE id=2 LIMIT 1;";
        //etc
        $con->multi_query($query);
        $con->close();
        

        希望对您有所帮助。

        【讨论】:

        • 这与单独发送查询相同。唯一的区别是您将其全部发送到一个网络数据包中,但更新仍将作为单独的查询进行处理。最好将它们包装在一个事务中,然后将更改立即提交到表中。
        • 如何将它们包装在一个事务中?请给我们看看。
        • @TomeeNS 在发送查询之前使用mysqli::begin_transaction(..),之后使用mysql::commit(..)。或者使用START TRANSACTION 作为第一条语句,COMMIT 作为查询本身的最后一条语句。
        【解决方案10】:

        您可以为同一个表设置别名,以便为您提供要插入的 id(如果您正在进行逐行更新:

        UPDATE table1 tab1, table1 tab2 -- alias references the same table
        SET 
        col1 = 1
        ,col2 = 2
        . . . 
        WHERE 
        tab1.id = tab2.id;
        

        此外,很明显您也可以从其他表中进行更新。在这种情况下,更新兼作“SELECT”语句,为您提供您指定的表中的数据。您在查询中明确说明更新值,因此第二个表不受影响。

        【讨论】:

          【解决方案11】:

          您可能还对在更新中使用联接感兴趣,这也是可能的。

          Update someTable Set someValue = 4 From someTable s Inner Join anotherTable a on s.id = a.id Where a.id = 4
          -- Only updates someValue in someTable who has a foreign key on anotherTable with a value of 4.
          

          编辑:如果您要更新的值不是来自数据库中的其他位置,您将需要发出多个更新查询。

          【讨论】:

            【解决方案12】:

            是的..可以使用 INSERT ON DUPLICATE KEY UPDATE sql 语句.. 句法: INSERT INTO table_name (a,b,c) 值 (1,2,3),(4,5,6) 在重复键更新时 a=VALUES(a),b=VALUES(b),c=VALUES(c)

            【讨论】:

              【解决方案13】:

              使用

              REPLACE INTO`table` VALUES (`id`,`col1`,`col2`) VALUES
              (1,6,1),(2,2,3),(3,9,5),(4,16,8);
              

              请注意:

              • id 必须是主唯一键
              • 如果您使用外键 引用表,REPLACE 删除然后插入,所以这可能 导致错误

              【讨论】:

                【解决方案14】:

                现在是简单的方法

                update my_table m, -- let create a temp table with populated values
                    (select 1 as id, 20 as value union -- this part will be generated
                     select 2 as id, 30 as value union -- using a backend code
                     -- for loop 
                     select N as id, X as value
                        ) t
                set m.value = t.value where t.id=m.id -- now update by join - quick
                

                【讨论】:

                  【解决方案15】:

                  我从@newtover 那里得到了答案,并使用 MySql 8 中的新 json_table 函数对其进行了扩展。这允许您创建一个存储过程来处理工作负载,而不是在代码中构建自己的 SQL 文本:

                  drop table if exists `test`;
                  create table `test` (
                    `Id` int,
                    `Number` int,
                    PRIMARY KEY (`Id`)
                  );
                  insert into test (Id, Number) values (1, 1), (2, 2);
                  
                  DROP procedure IF EXISTS `Test`;
                  DELIMITER $$
                  CREATE PROCEDURE `Test`(
                      p_json json
                  )
                  BEGIN
                      update test s
                          join json_table(p_json, '$[*]' columns(`id` int path '$.id', `number` int path '$.number')) v 
                          on s.Id=v.id set s.Number=v.number;
                  END$$
                  DELIMITER ;
                  
                  call `Test`('[{"id": 1, "number": 10}, {"id": 2, "number": 20}]');
                  select * from test;
                  
                  drop table if exists `test`;
                  

                  它比纯 SQL 慢了几毫秒,但我很乐意接受这一点,而不是在代码中生成 sql 文本。不确定它在处理大型记录集时的性能如何(JSON 对象的最大大小为 1Gb),但我在一次更新 10k 行时一直使用它。

                  【讨论】:

                    【解决方案16】:

                    以下将更新一个表中的所有行

                    Update Table Set
                    Column1 = 'New Value'
                    

                    下一个将更新Column2的值大于5的所有行

                    Update Table Set
                    Column1 = 'New Value'
                    Where
                    Column2 > 5
                    

                    Unkwntech的例子都有更新多张表的例子

                    UPDATE table1, table2 SET
                    table1.col1 = 'value',
                    table2.col1 = 'value'
                    WHERE
                    table1.col3 = '567'
                    AND table2.col6='567'
                    

                    【讨论】:

                      【解决方案17】:
                      UPDATE tableName SET col1='000' WHERE id='3' OR id='5'
                      

                      这应该可以实现您想要的。只需添加更多的ID。我已经测试过了。

                      【讨论】:

                        【解决方案18】:
                        UPDATE `your_table` SET 
                        
                        `something` = IF(`id`="1","new_value1",`something`), `smth2` = IF(`id`="1", "nv1",`smth2`),
                        `something` = IF(`id`="2","new_value2",`something`), `smth2` = IF(`id`="2", "nv2",`smth2`),
                        `something` = IF(`id`="4","new_value3",`something`), `smth2` = IF(`id`="4", "nv3",`smth2`),
                        `something` = IF(`id`="6","new_value4",`something`), `smth2` = IF(`id`="6", "nv4",`smth2`),
                        `something` = IF(`id`="3","new_value5",`something`), `smth2` = IF(`id`="3", "nv5",`smth2`),
                        `something` = IF(`id`="5","new_value6",`something`), `smth2` = IF(`id`="5", "nv6",`smth2`) 
                        

                        // 你只是在php中构建它

                        $q = 'UPDATE `your_table` SET ';
                        
                        foreach($data as $dat){
                        
                          $q .= '
                        
                               `something` = IF(`id`="'.$dat->id.'","'.$dat->value.'",`something`), 
                               `smth2` = IF(`id`="'.$dat->id.'", "'.$dat->value2.'",`smth2`),';
                        
                        }
                        
                        $q = substr($q,0,-1);
                        

                        所以你可以用一个查询更新孔表

                        【讨论】:

                        • 我没有投反对票,但我认为反对是在不需要时进行设置(当您将 something 设置为 something 时,您仍在这样做)
                        猜你喜欢
                        • 1970-01-01
                        • 2016-08-31
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-04-12
                        相关资源
                        最近更新 更多