寿司とビールについて話し合いをしてきました

こんにちわ。せじまです。
さいきんの kernel について調べてたら、俄に Chromebook への興味が湧いてきたので、遅まきながら C302CA ポチってみました。わたしにとって人生初 Core M ということもあって、早く届かないかなと心待ちにしている今日このごろです。

はじめに

MySQL5.7以前でおそらく最も有名な問題の一つに、Sushi-Beer issue of MySQL with utf8mb4 というものがあります。
忙しい人のために三行でまとめますと

  • MySQL は character-set に utf8mb4 を指定すると、寿司やビールなどの絵文字を扱える。
  • ただ、Collation(照合順序) が utf8mb4_general_ci や utf8mb4_unicode_ci だと、絵文字を区別できない(寿司とビールの絵文字を区別できない)。
  • utf8mb4_bin や utf8mb4_unicode_520_ci だと寿司とビールの絵文字を区別できるが、 utf8mb4_unicode_520_ci だと、 "ハハ" と "パパ" が区別できない。

といった問題があります。
これについて、 MySQL8.0 では UTF8 のサポートを改善する計画があると Sushi = Beer ?! An introduction of UTF8 support in MySQL 8.0 でアナウンスされていたのですが、
先日ご縁がありまして、このアナウンスをされた Manyi Lu さんとお話する機会がありましたので、そのときにお話したことなどについて、かるく触れさせていただこうかと思います。

MySQL 8.0 での UTF-8 サポートについて

MySQL8.0.1 から utf8mb4_ja_0900_as_cs が追加されました。yoku0825さんのまとめがわかりやすいのですが、補足しますと、utf8mb4_ja_0900_as_cs は

  • ひらがな・カタカナはセンシティブではない(例:「ア」と「あ」はマッチする)
  • 拗音は区別できる(例:「びょういん」と「びよういん」はマッチしない。区別される)
  • 濁点と半濁点は区別できる(例:「ハハ」と「パパ」はマッチしない。区別される)
  • 半角全角はセンシティブではない(例:「はは」と「ハハ」はマッチする)
  • 絵文字は区別できる(例:寿司の絵文字とビールの絵文字はマッチしない。区別される)

と、日本語圏の人々にとって素晴らしい Collation なのですが、一つ落とし穴があります。

  • アルファベットの大文字小文字を区別する(例:「a」と「A」が区別される)

MySQL5.7 以前にアルファベットの大文字小文字を区別しない Collation を使っていて、MySQL8.0 以降で utf8mb4_ja_0900_as_cs を使うと、ここでハマる可能性があります。
これについて Manyi さんらや、その場に同席されてた他のエンジニアの方々と議論させていただいたのですが、確かに、「よ」と「ょ」は文字的には大文字小文字であり、ひらがなやカタカナで大文字小文字を区別するならば、アルファベットでも大文字小文字を区別するのが自然だと言われれば、それはその通りという気がします。

くわしくいうと

ちなみに、 MySQL 8.0.1: Accent and case sensitive collations for utf8mb4 で次のような説明がされており

  • MySQL8.0.1 で ai_ci collations (accent insensitive, case insensitive) や as_cs collations (accent sensitive, case sensitive) という Collation が導入されました
  • これらの Collation では DUCET(Default Unicode Collation Element Table) にもとづいてソートされます
  • ai_ci では DUCET で定義されている primary weight だけでソートしますが、 as_cs では primary, secondary, そして tertiary weight も見てソートします

さらに、 MySQL 8.0.1: Japanese collation for utf8mb4

  • ひらがなやカタカナやいろいろあるので、日本語のソートや照合順序は複雑です。
  • utf8mb4_ja_0900_as_cs では、 CLDR(Common Locale Data Repository) で定義されてるようにソートします。
  • ただ、日本語でカタカナとひらがなのソートを厳密にやろうとすると、 quaternary level(4次レベル) まで比較しないといけないのですが、 CLDR が定義しているデフォルトの照合強度は tertiary level(3次レベル) まで見て(primary, secondary, そして tertiary weight も見て)ソートすることになっているので、現状は quaternary level まで見ていません。 tertiary level まで見ています。

といった解説がされております。 quaternary level まで見ないで tertiary level まで見るようにしているのはレベルが高次に成れば成るほど、ソートや照合の処理が重くなってしまうからでしょう。

UCA とか DUCET とか CLDR とか何やらむつかしいですが、わたしの個人的な雑な理解ですと

  • MySQL は UTF-8 の文字列を照合したりソートしたりするとき、 Unicode の仕様の一部である UCA(Unicode collation algorithm) にもとづいている。
  • DUCET(Default Unicode Collation Element Table) は Unicode で規定された、照合のためのデフォルトのテーブルだが、 DUCET は言語ごとにカスタマイズ可能である。 CLDR(Common Locale Data Repository) にはカスタムテーブルの一部が含まれている。
  • MySQL8.0.1 は最新の Unicode 9.0.0 、 UCA 9.0.0 にもとづいてソートや照合を行えるようになった。それが as_cs collations である。
  • 日本語は複雑なので、 utf8mb4_ja_0900_as_cs では CLDR で定義されたカスタムテーブルも参照している。
  • ただ、utf8mb4_ja_0900_as_cs は3次レベルまでしか比較してないので(4次レベルまで比較してないので)、結果として、ひらがなとカタカナがセンシティブではない

