magentadesk 0.9.0 のリリースでも触れましたが、

MySQLのUNIONに非常に苦しめられました。

具体的には、完全外部結合をする過程の中で、使っているUNIONの箇所で、

ERROR 1030 (HY000): Got error 124 - 'Wrong index given to function' from storage engine

というエラーが発生します。

この原因がどうもMySQL 8.0 から導入されたTempTableエンジンという一時テーブル用のエンジンの不具合のようです。。


そもそも完全外部結合で何をしたいかというと、以下のようなイメージとなります。

要は、同じ構成の2テーブルのレコード差を出す形です。

以下のテーブルとデータを用意してます。

create table base (
  id int not null primary key,
  name text not null
);
insert into base values (1, 'A'),(2, 'B'),(3, 'C');

create table compare (
  id int not null primary key,
  name text not null
);
insert into compare values (1, 'A'),(2, 'BB'),(4, 'D');

外部結合がサポートされているPostgreSQLだと簡単にできます。

select
  base.id as base_id, 
  base.name as base_name, 
  compare.id as compare_id, 
  compare.name as compare_name 
from
  base full outer join compare on base.id = compare.id
where
  base.id is null or compare.id is null
  or
  base.id != compare.id or base.name != compare.name
;

 base_id | base_name | compare_id | compare_name 
---------+-----------+------------+-------------
       2 | B         |          2 | BB
       3 | C         |       NULL | NULL
    NULL | NULL      |          4 | D

magentadesk での以前の実装は以下のとおりでした。

これはMariaDBでは問題ありませんでした。当然PostgreSQLでも問題なかったです。

ただし、MySQL 8.0系だと、正しくない結果でした。(結果的にはwhere句を追加して対応)

with
only_base as (
  select * from base
  except
  select * from compare
),
only_compare as (
  select * from compare
  except
  select * from base
),
merge_left as (
  select 
    only_base.id as base_id, 
    only_base.name as base_name, 
    only_compare.id as compare_id, 
    only_compare.name as compare_name
  from only_base left outer join only_compare on only_base.id = only_compare.id
),
merge_right as (
  select 
    only_base.id as base_id, 
    only_base.name as base_name, 
    only_compare.id as compare_id, 
    only_compare.name as compare_name 
  from only_base right outer join only_compare on only_base.id = only_compare.id
),
merge_full as (
  select * from merge_left
  union
  select * from merge_right
)
select
  *
from
  merge_full
;

(MariaDB 12.1)
+---------+-----------+------------+-------------+
| base_id | base_name | compare_id | compare_name |
+---------+-----------+------------+-------------+
|       2 | B         |          2 | BB          |
|       3 | C         |       NULL | NULL        |
|    NULL | NULL      |          4 | D           |
+---------+-----------+------------+-------------+

(PostgreSQL 18.1)
 base_id | base_name | compare_id | compare_name 
---------+-----------+------------+-------------
       3 | C         |       NULL | NULL
       2 | B         |          2 | BB
    NULL | NULL      |          4 | D

(MySQL 8.0.44)
+---------+-----------+------------+-------------+
| base_id | base_name | compare_id | compare_name |
+---------+-----------+------------+-------------+
|       2 | B         |          2 | BB          |
|       3 | C         |       NULL | NULL        |
|       1 | A         |          1 | A           |
|    NULL | NULL      |          4 | D           |
+---------+-----------+------------+-------------+

このクエリーはMySQL 8.4 だと動きません。

そのため、次の形に変更しました。

with
only_base as (
  select id, MD5(IFNULL(name, '')) as row_hash from base
  except
  select id, MD5(IFNULL(name, '')) as row_hash from compare
),
only_compare as (
  select id, MD5(IFNULL(name, '')) as row_hash from compare
  except
  select id, MD5(IFNULL(name, '')) as row_hash from base
),
diff_keys as (
  select id from only_base
  union
  select id from only_compare
),
merge_full as (
  select
    base.id as base_id, 
    base.name as base_name, 
    compare.id as compare_id, 
    compare.name as compare_name 
  from
    diff_keys
    left outer join base on diff_keys.id = base.id
    left outer join compare on diff_keys.id = compare.id
)
select
  *
from
  merge_full
;

ポイントは、

  • EXCEPTの比較時はハッシュ化しておく
  • UNIONでは主キーのみとする
  • 最後の外部結合で情報を取得する

といったところで、ハッシュ化する負荷はあるもののトータルで見れば軽くなった形となります。

これで、MariaDBも、MySQL 8.0および8.4 でも同じ結果を得ることができました。

さすがにわからかなったので、Geminiにも聞きながらやりました。

とはいえ、結構Geminiも間違えていたので、なかなか厄介な問題なのかなと思っています。


magentadesk での以前の実装のクエリーの問題点は、

merge_full as (
  select * from merge_left
  union
  select * from merge_right
)

の部分なのですが、文字列カラム(char/varchar/text)があると、エラーを引き起こすようです。

バイナリは試していないのですが、同じかもしれません。

もちろん、通常のテーブルのUNIONは問題ないのですが、仮想表でのUNIONには気をつけたほうがいいのかな、という所感です。

UNIONだけなの問題なのか、EXCEPTやINTERSECTまで及ぶのかはわからないのですが、

TempTableにはまだ不具合ありそうなので、テンポラリーに落ちるとき(Using temporary)はご注意ください。

以上

コメントがあればどうぞ


CAPTCHA Image
Reload Image