【问题标题】:MySQL insert on duplicate update for non-PRIMARY keyMySQL 在非 PRIMARY 键的重复更新上插入
【发布时间】:2022-03-10 18:57:28
【问题描述】:

我对重复更新查询中的插入有点困惑。 我有这样结构的 MySQL 表:

  • record_id(主要,唯一)
  • person_id(唯一)
  • some_text
  • some_other_text

如果它的 id 存在于我的 table.person 中,我想更新 person 的 some_text 和 some_other_text 值,否则在此表中插入新记录。如果 person_id 不是 PRIMARY 怎么办?

【问题讨论】:

    标签: mysql


    【解决方案1】:

    您需要一个查询来检查是否存在带有您的 record_id(或 person_id)的任何行。如果存在更新它,否则插入新行

    IF EXISTS (SELECT * FROM table.person WHERE record_id='SomeValue')
        UPDATE table.person 
        SET some_text='new_some_text', some_other_text='some_other_text' 
        WHERE record_id='old_record_id'
    ELSE
        INSERT INTO table.person (record_id, person_id, some_text, some_other_text) 
        VALUES ('new_record_id', 'new_person_id', 'new_some_text', 'new_some_other_text')
    

    另一种更好的方法是

    UPDATE table.person SET (...) WHERE person_id='SomeValue'
    IF ROW_COUNT()=0
        INSERT INTO table.person (...) VALUES (...)
    

    【讨论】:

    • @@ROW_COUNT() 抱歉。 @@ROWCOUNT 用于 SQL Server @wchiquito
    • @fattidare 检查是否存在是另一个可行的解决方案 - 没关系,谢谢
    • 事实证明,条件查询只能在存储过程中工作。我找到了一种适用于常规查询的替代方法,并将在此处发布单独的答案。
    • 注意如果您尝试使用相同的值更新帖子,ROW_COUNT() 将返回 0,从而导致重复帖子。
    【解决方案2】:

    您的问题非常有效。这是一个非常普遍的要求。由于 MySQL 提供的功能,大多数人都弄错了。

    • 要求:插入除非PRIMARY键存在,否则更新
    • 常用方法:ON DUPLICATE KEY UPDATE
    • 这种方法的结果令人不安:插入,除非PRIMARY 或任何UNIQUE 键存在,否则更新

    ON DUPLICATE KEY UPDATE 会出现什么严重问题?您插入了一条假定为新记录,具有新的 PRIMARY 键值(例如 UUID),但您的 UNIQUE 键值恰好重复。

    您想要的是一个适当的异常,表明您正在尝试将重复项插入到 UNIQUE 列中。

    但是你得到的是一个不需要的UPDATE! MySQL 将获取冲突记录并开始覆盖其值。如果这种情况无意中发生,则您已经破坏了旧记录,并且对旧记录的任何传入引用现在都在引用新记录。而且由于您可能不会告诉查询更新PRIMARY 列,因此找不到您的新UUID。如果您遇到过这些数据,它可能毫无意义,而且您将不知道它来自哪里。

    我们需要一个解决方案来实际上 插入,除非PRIMARY 键存在,否则更新

    我们将使用由两个语句组成的查询:

    1. 更新PRIMARY 键值匹配的位置(影响 0 或 1 行)。
    2. 如果 PRIMARY 键值不存在则插入(插入 1 或 0 行)。

    这是查询:

    UPDATE my_table SET
    unique_name = 'one', update_datetime = NOW()
    WHERE id = 1;
    
    INSERT INTO my_table
    SELECT 1, 'one', NOW()
    FROM my_table
    WHERE id = 1
    HAVING COUNT(*) = 0;
    

    这些查询中只有一个会生效。 UPDATE 很简单。至于INSERTWHERE id = 1 id 存在则为一行,不存在则无行。 HAVING COUNT(*) = 0 将其反转,如果 id 是新的,则产生一行,如果它已经存在,则没有行。

    我已经探索过相同想法的其他变体,例如LEFT JOINWHERE,但它们看起来都更加复杂。欢迎改进。

    【讨论】:

    • 我在使用 MariaDB 的这种方法插入时遇到了死锁,两个事务竞相锁定。
    • @jarnoan 两个问题:(1)您是作为单个查询还是多个查询提交?我建议将其作为一个传递。 (2) 您是否使用数据库事务?如果是,使用哪种隔离级别?
    • (1) 多个,一个用于更新,另一个用于插入。 (2) 是的,使用具有默认隔离级别的事务(我想这对于 innodb 来说是可重复读取的)。我确实在 update 语句中有另一个 where 子句,这是一个 Galera 集群(但使用相同的服务器实例写入),所以这可能会导致某些事情。
    • @jarnoan Galera 集群:宾果游戏。将其作为单个查询发送应该可以解决您的问题。将其发送到同一节点。如果您真的不能:这是一个需要评论的巨大主题,但这里是。 始终将您的集群设置为具有 1 个写入节点。这可以防止提交失败(这是地狱)。最好还为 all 查询类型(包括选择)启用因果读取 (wsrep_sync_wait)。这样做会启用“因果读取”:如果您先写后读,它会阻止您看到陈旧的数据,并确保您获得预期的结果。 (因为如果您的数据库难以推理,那么您就有麻烦了。)
    【解决方案3】:

    13.2.5.3 INSERT ... ON DUPLICATE KEY UPDATE Syntax

    如果您指定 ON DUPLICATE KEY UPDATE,并插入一行 将导致 UNIQUE 索引或 PRIMARY KEY、MySQL 中的重复值 对旧行执行 UPDATE。

    例子:

    DELIMITER //
    
    DROP PROCEDURE IF EXISTS `sp_upsert`//
    DROP TABLE IF EXISTS `table_test`//
    
    CREATE TABLE `table_test` (
      `record_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
      `person_id` INT UNSIGNED NOT NULL,
      `some_text` VARCHAR(50),
      `some_other_text` VARCHAR(50),
      UNIQUE KEY `record_id_index` (`record_id`),
      UNIQUE KEY `person_id_index` (`person_id`)
    )//
    
    INSERT INTO `table_test`
      (`person_id`, `some_text`, `some_other_text`)
    VALUES
      (1, 'AAA', 'XXX'),
      (2, 'BBB', 'YYY'),
      (3, 'CCC', 'ZZZ')//
    
    CREATE PROCEDURE `sp_upsert`(
      `p_person_id` INT UNSIGNED,
      `p_some_text` VARCHAR(50),
      `p_some_other_text` VARCHAR(50)
    )
    BEGIN
      INSERT INTO `table_test`
        (`person_id`, `some_text`, `some_other_text`)
      VALUES
        (`p_person_id`, `p_some_text`, `p_some_other_text`)
      ON DUPLICATE KEY UPDATE `some_text` = `p_some_text`,
                              `some_other_text` = `p_some_other_text`;
    END//
    
    DELIMITER ;
    
    mysql> CALL `sp_upsert`(1, 'update_text_0', 'update_text_1');
    Query OK, 2 rows affected (0.00 sec)
    
    mysql> SELECT
        ->   `record_id`,
        ->   `person_id`,
        ->   `some_text`,
        ->   `some_other_text`
        -> FROM
        ->   `table_test`;
    +-----------+-----------+---------------+-----------------+
    | record_id | person_id | some_text     | some_other_text |
    +-----------+-----------+---------------+-----------------+
    |         1 |         1 | update_text_0 | update_text_1   |
    |         2 |         2 | BBB           | YYY             |
    |         3 |         3 | CCC           | ZZZ             |
    +-----------+-----------+---------------+-----------------+
    3 rows in set (0.00 sec)
    
    mysql> CALL `sp_upsert`(4, 'new_text_0', 'new_text_1');
    Query OK, 1 row affected (0.00 sec)
    
    mysql> SELECT
        ->   `record_id`,
        ->   `person_id`,
        ->   `some_text`,
        ->   `some_other_text`
        -> FROM
        ->   `table_test`;
    +-----------+-----------+---------------+-----------------+
    | record_id | person_id | some_text     | some_other_text |
    +-----------+-----------+---------------+-----------------+
    |         1 |         1 | update_text_0 | update_text_1   |
    |         2 |         2 | BBB           | YYY             |
    |         3 |         3 | CCC           | ZZZ             |
    |         5 |         4 | new_text_0    | new_text_1      |
    +-----------+-----------+---------------+-----------------+
    4 rows in set (0.00 sec)
    

    SQL Fiddle demo

    【讨论】:

    • 我认为这应该是正确的答案。它更高效,并且确实符合 MySQL 认可的方式。
    • 这是一个非常好的答案,但仅适用于存在唯一索引的情况,这并不总是可取的。
    【解决方案4】:

    我的方法怎么样?

    假设您有一个具有自动增量 ID 和三个文本列的表。您想插入/更新 column3 的值,其中 column1 和 column2 中的值是(非唯一)键。

    我使用这个查询(没有显式锁定表):

    insert into myTable (id, col1, col2, col3)
    select tmp.id, 'col1data', 'col2data', 'col3data' from
    (select id from myTable where col1 = 'col1data' and col2 = 'col2data' union select null as id limit 1) tmp
    on duplicate key update col3 = values(col3)
    

    这有什么问题吗?对我来说,它按我想要的方式工作。

    【讨论】:

      【解决方案5】:

      灵活的解决方案应保留INSERT ... ON DUPLICATE KEY UPDATE 提供的原子性,并且不管它是否为autocommit=true 都可以工作,并且不依赖于隔离级别为REPEATABLE READ 或更高级别的事务。

      跨多个语句执行 check-then-act 的任何解决方案都不能满足这一点。

      以下是选项:

      如果插入比更新多:

      INSERT INTO table (record_id, ..., some_text, some_other_text) VALUES (...);
      
      IF <duplicate entry for primary key error>
        UPDATE table SET some_text = ..., some_other_text = ... WHERE record_id = ...;
      
        IF affected-rows = 0
          -- retry from INSERT OR ignore this conflict and defer to the other session
      

      如果更新多于插入:

      UPDATE table SET some_text = ..., some_other_text = ... WHERE record_id = ...;
      
      IF affected-rows = 0
        INSERT INTO table (record_id, ..., some_text, some_other_text) VALUES (...);
      
        IF <duplicate entry for primary key error>
          -- retry from UPDATE OR ignore this conflict and defer to the other session
      

      如果你不介意有点丑,你实际上可以使用INSERT ... ON DUPLICATE KEY UPDATE,并在一个语句中做到这一点:

      INSERT INTO table (record_id, ..., some_text, some_other_text) VALUES (...)
          ON DUPLICATE KEY UPDATE
            some_text = if(record_id = VALUES(record_id), VALUES(some_text), some_text),
            some_other_text = if(record_id = VALUES(record_id), VALUES(some_other_text), some_other_text)
      
      IF affected-rows = 0
        -- handle this as a unique check constraint violation
      

      注意:这些示例中的affected-rows 表示受影响 行,而不是找到 行。两者可能会混淆,因为a single parameter switches which of these values the client is returned

      另请注意,如果在您执行更新时 some_textsome_other_text 没有实际修改(并且记录没有以其他方式更改),则对 affected-rows = 0 的这些检查将失败。

      【讨论】:

        【解决方案6】:

        我看到这篇文章是因为我需要标题中写的内容,并且我找到了一个非常方便的解决方案,但是这里没有人提到它,所以我想把它粘贴在这里。请注意,如果您要启动数据库表,此解决方案非常方便。在这种情况下,当您创建相应的表时,像往常一样定义主键等,对于您想要唯一的列组合,只需添加

        UNIQUE(column_name1,column_name2,...)
        

        CREATE TABLE 语句的末尾,对于您希望唯一的指定列的任意组合。像这样,根据this page here,“MySQL使用列column_name1和column_name2中的值组合来评估唯一性”,如果您尝试进行插入已经具有column_name1和column_name2的值组合,则会报告错误您在插入中提供。将这种创建数据库表的方式与相应的INSERT ON DUPLICATE KEY 语法相结合似乎是最适合我的解决方案。在你真正开始使用你的桌子之前,只需要仔细考虑一下;设置数据库表时。

        【讨论】:

          【解决方案7】:

          对于像我这样的 DB 菜鸟的其他任何人....上述事情对我不起作用。我有一个主键和一个唯一键......如果唯一键不存在,我想插入。经过大量 Stack Overflow 和 Google 搜索后,我发现这方面的结果并不多……但我确实找到了一个给我一个有效答案的网站:https://thispointer.com/insert-record-if-not-exists-in-mysql/

          为了便于阅读,我在该网站上的回答是:

          INSERT INTO table (unique_key_column_name)
          SELECT * FROM (SELECT 'unique_value' AS unique_key_column_name) AS temp
          WHERE NOT EXISTS ( 
               SELECT unique_key_column_name FROM table
               WHERE unique_key_column_name = 'unique_value'
          ) LIMIT 1;
          
          

          还请注意 ' 标记为我环绕,因为在这种情况下我使用字符串。

          【讨论】:

            猜你喜欢
            • 2017-01-31
            • 1970-01-01
            • 2023-03-19
            • 1970-01-01
            • 1970-01-01
            • 2012-11-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多