読者です 読者をやめる 読者になる 読者になる

なからなLife

geekに憧れと敬意を抱きながら、SE、ITコンサル、商品企画、事業企画、管理会計、総務・情シス、再び受託でDB屋さんと流浪する人のブログです。

Oracle脳には馴染みの薄い、MySQLの「Collation」にまつわる挙動の話

MySQL

Collationとは

直訳すると「照合」。
MySQL的には、「照合順序」と訳されます。


ただでさえ面倒くさい文字コードの問題ですが、データを保存する際の文字コードとは別に、データを照合するときの方法を指定することができます。
照合って何かっていうと、=等での評価や、ソート順序の評価時に使われるものです。


大事なところなので繰り返しますが、「保存時の文字コードとは別」です。


どんなものがある?

200超ありますが、多くは
文字コードセット_照合種別」
で定義されています。


文字コードセットは、保存時の文字コードで使われる種類と同じで、40種類ほど。
日本語環境のシステム構築では、sjis,ujis(EUC-JP)やuft8、utf8mb4あたりの文字コードセットがよく見かけられると思います。


その後ろ、照合種別になると
_bin
_general_cs
_general_ci
_言語名_ci
_言語名_cs
binary(これだけ例外)
といったパターンになります。


ciとはcsというのは、それぞれ「Case Sensitive」「Case Insencitive」ということで、大文字小文字(Case)に過敏(Sensitive)≒区別するか否かを意味します。


MySQLの場合、_ciがデフォルトになっていることが多いです。

CI、つまり「大文字小文字を区別しない」が設定されている状態とは、どういうことか。

本題についてはここからです。
理屈については、MySQLのマニュアルを読んでいただくものとして、典型的な挙動を見ていただくのが早いと思います。


utf8mb4_general_ciを設定した環境を用意し、データを投入する所まで、準備します。

mysql> CREATE DATABASE test_collate_ci
    -> DEFAULT CHARACTER SET = 'utf8mb4' 
    -> DEFAULT COLLATE = 'utf8mb4_general_ci';
Query OK, 1 row affected (0.00 sec)

mysql> use test_collate_ci;
Database changed

mysql> CREATE TABLE test_tbl1(
    ->  col1 int,
    ->  col2 varchar(10),
    ->  PRIMARY KEY(col1)
    -> );
Query OK, 0 rows affected (0.02 sec)

mysql> SHOW CREATE TABLE  test_tbl1\G
*************************** 1. row ***************************
       Table: test_tbl1
