2017年5月26日金曜日

MySQLのSELECT .. FOR UPDATEはREPEATABLE-READでも直近にコミットされたレコードを返す

TL;DR


ドキュメント探してみたけどほんのちょっとだけしか書いてないような気がする。
SELECT … LOCK IN SHARE MODE は、 これらの行のいずれかがコミットされていない別のトランザクションによって変更された場合、クエリーはそのトランザクションが終了するまで待機してから、最新の値を使用します。
しゃらっと 最新の値を使用します と書いてあるけど、最新の値が意味するのは「最後にコミットされた時の値」であって、REPEATABLE-READのはずの分離レベル内でREAD-COMMITTEDっぽい動作が見える。
どういうことかというと
mysql57 4> START TRANSACTION;
mysql57 5> START TRANSACTION;

mysql57 4> SELECT * FROM t1;
+-----+--------+
| num | val    |
+-----+--------+
|   1 | before |
+-----+--------+
1 row in set (0.00 sec)

mysql57 5> SELECT * FROM t1;
+-----+--------+
| num | val    |
+-----+--------+
|   1 | before |
+-----+--------+
1 row in set (0.00 sec)

mysql57 4> UPDATE t1 SET val = 'after' WHERE num = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql57 4> COMMIT AND CHAIN;
Query OK, 0 rows affected (0.00 sec)

mysql57 4> SELECT * FROM t1;
+-----+-------+
| num | val   |
+-----+-------+
|   1 | after |
+-----+-------+
1 row in set (0.00 sec)
ほぼ同時に開始したトランザクション45がいて、4はある行の値をアップデートしてコミットした。 この時に”after”が見えるのがREAD-COMMITTED、”before”が見えるのがREPEATABLE-READのはずで確かにそうなるんだけど、ブロッキングリード(SELECT .. FOR UPDATE, SELECT .. LOCK IN SHARE MODE)の場合は
mysql57 5> SELECT * FROM t1 FOR UPDATE;
+-----+-------+
| num | val   |
+-----+-------+
|   1 | after |
+-----+-------+
1 row in set (0.00 sec)
コミット済みの値が見える。 同じトランザクションの中で非ブロッキングリードとブロッキングリードをすると
mysql57 5> SELECT * FROM t1;
+-----+--------+
| num | val    |
+-----+--------+
|   1 | before |
+-----+--------+
1 row in set (0.00 sec)

mysql57 5> SELECT * FROM t1 FOR UPDATE;
+-----+-------+
| num | val   |
+-----+-------+
|   1 | after |
+-----+-------+
1 row in set (0.00 sec)

mysql57 5> SELECT * FROM t1;
+-----+--------+
| num | val    |
+-----+--------+
|   1 | before |
+-----+--------+
1 row in set (0.00 sec)
返ってくる値が変わる。
この話自体は有名な気がしていたんだけれど、ドキュメントには小さくしか見つけられなかったのでひょっとしたら有名じゃないのかなって。 (これ実演してみせたら爆笑されたw)

2017年5月22日月曜日

MySQLユーザ会会 in 長野 2017に参加してきましたよ

