MySQLは設定によってはVARCHARの比較時にアルファベットの大文字小文字を区別しない

最近MySQLのVARCHARがデフォルトの設定で大文字と小文字を区別してくれていない事を知らずにバグを仕込んでしまったので軽く押さえてみた

検証

Dockerコンテナ

$ docker run -it --name mysql -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0
$ docker exec -it mysql bash

MySQLにログインしてテーブル作成

$ mysql -uroot -p
$ CREATE DATABASE sample_db;
$ CREATE TABLE sample_db.user (id int, name varchar(10));

nameを大文字でレコードを登録

$ INSERT INTO sample_db.user VALUES(1,'JHON');

WHEREでnameを指定して絞り込み

大文字

$ SELECT id,name FROM sample_db.user WHERE name = 'JHON';
+------+------+
| id   | name |
+------+------+
|    1 | JHON |
+------+------+
1 row in set (0.00 sec)

小文字

$ SELECT id,name FROM sample_db.user WHERE name = 'jhon';
+------+------+
| id   | name |
+------+------+
|    1 | JHON |
+------+------+
1 row in set (0.00 sec)

whereでの指定が小文字のjhonにも関わらずnameが大文字のJHONであるレコードがselectされる

なぜこんなことになるかドキュメントをひいてみる

10.1 一般の文字セットおよび照合順序

文字セットとは、記号とエンコーディングのセットです。 照合順序とは、文字セット内の文字を比較するためのルールを集めたものです。 架空の文字セットを例にして、文字セットと照合順序の違いを見てみましょう。

B.3.4.1 文字列検索での大文字/小文字の区別

非バイナリ文字列の場合 (CHARVARCHARTEXT)、文字列検索では比較オペランドの照合順序が使用されます。 バイナリ文字列 (BINARYVARBINARYBLOB) の場合、比較ではオペランド内のバイトの数値が使用されます。つまり、アルファベット文字の場合、比較では大文字と小文字が区別されます。

B.3.4.1 文字列検索での大文字/小文字の区別

デフォルトの文字セットおよび照合順序は utf8mb4 および utf8mb4_0900_ai_ci であるため、非バイナリ文字列比較ではデフォルトで大文字と小文字が区別されません。 これは、_`col_name`_ LIKE 'a%' を使用して検索した場合、A または a で始まるすべてのカラム値が取得されることを意味します。

かいつまんで言うとMySQLでは文字を扱うために文字セットと文字を比較するための照合順序(collate)の設定があってクエリを実行した際の結果はこの設定に左右されるらしいが、デフォルトでは大文字と小文字が区別されない設定になっているようだ。

ただし非バイナリ文字列比較とあるのでBINARY演算子によるCASTを試してみる。

CAST付きの絞り込み

小文字

SELECT id,name FROM sample_db.user WHERE BINARY name = 'jhon';
Empty set, 1 warning (0.00 sec)

大文字

SELECT id,name FROM sample_db.user WHERE BINARY name = 'JHON';
+------+------+
| id   | name |
+------+------+
|    1 | JHON |
+------+------+
1 row in set, 1 warning (0.00 sec)

今度は小文字ではヒットしなくなった

またクエリの実行時にcollateを指定する事も出来る

collateを使用した比較

まずは文字セットを指定したいcollateに対応したものに設定しなおす

SET character_set_connection=utf8mb4;

小文字

mysql> SELECT id,name FROM sample_db.user WHERE name = 'jhon' COLLATE utf8mb4_0900_bin;
Empty set (0.01 sec)

大文字

mysql> SELECT id,name FROM sample_db.user WHERE name = 'JHON' COLLATE utf8mb4_0900_bin;
+------+------+
| id   | name |
+------+------+
|    1 | JHON |
+------+------+
1 row in set (0.00 sec)

テーブルが使用するcollateの設定ごと変えてやればクエリごとに指定をしなくても良くなる

テーブルの設定

現在の設定の確認

SELECT TABLE_COLLATION FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sample_db' AND TABLE_NAME='user';
+--------------------+
| TABLE_COLLATION    |
+--------------------+
| utf8mb4_0900_ai_ci |
+--------------------+
1 row in set (0.00 sec)

collateの変更

ALTER TABLE sample_db.user COLLATE utf8mb4_0900_bin;

create table時にcollateを指定しても良い

CREATE TABLE sample_db.user (id int, name varchar(10)) COLLATE utf8mb4_0900_bin;

ただテーブルごとの設定もそれぞれに付与するのは手間だしそのデータベースを扱うサービス全体で大文字と小文字を区別して欲しいことも多いだろうから、その場合データベース単位で設定しまう方がラクだろう。

データベースの設定

ALTER DATABASE sample_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
CREATE DATABASE sample_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;

最後に

今まで既存のアプリケーションの改修が多かったことも原因かもしれないがが、MySQLを触りだして数年たつのにこの事を知らずに結構衝撃だった。 幸いリリース前にチーム内の人が指摘してくれてことなきを得たが、調べても最初BINARYのキャストの話が多く出てきてピンと来ずcollateという馴染みの薄い単語覚えておかないと調べるのに少し不便だったので記録を残しておく

おまけ

文字セットの確認

SHOW VARIABLES LIKE "chara%";
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| character_set_client     | latin1                         |
| character_set_connection | latin1                         |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | latin1                         |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8mb3                        |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.01 sec)