GA

2018/10/25

MySQL 8.0.13の式インデックス

嬉し楽しい式インデックス。
PostgreSQLのこれが結構うらやましかった機能がついにMySQLにも!
MySQL 5.7からgenerated columnが入ってそのカラムにインデックスを張ればそれっぽい高速化は実現できたんだけれども、generated columnは如何せんORMと相性が悪いことがあって(ORMはそのカラムがgeneratedかbasicか特に気にしてくれないけど、generatedなカラムは更新しようとするとエラーになる、など)そういうケースではカラムを定義せずに式インデックスが使えるといいのに…と思っていたのでしたん。
という訳で式インデックス、定義の仕方はこちら。
ALTER TABLE .. ADD KEY ..CREATE INDEX .. の、普段なら (カラム名, ..) になっているところを ((式), ..) にするだけ。式そのものを括弧でくくるのを忘れずに。
mysql80 41> SELECT * FROM t1;
+------+-------+
| num  | val   |
+------+-------+
|    1 | one   |
|    2 | two   |
|    3 | three |
|    4 | four  |
|    5 | five  |
+------+-------+
5 rows in set (0.00 sec)

mysql80 41> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `num` int(11) NOT NULL,
  `val` varchar(32) COLLATE utf8mb4_ja_0900_as_cs DEFAULT NULL,
  PRIMARY KEY (`num`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_ja_0900_as_cs
1 row in set (0.00 sec)
こんなテーブルがあるじゃろ?
mysql80 41> EXPLAIN SELECT * FROM t1 WHERE num = 2;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | t1    | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql80 41> EXPLAIN SELECT * FROM t1 WHERE num + 1 = 3;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
num + 1 とか左辺に演算子がくるとインデックスが使えんじゃろ?
mysql80 41> ALTER TABLE t1 ADD KEY ((num + 1));
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql80 41> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `num` int(11) NOT NULL,
  `val` varchar(32) COLLATE utf8mb4_ja_0900_as_cs DEFAULT NULL,
  PRIMARY KEY (`num`),
  KEY `functional_index` (((`num` + 1)))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_ja_0900_as_cs
1 row in set (0.00 sec)

mysql80 41> EXPLAIN SELECT * FROM t1 WHERE num + 1 = 3;
+----+-------------+-------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys    | key              | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | t1    | NULL       | ref  | functional_index | functional_index | 8       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
関数インデックスを使うとそれが引けるんじゃ。
しかしこれ、オプティマイザーでたたんでくれるわけではなさそうなので、 WHEREORDER BY に出てくる式と一致しないといけないっぽい。
AS を使ったエイリアスに ORDER BY からアクセスするのはいけた。
mysql80 41> EXPLAIN SELECT * FROM t1 WHERE num + 2 - 1 = 3;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql80 41> EXPLAIN SELECT num + 1 AS c FROM t1 ORDER BY c;
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key              | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t1    | NULL       | index | NULL          | functional_index | 8       | NULL |    5 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
ユニークキーは作れたけど、全文検索、あまりに変な(?)関数、 NOW(), AUTO_INCREMENT な列に対する関数はエラーになって弾かれる。
mysql80 41> ALTER TABLE t1 ADD FULLTEXT KEY ((num + 3));
ERROR 3759 (HY000): Fulltext functional index is not supported.

mysql80 41> ALTER TABLE t1 ADD KEY ((SLEEP(1)));
ERROR 3758 (HY000): Expression of functional index 'functional_index_4' contains a disallowed function.

mysql80 41> ALTER TABLE t1 ADD KEY ((RAND(1)));
ERROR 3758 (HY000): Expression of functional index 'functional_index_4' contains a disallowed function.

mysql80 41> ALTER TABLE t1 ADD KEY ((NOW(1)));
ERROR 3758 (HY000): Expression of functional index 'functional_index_4' contains a disallowed function.
個人的にはアレかな、 idx_status ((status IN (1, 2, 3)), (activate <> 1)) みたいな式インデックスを作ると捗るかなと思いました!

MySQL 8.0.13でカラム定義のDEFAULTに関数が指定できるようになった

みんなだいすき DEFAULT がついに関数を指定できるようになった。
8.0.12とそれ以前はリテラルのみが指定可能、例外として TIMESTAMP, DATETIME 型の CURRENT_TIMESTAMP のみだった。
記法は .. DEFAULT ( expression ) で、 DEFAULT のあとに括弧を入れてから関数なり表現なりを書く。
The MySQL 8.0.13 Maintenance Release is Generally Available | MySQL Server Blog に書いてある↓をそのまま試そうとしても、括弧が抜けているので通らない。。
mysql80 40> CREATE TABLE t2 (a BINARY(16) DEFAULT uuid_to_bin(uuid()));
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'uuid_to_bin(uuid()))' at line 1

mysql80 40> CREATE TABLE t2 (a BINARY(16) DEFAULT (uuid_to_bin(uuid())));
Query OK, 0 rows affected (0.11 sec)
( ´-`).oO(MySQL Server Teamのブログではまれにだがよくあること
mysql80 40> CREATE TABLE t1 (num serial, val varchar(32), val_len int NOT NULL DEFAULT (CHARACTER_LENGTH(val)));
Query OK, 0 rows affected (0.10 sec)

mysql80 40> INSERT INTO t1 (num, val) VALUES (1, 'one');
Query OK, 1 row affected (0.07 sec)

mysql80 40> SELECT * FROM t1;
+-----+------+---------+
| num | val  | val_len |
+-----+------+---------+
|   1 | one  |       3 |
+-----+------+---------+
1 row in set (0.00 sec)
しかしこれ迂闊にNOT NULLなカラムのデフォルトにNULLアンセーフな関数とNULLABLEなカラムの値を組み合わせるとおかしなことになった。
mysql80 40> INSERT INTO t1 (num, val) VALUES (2, NULL);
Query OK, 1 row affected, 1 warning (0.06 sec)

mysql80 40> SHOW WARNINGS;
+-------+------+---------------------------------+
| Level | Code | Message                         |
+-------+------+---------------------------------+
| Error | 1048 | Column 'val_len' cannot be null |
+-------+------+---------------------------------+
1 row in set (0.00 sec)

mysql80 40> SELECT * FROM t1;
+-----+------+---------+
| num | val  | val_len |
+-----+------+---------+
|   1 | one  |       3 |
|   2 | NULL |       0 |
+-----+------+---------+
2 rows in set (0.00 sec)
CHARACTER_LENGTH(NULL)NULL なので、NOT NULLな val_len のデフォルト値が NULL になるというなんか地獄のように矛盾した結果、INT型のフォールバック先である0に落ち着いた様子。
ただこれ STRICT_TRANS_TABLES の状態でこの動作になっちゃうのでちょっとあんまり嬉しくない(同じことをデフォルト値を使わずにやるとちゃんとエラーになるのに…)
mysql80 40> SELECT @@sql_mode;
+-----------------------------------------------------------------------------------------------------------------------+
| @@sql_mode                                                                                                            |
+-----------------------------------------------------------------------------------------------------------------------+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql80 40> INSERT INTO t1 (num, val, val_len) VALUES (3, NULL, CHARACTER_LENGTH(NULL));
ERROR 1048 (23000): Column 'val_len' cannot be null
使い道があんまり思いつかない(generated columnでもいいケースがほとんど?)けれど、ブログのサンプルにもあった uuid_to_bin(uuid()) を使って時間経過順に並ぶ形式にしたUUIDをサロゲートキーにする、とかとても上手く使えそう。

2018/10/24

utf8mb4_0900_ai_ciは "=" と "≠" を同じ文字だと思っている

TL;DR


MySQL 8.0の utf8mb4 のデフォルト照合順序として utf8mb4_0900_ai_ci というのがあって( default_collation_for_utf8mb4 で多少は変えられる)、これは kamipoのハハ=パパ問題 を引き起こす照合順序として日本人には知られている(と思う。といいな。広まれ!)
で、その utf8mb4_0900_ai_ci が今度はイコール=ノットイコール問題を発症したらしい。
mysql80 34> SELECT '=' = '≠';
+-------------+
| '=' = '≠'   |
+-------------+
|           1 |
+-------------+
1 row in set (0.00 sec)
…はは。
ちょっと気になって調べてみた感じ、どうもUCA準拠の照合順序では 最初っからそうだったっぽい
MySQL 5.0ですらこれは引っ掛かる。
mysql50> SELECT '=' = '≠' COLLATE utf8_unicode_ci;
+-------------------------------------+
| '=' = '≠' COLLATE utf8_unicode_ci   |
+-------------------------------------+
|                                   1 |
+-------------------------------------+
1 row in set (0.00 sec)

mysql50> SELECT '=' = '≠' COLLATE utf8_general_ci;
+-------------------------------------+
| '=' = '≠' COLLATE utf8_general_ci   |
+-------------------------------------+
|                                   0 |
+-------------------------------------+
1 row in set (0.00 sec)
今まで暗黙のデフォルトが「引っ掛からない」方だったから気にならなかった、というだけのアレだったようですね。
ちなみにどんな文字が同一視されるか、については 日本MySQLユーザ会 の代表えもんこと @tmtms さんがかなり昔にまとめてくれていましたね。
ありがたやありがたや(*-人-)

MySQL 8.0.13とそれ以降で「パスワード変更の際に今のパスワードを入力させる」オプション

TL;DR

  • ドキュメントの password_require_current だけ読むとちょっと足りなくて、実際にはこんな判定
if (mysqlスキーマへのUPDATE権限 || CREATE USER権限)
  return パスワード確認不要;
else
{
  if (global.password_require_currentがON || そのアカウントが mysql.user.Password_require_current = 'Y' になっている)
    return パスワード確認必要;
  else
    return パスワード確認不要;
}
  • パスワード確認必要な場合、パスワードを変えるようなステートメントの最後に REPLACE '元のパスワード' をつける
    • 対話的に聞かれるわけではない
    • REPLACE をつけないと MySQL error code MY-013226 (ER_MISSING_CURRENT_PASSWORD): Current password needs to be specified in the REPLACE clause in order to change it. と言われる
  • これをよく読むと実はちゃんと書いてある

最初、ずっと root@localhost でやってて全然反映されないな…と思ってたら、 特定の権限がある場合はこれ効かないのであった。一般ユーザーのみ。
mysql80 28> SELECT user, host, password_require_current FROM mysql.user WHERE user = 'yoku0825';
+----------+------+--------------------------+
| user     | host | password_require_current |
+----------+------+--------------------------+
| yoku0825 | %    | NULL                     |
+----------+------+--------------------------+
1 row in set (0.00 sec)
mysql.user.Password_require_currentの取りうる値はNULL(NULLは値じゃない…けど取り敢えず許して),Y,N` 。
Y は「REPLACE句がないとエラー」、 N は「REPLACE句はあってもなくても良い」(ただし、REPLACE句を書いた上で元のパスワードを間違えるとエラー)、 NULL は「 global.password_require_current の値に従う」。
ユーザー作成時の暗黙のデフォルトは NULL
Y, N, NULL に対応する ALTER USER ステートメントはこんな感じで、こっちの句を見るとそれぞれ要求する、あってもなくてもいい、システム設定に従う、みたいな感じがして良い。
mysql80 28> ALTER USER yoku0825 PASSWORD REQUIRE CURRENT; -- 'Y'にする
Query OK, 0 rows affected (0.04 sec)

mysql80 28> SELECT user, host, password_require_current FROM mysql.user WHERE user = 'yoku0825';
+----------+------+--------------------------+
| user     | host | password_require_current |
+----------+------+--------------------------+
| yoku0825 | %    | Y                        |
+----------+------+--------------------------+
1 row in set (0.00 sec)

mysql80 28> ALTER USER yoku0825 PASSWORD REQUIRE CURRENT OPTIONAL; -- 'N'にする
Query OK, 0 rows affected (0.03 sec)

mysql80 28> SELECT user, host, password_require_current FROM mysql.user WHERE user = 'yoku0825';
+----------+------+--------------------------+
| user     | host | password_require_current |
+----------+------+--------------------------+
| yoku0825 | %    | N                        |
+----------+------+--------------------------+
1 row in set (0.00 sec)

mysql80 28> ALTER USER yoku0825 PASSWORD REQUIRE CURRENT DEFAULT; -- NULLにする
Query OK, 0 rows affected (0.07 sec)

mysql80 28> SELECT user, host, password_require_current FROM mysql.user WHERE user = 'yoku0825';
+----------+------+--------------------------+
| user     | host | password_require_current |
+----------+------+--------------------------+
| yoku0825 | %    | NULL                     |
+----------+------+--------------------------+
1 row in set (0.00 sec)
最初のサンプルっぽいところで パスワード確認必要 になる組み合わせで、REPLACE句を指定しないでパスワードを触ろうとするとこうなる。
mysql80 31> SET PASSWORD = 'yoku0825';
ERROR 13226 (HY000): Current password needs to be specified in the REPLACE clause in order to change it.

mysql80 31> SET PASSWORD = 'yoku0825' REPLACE 'old_password';
Query OK, 0 rows affected (0.03 sec)
ただし、権限があるアカウントでも パスワード確認不要 の組み合わせでも、もとのパスワードを間違えるとこうなる。
mysql80 31> SET PASSWORD = 'yoku0826' REPLACE 'wrong_password';
ERROR 13225 (HY000): Incorrect current password. Specify the correct password which has to be replaced.

2018/10/23

MySQL 8.0.13の新機能でPRIMARY KEYのないテーブルを作成させない

TL;DR

  • sql_require_primary_key サーバー変数をONにすると、PRIMARY KEYのないテーブルを作ろうとした時にエラーにできる。
    • セッションスコープとグローバルスコープと両方あるやつで、実効値はセッションスコープなので注意。
    • ただし、 SET SESSION .. でも一般ユーザーでは値を変更することはできない( sql_log_bin とかもそうですね)
  • 超便利だ!! 秘伝のタレに入れる時は loose プレフィックスとかもいいと思うよ!!

取り敢えず基本的な使い方として、0(OFF)と1(ON)の時の動作の違い。
mysql80 9> SELECT @@sql_require_primary_key;
+---------------------------+
| @@sql_require_primary_key |
+---------------------------+
|                         0 |
+---------------------------+
1 row in set (0.00 sec)

mysql80 9> CREATE TABLE t1 (num int);
Query OK, 0 rows affected (0.04 sec)

mysql80 9> SET @@session.sql_require_primary_key= 1;
Query OK, 0 rows affected (0.00 sec)

mysql80 9> SELECT @@sql_require_primary_key;
+---------------------------+
| @@sql_require_primary_key |
+---------------------------+
|                         1 |
+---------------------------+
1 row in set (0.00 sec)

mysql80 9> CREATE TABLE t2 (num int);
ERROR 3750 (HY000): Unable to create a table without PK, when system variable 'sql_require_primary_key' is set. Add a PK to the table or unset this variable to avoid this message. Note that tables without PK can cause performance problems in row-based replication, so please consult your DBA before changing this setting.
おお、マニュアル通り。
Note that tables without PK can cause performance problems in row-based replication, so please consult your DBA before changing this setting. にちょっとニヤリとするwww
しかし、セッションとグローバルと両方持ってるってことは一般ユーザーで値書き換えられちゃうんでは? と思ったけど、それができない類の変数になっていた(昔から sql_log_bin とかもセッション変数だけど一般ユーザーは変更できないのでそれと同じところを通ってるのであろう)
mysql80 10> SHOW GRANTS;
+--------------------------------------------------+
| Grants for yoku0825@%                            |
+--------------------------------------------------+
| GRANT USAGE ON *.* TO `yoku0825`@`%`             |
| GRANT ALL PRIVILEGES ON `d1`.* TO `yoku0825`@`%` |
+--------------------------------------------------+
2 rows in set (0.00 sec)

mysql80 10> SET @@session.sql_require_primary_key= 1;
ERROR 1227 (42000): Access denied; you need (at least one of) the SUPER or SYSTEM_VARIABLES_ADMIN privilege(s) for this operation
これで勝手にPK必須が剥がれる心配もなく。
ちなみに、これがONの時にどれくらいPKを強制されるかというと
mysql80 11> OPTIMIZE TABLE t2;
+-------+----------+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Op       | Msg_type | Msg_text                                                                                                                                                                                                                                                                                                      |
+-------+----------+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| d1.t2 | optimize | note     | Table does not support optimize, doing recreate + analyze instead                                                                                                                                                                                                                                             |
| d1.t2 | optimize | error    | Unable to create a table without PK, when system variable 'sql_require_primary_key' is set. Add a PK to the table or unset this variable to avoid this message. Note that tables without PK can cause performance problems in row-based replication, so please consult your DBA before changing this setting. |
| d1.t2 | optimize | status   | Operation failed                                                                                                                                                                                                                                                                                              |
+-------+----------+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
3 rows in set, 1 warning (0.00 sec)

mysql80 11> ALTER TABLE t2 ADD val varchar(32);
ERROR 3750 (HY000): Unable to create a table without PK, when system variable 'sql_require_primary_key' is set. Add a PK to the table or unset this variable to avoid this message. Note that tables without PK can cause performance problems in row-based replication, so please consult your DBA before changing this setting.

mysql80 11> ALTER TABLE t2 ADD KEY(num);
ERROR 3750 (HY000): Unable to create a table without PK, when system variable 'sql_require_primary_key' is set. Add a PK to the table or unset this variable to avoid this message. Note that tables without PK can cause performance problems in row-based replication, so please consult your DBA before changing this setting.

mysql80 11> INSERT INTO t2 VALUES (2);
Query OK, 1 row affected (0.01 sec)
ALTER TABLE 関連( OPTIMIZE TABLE もInnoDBでは ALTER TABLE にマッピングされるので同類とする)は全滅。
Sql_cmd_alter_table::execute -> mysql_alter_table -> create_table_impl -> mysql_prepare_create_table でエラーになるので、 ALTER TABLE だけが引っ掛かりそうだしDMLは影響を受けなさそう。
ちなみにストレージエンジン問わないので、ONにしたままだとCSVストレージエンジンは身動きが取れない(*ノ∀ノ)
mysql80 12> CREATE TABLE t3 (num int) Engine= CSV;
ERROR 3750 (HY000): Unable to create a table without PK, when system variable 'sql_require_primary_key' is set. Add a PK to the table or unset this variable to avoid this message. Note that tables without PK can cause performance problems in row-based replication, so please consult your DBA before changing this setting.

mysql80 12> CREATE TABLE t3 (num int, PRIMARY KEY(num)) Engine= CSV;ERROR 1069 (42000): Too many keys specified; max 0 keys allowed