Create Table: CREATE TABLE `test_tbl1` (
  `col1` int(11) NOT NULL DEFAULT '0',
  `col2` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> SELECT table_name,table_collation FROM information_schema.tables WHERE table_schema = 'test_collate_ci';
+------------+--------------------+
| table_name | table_collation    |
+------------+--------------------+
| test_tbl1  | utf8mb4_general_ci |
+------------+--------------------+
1 row in set (0.00 sec)

mysql> SELECT table_name,table_collation FROM information_schema.tables WHERE table_schema = 'test_collate_ci';
+------------+--------------------+
| table_name | table_collation    |
+------------+--------------------+
| test_tbl1  | utf8mb4_general_ci |
| test_tbl2  | utf8mb4_general_ci |
+------------+--------------------+
2 rows in set (0.00 sec)

mysql> INSERT INTO test_tbl1 (col1,col2) VALUES (1,'A'),(2,'B'),(3,'C'),(4,'D'),(5,'E');
Query OK, 5 rows affected (0.07 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> INSERT INTO test_tbl2 (col1,col2) VALUES (1,'A'),(2,'B'),(3,'C'),(4,'D'),(5,'E');
Query OK, 5 rows affected (0.01 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> INSERT INTO test_tbl2 (col1,col2) VALUES (6,'a'),(7,'b'),(8,'c'),(9,'d'),(10,'e');
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> select * from test_tbl1;
+------+------+
| col1 | col2 |
+------+------+
|    1 | A    |
|    2 | B    |
|    3 | C    |
|    4 | D    |
|    5 | E    |
+------+------+
5 rows in set (0.00 sec)

mysql> select * from test_tbl2;
+------+------+
| col1 | col2 |
+------+------+
|    1 | A    |
|    2 | B    |
|    3 | C    |
|    4 | D    |
|    5 | E    |
|    6 | a    |
|    7 | b    |
|    8 | c    |
|    9 | d    |
|   10 | e    |
+------+------+
10 rows in set (0.00 sec)

Collationの設定はSHOW CREATE~では確認できないことがあるので、あえてinformation_schemaを参照しました。

では、とりあえず文字列型で検索してみましょう。

mysql> select * from test_tbl2 where col2 = 'a';
+------+------+
| col1 | col2 |
+------+------+
|    1 | A    |
|    6 | a    |
+------+------+
2 rows in set (0.00 sec)

_ci、つまり「大文字小文字を区別しない」というのは、そういうことです。

ですから、結合なんかでも影響してきます。

mysql> select * from test_tbl1 as a JOIN test_tbl2 as b ON a.col2 = b.col2;
+------+------+------+------+
| col1 | col2 | col1 | col2 |
+------+------+------+------+
|    1 | A    |    1 | A    |
|    2 | B    |    2 | B    |
|    3 | C    |    3 | C    |
|    4 | D    |    4 | D    |
|    5 | E    |    5 | E    |
|    1 | A    |    6 | a    |
|    2 | B    |    7 | b    |
|    3 | C    |    8 | c    |
|    4 | D    |    9 | d    |
|    5 | E    |   10 | e    |
+------+------+------+------+
10 rows in set (0.00 sec)


キモいのは、MySQLはデフォルト任せに環境構築すると、この「大文字小文字区別なし」のモードで出来上がってしまうこと。
このテの話が高じて、「ハハパパ問題」「ビールと寿司が同じ問題」にも発展します。

blog.kamipo.net
blog.kamipo.net


Oracleの場合、デフォルトは大文字小文字を識別します。。
ていうか、さっき調べるまでOracleに環境レベルで「大文字小文字を区別しない照合設定」の状態を作れることを知りませんでした。
NLS_COMPとNLS_SORTでできるらしいです。


MySQLで大文字小文字を区別するには

Collation設定は「_bin」を使う。utf8mb4_binにすれば、大文字小文字だけでなく、ハハパパも寿司ビールも解決。


さっきのDDLを、もう一つutf8mb4_binで作ったスキーマに流し込んで確認。

mysql> CREATE DATABASE test_collate_bin
    -> DEFAULT CHARACTER SET = 'utf8mb4' 
    -> DEFAULT COLLATE = 'utf8mb4_bin';
Query OK, 1 row affected (0.00 sec)

mysql> USE test_collate_bin;
Database changed

mysql> CREATE TABLE test_tbl1(
    ->  col1 int,
    ->  col2 varchar(10),
    ->  PRIMARY KEY(col1)
    -> );
Query OK, 0 rows affected (0.06 sec)

mysql> CREATE TABLE test_tbl2(
    ->  col1 int,
    ->  col2 varchar(10),
    ->  PRIMARY KEY(col1)
    -> );
Query OK, 0 rows affected (0.02 sec)

mysql> SHOW CREATE TABLE test_tbl1\G
*************************** 1. row ***************************
       Table: test_tbl1
Create Table: CREATE TABLE `test_tbl1` (
  `col1` int(11) NOT NULL DEFAULT '0',
  `col2` varchar(10) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
1 row in set (0.00 sec)

mysql> SHOW CREATE TABLE test_tbl2\G
*************************** 1. row ***************************
       Table: test_tbl2
Create Table: CREATE TABLE `test_tbl2` (
  `col1` int(11) NOT NULL DEFAULT '0',
  `col2` varchar(10) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
1 row in set (0.00 sec)

mysql> SELECT table_name,table_collation FROM information_schema.tables WHERE table_schema = 'test_collate_bin';
+------------+-----------------+
| table_name | table_collation |
+------------+-----------------+
| test_tbl1  | utf8mb4_bin     |
| test_tbl2  | utf8mb4_bin     |
+------------+-----------------+
2 rows in set (0.00 sec)

mysql> SELECT table_name,table_collation FROM information_schema.tables WHERE table_schema = 'test_collate_bin';
+------------+-----------------+
| table_name | table_collation |
+------------+-----------------+
| test_tbl1  | utf8mb4_bin     |
| test_tbl2  | utf8mb4_bin     |
+------------+-----------------+
2 rows in set (0.00 sec)

mysql> INSERT INTO test_tbl1 (col1,col2) VALUES (1,'A'),(2,'B'),(3,'C'),(4,'D'),(5,'E');
Query OK, 5 rows affected (0.21 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> INSERT INTO test_tbl2 (col1,col2) VALUES (1,'A'),(2,'B'),(3,'C'),(4,'D'),(5,'E');
Query OK, 5 rows affected (0.09 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> INSERT INTO test_tbl2 (col1,col2) VALUES (6,'a'),(7,'b'),(8,'c'),(9,'d'),(10,'e');
Query OK, 5 rows affected (0.01 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> select * from test_tbl1;
+------+------+
| col1 | col2 |
+------+------+
|    1 | A    |
|    2 | B    |
|    3 | C    |
|    4 | D    |
|    5 | E    |
+------+------+
5 rows in set (0.00 sec)

mysql> select * from test_tbl2;
+------+------+
| col1 | col2 |
+------+------+
|    1 | A    |
|    2 | B    |
|    3 | C    |
|    4 | D    |
|    5 | E    |
|    6 | a    |
|    7 | b    |
|    8 | c    |
|    9 | d    |
|   10 | e    |
+------+------+
10 rows in set (0.00 sec)

mysql> select * from test_tbl2 where col2 = 'a';
+------+------+
| col1 | col2 |
+------+------+
|    6 | a    |
+------+------+
1 row in set (0.00 sec)

mysql> select * from test_tbl1 as a JOIN test_tbl2 as b ON a.col2 = b.col2;
+------+------+------+------+
| col1 | col2 | col1 | col2 |
+------+------+------+------+
|    1 | A    |    1 | A    |
|    2 | B    |    2 | B    |
|    3 | C    |    3 | C    |
|    4 | D    |    4 | D    |
|    5 | E    |    5 | E    |
+------+------+------+------+
5 rows in set (0.01 sec)

期待通りの動きになりました。

環境レベルの設定変更ができない場合

どうしても「ベースはci=大文字小文字区別なし」としたい環境で、特別に区別をしたい場合、個別のSQLの中で「binary」を使います。



最初に作ったutf8mb4_general_ciのスキーマに対して、以下のようにクエリを投げましょう。

mysql> USE test_collate_ci;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

mysql> select * from test_tbl1 as a JOIN test_tbl2 as b ON a.col2 = binary b.col2;
+------+------+------+------+
| col1 | col2 | col1 | col2 |
+------+------+------+------+
|    1 | A    |    1 | A    |
|    2 | B    |    2 | B    |
|    3 | C    |    3 | C    |
|    4 | D    |    4 | D    |
|    5 | E    |    5 | E    |
+------+------+------+------+
5 rows in set (0.00 sec)

通常の検索と同様、使い方を間違えるとINDEX効かなくなってしまうので気をつけましょう。


まとめ

・Collationは、保存文字コードではなく、「照合順序」。
MySQLは、デフォルトで「大文字小文字区別なし」になりがちなので、このCollation絡みでハマることがある。
・Collationの指定に「utf8mb4_bin」を指定すれば、大体の問題が片付く(暴論極論)
・Collationの指定を「大文字小文字区別なし」にできない場合、SQL単位で「BINARY」を指定することで、大文字小文字を区別できる。


どのレベルでCollationを設定できるか、的な話もソコソコ面倒くさい話になるので、別途エントリを起こそうかと思っています。

MySQLトラブルシューティング

MySQLトラブルシューティング


追記

Collationとパフォーマンスの関係について、公式ドキュメントに記述があるのを見つけたので、こちらに追記しておきます。

ciよりbinの方が速いって言ってますが、どれくらい違いがあるのかはわかりません。
特に走査対象テーブルに入っているデータが全く同じ場合、違いがあるんでしょうかね?

8.4.2.2 文字および文字列型の最適化

文字および文字列カラムの場合、次のガイドラインに従います。

言語固有の照合機能が必要でない場合は、比較およびソート操作を速くするため、バイナリ照合順序を使用します。特定のクエリー内でバイナリ照合順序を使用するには、BINARY 演算子を使用できます。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 8.4.2.2 文字および文字列型の最適化