MySQLユーザ会会 in 長野 2017に逝ってきました!
開催の経緯とかはとみたさんのブログ MySQLユーザ会会 in 長野 を開催しました - @tmtms のメモ 、当日のふいんき(何故か変換できない)はTogetter MySQLユーザ会会 in 長野 2017 - Togetterまとめ がまとまってます。
セッションの内容は坂井さんMySQLとはみたいなことやると聞いていたし、かじやまさんは「MySQL 8.0」だって聞いてたし、bizstationさんTransactd PHP ORMと聞いていたので(というか、それが聞きたいと頼んだのはわたしだ)
( ´-`).oO(この豪勢なメンツに対抗して自分の色が出せそうなセッション…? 運用ネタ…?
とか考えたんですが、結局
いつもどおり「MySQLを楽しんでいるおじさんっぷり」を話すことにしました。






背景の舞奈たんがさかさまになっているのが謎。手元のPDFファイル(や、SlideshareからダウンロードしたPDF)では天地正しいんですけれども。
最近のお気に入りのGTID、本当はmysqlslavetrxの話も入れたかったし8.0のSET @@GLOBAL.GTID_PURGED = '+gtid_set'も入れたかったなあと思い出してみたり、PMMのデモ(というか本番だけど)を見せられたのは良かったなあと思ったりしました。
yoku0825は絶賛楽しそうなことを探していますので、他に楽しそうなものがあったら教えてください :)

(交通費は自腹でしたが)登壇枠くれたとみたさん、会場提供のケイケンシステムさんありがとうございました!

2017年4月28日金曜日

MySQL 8.0.1でバイナリーログに original_commit_timestamp と immediate_commit_timestamp が追加された

original_committed_timestampはマスターで実行された時のタイムスタンプが、immediate_commit_timestampはそのサーバーで実際に実行された時のタイムスタンプがそれぞれ入る。 単位はいずれもマイクロ秒。
マスターで実行した CREATE DATABASE d1 のバイナリーログ。
$ /usr/mysql/8.0.1/bin/mysqlbinlog master/data/mysql-bin.000002
..
#170428 17:54:25 server id 1  end_log_pos 226 CRC32 0xe0efd740  GTID    last_committed=0        sequence_number=1       original_committed_timestamp=1493369665593157 immediate_commit_timestamp=1493369665593157
# original_commit_timestamp=1493369665593157 (2017-04-28 17:54:25.593157 JST)
# immediate_commit_timestamp=1493369665593157 (2017-04-28 17:54:25.593157 JST)
/*!80001 SET @@session.original_commit_timestamp=1493369665593157*//*!*/;
SET @@SESSION.GTID_NEXT= '00012009-1111-1111-1111-111111111111:1'/*!*/;
# at 226
#170428 17:54:25 server id 1  end_log_pos 323 CRC32 0xc5704ebe  Query   thread_id=8     exec_time=0     error_code=0    Xid = 44
..
CREATE DATABASE d1
/*!*/;
それがレプリケートされたスレーブのバイナリーログ。
$ /usr/mysql/8.0.1/bin/mysqlbinlog node2/data/mysql-bin.000002
..
# at 154
#170428 17:54:25 server id 1  end_log_pos 233 CRC32 0xddb16cfd  GTID    last_committed=0        sequence_number=1       original_committed_timestamp=1493369665593157 immediate_commit_timestamp=1493369665643968
# original_commit_timestamp=1493369665593157 (2017-04-28 17:54:25.593157 JST)
# immediate_commit_timestamp=1493369665643968 (2017-04-28 17:54:25.643968 JST)
/*!80001 SET @@session.original_commit_timestamp=1493369665593157*//*!*/;
SET @@SESSION.GTID_NEXT= '00012009-1111-1111-1111-111111111111:1'/*!*/;
# at 233
#170428 17:54:25 server id 1  end_log_pos 330 CRC32 0x1ced03f5  Query   thread_id=8     exec_time=0     error_code=0    Xid = 11
..
CREATE DATABASE d1
/*!*/;
これに合わせて、 performance_schema.replication_applier_status_by_worker にも LAST_APPLIED_TRANSACTION_*_TIMESTAMPAPPLYING_TRANSACTION_*_TIMESTAMP が追加されてる。
これを使えば5.7とそれまでの Seconds_Behind_Master みたいにがんばって現在時刻との差とかを求めなくても良くなるようになる(んだと思う)
mysql> SELECT * FROM replication_applier_status_by_worker\G
*************************** 1. row ***************************
                                       CHANNEL_NAME:
                                          WORKER_ID: 0
                                          THREAD_ID: 39
                                      SERVICE_STATE: ON
                                  LAST_ERROR_NUMBER: 0
                                 LAST_ERROR_MESSAGE:
                               LAST_ERROR_TIMESTAMP: 0000-00-00 00:00:00.000000
                           LAST_APPLIED_TRANSACTION: 00012009-1111-1111-1111-111111111111:1
 LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 2017-04-28 17:54:25.593157
LAST_APPLIED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 2017-04-28 17:54:25.593157
     LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP: 2017-04-28 17:54:25.596175
       LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP: 2017-04-28 17:54:25.645874
                               APPLYING_TRANSACTION:
     APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
    APPLYING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
         APPLYING_TRANSACTION_START_APPLY_TIMESTAMP: 0000-00-00 00:00:00.000000
1 row in set (0.01 sec)
ところで、スレーブのバイナリーログ上の immediate_commit_timestampp_s. replication_applier_status_by_workerLAST_APPLIED_TRANSACTION_*_TIMESTAMP のどれとも合わないんだけど、これってこれでいいの…?

2017年4月21日金曜日

MySQL 8.0のDROP TABLEがアトミックになっているっぽい件

MySQL 5.7とそれ以前で、「存在するテーブルと存在しないテーブルを一緒にDROP TABLE」しようとすると
mysql57> CREATE TABLE t1 (num int);
Query OK, 0 rows affected (0.01 sec)

mysql57> CREATE TABLE t3 (num int);
Query OK, 0 rows affected (0.01 sec)

mysql57> SHOW TABLES;
+--------------+
| Tables_in_d1 |
+--------------+
| t1           |
| t3           |
+--------------+
2 rows in set (0.00 sec)

mysql57> DROP TABLE t1, t2, t3;
ERROR 1051 (42S02): Unknown table 'd1.t2'

mysql57> SHOW TABLES;
Empty set (0.00 sec)
エラーにはなるけど消せるものは消える。
MySQL 8.0だと
mysql80> CREATE TABLE t1 (num int);
Query OK, 0 rows affected (0.01 sec)

mysql80> CREATE TABLE t3 (num int);
Query OK, 0 rows affected (0.01 sec)

mysql80> SHOW TABLES;
+--------------+
| Tables_in_d1 |
+--------------+
| t1           |
| t3           |
+--------------+
2 rows in set (0.00 sec)

mysql80> DROP TABLE t1, t2, t3;
ERROR 1051 (42S02): Unknown table 'd1.t2'

mysql80> SHOW TABLES;
+--------------+
| Tables_in_d1 |
+--------------+
| t1           |
| t3           |
+--------------+
2 rows in set (0.00 sec)
おおおおお消えてない! ちゃんと「エラーが返った = 操作は失敗している」が成立しているぞぞぞ。
mysql80> ALTER TABLE t1 ENGINE= MyISAM;
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql80> DROP TABLE t1, t2, t3;
ERROR 1051 (42S02): Unknown table 'd1.t2'

mysql80> SHOW TABLES;
+--------------+
| Tables_in_d1 |
+--------------+
| t1           |
| t3           |
+--------------+
2 rows in set (0.00 sec)
マイア勇 MyISAMに変えても同じ動きだった。ちょっとびっくり。

2017年4月20日木曜日

MySQLのDATETIME型で秒の小数部を扱うときのDEFAULT句

この CREATE TABLE ステートメントは転ける。
mysql57> CREATE TABLE t1 (num INT UNSIGNED NOT NULL, dt DATETIME(3) DEFAULT CURRENT_TIMESTAMP);
ERROR 1067 (42000): Invalid default value for 'dt'
CURRENT_TIMESTAMP関数DATETIME(0)型 を返すので、dtカラムの型である DATETIME(3)型 と合わないからだというわけで、
mysql57> CREATE TABLE t1 (num INT UNSIGNED NOT NULL, dt DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3));
Query OK, 0 rows affected (0.01 sec)

mysql57> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `num` int(10) unsigned NOT NULL,
  `dt` datetime(3) DEFAULT CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
これで通る。 なお、 ON UPDATE 句を書いた場合も同じ。
mysql57> CREATE TABLE t2 (num INT UNSIGNED NOT NULL, dt DATETIME(3) ON UPDATE CURRENT_TIMESTAMP);
ERROR 1294 (HY000): Invalid ON UPDATE clause for 'dt' column

mysql57> CREATE TABLE t2 (num INT UNSIGNED NOT NULL, dt DATETIME(3) ON UPDATE CURRENT_TIMESTAMP(3));Query OK, 0 rows affected (0.01 sec)

mysql57> SHOW CREATE TABLE t2\G
*************************** 1. row ***************************
       Table: t2
Create Table: CREATE TABLE `t2` (
  `num` int(10) unsigned NOT NULL,
  `dt` datetime(3) DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
とても余談として、CURRENT_TIMESTAMP関数が NOW関数 のシノニムってことは、これひょっとして DEFAULT NOW() でもいけるのでは? と思ったらいけた。しかも昔からだった。
mysql55> CREATE TABLE t1 (num INT UNSIGNED NOT NULL, dt TIMESTAMP DEFAULT NOW()); -- 5.5だからTIMESTAMP型じゃないとデフォルトを受けられない
Query OK, 0 rows affected (0.01 sec)

mysql55> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `num` int(10) unsigned NOT NULL,
  `dt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
ちゃんとCURRENT_TIMESTAMPとして扱われている。 ただし、CURRENT_TIMESTAMPと違ってNOWはかっこは省略できない。
mysql55> SELECT CURRENT_TIMESTAMP;
+---------------------+
| CURRENT_TIMESTAMP   |
+---------------------+
| 2017-04-20 15:07:50 |
+---------------------+
1 row in set (0.00 sec)

mysql55> SELECT CURRENT_TIMESTAMP();
+---------------------+
| CURRENT_TIMESTAMP() |
+---------------------+
| 2017-04-20 15:08:03 |
+---------------------+
1 row in set (0.00 sec)

mysql55> SELECT NOW;
ERROR 1054 (42S22): Unknown column 'NOW' in 'field list'

mysql55> SELECT NOW();
+---------------------+
| NOW()               |
+---------------------+
| 2017-04-20 15:08:15 |
+---------------------+
1 row in set (0.00 sec)

2017年4月19日水曜日

MySQL 5.7.17からエラーログに出るようになった deprecated partition engine に関するNote

こんなやつのこと。
2017-04-18T23:54:08.224673+09:00 0 [Note] /usr/mysql/5.7.18/bin/mysqld: ready for connections.
Version: '5.7.18-log'  socket: '/usr/mysql/5.7.18/data/mysql.sock'  port: 64057  Source distribution
2017-04-18T23:54:08.224691+09:00 0 [Note] Executing 'SELECT * FROM INFORMATION_SCHEMA.TABLES;' to get a list of tables using the deprecated partition engine. You may use the startup option '--disable-partition-engine-check' to skip this check.
2017-04-18T23:54:08.224696+09:00 0 [Note] Beginning of list of non-natively partitioned tables
2017-04-18T23:54:08.316219+09:00 0 [Note] End of list of non-natively partitioned tables
MySQL 8.0.0 で完全になくなることが決まった(というか8.0.0の時点でもうない) PARTITIONストレージエンジン を「使ってないよね? チェックするぞ?」という機能が MySQL 5.7.17 に入った。
MySQLのパーティショニングはストレージエンジンとして実装されてい ./configure --helpcmake -i を使ったことがあれば、 -DWITH_PARTITION_STORAGE_ENGINE=ON とかそういうのに憶えがあるかも知れない)
MySQL 5.7のInnoDBに関しては InnoDBネイティブパーティショニング といってPARTITIONストレージエンジンを使わずにInnoDBの内部でパーティションを表現するようになった。
「今後、パーティションを使っててもFOREIGN KEY制約がつけられるようになるかも知れない」というのは、このInnoDBネイティブパーティショニングによるもの。
フツーに mysql_upgrade をかましていれば5.7にアップグレードした時点で変換されるはずなのだが、 mysql_upgrade -s とか avoid_temporal_upgradeを使ってゴニョゴニョするとか をしている…あるいはいっこ飛ばして5.6から8.0へダイレクトジャンプするとかだと、 PARTITIONストレージエンジン によってパーティショニングされていたInnoDBのテーブルがそのまま残ってしまう。
Prior to MySQL 5.7.6, partitioned InnoDB tables used the generic ha_partition partitioning handler employed by MyISAM and other storage engines not supplying their own partitioning handlers; in MySQL 5.7.6 and later, such tables are created using the InnoDB storage engine's own (or “native”) partitioning handler. Beginning with MySQL 5.7.9, you can upgrade an InnoDB table that was created in MySQL 5.7.6 or earlier (that is, created using ha_partition) to the InnoDB native partition handler using ALTER TABLE ... UPGRADE PARTITIONING. (Bug #76734, Bug #20727344) This version of ALTER TABLE does not accept any other options and can be used only on a single table at a time. You can also use mysql_upgrade in MySQL 5.7.9 or later to upgrade older partitioned InnoDB tables to the native partitioning handler.
PARTITIONストレージエンジン そのものがなくなってしまうMySQL 8.0とそれ以降ではそのテーブルはそのままでは生きていけない……というわけで、過渡期にあたるMySQL 5.7にこのログが追加されたのだ(と思う)
default_password_lifetimeのときWe agree with the original bug reporter that the default of 360 is surprising for users upgrading from previous releases of MySQL. と言っていたので、おそらく多少 ショックの少なそうな方法 を模索した結果なんだと思う。
ちなみに PARTITIONストレージエンジン なテーブルがあってもリストされるだけ(だと思う)なので、 ALTER TABLE .. ENGINE = INNODB はセルフサービスでやる必要がある。
さて、 InnoDBネイティブパーティショニング なので、もちろんInnoDB以外のストレージエンジンはサポートされていない(というかInnoDBの実装なのだから他のストレージエンジンに手を出せるわけがない)ので、MyISAMでパーティショニングしているヤーツがもし万一あったら今のうちにInnoDBにしておくのがよろしいかと思います。

