[MDEV-25714] Join using derived with aggregation returns incorrect results Created: 2021-05-18  Updated: 2022-04-08  Resolved: 2021-06-08

Status: Closed
Project: MariaDB Server
Component/s: Data Manipulation - Subquery
Affects Version/s: 10.5.10, 10.3, 10.4, 10.5
Fix Version/s: 10.3.30, 10.4.20, 10.5.11

Type: Bug Priority: Blocker
Reporter: Marina Glancy Assignee: Igor Babaev
Resolution: Fixed Votes: 1
Labels: regression

Attachments: File demo_sql_error_simplified.sql    
Issue Links:
Blocks
Duplicate
is duplicated by MDEV-25841 group by in subquery returns wrong re... Closed
is duplicated by MDEV-25843 Wrong Query Result with LATERAL DERIV... Closed
Problem/Incident
is caused by MDEV-25128 Wrong result from join with material... Closed
Relates
relates to MDEV-27132 Wrong result from query when using sp... Closed
relates to MDEV-27694 regression? Join using derived with a... Closed
relates to MDEV-25725 Suddenly Queryplan skip LEFT JOINS an... Closed

 Description   

One of the unittests in Moodle LMS started failing on MariaDB since testing docker image was upgraded to version 10.5.10.

The same unittest and the same query was passing on MariaDB 10.5.9. Also it passes on MySQL, Postgres, MsSQL and Oracle (all databases supported by Moodle).

I have created an SQL file to demonstrate the problem. It creates two database tables, fills them with the data and performs a query:

