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

Wrong result (missing rows) with LEFT JOIN and two semi-joins when DuplicateWeedout uses a join buffer (11.0+)

    XMLWordPrintable

Details

    • Bug
    • Status: Confirmed (View Workflow)
    • Major
    • Resolution: Unresolved
    • 11.4, 11.8, 12.3, 13.1, 11.4.13, 11.8.9, 12.3.3, 13.0.2, 13.1.1
    • 11.4, 11.8, 12.3, 13.1
    • Optimizer
    • Official mariadb Docker images (Ubuntu) and Homebrew 12.3.2 on macOS (arm64); default configuration, InnoDB
    • Unexpected results

    Description

      A LEFT JOIN with two IN (subquery) conditions loses rows on 11.0 and later. 10.11 is correct. In the wrong plan, the second semi-join table is read through a BNL join buffer inside a DuplicateWeedout range, after the LEFT JOINed table.

      Test case

      CREATE TABLE t1 (a INT PRIMARY KEY, b INT NOT NULL) ENGINE=InnoDB;
      CREATE TABLE t2 (pk INT PRIMARY KEY, a INT NOT NULL, c INT NOT NULL, KEY (a, c)) ENGINE=InnoDB;
      CREATE TABLE t3 (pk INT PRIMARY KEY, d INT NOT NULL, b INT, KEY (b), KEY (d)) ENGINE=InnoDB;
       
      INSERT INTO t1 VALUES (1, 1);
      INSERT INTO t2 VALUES (1, 1, 1), (2, 1, 2);
      INSERT INTO t3 VALUES (1, 2, 2), (2, 1, 1);
       
      SELECT t1.a, t2.pk
      FROM t1 LEFT JOIN t2 ON t2.a = t1.a
      WHERE t1.a = 1
        AND t1.b IN (SELECT t3.b FROM t3 WHERE t3.d = 1)
        AND t1.a IN (SELECT x.a FROM t2 x WHERE x.c IN (1, 2, 3, 4));
      

      Both IN conditions are true for t1.a = 1, so the expected result is both t2 rows.

      10.11.18: correct

      +---+------+
      | a | pk   |
      +---+------+
      | 1 |    1 |
      | 1 |    2 |
      +---+------+
       
      +------+--------------+-------------+------------+---------------+--------------+---------+-------+---------+---------------------------------+
      | id   | select_type  | table       | type       | possible_keys | key          | key_len | ref   | rows    | Extra                           |
      +------+--------------+-------------+------------+---------------+--------------+---------+-------+---------+---------------------------------+
      |    1 | PRIMARY      | t1          | const      | PRIMARY       | PRIMARY      | 4       | const | 1       |                                 |
      |    1 | PRIMARY      | <subquery2> | eq_ref     | distinct_key  | distinct_key | 4       | func  | 1       |                                 |
      |    1 | PRIMARY      | <subquery3> | eq_ref     | distinct_key  | distinct_key | 4       | func  | 1       |                                 |
      |    1 | PRIMARY      | t2          | ref        | a             | a            | 4       | const | 2       | Using index                     |
      |    2 | MATERIALIZED | t3          | ref|filter | b,d           | b|d          | 5|4     | const | 1 (50%) | Using where; Using rowid filter |
      |    3 | MATERIALIZED | x           | range      | a             | a            | 8       | NULL  | 2       | Using where; Using index        |
      +------+--------------+-------------+------------+---------------+--------------+---------+-------+---------+---------------------------------+
      

      12.3.2 and 12.3.3: wrong

      +---+------+
      | a | pk   |
      +---+------+
      | 1 |    1 |
      +---+------+
       
      +------+-------------+-------+-------------+---------------+---------+---------+-------+------+----------------------------------------------------------------------------------------------+
      | id   | select_type | table | type        | possible_keys | key     | key_len | ref   | rows | Extra                                                                                        |
      +------+-------------+-------+-------------+---------------+---------+---------+-------+------+----------------------------------------------------------------------------------------------+
      |    1 | PRIMARY     | t1    | const       | PRIMARY       | PRIMARY | 4       | const | 1    |                                                                                              |
      |    1 | PRIMARY     | t3    | index_merge | b,d           | d,b     | 4,5     | NULL  | 1    | Using intersect(d,b); Using where; Using index; Start temporary; End temporary               |
      |    1 | PRIMARY     | t2    | ref         | a             | a       | 4       | const | 2    | Using index                                                                                  |
      |    1 | PRIMARY     | x     | range       | a             | a       | 8       | NULL  | 2    | Using where; Using index; Start temporary; End temporary; Using join buffer (flat, BNL join) |
      +------+-------------+-------+-------------+---------------+---------+---------+-------+------+----------------------------------------------------------------------------------------------+
      

      The row with t2.pk = 2 is lost; COUNT(*) also returns 1.

      What changes the result (12.3.2)

      • Correct, same plan with no join buffer on x: semijoin_with_cache=off, or join_cache_level=0.
      • Correct, different plan: semijoin=off, or duplicateweedout=off (both subqueries materialized).
      • Still wrong: firstmatch=off, loosescan=off, materialization=off, outer_join_with_cache=off, index_merge_intersection=off, rowid_filter=off; join_cache_level=1; ANALYZE TABLE ... PERSISTENT FOR ALL.
      • Correct: INNER JOIN (plan becomes FirstMatch), removing either IN, removing the x.c filter, or KEY (a) instead of KEY (a, c) on t2.

      The =off items are optimizer_switch flags.

      10.11 with DuplicateWeedout

      With optimizer_switch='materialization=off', 10.11.18 also uses DuplicateWeedout with a BNL buffer on x and returns both rows, but puts x before t2:

      +------+-------------+-------+------------+---------------+---------+---------+-------+---------+----------------------------------------------------------------------------------------------+
      | id   | select_type | table | type       | possible_keys | key     | key_len | ref   | rows    | Extra                                                                                        |
      +------+-------------+-------+------------+---------------+---------+---------+-------+---------+----------------------------------------------------------------------------------------------+
      |    1 | PRIMARY     | t1    | const      | PRIMARY       | PRIMARY | 4       | const | 1       |                                                                                              |
      |    1 | PRIMARY     | t3    | ref|filter | b,d           | b|d     | 5|4     | const | 1 (50%) | Using where; Start temporary; End temporary; Using rowid filter                              |
      |    1 | PRIMARY     | x     | range      | a             | a       | 8       | NULL  | 2       | Using where; Using index; Start temporary; End temporary; Using join buffer (flat, BNL join) |
      |    1 | PRIMARY     | t2    | ref        | a             | a       | 4       | const | 2       | Using index                                                                                  |
      +------+-------------+-------+------------+---------------+---------+---------+-------+---------+----------------------------------------------------------------------------------------------+
      

      On a larger private query of the same shape, 10.11.18 and 12.3.2 chose the same plan (const tables, an SJ-Materialization eq_ref lookup, the LEFT JOINed tables, then a DuplicateWeedout range with a BNL-joined semi-join table). 10.11.18 returned the right rows; 12.3.2 lost rows.

      Versions tested

      Official Docker images and quay.io/mariadb-foundation/mariadb-devel nightlies (built 2026-10-24 to 2026-10-28):

      • Correct: 10.11.18, 10.11.19, 10.11.20 nightly
      • Wrong (join order t1, t3, t2, x in every case): 11.0.6, 11.4.13, 11.8.9, 11.8.10 nightly (includes the MDEV-41146 fix), 12.0.2, 12.1.2, 12.2.2, 12.3.2, 12.3.3, 13.0.2, 13.1.1-rc, 13.2.0 nightly
      • MySQL 8.0.46, 8.4.13 and 9.7.4 return both rows.

      Related issues

      • MDEV-29382 (Stalled; fix unmerged on bb-10.9-mdev29382): also DuplicateWeedout + join buffer, but triggered by an SJ-Materialization table in the prefix, and returns extra rows. Its test case passes on 10.11.18 and 12.3.2.
      • MDEV-41146 (fixed in 12.3.4): extra row, needs rand() and non-default cost settings; unrelated cause.
      • MDEV-5161, MDEV-6706 (missing rows, semi-join + LEFT JOIN + ORDER BY, fixed in 5.5/10.0): their test cases pass on 12.3.2.

      Attachments

        Issue Links

          Activity

            People

              psergei Sergei Petrunia
              shrinath Shrinath
              Votes:
              0 Vote for this issue
              Watchers:
              2 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.