2017年4月18日火曜日

MySQL 8.0.1からJOIN_ORDERヒントが書ける

こんな、ORDER BY狙いのキーを使いたくなるクエリーがあるじゃろ?
mysql80> EXPLAIN SELECT Name, Language, Population, Percentage FROM CountryLanguage LEFT JOIN Country ON Country.Code= CountryLanguage.CountryCode WHERE Country.continent = 'Asia' ORDER BY Percentage LIMIT 5;
+----+-------------+-----------------+------------+------+------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------+
| id | select_type | table           | partitions | type | possible_keys                | key     | key_len | ref                | rows | filtered | Extra                                        |
+----+-------------+-----------------+------------+------+------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------+
|  1 | SIMPLE      | Country         | NULL       | ALL  | PRIMARY,index_code_continent | NULL    | NULL    | NULL               |  239 |    14.29 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | CountryLanguage | NULL       | ref  | PRIMARY,CountryCode          | PRIMARY | 3       | world.Country.Code |    4 |   100.00 | NULL                                         |
+----+-------------+-----------------+------------+------+------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
  1. STRAIGHT_JOIN に書き換えてORDER BYで使っているカラムを駆動表に固定する。ただしSTRAIGHT_JOINは内部結合なので、LEFT JOINは書き換えられない。
  2. USE INDEXかFORCE INDEX でORDER BY狙いのキーを狙い撃つ。大概の場合はこれで上手く動くんだけれど、最悪の場合 内部表のままORDER BY狙いのキーを使ってインデックススキャンががががが
