Bug #111811 UPDATE statement with JOIN doesn't update any records
Submitted: 19 Jul 2023 9:38 Modified: 20 Jul 2023 11:40
Reporter: Samuel Melrose Email Updates:
Status: Can't repeat Impact on me:
None 
Category:MySQL Server Severity:S2 (Serious)
Version:8.0.33 OS:Linux
Assigned to: CPU Architecture:x86

[19 Jul 2023 9:38] Samuel Melrose
Description:
After upgrading from MySQL 5.7 -> MySQL 8.0, our more complex UPDATE statements with JOINs stopped working properly.

Initially we thought they were failing all the time, but they appear to be alternating between working and not working, i.e. no, yes, no, yes, no, yes, no - this pattern is reliably observed.

There are many queries affected in the application in question, but we'll focus on a specific one:

```
UPDATE
    matrix m
    INNER JOIN(
        SELECT
            m.handset_id,
            m.tariff_id,
            m.addon_id,
            m.gift_id,
            GREATEST(
                (
                    (
                        (
                            (
                                (
                                    IF(
                                        tc.gp_override_base > 0,
                                        tc.gp_override_base,
                                        130.00
                                    ) + IF(
                                        ta.gp_adjustment_base IS NULL,
                                        0,
                                        ta.gp_adjustment_base
                                    ) - m.ongoing_gp
                                ) * 1.2
                            ) + IF(hc.gp_adjustment IS NULL, 0, hc.gp_adjustment)
                        ) DIV 5
                    ) * 5
                ) - 0.01,
                0
            ) AS upfront_amount
        FROM
            matrix m
            JOIN tariff_commercials tc ON tc.tariff_id = m.tariff_id
            AND tc.site_affiliate_id = 12
            AND tc.allow_base = 1
            LEFT JOIN tariff_addon ta ON ta.tariff_id = m.tariff_id
            AND ta.addon_id = m.addon_id
            LEFT JOIN handset_commercials hc ON hc.handset_id = m.handset_id
            AND hc.network_id = m.network_id
            AND hc.site_affiliate_id = 12
            JOIN handset_site_affiliate hsa ON hsa.handset_id = m.handset_id
            AND hsa.site_affiliate_id = 12
            AND hsa.active = 1
        WHERE
            m.network_id = 15
            AND m.contract_type_id = 2
            AND m.tariff_type_id = 13
            AND m.gift_id = 0
    ) AS t ON m.handset_id = t.handset_id
    AND m.tariff_id = t.tariff_id
    AND m.addon_id = t.addon_id
    AND m.gift_id = t.gift_id
SET
    m.upfront_amount = t.upfront_amount,
    m.base_upfront_gp = m.upfront_gp + ROUND(t.upfront_amount / 1.2, 2),
    m.base_ongoing_gp = m.ongoing_gp + ROUND(t.upfront_amount / 1.2, 2),
    m.base_partner_gp = m.partner_gp + ROUND(t.upfront_amount / 1.2, 2),
    m.valid = 1
WHERE
    t.upfront_amount < 399.99
```

Running this returned affected rows 0, even though we knew there were records that needed updating.

To try and debug it, we turned it into a SELECT:

