sky’s 雑記

主にAndroidとサーバーサイドの技術について記事を書きます

EXISTS と INNER JOIN のパフォーマンスについて

EXISTSをINNER JOINに置き換えるとパフォーマンスが上がるという主張をよく見かけますが、 ちょっと怪しい記事が多かったので調べました。

環境

他のDBでは確認していないのでMySQLに限定した話題です。

usersテーブルは複数のroom_usersを持ちます、レコード数は100万程度で試しています。

sh-4.4# mysql --version
mysql  Ver 8.0.30 for Linux on aarch64 (MySQL Community Server - GPL)
erDiagram

users {
  id integer PK
}

room_users {
  id integer PK
  user_id integer FK
}

users ||--o{ room_users : has_many

事前調査

調べていくと冒頭の主張とは異なるものの確度が高そうな情報を見つけた。

DBサーバーの種類に言及はなかったがStack Overflow[1][2]とQuora[3]の質問の回答をまとめると

  • uniqueなカラムについてINNER JOINする場合はEXISTSとパフォーマンスは変わらない
  • DISTINCTが適用されるレコードに対してINNER JOINする場合はEXISTSのほうがパフォーマンスが良い

INNER JOINとEXISTSはパフォーマンスは変わらず、重複削除の必要があるDISTINCT + INNER JOINになるとパフォーマンスが悪くなるとのこと。

実行計画

EXISTS, INNER JOIN, DISTINCT + INNER JOINそれぞれについて実行計画を出力して比較する。

EXISTS

select `users`.* from `users` where `users`.`id` != 1 and exists (select * from `room_users` where `users`.`id` = `room_users`.`user_id` and `room_users`.`room_id` in (10,11,12,13,14)) order by `users`.`updated_at` desc limit 30;

INNER JOIN

select `users`.* from `users` inner join `room_users` on `room_users`.`user_id` = `users`.`id` and `room_users`.`room_id` in (10,11,12,13,14) where `users`.`id` != 1 order by `users`.`updated_at` desc limit 30;

DISTINCT + INNER JOIN

select distinct `users`.* from `users` inner join `room_users` on `room_users`.`user_id` = `users`.`id` and `room_users`.`room_id` in (10,11,12,13,14) where `users`.`id` != 1 order by `users`.`updated_at` desc limit 30;

Optimizer Trace

 SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G

出力された join_preparation で最終的に展開されるクエリは以下のようになる、semi joinは重複を取り除いた結果を結合するというもので、結合後に重複を取り除くDISTINCT + INNER JOINよりも効率が良いとのこと[4]

長くなるので省略するが considered_execution_plans に表示されるEXISTSとINNER JOINの実行計画は同じものだった、EXISTSとINNER JOINのパフォーマンスの差は重複を取り除くタイミングによることになる。

EXISTS

 select `users`.`id` AS `id`... from `users` semi join (`room_users`) where ((`users`.`id` <> 1) and (`room_users`.`room_id` in (10,11,12,13,14)) and (`users`.`id` = `room_users`.`user_id`)) order by `users`.`updated_at` desc limit 30

INNER JOIN

select `users`.`id` AS `id`... from `users` join `room_users` where ((`users`.`id` <> 1) and (`room_users`.`user_id` = `users`.`id`) and (`room_users`.`room_id` in (10,11,12,13,14))) order by `users`.`updated_at` desc limit 30

DISTINCT + INNER JOIN

select distinct `users`.`id` AS `id`... from `users` join `room_users` where ((`users`.`id` <> 1) and (`room_users`.`user_id` = `users`.`id`) and (`room_users`.`room_id` in (10,11,12,13,14))) order by `users`.`updated_at` desc limit 30

オプティマイザトレースの読み方は[5]-[7]に詳しい。

まとめ

MySQL8.0.30では

  • レコードの重複の削除が必要ない場合EXISTSとINNER JOINのパフォーマンスは変わらない
  • レコードの重複の削除が必要な場合EXISTSに比べINNER JOINのパフォーマンスは悪い

参考

[1] sql - Can an INNER JOIN offer better performance than EXISTS - Stack Overflow

[2] IN vs. JOIN vs. EXISTS at EXPLAIN EXTENDED

[3] Does select distinct slow down a query? - Quora

[4] MySQL :: MySQL 5.7 Reference Manual :: 8.2.2.1 Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations

[5] The MySQL Query Optimizer Explained Through Optimizer Trace | Percona Live - Open Source Database Conference 2019

[6] 非公式MySQL 8.0オプティマイザガイド by yakst

[7] 漢(オトコ)のコンピュータ道: オプティマイザトレースによるちょっとディープな快適チューニング生活