Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-36832

Unexpected gap locks with UPDATE with a subquery on PK

    XMLWordPrintable

Details

    Description

      Given the following database structure:

      CREATE DATABASE `test`;
      USE `test`;
      CREATE TABLE `booking` (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB AUTO_INCREMENT=1;
       
      CREATE TABLE `ticket` (
        `ticket_id` bigint(20) NOT NULL AUTO_INCREMENT,
        `is_valid` tinyint(1) NOT NULL DEFAULT 1,
        `booking_id` int(11) DEFAULT NULL,
        PRIMARY KEY (`ticket_id`),
        KEY `fk_ticket_2_booking` (`booking_id`),
        CONSTRAINT `fk_ticket_2_booking` FOREIGN KEY (`booking_id`) REFERENCES `booking` (`id`)
      ) ENGINE=InnoDB AUTO_INCREMENT=1;
       
      INSERT INTO booking VALUES();
      INSERT INTO booking VALUES();
       
      /* Without these two the lock is "X, supremum presudo-record", without -  it's "X,GAP" */
      INSERT INTO ticket (booking_id) VALUE(1);
      INSERT INTO ticket (booking_id) VALUE(2);
      

      Now, the expected locks occur in the following situation (all connections here and below are SET autocommit=0):

      Conn 1: `UPDATE ticket SET is_valid = 0 WHERE booking_id = 1;`
      Conn 2: `UPDATE ticket SET is_valid = 0 WHERE booking_id = 2;`
      Conn 1: `INSERT INTO ticket (booking_id) VALUE(1);`

      Conn 1 is now blocked by the following lock:

                       lock_mode: X
                       lock_type: RECORD
                      lock_table: `test`.`ticket`
                      lock_index: fk_ticket_2_booking
                      lock_space: 9
                       lock_page: 4
                        lock_rec: 1
                       lock_data: supremum pseudo-record
      

      If Conn 2 attempts to do `INSERT INTO ticket (booking_id) VALUE(2);` deadlock occurs.

      Full locks for both TXs.

      > SELECT t.*, l.* FROM INNODB_TRX t LEFT OUTER JOIN INNODB_LOCKS l ON t.trx_id = l.lock_trx_id \G
       
      1. row
       
                          trx_id: 29
                       trx_state: LOCK WAIT
                     trx_started: 2025-05-16 19:59:37
           trx_requested_lock_id: 29:9:4:1
                trx_wait_started: 2025-05-16 20:07:20
                      trx_weight: 9
             trx_mysql_thread_id: 3
                       trx_query: INSERT INTO ticket (booking_id) VALUES(1)
             trx_operation_state: inserting
               trx_tables_in_use: 1
               trx_tables_locked: 2
                trx_lock_structs: 7
           trx_lock_memory_bytes: 1128
                 trx_rows_locked: 5
               trx_rows_modified: 2
         trx_concurrency_tickets: 0
             trx_isolation_level: REPEATABLE READ
               trx_unique_checks: 1
          trx_foreign_key_checks: 1
      trx_last_foreign_key_error: NULL
                trx_is_read_only: 0
      trx_autocommit_non_locking: 0
                         lock_id: 29:9:4:1
                     lock_trx_id: 29
                       lock_mode: X
                       lock_type: RECORD
                      lock_table: `test`.`ticket`
                      lock_index: fk_ticket_2_booking
                      lock_space: 9
                       lock_page: 4
                        lock_rec: 1
                       lock_data: supremum pseudo-record
       
       2. row 
       
                          trx_id: 34
                       trx_state: RUNNING
                     trx_started: 2025-05-16 20:04:19
           trx_requested_lock_id: NULL
                trx_wait_started: NULL
                      trx_weight: 2
             trx_mysql_thread_id: 4
                       trx_query: NULL
             trx_operation_state: 
               trx_tables_in_use: 0
               trx_tables_locked: 1
                trx_lock_structs: 2
           trx_lock_memory_bytes: 1128
                 trx_rows_locked: 1
               trx_rows_modified: 0
         trx_concurrency_tickets: 0
             trx_isolation_level: REPEATABLE READ
               trx_unique_checks: 1
          trx_foreign_key_checks: 1
      trx_last_foreign_key_error: NULL
                trx_is_read_only: 0
      trx_autocommit_non_locking: 0
                         lock_id: NULL
                     lock_trx_id: NULL
                       lock_mode: NULL
                       lock_type: NULL
                      lock_table: NULL
                      lock_index: NULL
                      lock_space: NULL
                       lock_page: NULL
                        lock_rec: NULL
                       lock_data: NULL
      2 rows in set (0.001 sec)
      

      So far so good.

      If we were to add a few tickets, let's say Ticket 1/Booking 1 and Ticket 2/Booking 2 to the ticket table and then ran:

      Conn 1: `UPDATE ticket SET is_valid = 0 WHERE ticket_id IN (1);`
      Conn 2: `UPDATE ticket SET is_valid = 0 WHERE ticket_id IN (2);`

      There would be NO X gap lock on `fk_ticket_2_booking`. Also an expected result.

      Here's where the bug seems to happen:

      What about the following?

      `UPDATE ticket SET is_valid WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id = 1);`

      This unexpectedly behaves lock-wise exactly like `UPDATE ticket SET is_valid=0 WHERE booking_id = 1;`.

      In my opinion it should not.

      The `ticket_id` IN-operation in the UPDATE should rightfully take the PK row locks but not the booking_id FK X gap locks, as the booking_id is not actually compared anywhere in the UPDATE query, only in the SELECT subquery.

      Looking at the analysis both `UPDATE ... WHERE booking_id = X` AND `UPDATE ... WHERE ticket_id IN (<subq>)` produce distinctly different results:

      > ANALYZE FORMAT=JSON UPDATE ticket SET is_valid = 0 WHERE booking_id = 1 \G
       
       1. row 
       
      ANALYZE: {
        "query_optimization": {
          "r_total_time_ms": 0.072844233
        },
        "query_block": {
          "select_id": 1,
          "r_total_time_ms": 0.040248096,
          "table": {
            "update": 1,
            "table_name": "ticket",
            "access_type": "range",
            "possible_keys": ["fk_ticket_2_booking"],
            "key": "fk_ticket_2_booking",
            "key_length": "5",
            "used_key_parts": ["booking_id"],
            "rows": 1,
            "r_rows": 1,
            "r_total_filtered": 100,
            "r_total_time_ms": 0.027836258,
            "r_engine_stats": {
              "pages_accessed": 4,
              "old_rows_read": 1
            },
            "attached_condition": "ticket.booking_id = 1",
            "r_filtered": 100
          }
        }
      }
      1 row in set (0.001 sec)
      

      vs

      > ANALYZE FORMAT=JSON UPDATE ticket SET is_valid = 0 WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id = 1) \G
       
       1. row 
      ANALYZE: {
        "query_optimization": {
          "r_total_time_ms": 0.148751258
        },
        "query_block": {
          "select_id": 1,
          "cost": 0.003506245,
          "r_loops": 1,
          "r_total_time_ms": 0.045560282,
          "nested_loop": [
            {
              "table": {
                "table_name": "ticket",
                "access_type": "ref",
                "possible_keys": ["PRIMARY", "fk_ticket_2_booking"],
                "key": "fk_ticket_2_booking",
                "key_length": "5",
                "used_key_parts": ["booking_id"],
                "ref": ["const"],
                "loops": 1,
                "r_loops": 1,
                "rows": 1,
                "r_rows": 1,
                "cost": 0.001792605,
                "r_table_time_ms": 0.015093036,
                "r_other_time_ms": 0.004980802,
                "r_engine_stats": {
                  "pages_accessed": 3,
                  "old_rows_read": 1
                },
                "filtered": 100,
                "r_total_filtered": 100,
                "r_filtered": 100,
                "using_index": true
              }
            },
            {
              "table": {
                "table_name": "ticket",
                "access_type": "eq_ref",
                "possible_keys": ["PRIMARY"],
                "key": "PRIMARY",
                "key_length": "8",
                "used_key_parts": ["ticket_id"],
                "ref": ["test.ticket.ticket_id"],
                "loops": 1,
                "r_loops": 1,
                "r_table_loops": 2,
                "rows": 1,
                "r_rows": 1,
                "cost": 0.00171364,
                "r_table_time_ms": 0.002460275,
                "r_other_time_ms": 0.018848721,
                "r_engine_stats": {
                  "pages_accessed": 2
                },
                "filtered": 100,
                "r_total_filtered": 100,
                "r_filtered": 100
              }
            }
          ]
        }
      }
      1 row in set (0.001 sec)
      

      You can clearly see that the optimizer does not eliminate the SELECT subquery by folding it into the UPDATE query WHERE clause itself. Yet the gap lock is somehow acquired as if the UPDATE was filtering on the FK index itself.

      ====
      To summarize:

      I believe that an update filtering on a primary key using a subquery SELECT on a foreign key should not behave any differently with respect to an exclusive gap lock on that foreign key index from an UPDATE on the primary key where the list of the primary keys was provided directly rather than via a subquery.

      After all, if within the same transaction I was to do via the client:

      ids := SELECT ticket_id FROM ticket WHERE booking_id = 1;
      UPDATE ticket SET is_valid = 0  WHERE ticket_id IN (<ids>);
      

      then no x gap lock on the FK would be acquired. The subquery embedded in the UPDATE shouldn't produce one either.

      Attachments

        Activity

          People

            vlad.lesin Vladislav Lesin
            arcivanov Arcadiy Ivanov
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

            Dates

              Created:
              Updated:

              Git Integration

                Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.