```
SELECT m.*
FROM matrix m
    INNER JOIN(
        SELECT
            m.handset_id,
            m.tariff_id,
            m.addon_id,
            m.gift_id,
            GREATEST(
                (
                    (
                        (
                            (
                                (
                                    IF(
                                        tc.gp_override_base > 0,
                                        tc.gp_override_base,
                                        130.00
                                    ) + IF(
                                        ta.gp_adjustment_base IS NULL,
                                        0,
                                        ta.gp_adjustment_base
                                    ) - m.ongoing_gp
                                ) * 1.2
                            ) + IF(hc.gp_adjustment IS NULL, 0, hc.gp_adjustment)
                        ) DIV 5
                    ) * 5
                ) - 0.01,
                0
            ) AS upfront_amount
        FROM
            matrix m
            JOIN tariff_commercials tc ON tc.tariff_id = m.tariff_id
            AND tc.site_affiliate_id = 12
            AND tc.allow_base = 1
            LEFT JOIN tariff_addon ta ON ta.tariff_id = m.tariff_id
            AND ta.addon_id = m.addon_id
            LEFT JOIN handset_commercials hc ON hc.handset_id = m.handset_id
            AND hc.network_id = m.network_id
            AND hc.site_affiliate_id = 12
            JOIN handset_site_affiliate hsa ON hsa.handset_id = m.handset_id
            AND hsa.site_affiliate_id = 12
            AND hsa.active = 1
        WHERE
            m.network_id = 15
            AND m.contract_type_id = 2
            AND m.tariff_type_id = 13
            AND m.gift_id = 0
    ) AS t ON m.handset_id = t.handset_id
    AND m.tariff_id = t.tariff_id
    AND m.addon_id = t.addon_id
    AND m.gift_id = t.gift_id
WHERE
    t.upfront_amount < 399.99
```

and this returned just over 19k rows.

Initially we thought this was related to the subquery, so we transformed it into this:

```
CREATE TEMPORARY TABLE tmp_matrix_update_1 (
`id` int unsigned not null primary key auto_increment,
`handset_id` int unsigned NOT NULL,
  `tariff_id` int unsigned NOT NULL,
  `addon_id` int unsigned NOT NULL DEFAULT '0',
  `gift_id` int unsigned NOT NULL,
`upfront_amount` decimal(10,2) NOT NULL DEFAULT '0.00',
key `idx_composite` (`handset_id`, `tariff_id`, `addon_id`, `gift_id`, `upfront_amount`)
);

insert into tmp_matrix_update_1 ( `handset_id`, `tariff_id`, `addon_id`, `gift_id`, `upfront_amount` ) SELECT
            m.handset_id,
            m.tariff_id,
            m.addon_id,
            m.gift_id,
            GREATEST(
                (
                    (
                        (
                            (
                                (
                                    IF(
                                        tc.gp_override_base > 0,
                                        tc.gp_override_base,
                                        70.00
                                    ) + IF(
                                        ta.gp_adjustment_base IS NULL,
                                        0,
                                        ta.gp_adjustment_base
                                    ) - m.ongoing_gp
                                ) * 1.2
                            ) + IF(hc.gp_adjustment IS NULL, 0, hc.gp_adjustment)
                        ) DIV 5
                    ) * 5
                ) - 0.01,
                0
            ) AS upfront_amount
        FROM
            matrix m
            JOIN tariff_commercials tc ON tc.tariff_id = m.tariff_id
            AND tc.site_affiliate_id = 12
            AND tc.allow_base = 1
            LEFT JOIN tariff_addon ta ON ta.tariff_id = m.tariff_id
            AND ta.addon_id = m.addon_id
            LEFT JOIN handset_commercials hc ON hc.handset_id = m.handset_id
            AND hc.network_id = m.network_id
            AND hc.site_affiliate_id = 12
            JOIN handset_site_affiliate hsa ON hsa.handset_id = m.handset_id
            AND hsa.site_affiliate_id = 12
            AND hsa.active = 1
        WHERE
            m.network_id = 13
            AND m.contract_type_id = 2
            AND m.tariff_type_id = 13
            AND m.gift_id = 0;

UPDATE
    matrix m
    INNER JOIN tmp_matrix_update_1 AS t ON m.handset_id = t.handset_id
    AND m.tariff_id = t.tariff_id
    AND m.addon_id = t.addon_id
    AND m.gift_id = t.gift_id
SET
    m.upfront_amount = t.upfront_amount,
    m.base_upfront_gp = m.upfront_gp + ROUND(t.upfront_amount / 1.2, 2),
    m.base_ongoing_gp = m.ongoing_gp + ROUND(t.upfront_amount / 1.2, 2),
    m.base_partner_gp = m.partner_gp + ROUND(t.upfront_amount / 1.2, 2),
    m.valid = 1
WHERE
    t.upfront_amount < 399.99
```

yet this still returned affected rows 0.