INNER JOINなら 1. + 2. (たまに、STRAIGHT_JOINでもORDER BY狙いのキーを取らないことがあったりした。最近少ない気がする)、そうでなければ 2. だけとしてORDER BY狙いのキーを押し込むことが多かったけれど、MySQL 8.0.1からは JOIN_ORDERのヒント句 が使えるようになった。
mysql80> EXPLAIN SELECT /*+ JOIN_ORDER (CountryLanguage, Country) */ Name, Language, Population, Percentage FROM CountryLanguage LEFT JOIN Country ON Country.Code= CountryLanguage.CountryCode WHERE Country.continent = 'Asia' ORDER BY Percentage LIMIT 5;
+----+-------------+-----------------+------------+--------+------------------------------+------------------+---------+-----------------------------------+------+----------+-------------+
| id | select_type | table           | partitions | type   | possible_keys                | key              | key_len | ref                               | rows | filtered | Extra       |
+----+-------------+-----------------+------------+--------+------------------------------+------------------+---------+-----------------------------------+------+----------+-------------+
|  1 | SIMPLE      | CountryLanguage | NULL       | index  | PRIMARY,CountryCode          | index_percentage | 4       | NULL                              |    5 |   100.00 | Using index |
|  1 | SIMPLE      | Country         | NULL       | eq_ref | PRIMARY,index_code_continent | PRIMARY          | 3       | world.CountryLanguage.CountryCode |    1 |    14.29 | Using where |
+----+-------------+-----------------+------------+--------+------------------------------+------------------+---------+-----------------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

