Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MySQLが得意なこと、不得意なこと(仮)

yoku0825
PRO
December 17, 2021

 MySQLが得意なこと、不得意なこと(仮)

2021/12/17 Engineers in CARTA vol.2 #MySQL
https://voyagegroup.connpass.com/event/231708/

得意なことというより特異なことを紹介するコーナーになってしまった

yoku0825
PRO

December 17, 2021
Tweet

More Decks by yoku0825

Other Decks in Technology

Transcript

  1. MySQLが得意なこと、苦手なこと(仮)
     
    2021/12/17
    yoku0825
    Engineers in CARTA vol.2 #MySQL

    View Slide

  2. TL;DR
    1/63

    View Slide

  3. MySQLが得意なこと/Oracleの方が得意そうなこと
    小さな結果セットを、インデックスを使い切って絞りだすのは得意
    インデックスを使い切らずに取り出したり、結果セットがそもそも大きいものはOracleの方が
    得意なはず

    それはそういうものだと割り切って、「じゃあ如何にMySQLが苦手な処理を得意な
    処理の側に手繰り寄せていくか」がMySQLと上手く付き合うのに必要な考え方だと
    思う
    明示的なテンポラリーテーブルを作るのとかもそう

    CREATE TEMPORARY TABLEは(吊るしのMySQLなら) read_only = ON なレプリカでも作成できる

    本気でMySQLを楽しむなら、MySQLは良くも悪くも柔軟すぎるので
    それを逆手に取ってレプリケーションで上手くゴニョるのが醍醐味
    レプリカでインデックス作って/データのサクッとスイッチオーバーするとか、そういうやつ

    2/63

    View Slide

  4. \こんばんわ/
    yoku0825@とある企業のDBA
    オラクらない

    ポスグらない

    マイエスキューエる

    生息域
    Twitter: @yoku0825

    Blog: 日々の覚書

    日本MySQLユーザ会

    MySQL Casual

    3/63

    View Slide

  5. MySQL < Oracle ?
    Oracleがわからないので真偽はわからない
    MySQL < NULL は NULL

    「MySQLではこういうのは危ない」は言えるけど、それがOracleでできたかどうか
    は私はわかりません。。
    4/63

    View Slide

  6. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    5/63

    View Slide

  7. sql_mode
    「ちょっとそれってどうなの?」な動作を許せるようにするフラグ
    かつては「ちゃんと制限を効かせるようにするフラグ」だけど、今日となっては制限が強い方
    がデフォルトなので「敢えて弱くしたい時に」フラグを下ろすのが一般的ではないか

    特にヤバいのは STRICT_TRANS_TABLES と ONLY_FULL_GROUP_BY
    6/63

    View Slide

  8. STRICT_TRANS_TABLES
    フツーはINT型にVARCHARな文字列を突っ込めばエラーになるべき
    mysql> CREATE TABLE t1 (num INT);
    Query OK, 0 rows affected (0.02 sec)
    mysql> INSERT INTO t1 VALUES ('one');
    ERROR 1366 (HY000): Incorrect integer value: 'one' for column 'num' at row 1
    mysql> SELECT * FROM t1;
    Empty set (0.00 sec)
    7/63

    View Slide

  9. STRICT_TRANS_TABLES
    このフラグが下りていると主に「本来キャスト不可能なデータを突っ込もうとして
    もエラーにならなく」なる
    mysql> SET SESSION sql_mode = '';
    Query OK, 0 rows affected (0.00 sec)
    mysql> INSERT INTO t1 VALUES ('one');
    Query OK, 1 row affected, 1 warning (0.00 sec)
    mysql> SHOW WARNINGS;
    +---------+------+----------------------------------------------------------+
    | Level | Code | Message |
    +---------+------+----------------------------------------------------------+
    | Warning | 1366 | Incorrect integer value: 'one' for column 'num' at row 1 |
    +---------+------+----------------------------------------------------------+
    1 row in set (0.00 sec)
    mysql> SELECT * FROM t1;
    +------+
    | num |
    +------+
    | 0 |
    +------+
    1 row in set (0.00 sec)
    8/63

    View Slide

  10. STRICT_TRANS_TABLES
    「長さ制約を超えた文字列」もキャスト不可能なデータ
    mysql> CREATE TABLE t2 (val VARCHAR(2));
    Query OK, 0 rows affected (0.02 sec)
    mysql> INSERT INTO t2 VALUES ('two');
    ERROR 1406 (22001): Data too long for column 'val' at row 1
    mysql> SELECT * FROM t2;
    Empty set (0.00 sec)
    9/63

    View Slide

  11. STRICT_TRANS_TABLES
    黙って入らない分の文字列が切り捨てられる
    mysql> SET SESSION sql_mode = '';
    Query OK, 0 rows affected (0.00 sec)
    mysql> INSERT INTO t2 VALUES ('two');
    Query OK, 1 row affected, 1 warning (0.00 sec)
    mysql> SHOW WARNINGS;
    +---------+------+------------------------------------------+
    | Level | Code | Message |
    +---------+------+------------------------------------------+
    | Warning | 1265 | Data truncated for column 'val' at row 1 |
    +---------+------+------------------------------------------+
    1 row in set (0.00 sec)
    mysql> SELECT * FROM t2;
    +------+
    | val |
    +------+
    | tw |
    +------+
    1 row in set (0.00 sec)
    10/63

    View Slide

  12. STRICT_TRANS_TABLES
    文字列の '1' と整数の 1 のような可逆なキャストは成功する
    暗黙の型変換との兼ね合いに注意…
    mysql> CREATE TABLE t2 (num INT, val varchar(32), registered datetime);
    Query OK, 0 rows affected (0.02 sec)
    mysql> INSERT INTO t2 VALUES ('1', 'one', 11111111); -- 8桁の整数はYYYYMMDDのDATE/DATETI
    ME型に可逆なキャストがされる
    Query OK, 1 row affected (0.01 sec)
    mysql> SELECT * FROM t2;
    +------+------+---------------------+
    | num | val | registered |
    +------+------+---------------------+
    | 1 | one | 1111-11-11 00:00:00 |
    +------+------+---------------------+
    1 row in set (0.00 sec)
    11/63

    View Slide

  13. ONLY_FULL_GROUP_BY
    GROUP BY の出てくるクエリは、select_listは全て GROUP BY か集約関数に含まれ
    るべき
    continent と indepyear は1対1で紐づかないので、そんなものを許すと(本来)何が返ってく
    るかわからない

    mysql> SELECT continent, COUNT(*), indepyear FROM country GROUP BY continent;
    ERROR 1055 (42000): Expression #3 of SELECT list is not in GROUP BY clause and contains n
    onaggregated column 'world.country.IndepYear' which is not functionally dependent on colu
    mns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
    12/63

    View Slide

  14. ONLY_FULL_GROUP_BY
    mysql> SET SESSION sql_mode = '';
    Query OK, 0 rows affected (0.01 sec)
    mysql> SELECT continent, COUNT(*), indepyear FROM country GROUP BY continent;
    +---------------+----------+-----------+
    | continent | COUNT(*) | indepyear |
    +---------------+----------+-----------+
    | North America | 37 | NULL |
    | Asia | 51 | 1919 |
    | Africa | 58 | 1975 |
    | Europe | 46 | 1912 |
    | South America | 14 | 1816 |
    | Oceania | 28 | NULL |
    | Antarctica | 5 | NULL |
    +---------------+----------+-----------+
    7 rows in set (0.03 sec)
    13/63

    View Slide

  15. ONLY_FULL_GROUP_BY
    このフラグを下ろした時に、 GROUP BY リストにも集約関数にもなっていないやつ
    は何が表示されているの?
    不定

    ちなみにこの動作をフォローする(?)ために ANY_VALUE という疑似集約関数が提供されている

    mysql> SELECT continent, COUNT(*), ANY_VALUE(indepyear) FROM country GROUP BY continent;
    +---------------+----------+----------------------+
    | continent | COUNT(*) | ANY_VALUE(indepyear) |
    +---------------+----------+----------------------+
    | North America | 37 | NULL |
    | Asia | 51 | 1919 |
    | Africa | 58 | 1975 |
    | Europe | 46 | 1912 |
    | South America | 14 | 1816 |
    | Oceania | 28 | NULL |
    | Antarctica | 5 | NULL |
    +---------------+----------+----------------------+
    7 rows in set (0.01 sec)
    14/63

    View Slide

  16. ONLY_FULL_GROUP_BY
    使うインデックスが変わると結果セットが変わる。
    mysql> SELECT continent, ANY_VALUE(name), ANY_VALUE(indepyear) FROM country GROUP BY continent;
    +---------------+------------------+----------------------+
    | continent | ANY_VALUE(name) | ANY_VALUE(indepyear) |
    +---------------+------------------+----------------------+
    | Asia | Hong Kong | NULL |
    | Europe | Faroe Islands | NULL |
    | North America | Aruba | NULL |
    | Africa | Western Sahara | NULL |
    | Oceania | American Samoa | NULL |
    | Antarctica | Antarctica | NULL |
    | South America | Falkland Islands | NULL |
    +---------------+------------------+----------------------+
    7 rows in set (0.00 sec)
    mysql> SELECT continent, ANY_VALUE(name), ANY_VALUE(indepyear) FROM country GROUP BY continent;
    +---------------+-----------------+----------------------+
    | continent | ANY_VALUE(name) | ANY_VALUE(indepyear) |
    +---------------+-----------------+----------------------+
    | North America | Aruba | NULL |
    | Asia | Afghanistan | 1919 |
    | Africa | Angola | 1975 |
    | Europe | Albania | 1912 |
    | South America | Argentina | 1816 |
    | Oceania | American Samoa | NULL |
    | Antarctica | Antarctica | NULL |
    +---------------+-----------------+----------------------+
    7 rows in set (0.00 sec)
    15/63

    View Slide

  17. ONLY_FULL_GROUP_BY
    ついでに、集約した行とANY_VALUEの行の一貫性もない
    mysql> SELECT continent, ANY_VALUE(name), MAX(indepyear) FROM country GROUP BY continent;
    +---------------+-----------------+----------------+
    | continent | ANY_VALUE(name) | MAX(indepyear) |
    +---------------+-----------------+----------------+
    | North America | Aruba | 1983 | <-- Arubaのindepyearは1983ではない
    | Asia | Afghanistan | 1991 |
    | Africa | Angola | 1993 |
    | Europe | Albania | 1993 |
    | South America | Argentina | 1975 |
    | Oceania | American Samoa | 1994 |
    | Antarctica | Antarctica | NULL |
    +---------------+-----------------+----------------+
    7 rows in set (0.00 sec)
    mysql> SELECT continent, name, indepyear FROM country WHERE name = 'Aruba';
    +---------------+-------+-----------+
    | continent | name | indepyear |
    +---------------+-------+-----------+
    | North America | Aruba | NULL |
    +---------------+-------+-----------+
    1 row in set (0.00 sec)
    16/63

    View Slide

  18. ONLY_FULL_GROUP_BY
    たぶんこんなSQLを許す(許していた)のはMySQLくらいだと思うので、Oracle to
    MySQLの文脈では問題にならないかも知れないけれども
    MySQLでこのフラグが下りているからといってそういうクエリーを書くと思わぬと
    ころで足元をすくわれるので注意を…
    sql_mode, セッション単位で変更できるので押し込んでくるフレームワークもある
    けれど個人的にはそういうことをして欲しくない…
    17/63

    View Slide

  19. NO_AUTO_VALUE_ON_ZERO
    デフォルトでは AUTO_INCREMENT なカラムに0を入れようとすると連番が払い出さ
    れる
    mysql> CREATE TABLE t3 (num INT AUTO_INCREMENT PRIMARY KEY);
    Query OK, 0 rows affected (0.02 sec)
    mysql> INSERT INTO t3 VALUES (NULL), (0);
    Query OK, 2 rows affected (0.00 sec)
    Records: 2 Duplicates: 0 Warnings: 0
    mysql> SELECT * FROM t3;
    +-----+
    | num |
    +-----+
    | 1 |
    | 2 |
    +-----+
    2 rows in set (0.00 sec)
    18/63

    View Slide

  20. NO_AUTO_VALUE_ON_ZERO
    NO_AUTO_VALUE_ON_ZERO をセットすると0が入れられるようになる
    というよりは、セットしないと 0 が使えないので使う時は注意…

    mysql> SET SESSION sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
    Query OK, 0 rows affected (0.00 sec)
    mysql> INSERT INTO t3 VALUES (NULL), (0);
    Query OK, 2 rows affected (0.00 sec)
    Records: 2 Duplicates: 0 Warnings: 0
    mysql> SELECT * FROM t3;
    +-----+
    | num |
    +-----+
    | 0 |
    | 1 |
    | 2 |
    | 3 |
    +-----+
    4 rows in set (0.00 sec)
    19/63

    View Slide

  21. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    20/63

    View Slide

  22. 暗黙の型変換
    数値の 1 と文字列の '1' はキャスト可能、これは良いとして
    真偽値を返す演算をした時に返ってくる 1 は真、 0 は偽

    mysql> SELECT CAST(1 AS SIGNED) = CAST('1' AS CHAR);
    +---------------------------------------+
    | CAST(1 AS SIGNED) = CAST('1' AS CHAR) |
    +---------------------------------------+
    | 1 |
    +---------------------------------------+
    1 row in set (0.00 sec)
    21/63

    View Slide

  23. 暗黙の型変換
    文字列と数値を比較した時は、文字列に寄せるのではなく 数値に寄せる
    不可逆な文字列から数値へのキャストは「数値として読めるところまでを数値として扱う」の
    で悲惨

    STRICT_TRANS_TABLES がないところだとつらい
    STRICT_TRANS_TABLES でもSELECTはエラーにならないのでつらい

    mysql> SELECT CAST(1 AS SIGNED) = CAST('1Q84' AS CHAR);
    +------------------------------------------+
    | CAST(1 AS SIGNED) = CAST('1Q84' AS CHAR) |
    +------------------------------------------+
    | 1 |
    +------------------------------------------+
    1 row in set, 1 warning (0.00 sec)
    mysql> SHOW WARNINGS;
    +---------+------+------------------------------------------+
    | Level | Code | Message |
    +---------+------+------------------------------------------+
    | Warning | 1292 | Truncated incorrect DOUBLE value: '1Q84' |
    +---------+------+------------------------------------------+
    1 row in set (0.00 sec)
    22/63

    View Slide

  24. 暗黙の型変換
    DATE/DATETIME型と数値を演算すると、数値側に寄せてから演算する
    mysql> SELECT CURDATE(), CURDATE() + 1, CURDATE() + INTERVAL 1 DAY, CAST(CURDATE() + 1 AS DATE);
    +------------+---------------+----------------------------+-----------------------------+
    | CURDATE() | CURDATE() + 1 | CURDATE() + INTERVAL 1 DAY | CAST(CURDATE() + 1 AS DATE) |
    +------------+---------------+----------------------------+-----------------------------+
    | 2021-12-10 | 20211211 | 2021-12-11 | 2021-12-11 |
    +------------+---------------+----------------------------+-----------------------------+
    1 row in set (0.00 sec)
    mysql> SELECT CURDATE(), CURDATE() + 30, CURDATE() + INTERVAL 30 DAY, CAST(CURDATE() + 30 AS DATE);
    +------------+----------------+-----------------------------+------------------------------+
    | CURDATE() | CURDATE() + 30 | CURDATE() + INTERVAL 30 DAY | CAST(CURDATE() + 30 AS DATE) |
    +------------+----------------+-----------------------------+------------------------------+
    | 2021-12-10 | 20211240 | 2022-01-09 | NULL |
    +------------+----------------+-----------------------------+------------------------------+
    1 row in set, 1 warning (0.00 sec)
    mysql> SHOW WARNINGS;
    +---------+------+--------------------------------------+
    | Level | Code | Message |
    +---------+------+--------------------------------------+
    | Warning | 1292 | Incorrect datetime value: '20211240' |
    +---------+------+--------------------------------------+
    1 row in set (0.00 sec)
    23/63

    View Slide

  25. 暗黙の型変換
    型変換と言っていいのかどうかはわからないけれど、MySQLの文字列型は「CHARSET
    とCOLLATIONまで含めて型」みたいな感じ
    utf8mb4_general_ci の 'あ' と utf8_general_ci の 'あ' はキャストが可能だけれど4バイト
    範囲から3バイト範囲への単射なので不可逆

    キャストが発生するとインデックスが使えない

    mysql> CREATE TABLE t1 (num INT PRIMARY KEY, val VARCHAR(32) COLLATE utf8mb4_general_ci,
    KEY(val));
    Query OK, 0 rows affected (0.02 sec)
    mysql> CREATE TABLE t2 (num INT PRIMARY KEY, val VARCHAR(32) COLLATE utf8_general_ci, KEY
    (val));
    Query OK, 0 rows affected (0.03 sec)
    mysql> CREATE TABLE t3 (num INT PRIMARY KEY, val VARCHAR(32) COLLATE utf8mb4_ja_0900_as_c
    s, KEY(val));
    Query OK, 0 rows affected (0.02 sec)
    24/63

    View Slide

  26. 暗黙の型変換
    3バイト範囲(t2)から4バイト範囲(t1)へのキャストは無損失だけど逆がそうではな
    いので、 WHERE にt1を絞り込める条件があるけれどJOINに使えなくなるので使わ
    ない
    mysql> EXPLAIN SELECT * FROM t1 JOIN t2 ON t1.val = t2.val WHERE t1.val LIKE 'a%';
    +----+-------------+-------+------------+-------+---------------+------+---------+-----------+--------+----------+--------------------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+-------+------------+-------+---------------+------+---------+-----------+--------+----------+--------------------------+
    | 1 | SIMPLE | t2 | NULL | index | val | val | 99 | NULL | 996424 | 100.00 | Using where; Using index |
    | 1 | SIMPLE | t1 | NULL | ref | val | val | 131 | d1.t2.val | 1 | 100.00 | Using where; Using index |
    +----+-------------+-------+------------+-------+---------------+------+---------+-----------+--------+----------+--------------------------+
    2 rows in set, 2 warnings (0.02 sec)
    mysql> SHOW WARNINGS;
    +---------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    --------------------------------------------------+
    | Level | Code | Message
    |
    +---------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    --------------------------------------------------+
    | Warning | 1739 | Cannot use ref access on index 'val' due to type or collation conversion on field 'val'
    |
    | Note | 1003 | /* select#1 */ select `d1`.`t1`.`num` AS `num`,`d1`.`t1`.`val` AS `val`,`d1`.`t2`.`num` AS `num`,`d1`.`t2`.`val` AS `val` from `d1`.`t1` join `d1`.`t2` where ((`d1`.`t1`.`val` li
    ke 'a%') and (`d1`.`t1`.`val` = `d1`.`t2`.`val`)) |
    +---------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    --------------------------------------------------+
    2 rows in set (0.00 sec)
    25/63

    View Slide

  27. 暗黙の型変換
    utf8mb4同士の別COLLATIONは「どっちのCOLLATIONを優先すべきか」が判定できず
    にそのままでは転ける
    COLLATE句で固定することはできるけど酷いことになったりもする

    mysql> EXPLAIN SELECT * FROM t1 JOIN t3 ON t1.val = t3.val WHERE t1.val LIKE 'a%';
    ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_ja_0900_as
    _cs,IMPLICIT) for operation '='
    mysql> EXPLAIN SELECT * FROM t1 JOIN t3 ON t1.val = t3.val COLLATE utf8mb4_unicode_ci WHERE t1.val
    LIKE 'a%';
    +----+-------------+-------+------------+-------+---------------+------+---------+------+--------+-
    ---------+---------------------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows |
    filtered | Extra |
    +----+-------------+-------+------------+-------+---------------+------+---------+------+--------+-
    ---------+---------------------------------------------------------+
    | 1 | SIMPLE | t1 | NULL | range | val | val | 131 | NULL | 125750 |
    100.00 | Using where; Using index |
    | 1 | SIMPLE | t3 | NULL | index | val | val | 131 | NULL | 996424 |
    100.00 | Using where; Using index; Using join buffer (hash join) |
    +----+-------------+-------+------------+-------+---------------+------+---------+------+--------+-
    ---------+---------------------------------------------------------+
    2 rows in set, 3 warnings (0.02 sec)
    26/63

    View Slide

  28. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    27/63

    View Slide

  29. ネクストキーロック
    トランザクション分離レベルが(デフォルトの)REPEATABLE-READ以上の時に使う
    ロック
    READ-COMMITTEDに設定している場合は使わない

    どうやらMySQLは「ネクストキーロックこそ lock REC 、レコードのみのロックは
    lock REC not GAP 」と思っているフシがあるので、ロック解析をする時の用語の
    違いに気を付けて
    28/63

    View Slide

  30. ネクストキーロック
    たとえばこんなテーブル
    PRIMARY KEY(name)

    KEY(age)

    KEY(gender)

    mysql> SELECT * FROM user;
    +----------------------+------+--------+
    | name | age | gender |
    +----------------------+------+--------+
    | yoku0825 | 38 | M |
    | yoku0825のムスメ | 6 | F |
    | yoku0825の妻 | NULL | F |
    | you0825のせがれ | 11 | M |
    +----------------------+------+--------+
    4 rows in set (0.00 sec)
    29/63

    View Slide

  31. ネクストキーロック
    infimum .. 無限小を表すレコード
    supremum .. 無限大を表すレコード
    [gap] .. レコードとレコードの隙間
    30/63

    View Slide

  32. ネクストキーロック
    REC .. そのレコードと手前のレコードの間のギャップ
    REC not GAP .. そのレコードそのもの
    GAP .. そのレコードの手前のギャップ
    31/63

    View Slide

  33. ネクストキーロック
    UPDATE user SET age = 39 WHERE name = 'yoku0825'
    トランザクション分離レベルにかかわらずPK引き

    32/63

    View Slide

  34. ネクストキーロック
    transaction_isolation= REPEATABLE-READ && UPDATE user SET age = 12 WHERE age
    = 11
    REPEATABLE-READで非ユニークなインデックスを引いたUPDATE

    33/63

    View Slide

  35. ネクストキーロック
    transaction_isolation= REPEATABLE-READ && UPDATE user SET age = 12 WHERE age
    = 11
    REPEATABLE-READで非ユニークなインデックスを引いたUPDATE

    34/63

    View Slide

  36. ネクストキーロック
    (6, yoku0825のムスメ) の直後から (38, yoku0825) の直前までがロックされている
    mysql> INSERT INTO user VALUES ('けんつ', 24, 'M');
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql> INSERT INTO user VALUES ('けんつ', 10, 'M');
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql> INSERT INTO user VALUES ('けんつ', 5, 'M');
    Query OK, 1 row affected (0.00 sec)
    35/63

    View Slide

  37. ネクストキーロック
    (6, yoku0825のムスメ) の直後から (38, yoku0825) の直前までがロックされている
    mysql80 14> INSERT INTO user VALUES ('yoku0824のムスメ', 6, 'F');
    Query OK, 1 row affected (0.01 sec)
    mysql80 14> INSERT INTO user VALUES ('yoku0826のムスメ', 6, 'F');
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql80 14> INSERT INTO user VALUES ('yoku0824', 38, 'M');
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql80 14> INSERT INTO user VALUES ('yoku0826', 38, 'M');
    Query OK, 1 row affected (0.00 sec)
    36/63

    View Slide

  38. ネクストキーロック
    transaction_isolation= REPEATABLE-READ && UPDATE user SET age = age + 1 WHERE
    gender = 'F'
    REPEATABLE-READで非ユニークなインデックスを引いたUPDATEその2

    37/63

    View Slide

  39. ネクストキーロック
    transaction_isolation= REPEATABLE-READ && UPDATE user SET age = age + 1 WHERE
    gender = 'F'
    REPEATABLE-READで非ユニークなインデックスを引いたUPDATEその2

    38/63

    View Slide

  40. ネクストキーロック
    infimumから gender = M && name < yoku0825 がロックされている
    mysql> INSERT INTO user VALUES ('yoku0824のムスメ?', NULL, 'F');
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql> INSERT INTO user VALUES ('yoku0824', NULL, 'M');
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql> INSERT INTO user VALUES ('yoku0826', NULL, 'M');
    Query OK, 1 row affected (0.00 sec)
    39/63

    View Slide

  41. ネクストキーロック
    InnoDBにおいてはインデックスはルックアップの高速化のみならず「ロックの粒度
    そのもの」でもある
    READ-COMMITTEDだとこれは成り立たない

    インデックスは丁寧に貼りましょう…
    40/63

    View Slide

  42. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    41/63

    View Slide

  43. ORDER BY .. LIMIT .. OFFSET
    Oracleだと OFFSET m ROWS FETCH FIRST n ROWS ONLY ?
    MySQLの場合は LIMIT n OFFSET m とか LIMIT m, n
    Oracleは知らないけどMySQLのOFFSETは0オリジン

    ユニークキーで ORDER BY していない限り、 LIMIT .. OFFSET .. で返ってくる行
    は「不定」
    WHEREとかとの兼ね合いで選ばれたインデックスで「見つかった順」にしか返さないので、使う
    インデックスが変わると戻ってくる行が変わる

    ユニークでない限り、 ORDER BY c1 ASC, c2 ASC でc1もc2も同じレコード同士がどういう順番
    で返ってくるかはわからない

    42/63

    View Slide

  44. WHEREとORDER BY LIMITの連携
    // WHERE continent = 'Asia' ORDER BY gnp ASC LIMIT 5
    $asia= $index_continent_population["Asia"];
    foreach ($asia as $row)
    {
    array_push($sort_buffer, $row);
    }
    uasort($sort_buffer, ..); // ソートバッファのソート
    // ソートが終わってからでないと LIMIT 5 が確定しない
    var_dump(array($sort_buffer[0], $sort_buffer[1], $sort_buffer[2], $sort_buffer[3], $sort_
    buffer[4]));
    43/63

    View Slide

  45. WHEREとORDER BY LIMITの連携
    // WHERE continent = 'Asia' ORDER BY population ASC LIMIT 5
    $n= 0;
    $result_buffer= [];
    foreach ($index_continent_population["Asia"] as $population => $code)
    {
    // populationの順番に並んでいることが保証されている
    array_push($result_buffer, $code);
    // 事前にトップ5が確定しているので、LIMIT 5を満たしたらループを抜けられる
    if (++$n == 5)
    break;
    }
    var_dump($result_buffer);
    44/63

    View Slide

  46. ORDER BY .. LIMIT .. OFFSET
    ORDER BY狙いのキーを使わない場合の処理
    WHERE , HAVING でフィルタ後の件数が多ければ多いほどレスポンスが悪化していく
    逆を言うとソートは常に最後までやってしまうので、OFFSETがいくら深くても性能にはそれほど寄与しないことが多
    い( sort_buffer_size をあふれるくらいまでいくと強く影響する)

    LIMIT .. OFFSET .. は「結果セットの転送に使うネットワーク使用量」くらいにしか寄与しな


    45/63

    View Slide

  47. ORDER BY .. LIMIT .. OFFSET
    ORDER BY狙いのキーを使った場合の処理の順番
    LIMIT n OFFSET m の場合、カーソルを合わせたインデックスレコードの先頭から n+(m+1)行
    ぶん読み出して、先頭 m+1 行を捨てる

    OFFSET が深くなれば深くなるほど線形に近くレスポンスタイムが劣化していく
    特にどうもこれselect_listを一緒に処理してから捨てているらしく、カバリングインデックスを使ったサブクエリ
    と組み合わせると速くなることが多い

    46/63

    View Slide

  48. ORDER BY .. LIMIT .. OFFSET
    SELECT * FROM t1 ORDER BY num LIMIT 10 OFFSET $n;
    OFFSET 0 : 0.016209
    OFFSET 1000000 : 0.444942
    OFFSET 2000000 : 0.755690
    OFFSET 3000000 : 0.811017
    OFFSET 4000000 : 1.192051
    OFFSET 5000000 : 1.358769
    OFFSET 6000000 : 1.617762
    OFFSET 7000000 : 1.806320
    OFFSET 8000000 : 2.227395
    OFFSET 9000000 : 3.315964
    OFFSET 10000000 : 3.497472
    47/63

    View Slide

  49. ORDER BY .. LIMIT .. OFFSET
    典型的な解決策は、 ORDER BY のカラムの最後の値を(App側で)おぼえておいて、
    それをWHERE句に追加してOFFSETを使わないこと
    SELECT * FROM t1 WHERE num > ? /* 前回の最後の値 */ ORDER BY num LIMIT 10;
    OFFSET 0 : 0.000568
    OFFSET 1000000 : 0.000338
    OFFSET 2000000 : 0.000278
    OFFSET 3000000 : 0.000301
    OFFSET 4000000 : 0.000282
    OFFSET 5000000 : 0.000263
    OFFSET 6000000 : 0.000273
    OFFSET 7000000 : 0.000304
    OFFSET 8000000 : 0.000261
    OFFSET 9000000 : 0.000259
    OFFSET 10000000 : 0.000288
    ただし ORDER BY のカラムがユニークでない場合、ページネーションで参照されな
    いレコードが出てきたりするので採用できないことも
    48/63

    View Slide

  50. ORDER BY .. LIMIT .. OFFSET
    特にselect_listが長い場合、サブクエリで多少高速化できる
    SELECT t1.* FROM t1 JOIN (SELECT num FROM t1 ORDER BY num LIMIT 10 OFFSET $n) AS picker O
    N t1.num = picker.num ORDER BY num;
    OFFSET 0 : 0.020770
    OFFSET 1000000 : 0.515550
    OFFSET 2000000 : 0.674445
    OFFSET 3000000 : 0.641850
    OFFSET 4000000 : 0.698979
    OFFSET 5000000 : 0.864459
    OFFSET 6000000 : 1.039336
    OFFSET 7000000 : 1.223000
    OFFSET 8000000 : 1.605361
    OFFSET 9000000 : 2.517832
    OFFSET 10000000 : 2.647445
    49/63

    View Slide

  51. ORDER BY .. LIMIT .. OFFSET
    焼け石に水に見えなくもないけれど効果はある
    SELECT * FROM t1 ORDER BY num LIMIT 10 OFFSET $n;
    OFFSET 0 : 0.016209
    OFFSET 1000000 : 0.444942
    OFFSET 2000000 : 0.755690
    OFFSET 3000000 : 0.811017
    OFFSET 4000000 : 1.192051
    OFFSET 5000000 : 1.358769
    OFFSET 6000000 : 1.617762
    OFFSET 7000000 : 1.806320
    OFFSET 8000000 : 2.227395
    OFFSET 9000000 : 3.315964
    OFFSET 10000000 : 3.497472
    50/63

    View Slide

  52. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    51/63

    View Slide

  53. AUTO_INCREMENT vs. シーケンス
    DELETE / ROLLBACKで欠番するのはOracleも一緒
    MySQLは INSERT .. SELECT .. や INSERT .. ON DUPLICATE KEY UPDATE .. や
    REPLACE INTO であっさり起こる
    8.0のデフォルトは、INSERT .. SELECT .. を多重で走らせると、AUTOINCが互い違いに払い出
    されることがある設定
    日々の覚書: innodb_autoinc_lock_mode = 1 vs 2 でバルクインサートが競合した時のAUTO_INCREMENTの挙動が違う
    はなし

    52/63

    View Slide

  54. AUTO_INCREMENT vs. シーケンス
    MySQL 5.7とそれ以前では、mysqldの再起動でAUTOINCが巻き戻ったように見えるこ
    とがある
    mysqldの起動時に、各テーブルの MAX(auto_increment_column) + 1 で次に払い出される
    AUTOINCが再計算される
    何なら実はALTER TABLEでもオペレーションによっては再計算される Bug#94797

    INSERT .. + DELETE .. で消えたはずの行の番号が再利用されることがある

    8.0でこれは起こらなくなったけれども挙動としてはインコンパチなのでそれはそれで注意

    53/63

    View Slide

  55. AUTO_INCREMENT vs. シーケンス
    InnoDBの各セカンダリーインデックスはPRIMARY KEYの値をリーフに格納するの
    で、InnoDBでPRIMARY KEYをAUTO_INCREMENT にすると容量効率が良い
    逆にたとえばURLのQueryStringみたいなのが本物の一意識別子だったりするとしても、それを
    PRIMARY KEYにするとものすごく容量を持っていかれる

    PRIMARY KEYは AUTO_INCREMENT にしておいて、本来の一意識別子を NOT NULL UNIQUE に落と
    すと両立できる

    PRIMARY KEY以外, PRIMARY KEYでも先頭以外のカラムで AUTO_INCREMENT を使った
    り、BIGINT UNSIGNEDの限界近くまで使ったりはあんまり想定されていなさそう
    MySQL Bugs: #84186: mysqlpump couldn’t restore auto_increment column with secondary
    key

    MySQL Bugs: #87926: Incorrect auto_increment ids issued when close to ULLONG_MAX

    MySQL Bugs: #105310: slave stops with HA_ERR_KEY_NOT_FOUND when update the
    auto_increment column

    54/63

    View Slide

  56. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    55/63

    View Slide

  57. MySQLにご用心
    sql_mode
    エラーが出たからといって安易にAppから変更してはいけない

    5.7とそれ以降のデフォルトにしてようやく「フツー」

    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    56/63

    View Slide

  58. MySQLにご用心
    sql_mode
    暗黙の型変換
    残念ながらどうやっても無効化できない

    静的型がある言語ならPrepared Statementにbindする時点でバリデーションできる…?

    取り敢えず「日付型に +- 」はOracle使いには稀にだがよくあるらしい

    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    57/63

    View Slide

  59. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ルックアップだけじゃなくロックの粒度だったりする。インデックスは丁寧に

    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    58/63

    View Slide

  60. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    基本的には OFFSET を使わずに、欲しい行を引っ張ってこられるような仕組みを考える

    AUTO_INCREMENT vs. シーケンス
    59/63

    View Slide

  61. MySQLにご用心
    sql_mode
    暗黙の型変換
    ネクストキーロック
    ORDER BY .. LIMIT .. OFFSET
    AUTO_INCREMENT vs. シーケンス
    使い心地はまあまあ違うけれど、これはアプリからは比較的透過的に見えるかしら…

    巻き戻りには注意!!

    60/63

    View Slide

  62. MySQLが得意なこと/Oracleの方が得意そうなこと
    小さな結果セットを、インデックスを使い切って絞りだすのは得意
    インデックスを使い切らずに取り出したり、結果セットがそもそも大きいものはOracleの方が
    得意なはず

    それはそういうものだと割り切って、「じゃあ如何にMySQLが苦手な処理を得意な
    処理の側に手繰り寄せていくか」がMySQLと上手く付き合うのに必要な考え方だと
    思う
    明示的なテンポラリーテーブルを作るのとかもそう

    CREATE TEMPORARY TABLEは(吊るしのMySQLなら) read_only = ON なレプリカでも作成できる

    本気でMySQLを楽しむなら、MySQLは良くも悪くも柔軟すぎるので
    それを逆手に取ってレプリケーションで上手くゴニョるのが醍醐味
    レプリカでインデックス作って/データのサクッとスイッチオーバーするとか、そういうやつ

    61/63

    View Slide

  63. 参考
    MySQLとインデックスとPHPer
    MySQLとインデックスと私
    MySQLのALTER TABLEについて少々
    磯野ー、MySQLのロック競合を表示しようぜー
    Shooting a trouble of MySQL
    Dive into MySQL Error
    62/63

    View Slide

  64. Any Questions
    and/or
    Suggestions?
    63/63

    View Slide