How to repeat:
In the end, we ended up running:

```
UPDATE matrix SET valid = 0 (successfully updated all records, every time)

SELECT count(*)
FROM matrix m
JOIN tmp_matrix_update_1 t
ON m.handset_id = t.handset_id
AND m.tariff_id = t.tariff_id
AND m.addon_id = t.addon_id
AND m.gift_id = t.gift_id (always returns over 19k records)

UPDATE matrix m
JOIN tmp_matrix_update_1 t
ON m.handset_id = t.handset_id
AND m.tariff_id = t.tariff_id
AND m.addon_id = t.addon_id
AND m.gift_id = t.gift_id
SET m.valid = 1 (alternating between either, affected rows 0, or affected rows 19k)
```

As you can see, we ran a query to set all records `valid = 0`, so there would definitely be changes to the records required (to cover affected rows 0 because nothing needed to change).

The SELECT statement proves the join works properly.

And the final UPDATE, on repeated testing, would alternately return `affected rows 0` and when running all those above queries again, would return `affected rows 19k` as expected.

There was no-one else using the DB server, the application was disconnected & we were running these interactively for debugging - I can confirm the behaviour was alternating between updating nothing, or updating as expected, alternately, i.e. no, yes, no, yes, no, yes, no.

Suggested fix:
As a workaround, we had to use a `REPLACE INTO` statement instead:

```
CREATE TEMPORARY TABLE tmp_matrix_update_1 (
`id` int unsigned not null primary key auto_increment,
`handset_id` int unsigned NOT NULL,
  `tariff_id` int unsigned NOT NULL,
  `addon_id` int unsigned NOT NULL DEFAULT '0',
  `gift_id` int unsigned NOT NULL,
`upfront_amount` decimal(10,2) NOT NULL DEFAULT '0.00',
key `idx_composite` (`handset_id`, `tariff_id`, `addon_id`, `gift_id`, `upfront_amount`)
);

insert into tmp_matrix_update_1 ( `handset_id`, `tariff_id`, `addon_id`, `gift_id`, `upfront_amount` ) SELECT
            m.handset_id,
            m.tariff_id,
            m.addon_id,
            m.gift_id,
            GREATEST(
                (
                    (
                        (
                            (
                                (
                                    IF(
                                        tc.gp_override_base > 0,
                                        tc.gp_override_base,
                                        70.00
                                    ) + IF(
                                        ta.gp_adjustment_base IS NULL,
                                        0,
                                        ta.gp_adjustment_base
                                    ) - m.ongoing_gp
                                ) * 1.2
                            ) + IF(hc.gp_adjustment IS NULL, 0, hc.gp_adjustment)
                        ) DIV 5
                    ) * 5
                ) - 0.01,
                0
            ) AS upfront_amount
        FROM
            matrix m
            JOIN tariff_commercials tc ON tc.tariff_id = m.tariff_id
            AND tc.site_affiliate_id = 12
            AND tc.allow_base = 1
            LEFT JOIN tariff_addon ta ON ta.tariff_id = m.tariff_id
            AND ta.addon_id = m.addon_id
            LEFT JOIN handset_commercials hc ON hc.handset_id = m.handset_id
            AND hc.network_id = m.network_id
            AND hc.site_affiliate_id = 12
            JOIN handset_site_affiliate hsa ON hsa.handset_id = m.handset_id
            AND hsa.site_affiliate_id = 12
            AND hsa.active = 1
        WHERE
            m.network_id = 13
            AND m.contract_type_id = 2
            AND m.tariff_type_id = 13
            AND m.gift_id = 0;

replace into matrix
select
m.`id`,
  m.`contract_type_id`,
  m.`tariff_type_id`,
  m.`handset_id`,
  m.`tariff_id`,
  m.`addon_id`,
  m.`gift_id`,
  m.`network_id`,
  m.`upfront_payment`,
  m.`rev_share`,
  m.`pre_payment`,
  m.`length`,
  m.`line_rental`,
  m.`family`,
  m.`inc_data`,
  m.`plan_id`,
  m.`sim_cost`,
  m.`h_cb_cost`,
  m.`h_cb_bonus`,
  m.`network_support`,
  m.`handset_postage`,
  m.`gift_cost`,
  m.`gift_postage`,
  m.`total_cost`,
  m.`commission_upfront`,
  m.`commission_ongoing`,
  m.`commission_partner`,
  m.`upfront_gp`,
  m.`ongoing_gp`,
  m.`partner_gp`,
  m.`tcc`,
  t.`upfront_amount`,
  m.upfront_gp + ROUND(t.upfront_amount / 1.2, 2), /*m.`base_upfront_gp`,*/
  m.ongoing_gp + ROUND(t.upfront_amount / 1.2, 2), /*m.`base_ongoing_gp`,*/
  m.partner_gp + ROUND(t.upfront_amount / 1.2, 2), /*m.`base_partner_gp`,*/
  m.`cashback_amount`,
  m.`cashback_upfront_gp`,
  m.`cashback_ongoing_gp`,
  m.`redemption_amount`,
  m.`redemption_value`,
  m.`redemption_upfront_gp`,
  m.`redemption_ongoing_gp`,
  m.`line_rental_full`,
  m.`line_rental_full_value`,
  m.`line_rental_full_upfront_gp`,
  m.`line_rental_full_ongoing_gp`,
  m.`line_rental_half`,
  m.`line_rental_half_value`,
  m.`line_rental_half_upfront_gp`,
  m.`line_rental_half_ongoing_gp`,
  1
from matrix m
inner join tmp_matrix_update_1 t ON m.handset_id = t.handset_id
    AND m.tariff_id = t.tariff_id
    AND m.addon_id = t.addon_id
    AND m.gift_id = t.gift_id
where
t.upfront_amount < 399.99;
```