mysql80> EXPLAIN SELECT /*+ JOIN_PREFIX (Country) */ Name, Language, Population, Percentage FROM CountryLanguage LEFT JOIN Country ON Country.Code= CountryLanguage.CountryCode WHERE Country.continent = 'Asia' ORDER BY Percentage LIMIT 5;
+----+-------------+-----------------+------------+------+------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------+
| id | select_type | table           | partitions | type | possible_keys                | key     | key_len | ref                | rows | filtered | Extra                                        |
+----+-------------+-----------------+------------+------+------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------+
|  1 | SIMPLE      | Country         | NULL       | ALL  | PRIMARY,index_code_continent | NULL    | NULL    | NULL               |  239 |    14.29 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | CountryLanguage | NULL       | ref  | PRIMARY,CountryCode          | PRIMARY | 3       | world.Country.Code |    4 |   100.00 | NULL                                         |
+----+-------------+-----------------+------------+------+------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

mysql80> EXPLAIN SELECT /*+ JOIN_SUFFIX (Country) */ Name, Language, Population, Percentage FROM CountryLanguage LEFT JOIN Country ON Country.Code= CountryLanguage.CountryCode WHERE Country.continent = 'Asia' ORDER BY Percentage LIMIT 5;
+----+-------------+-----------------+------------+--------+------------------------------+------------------+---------+-----------------------------------+------+----------+-------------+
| id | select_type | table           | partitions | type   | possible_keys                | key              | key_len | ref                               | rows | filtered | Extra       |
+----+-------------+-----------------+------------+--------+------------------------------+------------------+---------+-----------------------------------+------+----------+-------------+
|  1 | SIMPLE      | CountryLanguage | NULL       | index  | PRIMARY,CountryCode          | index_percentage | 4       | NULL                              |    5 |   100.00 | Using index |
|  1 | SIMPLE      | Country         | NULL       | eq_ref | PRIMARY,index_code_continent | PRIMARY          | 3       | world.CountryLanguage.CountryCode |    1 |    14.29 | Using where |
+----+-------------+-----------------+------------+--------+------------------------------+------------------+---------+-----------------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
JOIN_ORDER(先に来るテーブル, 後に来るテーブル), または JOIN_PREFIX(先に来るテーブル), JOIN_SUFFIX(後に来るテーブル) の3つの書き方で指定できるぽい。
ORDER BY狙いのキーなら一番外側にあればそれでいいので、3つ以上の時も JOIN_ORDER より JOIN_PREFIX がいいのかな。
これでLEFT JOINでもORDER BY狙いのキーが狙いやすくなってすてきだ。