SELECT h.id, gi.itemtype, gi.itemmodule, h.userid, h.rawgrade
FROM grade_grades_history h
         JOIN (SELECT itemid, MAX(id) AS id
               FROM grade_grades_history
               WHERE userid = 131000
               GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
         JOIN grade_items gi ON gi.id = h.itemid
WHERE gi.courseid = 128000;

This query is slightly simplified from what we actually use in Moodle in order to demonstrate the problem.

On MariaDB 10.5.9 and all other databases it returns:

id	itemtype	itemmodule	userid	rawgrade
330004	course	NULL	131000	NULL
330003	mod	assign	131000	50.00000

On MariaDB 10.5.10 it returns:

id	itemtype	itemmodule	userid	rawgrade
330004	course	NULL	131000	NULL

To make it even more interesting, the following query (using "LEFT JOIN") returns correct results. This is even more confusing because in the return values you can see that data in the grade_items table is present and it is actually an inner join.

SELECT h.id, gi.itemtype, gi.itemmodule, h.userid, h.rawgrade
FROM grade_grades_history h
         JOIN (SELECT itemid, MAX(id) AS id
               FROM grade_grades_history
               WHERE userid = 131000
               GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
         LEFT JOIN grade_items gi ON gi.id = h.itemid

Attaching the test file demo_sql_error_simplified.sql



 Comments   
Comment by Marina Glancy [ 2021-05-18 ]

Latest docker image mariadb:10 (10.5.10)

$ docker run --name mariadbtest -e MYSQL_ROOT_PASSWORD=mypass -p 3307:3306 -d docker.io/library/mariadb:10
$ docker start mariadbtest
$ mysql -h 172.17.0.2 -P 3306 -u root --password=mypass mysql < ~/Downloads/demo_sql_error_simplified.sql 
mysql: [Warning] Using a password on the command line interface can be insecure.
id	itemtype	itemmodule	userid	rawgrade
330004	course	NULL	131000	NULL
$ docker stop mariadbtest

Using previous version (10.5.9):

$ docker run --name mariadbtestprev -e MYSQL_ROOT_PASSWORD=mypass -p 3307:3306 -d docker.io/library/mariadb:10.5.9
$ docker start mariadbtestprev
$ mysql -h 172.17.0.2 -P 3306 -u root --password=mypass mysql < ~/Downloads/demo_sql_error_simplified.sql 
mysql: [Warning] Using a password on the command line interface can be insecure.
id	itemtype	itemmodule	userid	rawgrade
330004	course	NULL	131000	NULL
330003	mod	assign	131000	50.00000
$ docker stop mariadbtestprev

Comment by Alice Sherepa [ 2021-05-19 ]

Thank you!
I reproduced on 10.5.10, 10.5 7b51d11cca8898f319ddd, correct results on 10.2-10.4.

-- source include/have_innodb.inc
set global innodb_stats_persistent= 1;
 
CREATE TABLE t1 ( 
	id int NOT NULL PRIMARY KEY, 
	itemid int NOT NULL, 
	userid int NOT NULL,
 	KEY (userid,itemid), 
 	KEY (itemid)) engine=innodb;
INSERT INTO t1  VALUES (0,6,1), (1,5,1), (2,5,1), (3,6,1), (4,5,1);
 
CREATE TABLE t2 (id int NOT NULL) engine=innodb;
INSERT INTO t2 VALUES (5),(6);
 
SELECT 1 a FROM t1 
JOIN (SELECT itemid, max(id) AS id FROM t1 WHERE userid = 1 GROUP BY itemid) dt 
	ON t1.id = dt.id AND t1.itemid = dt.itemid
JOIN t2 ON t2.id = t1.itemid;
 
drop table t1,t2;

10.5.10

MariaDB [test]> SELECT 1 a FROM t1 
    -> JOIN (SELECT itemid, max(id) AS id FROM t1 WHERE userid = 1 GROUP BY itemid) dt 
    -> ON t1.id = dt.id AND t1.itemid = dt.itemid
    -> JOIN t2 ON t2.id = t1.itemid;
+---+
| a |
+---+
| 1 |
+---+
1 row in set (0.001 sec)

10.5.9

MariaDB [test]> SELECT 1 a FROM t1 
    -> JOIN (SELECT itemid, max(id) AS id FROM t1 WHERE userid = 1 GROUP BY itemid) dt 
    -> ON t1.id = dt.id AND t1.itemid = dt.itemid
    -> JOIN t2 ON t2.id = t1.itemid;
+---+
| a |
+---+
| 1 |
| 1 |
+---+
2 rows in set (0.002 sec)

git bisect leads to this merge https://github.com/MariaDB/server/commit/80459bcbd4

Comment by Andrew Lyons [ 2021-05-19 ]

For reference, if I remove the t_gradgradhist_useitetim_ix index from the grade_grades_history table I get the correct result too.

Comment by Sergei Petrunia [ 2021-05-19 ]

Explain on current 10.5 (with the wrong result):

+------+-----------------+------------+------+----------------+--------+---------+-----------------------+------+-------------+
| id   | select_type     | table      | type | possible_keys  | key    | key_len | ref                   | rows | Extra       |
+------+-----------------+------------+------+----------------+--------+---------+-----------------------+------+-------------+
|    1 | PRIMARY         | t2         | ALL  | NULL           | NULL   | NULL    | NULL                  | 2    |             |
|    1 | PRIMARY         | t1         | ref  | PRIMARY,itemid | itemid | 4       | test.t2.id            | 1    | Using index |
|    1 | PRIMARY         | <derived2> | ref  | key0           | key0   | 9       | test.t2.id,test.t1.id | 2    |             |
|    2 | LATERAL DERIVED | t1         | ref  | userid,itemid  | userid | 4       | const                 | 5    | Using index |
+------+-----------------+------------+------+----------------+--------+---------+-----------------------+------+-------------+

It is not clear to me what is the benefit of the LATERAL DERIVED optimization here. The table access inside the subquery is ref(const). Lateral execution might allow the subquery to perform filtering on itemid internally but I don't see that done in the FORMAT=JSON output: https://gist.github.com/spetrunia/611eca348376f43df0154c1078e34b9c

Comment by Sergei Petrunia [ 2021-05-19 ]

Explain on 10.5.9 (with the correct query result) is the same: (EDIT: NOT the same, note the extra Using Where )

+------+-----------------+------------+------+----------------+--------+---------+-----------------------+------+--------------------------+
| id   | select_type     | table      | type | possible_keys  | key    | key_len | ref                   | rows | Extra                    |
+------+-----------------+------------+------+----------------+--------+---------+-----------------------+------+--------------------------+
|    1 | PRIMARY         | t2         | ALL  | NULL           | NULL   | NULL    | NULL                  | 2    |                          |
|    1 | PRIMARY         | t1         | ref  | PRIMARY,itemid | itemid | 4       | test.t2.id            | 1    | Using index              |
|    1 | PRIMARY         | <derived2> | ref  | key0           | key0   | 9       | test.t2.id,test.t1.id | 2    |                          |
|    2 | LATERAL DERIVED | t1         | ref  | userid,itemid  | userid | 4       | const                 | 5    | Using where; Using index |
+------+-----------------+------------+------+----------------+--------+---------+-----------------------+------+--------------------------+

Comment by Sergei Petrunia [ 2021-05-19 ]

Diff'ing EXPLAIN FORMAT=JSON output:

    "table": {
      "table_name": "<derived2>",
      "access_type": "ref",
      "possible_keys": ["key0"],
      "key": "key0",
      "key_length": "9",
      "used_key_parts": ["itemid", "id"],
      "ref": ["test.t2.id", "test.t1.id"],
      "rows": 2,
      "filtered": 100,
      "materialized": {
        "lateral": 1,
        "query_block": {
          "select_id": 2,
          "table": {
            "table_name": "t1",
            "access_type": "ref",
            "possible_keys": ["userid", "itemid"],
            "key": "userid",
            "key_length": "4",
            "used_key_parts": ["userid"],
            "ref": ["const"],
            "rows": 5,
            "filtered": 100,
            "attached_condition": "t1.itemid = t2.`id` and t1.itemid = t1.itemid",
# ^^  this line is present in 10.5.9 and gone in 10.5.10 ^^^
            "using_index": true
          }
        }
      }
    }
  }

Comment by Sergei Petrunia [ 2021-05-19 ]

diff'ing the traces:

psergey@blackbox:~$ diff -u10 /tmp/trace-10.5.9.txt /tmp/trace-10.5.10.txt 
--- /tmp/trace-10.5.9.txt       2021-05-19 19:13:31.913593286 +0300
+++ /tmp/trace-10.5.10.txt      2021-05-19 19:12:45.152411986 +0300
@@ -590,21 +590,21 @@                                                                                                                                       
           {                                                                                                                                                
             "best_join_order": ["t1"]                                                                                                                      
           },                                                                                                                                               
           {                                                                                                                                                
             "attaching_conditions_to_tables": {                                                                                                            
               "original_condition": "t1.userid = 1 and t1.itemid = t2.`id` and t1.itemid = t1.itemid",                                                     
               "attached_conditions_computation": [],                                                                                                       
               "attached_conditions_summary": [                                                                                                             
                 {
                   "table": "t1",
-                  "attached": "t1.itemid = t2.`id` and t1.itemid = t1.itemid"
+                  "attached": null
                 }
               ]
             }
           },

Comment by Sergei Petrunia [ 2021-05-19 ]

Ok the difference between traces shown above comes from this patch:

commit 480a06718d137c9ee7784012ccb609b9e79ff08c
Author: Igor Babaev <igor@askmonty.org>
Date:   Tue Mar 23 20:54:54 2021 -0700
 
    MDEV-25128 Wrong result from join with materialized semi-join and
               splittable derived
    

Comment by Sergei Petrunia [ 2021-05-19 ]

... and the wrong result in Alice's testcase goes away if I revert that patch. igor, can you take a look at this please?

Comment by Igor Babaev [ 2021-05-19 ]

Checking for the current 10.3:
See that split optimization is not used in the execution plan and the result is correct:

MariaDB [test]> EXPLAIN SELECT 1 a FROM t1  JOIN (SELECT itemid, max(id) AS id FROM t1 WHERE userid = 1 GROUP BY itemid) dt  ON t1.id = dt.id AND t1.itemid = dt.itemid JOIN t2 ON t2.id = t1.itemid;
+------+-------------+------------+------+----------------+--------+---------+-----------------------+------+--------------------------+
| id   | select_type | table      | type | possible_keys  | key    | key_len | ref                   | rows | Extra                    |
+------+-------------+------------+------+----------------+--------+---------+-----------------------+------+--------------------------+
|    1 | PRIMARY     | t2         | ALL  | NULL           | NULL   | NULL    | NULL                  |    2 |                          |
|    1 | PRIMARY     | t1         | ref  | PRIMARY,itemid | itemid | 4       | test.t2.id            |    1 | Using index              |
|    1 | PRIMARY     | <derived2> | ref  | key0           | key0   | 9       | test.t2.id,test.t1.id |    2 |                          |
|    2 | DERIVED     | t1         | ref  | userid,itemid  | userid | 4       | const                 |    5 | Using where; Using index |
+------+-------------+------------+------+----------------+--------+---------+-----------------------+------+--------------------------+
 
MariaDB [test]> SELECT 1 a FROM t1 
    -> JOIN (SELECT itemid, max(id) AS id FROM t1 WHERE userid = 1 GROUP BY itemid) dt 
    -> ON t1.id = dt.id AND t1.itemid = dt.itemid
    -> JOIN t2 ON t2.id = t1.itemid;
+---+
| a |
+---+
| 1 |
| 1 |
+---+

Comment by Igor Babaev [ 2021-05-22 ]

The following test case demonstrates the problem in 10.3

create table t1 (id int not null, itemid int not null, index idx (itemid) ) engine=innodb;
insert into t1 values (1, 2), (2,2), (4,2), (4,2), (0,3), (3,3);
create table t2 (id int not null) engine=innodb;
insert into t2 values (2);
create table t3 (
  id int not null, itemid int not null, userid int not null, primary key (id),  
  index idx1 (userid, itemid), index idx2 (itemid)
) engine innodb;
insert into t3 values (1,1,1), (2,1,1), (3,2,1), (4,2,1), (5,3,1);
analyze table t1,t2,t3;
set optimizer_switch='split_materialized=on';
explain 
select t1.id, t1.itemid, dt.id, t2.id 
   from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
     where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;
select t1.id, t1.itemid, dt.id, t2.id 
   from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
     where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;
set optimizer_switch='split_materialized=off';
explain 
select t1.id, t1.itemid, dt.id, t2.id 
   from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
     where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;
select t1.id, t1.itemid, dt.id, t2.id 
   from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
     where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;

For this test case we have either an empty result set with optimizer_switch='split_materialized=on' and non-empty result set with optimizer_switch='split_materialized=off'.

MariaDB [test]> set optimizer_switch='split_materialized=on';
Query OK, 0 rows affected 
 
MariaDB [test]> explain 
    -> select t1.id, t1.itemid, dt.id, t2.id 
    ->    from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
    ->      where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;
+------+-----------------+------------+------+---------------+------+---------+-----------------------+------+-------------+
| id   | select_type     | table      | type | possible_keys | key  | key_len | ref                   | rows | Extra       |
+------+-----------------+------------+------+---------------+------+---------+-----------------------+------+-------------+
|    1 | PRIMARY         | t2         | ALL  | NULL          | NULL | NULL    | NULL                  |    1 |             |
|    1 | PRIMARY         | t1         | ref  | idx           | idx  | 4       | test.t2.id            |    1 |             |
|    1 | PRIMARY         | <derived2> | ref  | key0          | key0 | 9       | test.t2.id,test.t1.id |    2 |             |
|    2 | LATERAL DERIVED | t3         | ref  | idx1,idx2     | idx1 | 4       | const                 |    5 | Using index |
+------+-----------------+------------+------+---------------+------+---------+-----------------------+------+-------------+
4 rows in set
 
MariaDB [test]> select t1.id, t1.itemid, dt.id, t2.id 
    ->    from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
    ->      where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;
Empty set (5.988 sec)
 
MariaDB [test]> set optimizer_switch='split_materialized=off';
Query OK, 0 rows affected (0.000 sec)
 
MariaDB [test]> explain 
    -> select t1.id, t1.itemid, dt.id, t2.id 
    ->    from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
    ->      where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;
+------+-------------+------------+------+---------------+------+---------+-----------------------+------+--------------------------+
| id   | select_type | table      | type | possible_keys | key  | key_len | ref                   | rows | Extra                    |
+------+-------------+------------+------+---------------+------+---------+-----------------------+------+--------------------------+
|    1 | PRIMARY     | t2         | ALL  | NULL          | NULL | NULL    | NULL                  |    1 |                          |
|    1 | PRIMARY     | t1         | ref  | idx           | idx  | 4       | test.t2.id            |    1 |                          |
|    1 | PRIMARY     | <derived2> | ref  | key0          | key0 | 9       | test.t2.id,test.t1.id |    2 |                          |
|    2 | DERIVED     | t3         | ref  | idx1          | idx1 | 4       | const                 |    5 | Using where; Using index |
+------+-------------+------------+------+---------------+------+---------+-----------------------+------+--------------------------+
4 rows in set 
 
MariaDB [test]> select t1.id, t1.itemid, dt.id, t2.id 
    ->    from t1, (select itemid, max(id) as id from t3 where userid = 1 group by itemid) dt, t2
    ->      where t1.id = dt.id and t1.itemid = dt.itemid and t2.id=t1.itemid;
+----+--------+------+----+
| id | itemid | id   | id |
+----+--------+------+----+
|  4 |      2 |    4 |  2 |
|  4 |      2 |    4 |  2 |
+----+--------+------+----+
2 rows in set 

Comment by Marina Glancy [ 2021-06-03 ]

Hello ! Can you please update us on the state of this issue. It has been transitioned to "In review" two weeks ago without any patch or comment and also reassigned to Oleksandr Byelkin and there has been a radio silence since then.

Comment by Sergei Golubchik [ 2021-06-03 ]

marinaglancy, a couple of tips about this tool, Jira, and our workflow. In the left pane you can see "1 commit" and a link to the fix, which is one week old. Also, the issue priority is "Blocker" which means the next release simply won't go out until this issue is fixed. No matter how much silence will be or if anybody will be working on it at all, a blocker issue will definitely be in the next release.

Comment by Igor Babaev [ 2021-06-03 ]

This was checked before the patch was submitted for review:
The patch was applied to 10.5. all mtr tests passed. It was also checked that the test case provided by the reporter also passed (returned the expected result set).

Comment by Marina Glancy [ 2021-06-03 ]

Thanks for the update. We also use Jira in Moodle but a different workflow. By the way, the "Commit" link was almost unnoticeable - it was not included in the notification email and it is not visible unless I log in.

Curious about your process - why do you change assignee to the person who reviews the issue? Don't you want to be able to see in the issues list who has been working on which issues in the past?

Comment by Oleksandr Byelkin [ 2021-06-03 ]

Ok to push.

Comment by Sergei Golubchik [ 2021-06-04 ]

We do. But more often we need to know on who's plate the issue is at the moment. Or for a developer to know what issue to work on next. "Who's been working in the past" can also be seen from the history, when needed.

Comment by Calin [ 2021-06-04 ]

I'm sorry, are we still expecting a release for 10.3? Not sure what "STALLED" means. Thanks!

Comment by Arkadiusz Rzadkowolski [ 2021-06-04 ]

I am curious, if such issues shouldn't be hotfixed as quickly as possible.
Or at least mentioned in red on 10.5.10 page that such bug occurs in current version?

Probably lots of grouping functions are broken for people now with subquery joins (and probably in some cases involves money like in ours).

Either way - keep up the great work!

Comment by Igor Babaev [ 2021-06-08 ]

A fix for this bug was pushed into 10.3 and cherry-picked into 10.4 and 10.5.

Comment by Marina Glancy [ 2021-06-14 ]

Thanks for fixing it guys!

I can see on the dashboard that the preliminary release date for 1.5.11 is July 30th (in 1.5 months). Don't you think that this bug is big enough to have an emergency release?

I can imagine that it might have very serious effect on production systems, I can see that there are already three or four other reports linked to this issue.

Particularly in case of Moodle LMS, it will cause loss of students grades on re-enrollment when they are being restored from the grade history.

Comment by Simon Lewis [ 2021-06-14 ]

I agree with Marina about the emergency release. It has broken several of our systems and we have had to roll back the database server back to 10.4.18. A fairly simple query returning the wrong data is going to cause problems for a lot of people.

Comment by Marina Glancy [ 2021-06-14 ]

We can't just simply rollback the database version - Moodle makes a product that other people host in their own environment. They may update the database more often than Moodle itslef and this query has been working well and was not changed for the last 10 years. We have almost 200K registered sites (see https://stats.moodle.org/sites/ ) and nobody knows how many sites in total - this is an open-source product and registration is optional.

All we could do is email the admins of registered sites advising them not to use the affected versions of MariaDB in production and we really don't want to do it because people will think that MariaDB is not a reliable database but we always recommended it to our users.

Comment by Andrew Lyons [ 2021-06-15 ]

I also agree that this issue should be included in an urgent emergency release. From what I see there is precedent for this (10.4.17 was an out-of-cycle release for regressions caused by 10.4.16).

Comment by Sergei Golubchik [ 2021-06-21 ]

Yes, we'll do an emergency release now.

Comment by Luke Cousins [ 2021-11-03 ]

We have just upgraded from 10.5.6 to 10.6.4 and this issue seems to have come up again. As far as I can tell it's only occurring on left joins to derived tables, where there's more than one field in the join.

When running with optimizer_switch="split_materialized=off" the problem goes away, much like in https://jira.mariadb.org/browse/MDEV-25725

Can this be reopened?

Comment by Daniel Howard [ 2022-01-12 ]

I have encountered this issue again in v10.5.13.

Just as with the previous commenter, we are doing a left join against a derived table which contains aggregates, and there are 2 ON conditions in the join.

I tried to reduce the problem to the simplest possible test case, but I've found it extremely difficult to pin down. Sometimes we get the right results, and sometimes we don't, even when using the exact same SQL test script in the setup. Furthermore, sometimes we get the right results when the test data is first added, then a few minutes later, we will start getting the wrong results.

I have seen it change from correct to incorrect many times in my testing. But I have never seen it change back from incorrect to correct. Once it breaks, it seems to stay broken.

When we are getting the incorrect results, there are always 2 'LATERAL DERIVED' rows in the EXPLAIN output.

Comment by Igor Babaev [ 2022-01-13 ]

danhowardmws,
Please provide the query returning a wrong result and the output from EXPLAIN FORMAT=JSON for this query.

Comment by Daniel Howard [ 2022-01-14 ]

@Igor Babaev

On version 10.5.13, the test script below reliably exhibits the bug. I run the setup script to create the tables and populate them with some dummy data. The query below counts the number of transaction_item rows for each (ledger_id, charge_id) pair. I've been careful in my test data to ensure that there is only ever 1 transaction_item row for each (ledger_id, charge_id) pair. Usually when I first run the query, I get the correct results (this is obvious because we see from_num_rows=1 on every row in the results set). After a short time (less than 1 minute for me), I start getting the incorrect results, and we see from_num_rows=2 on every row in the results set. I've captured below the output from EXPLAIN FORMAT=JSON for the same query, before and after it starts failing.

Note that that the number of rows of dummy data I have seems to be significant. The more rows I have, the faster the query starts giving incorrect results.

Setup:

DROP TABLE IF EXISTS transaction_items;
DROP TABLE IF EXISTS transactions;
DROP TABLE IF EXISTS charges;
DROP TABLE IF EXISTS ledgers;
CREATE TABLE ledgers (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(32)
);
CREATE TABLE charges (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  from_ledger_id BIGINT UNSIGNED NOT NULL,
  to_ledger_id BIGINT UNSIGNED NOT NULL,
  amount INT NOT NULL,
  CONSTRAINT fk_charge_from_ledger FOREIGN KEY (from_ledger_id) REFERENCES ledgers (id) ON DELETE CASCADE ON UPDATE RESTRICT,
  CONSTRAINT fk_charge_to_ledger FOREIGN KEY (to_ledger_id) REFERENCES ledgers (id) ON DELETE CASCADE ON UPDATE RESTRICT
);
CREATE TABLE transactions (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  ledger_id BIGINT UNSIGNED NOT NULL,
  CONSTRAINT fk_transactions_ledger FOREIGN KEY (ledger_id) REFERENCES ledgers (id) ON DELETE CASCADE ON UPDATE RESTRICT
);
CREATE TABLE transaction_items (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  transaction_id BIGINT UNSIGNED NOT NULL,
  charge_id BIGINT UNSIGNED,
  amount INT NOT NULL,
  CONSTRAINT fk_items_transaction FOREIGN KEY (transaction_id) REFERENCES transactions (id) ON DELETE CASCADE ON UPDATE RESTRICT,
  CONSTRAINT fk_items_charge FOREIGN KEY (charge_id) REFERENCES charges (id) ON DELETE CASCADE ON UPDATE RESTRICT
);
INSERT INTO `ledgers` (`id`, `name`) VALUES (1, 'Anna'), (2, 'John'), (3, 'Fred');
INSERT INTO `charges` (`id`, `from_ledger_id`, `to_ledger_id`, `amount`) VALUES (1, 2, 1, 200), (2, 1, 2, 330), (3, 1, 2, 640), (4, 3, 1, 640), (5, 3, 2, 1000);
INSERT INTO `charges` (`id`, `from_ledger_id`, `to_ledger_id`, `amount`) VALUES (6, 3, 1, 660), (7, 2, 3, 650), (8, 3, 2, 160), (9, 2, 1, 740), (10, 3, 2, 310);
INSERT INTO `charges` (`id`, `from_ledger_id`, `to_ledger_id`, `amount`) VALUES (11, 2, 1, 640), (12, 3, 2, 240), (13, 3, 2, 340), (14, 2, 1, 720), (15, 2, 3, 100);
INSERT INTO `charges` (`id`, `from_ledger_id`, `to_ledger_id`, `amount`) VALUES (16, 2, 3, 980), (17, 2, 1, 80), (18, 1, 2, 760), (19, 2, 3, 740), (20, 2, 1, 990);
INSERT INTO `transactions` (`id`, `ledger_id`) VALUES (2, 1), (3, 1), (5, 1), (8, 1), (12, 1), (18, 1), (22, 1), (28, 1), (34, 1), (35, 1);
INSERT INTO `transactions` (`id`, `ledger_id`) VALUES (40, 1), (1, 2), (4, 2), (6, 2), (10, 2), (13, 2), (16, 2), (17, 2), (20, 2), (21, 2);
INSERT INTO `transactions` (`id`, `ledger_id`) VALUES (24, 2), (26, 2), (27, 2), (29, 2), (31, 2), (33, 2), (36, 2), (37, 2), (39, 2), (7, 3);
INSERT INTO `transactions` (`id`, `ledger_id`) VALUES (9, 3), (11, 3), (14, 3), (15, 3), (19, 3), (23, 3), (25, 3), (30, 3), (32, 3), (38, 3);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (1, 1, 1, -200), (2, 2, 1, 200), (3, 3, 2, -330), (4, 4, 2, 330), (5, 5, 3, -640);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (6, 6, 3, 640), (7, 7, 4, -640), (8, 8, 4, 640), (9, 9, 5, -1000), (10, 10, 5, 1000);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (11, 11, 6, -660), (12, 12, 6, 660), (13, 13, 7, -650), (14, 14, 7, 650), (15, 15, 8, -160);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (16, 16, 8, 160), (17, 17, 9, -740), (18, 18, 9, 740), (19, 19, 10, -310), (20, 20, 10, 310);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (21, 21, 11, -640), (22, 22, 11, 640), (23, 23, 12, -240), (24, 24, 12, 240), (25, 25, 13, -340);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (26, 26, 13, 340), (27, 27, 14, -720), (28, 28, 14, 720), (29, 29, 15, -100), (30, 30, 15, 100);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (31, 31, 16, -980), (32, 32, 16, 980), (33, 33, 17, -80), (34, 34, 17, 80), (35, 35, 18, -760);
INSERT INTO `transaction_items` (`id`, `transaction_id`, `charge_id`, `amount`) VALUES (36, 36, 18, 760), (37, 37, 19, -740), (38, 38, 19, 740), (39, 39, 20, -990), (40, 40, 20, 990);

The query:

SELECT
    charges.id,
    charges.from_ledger_id,
    charges.to_ledger_id,
    from_agg_items.num_rows AS from_num_rows
FROM charges
LEFT JOIN (
    SELECT
        transactions.ledger_id,
        transaction_items.charge_id,
        count(*) as num_rows
    FROM transaction_items
    INNER JOIN transactions ON transaction_items.transaction_id = transactions.id
    GROUP BY transactions.ledger_id, transaction_items.charge_id
) AS from_agg_items ON from_agg_items.charge_id = charges.id AND from_agg_items.ledger_id = charges.from_ledger_id
WHERE charges.to_ledger_id = 2

EXPLAIN result when the query is returning correct results:

{
 "query_block": {
   "select_id": 1,
   "table": {
     "table_name": "charges",
     "access_type": "ALL",
     "possible_keys": ["fk_charge_to_ledger"],
     "rows": 20,
     "filtered": 40,
     "attached_condition": "charges.to_ledger_id = 2"
   },
   "table": {
     "table_name": "<derived2>",
     "access_type": "ref",
     "possible_keys": ["key0"],
     "key": "key0",
     "key_length": "18",
     "used_key_parts": ["ledger_id", "charge_id"],
     "ref": ["bugtest.charges.from_ledger_id", "bugtest.charges.id"],
     "rows": 4,
     "filtered": 100,
     "materialized": {
       "query_block": {
         "select_id": 2,
         "filesort": {
           "sort_key": "transactions.ledger_id, transaction_items.charge_id",
           "temporary_table": {
             "table": {
               "table_name": "transaction_items",
               "access_type": "ALL",
               "possible_keys": ["fk_items_transaction", "fk_items_charge"],
               "rows": 40,
               "filtered": 100
             },
             "table": {
               "table_name": "transactions",
               "access_type": "eq_ref",
               "possible_keys": ["PRIMARY", "fk_transactions_ledger"],
               "key": "PRIMARY",
               "key_length": "8",
               "used_key_parts": ["id"],
               "ref": ["bugtest.transaction_items.transaction_id"],
               "rows": 1,
               "filtered": 100
             }
           }
         }
       }
     }
   }
 }
}

After a short time (less than 1 minute usually), the query will start returning the wrong results.

EXPLAIN result when the query is returning incorrect results:

{
 "query_block": {
   "select_id": 1,
   "table": {
     "table_name": "charges",
     "access_type": "ALL",
     "possible_keys": ["fk_charge_to_ledger"],
     "rows": 20,
     "filtered": 35,
     "attached_condition": "charges.to_ledger_id = 2"
   },
   "table": {
     "table_name": "<derived2>",
     "access_type": "ref",
     "possible_keys": ["key0"],
     "key": "key0",
     "key_length": "18",
     "used_key_parts": ["ledger_id", "charge_id"],
     "ref": ["bugtest.charges.from_ledger_id", "bugtest.charges.id"],
     "rows": 2,
     "filtered": 100,
     "materialized": {
       "lateral": 1,
       "query_block": {
         "select_id": 2,
         "table": {
           "table_name": "transaction_items",
           "access_type": "ref",
           "possible_keys": ["fk_items_transaction", "fk_items_charge"],
           "key": "fk_items_charge",
           "key_length": "9",
           "used_key_parts": ["charge_id"],
           "ref": ["bugtest.charges.id"],
           "rows": 1,
           "filtered": 100
         },
         "table": {
           "table_name": "transactions",
           "access_type": "eq_ref",
           "possible_keys": ["PRIMARY", "fk_transactions_ledger"],
           "key": "PRIMARY",
           "key_length": "8",
           "used_key_parts": ["id"],
           "ref": ["bugtest.transaction_items.transaction_id"],
           "rows": 1,
           "filtered": 100
         }
       }
     }
   }
 }
}

Comment by Jens-U. Mozdzen [ 2022-01-30 ]

I see the same problem in MariaDB v10.6.5, using the queries provided by Daniel:

+----+----------------+--------------+---------------+ 
| id | from_ledger_id | to_ledger_id | from_num_rows | 
+----+----------------+--------------+---------------+ 
|  2 |              1 |            2 |             2 | 
|  3 |              1 |            2 |             2 | 
|  5 |              3 |            2 |             2 | 
|  8 |              3 |            2 |             2 | 
| 10 |              3 |            2 |             2 | 
| 12 |              3 |            2 |             2 | 
| 13 |              3 |            2 |             2 | 
| 18 |              1 |            2 |             2 | 
+----+----------------+--------------+---------------+ 
8 rows in set (0,001 sec)

EXPLAIN output:

{ 
  "query_block": { 
    "select_id": 1, 
    "table": { 
      "table_name": "charges", 
      "access_type": "ALL", 
      "possible_keys": ["fk_charge_to_ledger"], 
      "rows": 20, 
      "filtered": 40, 
      "attached_condition": "charges.to_ledger_id = 2" 
    }, 
    "table": { 
      "table_name": "<derived2>", 
      "access_type": "ref", 
      "possible_keys": ["key0"], 
      "key": "key0", 
      "key_length": "18", 
      "used_key_parts": ["ledger_id", "charge_id"], 
      "ref": ["mariadbtest.charges.from_ledger_id", "mariadbtest.charges.id"], 
      "rows": 2, 
      "filtered": 100, 
      "materialized": { 
        "lateral": 1, 
        "query_block": { 
          "select_id": 2, 
          "table": { 
            "table_name": "transaction_items", 
            "access_type": "ref", 
            "possible_keys": ["fk_items_transaction", "fk_items_charge"], 
            "key": "fk_items_charge", 
            "key_length": "9", 
            "used_key_parts": ["charge_id"], 
            "ref": ["mariadbtest.charges.id"], 
            "rows": 1, 
            "filtered": 100 
          }, 
          "table": { 
            "table_name": "transactions", 
            "access_type": "eq_ref", 
            "possible_keys": ["PRIMARY", "fk_transactions_ledger"], 
            "key": "PRIMARY", 
            "key_length": "8", 
            "used_key_parts": ["id"], 
            "ref": ["mariadbtest.transaction_items.transaction_id"], 
            "rows": 1, 
            "filtered": 100 
          } 
        } 
      } 
    } 
  } 
}

Real-life problem that brought me here is that Openstack malfunctions because of this error (the amount of supposedly allocated VCPUs is way above the actual number, because the query used by "placement" API also accounts for other resources, especially memory, too).

Comment by Shi Yan [ 2022-04-08 ]

We met with the same issue from Openstack/Placement malfunction(as Jens-U.Mozdzen mentioned) on Mariadb version 10.5.13.
And using the SQL queried provided by Daniel, can confirm it breaks in versions 10.5.12, 10.5.13, and 10.6.5.

But in versions 10.5.15 and 10.6.7, the issue looks be fixed, and we cannot replicate the issue anymore after the upgrade. Although I cannot see any relevant info in their release notes.

Generated at Thu Feb 08 09:39:47 UTC 2024 using Jira 8.20.16#820016-sha1:9d11dbea5f4be3d4cc21f03a88dd11d8c8687422.