and this was updating the rows as expected.
[19 Jul 2023 12:27] MySQL Verification Team
Hi Mr. Melrose,

Thank you for your bug report.

However, we can not repeat it.

In order to repeat it, we need all the tables involved and all their data. Number of rows from each table has to be sufficient to enable us to repeat the problem.

When you send us all this info, we shall continue with the processing of this report.

Can't repeat.
[19 Jul 2023 15:29] Samuel Melrose
Thanks for your response,

I've just uploaded a mysqldump export of the database, including all relevant tables & their data:

/support/incoming/mysql-bug-data-111811.zip

Are you able to confirm receipt of the data please?
[20 Jul 2023 11:17] MySQL Verification Team
HI,

We received your data.

We are working on it now ...
[20 Jul 2023 11:37] MySQL Verification Team
Hi Mr. Melrose,

Sorry, but your UPDATE works just fine for us:

Query OK, 7006 rows affected (15.63 sec)
Rows matched: 15779  Changed: 7006  Warnings: 0

We ran it again and got the expected result;

Rows matched: 15779  Changed: 0  Warnings: 0

Because all matched rows were already changed.

We still can not repeat your problem.
[20 Jul 2023 11:40] Samuel Melrose
Thanks for your reply

Did you run

`UPDATE matrix SET valid = 0`

in between each run of the longer update statement (so there is always data that should be updated), as we did during debugging? And if you do, do you still see affected rows 0 on the second run?

This is what we experienced.
[20 Jul 2023 14:06] MySQL Verification Team
Hi,

After running that UPDATE that changes all rows we got:

UPDATE matrix SET valid = 0;
Query OK, 49346 rows affected (1.27 sec)
Rows matched: 169678  Changed: 49346  Warnings: 0

Then we repeated the same very long UPDATE and we got:

Query OK, 15779 rows affected (18.12 sec)
Rows matched: 15779  Changed: 15779  Warnings: 0

We then repeated both commands and we have got:

Query OK, 15779 rows affected (0.54 sec)
Rows matched: 169678  Changed: 15779  Warnings: 0

Query OK, 15779 rows affected (14.96 sec)
Rows matched: 15779  Changed: 15779  Warnings: 0

Hence, everything is OK.

If we can not repeat it , then it is not a bug.