といったものなのですが、もし間違ってたらどなたかご指摘お願いします。

utf8mb4_ja_0900_as_cs の 0900 は、 UCA 9.0.0 の 900 ということでしょう。UCA9.0.0通りにやるんだという、強い意志を感じさせます。
MySQL の挙動が Unicode collation algorithm の通りになってくれれば、我々はMySQLのソースコードを解析してソートなどの挙動を調べる必要がなく、(込み入ったことを調べたいときは)Unicode collation algorithm の仕様を理解すれば良くなるので、移植性の観点からも有利と言えるのではないでしょうか。

MySQL の UTF-8 サポートについてお願いしたこと

わたしから Manyi さんに説明してお願いしたことを列挙すると、次のようなものになります。

  • 私たちが utf8mb4 を使っている理由の一つは、iPhone などのスマートフォンから 4byte の UTF-8 の絵文字を入力できるからです。ユーザが入力してくるデータを、我々はデータベースに INSERT し、適切に扱えるようにする必要があります。
  • MySQL が utf8mb4 をサポートしているのは、 iPhone や Android 向けにサービスを提供している私たちとしては、 MySQL を使う強いモチベーションの一つと言えます。
  • MySQL が strict に Unicode を扱ってくれることを望みます。日本のエンジニアは、かつて CP932 などでめんどくさい思いをしました。
  • 日本語の Collation は、使う人によって求める答えが異なってしまうと思います。 MySQL は Collaion を XML で拡張できるようになっているので、 MySQL にdefaultで組み込まれる Collaction は、strict に Unicode を扱ってくれれば良いのではないでしょうか。
  • MySQL 8.0.1 で Unicode 9.0.0 対応してくれたことを歓迎します。
  • 今後、 iOS や Android で扱える絵文字が増えていくとしたならば、 MySQL がそれらをネイティブサポートしてくれるのは、 我々がMySQL のバージョンアップを続けていく大きな理由になりえます。 Apple や Google がスマートフォンで使える絵文字を増やしていったとき、MySQLがそれに追随してくれると助かります。

かつて、日本の文字コードはいろいろ大変でした

わたしはこの分野において素人なんでえらそうなことは言えませんが、かつて、日本語の文字コードは、互換性においてかなり大変でした。
パッと思いつくだけでも

  • CP932 の wave dash を UTF-8 の wave dash に上手く変換できない
  • フィーチャーフォン時代の携帯電話で扱われた絵文字は、事業者ごとに仕様が異なる機種依存文字であった

といった問題がありました。これらに対して、かつてエンジニアは、自前で変換テーブル用意して、バイナリ読んで変換したりしていました。
お客様が入力した文字列を表示するためだけに、そういった地道な苦労があったわけですが、
iOS も Android も MySQL も、すべて Unicode の仕様に則ってソートや照合をしてくれれば、エンジニアは手間を掛けずに、同じ絵文字を表示し、同じ順序でソートできるようになるのです。
これからも Unicode に新しい文字は追加されていくと思いますが、そうなってくれると良いなと、心から願ってやみません。

おまけ(MySQL8.0のUUIDサポートについて)

Manyi さんが担当されてる機能は多岐にわたり、MySQL8.0のUTF-8サポート以外にもいろいろお話できたのですが MySQL 8.0 の UUID サポートについて改善があったので、それについても触れておきたいと思います。

MySQL5.7 以前の InnoDB で UUID を Primary Key にすると、次のデメリットがありました

  • MySQL は組み込み型で UUID 型をサポートしていなかったので、 128bit の数値型ではなく文字列として保存しなければならないので、 Primary Key にするにはデータとして大きい
  • MySQLが生成するUUIDは UUID version 1 で、 timestamp を含むのだが、 上位ビットにナノ秒が格納されるため、InnoDB の Clustered Index と極めて相性が悪い性質がある

それに対し、 MySQL8.0 で UUID_TO_BIN(string_uuid, swap_flag) という関数ができたのですが、これがなかなかスグレモノです。 UUIDの文字列を binary format に変換することも可能ですし、 swap_flag を true にすると

If swap_flag is 1, the format of the return value differs: The time-low and time-high parts (the first and third groups of hexadecimal digits, respectively) are swapped. This moves the more rapidly varying part to the right and can improve indexing efficiency if the result is stored in an indexed column.

といった機能があるため、 binary に変換するとき、最上位ビットにナノ秒が格納されないため、 Clustered Index で使ったとしても、フラグメントしにくくなりました。
MySQL 8.0 以降では、 UUID_TO_BIN(string_uuid, swap_flag) を使うならば、 UUID を Primary Key として使うことも検討して良いかもしれません。

おわりに

MySQL開発チームは、かなり真摯に日本語の問題に取り組んでくれてるみたいですし、(元々InnoDBと相性の悪かった)UUIDを Primary Key として使いたいという要望についても、現実的な解決策を MySQL8.0 で取り込もうとしてくれています。
かつて MySQL5.7 の DMR(Development Milestone Release) のとき、 booking.com が積極的に評価し、いろいろフィードバックされたという話がありました。
現在、 MySQL8.0 は DMR の状態で、ユーザからの要望を受け入れやすい段階にありますので、 MySQL 8.0 への要望がある方は、今のうちに評価して要望を上げておくと、みんなハッピーになれるかもしれません。

References