MySQLの文字コード事情

この前 MySQL Casual に登壇して、「MySQLの文字コード事情」と称して発表してきました。

終電の都合で途中退席しましたが楽しかったです。また機会があれば参加したいです。

発表スライドはこちら

www.slideshare.net

以下、補足のような何か。

「Charset≒エンコーディング (MySQLに限らない)」

英語版のWikipediaでもcharsetCharacter encoding にリダイレクトされます。

自分がcharsetという用語に出会ったのはおそらくメールのContent-Typeヘッダが初めてだったと思います。 今ではメールだけではなくHTTPのヘッダでも使用されています。

なお、CharsetはInternet Assigned Numbers Authority(IANA)という組織で管理されています。http://www.iana.org/assignments/character-sets/character-sets.xhtml

ujis

MySQLで日本語を使用できるようになった時にEUC-JPエンコーディングのcharsetの名前をujisとしてしまったのは自分です。ゴメンナサイ。

いや、今でこそEUC-JPの方がメジャーでujisなんてもう見かけないんですけど、1998年当時はeucJPとujisのどちらも使われたんですよ。(あと ujis と sjis で韻を踏んでいていいかなーとか…)

EUCは Extended Unix Code の略で、ujisは Unixized JIS の略です。ググってみたら ujis の方が EUC-JP よりも歴史は古いようですね。

「歴史的には、まず「日本語EUC」の元 (ujis) があって、その工夫を I18N 的枠組に拡張したものが EUC」

http://naruse.hateblo.jp/entry/20090308/1236517235

「ふつうはutf8mb4」

とスライドには書きましたが、cp932を使うメリットも無いことはないです。

UTF-8は日本語の文字は普通3バイトですがCP932は2バイトです。 つまりディスクやメモリの消費量がUTF-8に比べて2/3で済むということです。

扱える文字が Windows-31J の範囲の文字だけで良くて、少しでも資源を節約したいのであれば、cp932を使用するのもいいかもしれません。

🍣=🍺問題とCollation

伝統的にMySQLは標準で大文字小文字を区別しないので、ci(Case Insensitive)を使いたくなってしまうのですけど、PostgreSQL とかは普通に大文字小文字は別の文字扱いだし、実は MySQL も utf8mb4_bin でも全然問題ないのかもしれません。

utf8mb4_bin であれば🍣=🍺問題も、ハハ=パパ問題も発生しませんし。

‘🍣’=‘🍺’=‘�’

MySQLで同じ文字とみなされるかどうかは WEIGHT_STRING() という関数の戻り値が同じかどうかで確かめられます。

SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;
SELECT HEX(WEIGHT_STRING('🍣'));  -- => FFFD
SELECT HEX(WEIGHT_STRING('🍺'));  -- => FFFD
SELECT HEX(WEIGHT_STRING('�'));  -- => FFFD

SET NAMES utf8mb4 COLLATE utf8mb4_unicode_520_ci;
SELECT HEX(WEIGHT_STRING('🍣'));  -- => FBC3F363
SELECT HEX(WEIGHT_STRING('🍺'));  -- => FBC3F37A

SET NAMES utf8mb4 COLLATE utf8mb4_bin;
SELECT HEX(WEIGHT_STRING('🍣'));  -- => 01F363
SELECT HEX(WEIGHT_STRING('🍺'));  -- => 01F37A

utf8mb4_bin の場合は Unicode のコードポイントがそのまま使用されるようです。

「'パ'=‘パ'」と「'パ’ LIKE ‘パ'」

半角カナの「パ」は「ハ」と「゚」の二文字で構成されていますが、unicode_ci では一文字の「パ」と一致します。

SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
SELECT 'パ'='パ'; -- => 1

ですが、LIKE では一致しません。

SELECT 'パ' LIKE 'パ'; -- => 0

SQL 標準では、LIKE は文字ごとに一致を実行するため、= 比較演算子とは異なる結果が生成される可能性があります。

https://dev.mysql.com/doc/refman/5.6/ja/string-comparison-functions.html#operator_like

…ということのようです。

Unicode Collation Algorithm

そのうち書きたい(書くとは言ってない)。

書いた

ThinkPad T460s 購入

ちょっと前ですが ThinkPad T460s を買いました。

軽いPCが欲しくて、1kg前後のPCを探したりしてたんですけど、ThinkPad T460s が 10万円以下で買えるようになったので値段に負けました。

購入したモデルはこんな感じです:

  • Core i5-6200U (2.30GHz, 3MB)
  • ディスプレイ 14.0 FHD (1920x1800 IPS)
  • メモリー 12GB (4GB + 8GB)
  • 日本語キーボード
  • 指紋センサーなし
  • SSD 256GB
  • 1.36kg
  • 米沢生産
  • 102,211円

9/25 に注文して 10/7 に届きました。

振り返ってみると16年くらいずっと ThinkPad です。

前に使ってた ThinkPad X220 よりも画面が広くて薄くて軽くていい感じです。

X220 の時は Xubuntu 使っててサスペンド&レジュームが不安定で良くフリーズしたり、bluetooth マウスの接続がうまくいかなかったりしたんで、新しい PC では Windows の上に仮想マシンで Linux Desktop を動かそうと考えていました。

ということで初めは VirtualBox に Xubuntu 入れてフルスクリーンでデスクトップを使ってみましたが、 トラックポイントの中ボタンをクリックとスクロールの両方で使えなかったり、動画が見れなかったり。

VirtualBox の代わりに VMware Workstation Player で試してみたら動画はちゃんと見れるようになったのですが、トラックポイントの動きは変わらず。

トラックポイントのはどうも Linux か X の制御の問題のような感じでした。

それでもしばらくは Xubuntu on VMware on Windows で使おうと思ってたのですが、突然 Windows update によって勝手にリブートされたので、嫌になって直接 Xubuntu を動かすようにしました。そしたらサスペンドも bluetooth も普通に動いたので拍子抜け。

一応 Windows は残してデュアルブートできるようにしてあります。ヘタレなので。

Firefoxで絵文字が白黒で表示される

Firefox 50 で絵文字に色がつきました。

次のようなテキストファイルを表示すると

?と?

次のように表示されるようになりました。

f:id:tmtms:20161123163111p:plain

ですが、Ubuntu で次のファイルを表示すると

??????

次のように「?」と「?」だけ白黒で表示されてしまいました。

f:id:tmtms:20161123163115p:plain

どうやら、OSのフォントに該当する文字があればそれが優先されるようです。

Dejavu Sans フォントに該当するフォントが入ってるようなので、それを読ませないようにすればいいようです。

% sudo apt purge fonts-dejavu-core

ちゃんと色付きで表示されるようになりました。

f:id:tmtms:20161123163119p:plain

なお、fonts-dejavu-core を削除すると、合わせて xubuntu-core や xubuntu-desktop も削除されてしまいますが、これらはメタパッケージなので特に問題ありません。 それが嫌な場合は、要するにフォントファイルを見せなければいいので、次のようにするだけでも良いようです。

% sudo chmod 000 /usr/share/fonts/truetype/dejavu

[追記]

font-manager を使って Dejavu Sans のチェックボックスを外すことで無効に出来ます。 そのユーザーだけに影響してシステムには影響しないため、上記の方法よりも望ましいと思います。

どのフォントファイルに定義されているのかを調べる

「?」と「?」がどのフォントファイルで定義されているか調べるのにいくつかツールを使ってみました。

gnome-font-viewer

特定の文字しか表示されないのでダメでした。

fontforge

UIが使いづらかったのでやめました。

gnome-specimen

そのフォントに定義されていない文字はデフォルトのフォントで表示されるようなので使えませんでした。

font-manager

これで見つけることができました。

waterfall

ちょっと使いにくかったですが、これでも見つけられました。

Ruby の Enumerable#sum

最近のruby-core (2016年7月)」に次のような記述がありました。

Enumerable#sum というメソッドが追加されており、特定の場合(浮動小数点数の配列とか)には誤差が累積しないアルゴリズムが採用されています。

Ruby 2.4 に Enumerable#sum が追加されたのは知ってましたが、「誤差が累積しない」というのは知りませんでした。

というか、元々誤差が累積しない加算の目的で追加されたものだったのですね(最近のruby-core (2016年3月))。単純に 「#inject(:+)」にわかりやすい名前をつけただけなのかと思ってました。

簡単に試してみます。浮動小数点演算で、0.1を10個足すと1.0にならないというのはよく知られていますね。

> 0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1
=> 0.9999999999999999

> [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1].inject(:+)
=> 0.9999999999999999

> [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1].sum
=> 1.0

sum を使った場合はちゃんと 1.0 になりました!

軽く Ruby のソースを眺めてみたのですが、Array#sumEnumerable#sum とは別に実装されていました。 ロジックは同じようなのでおそらく高速化のためだと思います。

ソースのコメント中に次のように書かれていました。

Array#sum method may not respect method redefinition of "+" methods such as Fixnum#+.

数値クラスの + メソッドを上書きしても sum メソッドでは効かないようです。

sum メソッド中の数値の足し算は C レベルでやってるので Ruby で定義したメソッドが呼ばれないのですね。

「合計を返すとは言ったが + を使うとは言ってない!」( ー`дー´)キリッ

試してみます。

class Integer
  # a + b で a * b の結果を返す
  def +(other)
    self * other
  end
end
p 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1  #=> 1
p [1, 1, 1, 1, 1, 1, 1, 1, 1, 1].sum     #=> 10

sum メソッドのループの実装は、次のようになっているようです。

  1. Integer または Rational オブジェクトの間繰り返し。それ以外のオブジェクトが現れたら 2 へ。
  2. Integer, Rational, Float オブジェクトの間繰り返し(ここで誤差が累積しないアルゴリズムが使用される)。それ以外のオブジェクトが現れたら 3 へ。
  3. + メソッド呼び出しを繰り返し。

ということで、次のように数値オブジェクトではないものが配列にあると、それ以降は「誤差が累積しないアルゴリズム」は使われません。

class A
  # 最初に 0 + self が呼ばれるので 0.0 を返す
  def coerce(other)
    [other, 0.0]
  end
end

p [A.new, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1].sum  #=> 0.9999999999999999

まあ、わざわざこんなことする人がいるとは思えませんけど。

Suica対応スマホに変更

f:id:tmtms:20160919235256j:plain

データ通信用のスマホの他に、通話とモバイルSuicaのためにガラケー(F-01E)を持っていたのですが、ガラケーの2年縛りの契約の更新時期になりました。最近SIMロックフリーのスマホでもSuicaが使えるものが出てきたので、1台にまとめました。

arrows M03 の評判が良いみたいだったのでそれにしました。宗教上の理由で iPhone 7 ではありません。

今までも NifMo を使ってたし、キャッシュバックキャンペーン をやっていたので、NifMo で購入。

月々の維持費はこんな感じです。(税抜き)

docomo タイプシンプルバリュー 1483円
docomo ひとりでも割50         -740円
docomo iモード                 300円
NifMo SMS対応3GB              1050円
------------------------------------
合計                          2093円

NifMo 音声通話対応3GB         1600円
スマホセット割                -200円
arrows M03                    1482円 (24回払いの月額)
キャッシュバック              -416円 (1万円を24分割した値)
------------------------------------
                              2466円

2年経過後は 1600円/月 になる予定。

NifMo は「NifMo バリュープログラム」というのがあって、アプリをインストールしたりショッピングしたりすると料金が割り引かれるみたいなんですけど、面倒なのでやってません。

ただ、「勝てばスマホ利用料が安くなる!川崎フロンターレウイニングキャンペーン」は登録しました。何もしなくても川崎フロンターレが勝つ毎に20円割引されます。 サッカーには興味ないんですけど、ちょっとフロンターレを応援しようかという気になりますね :-)

スマホ移行

元々使ってたスマホは ASUS Zenfone 5 でした。

アプリの移行

基本的には Zenfone で Helium を使って SDカードにバックアップして、それを arrows M03 にリストアする感じでやりました。 詳しくはこちら

バックアップが禁止されているアプリがいくつかあったので、それは個別に。

slack

slack はバックアップできませんでした。まあログインし直せばいいだけです。

LINE

事前にメールアドレスを登録しておけば、新しい環境に LINE でログインするだけで使えるようになりますが、トーク履歴は引き継がれません。

移行前に元の環境で「トーク履歴のバックアップ」をしておいて、新しい環境で「トーク履歴をインポート」すればよいです。トーク毎にやらないといけないのでちょっと面倒です。

詳しくはこちら

データの移行

写真や音楽データは SDカードに元から入れてたので特に何もしなくても良かったです。

Zenfone だと文字化けしていた mp3 タグの「Boøwy」がちゃんと表示されました。Android のバージョンも違うのでそのせいかもしれません。

ガラケー移行

電子マネー

モバイルSuica と nanaco を使っていました。どちらもガラケーが使えなくなる前に機種変更手続きをしておく必要がありました。

モバイルSuica 機種変更

nanaco 携帯電話の機種変更について

ちゃんとやっとかないと、かなり面倒なことになりそうな感じです。

メール

実はちょっとトリッキーなことをして、imode メールをケータイじゃなくてIMAPを使ってスマホで読んでました。

iモードメールをスマホで送受信する(iPhone偽装による設定方法)

なので、PCのメールアプリからメールを全部コピーすれば終わりです。

電話帳&写真

docomo のサイトで新旧の機種を入力すると手順が表示されます。

データ移行ナビ

この手順は、SDカード経由でデータを移行するので通信ができなくなった後からでも行うことができます。

MNP切り替え

同梱されてた紙にURLが書かれてたので、そこにアクセスしてポチっとすれば申請は完了です。

夜に申請したのですが、すぐには切り替わらないので(たぶん人手で処理してる)、翌日は今までのガラケー&スマホと新しいスマホの3台持って出かけました。

切り替わるまでは新しいスマホはデータ通信できないので、元のスマホでテザリングしてました。

昼くらいにガラケーのアンテナが圏外になりました。新しいスマホは通信できないままだったのですが、再起動したら使えるようになりました。

移行後

それまで使ってた Zenfone 5 よりもちょっと小さいので違和感ありましたが、すぐに慣れました。

元から入ってた日本語入力の「Super ATOK ULTIAS」はかなりもっさりだったので、今まで使ってた普通の ATOK にしました。 キーボードを切り替えずに手書き入力ができたりして面白そうだったんですけどね…。

Suica は普通に使えました。これが使えないと話にならない…。

全体的に Zenfone 5 よりもちょっと遅い気がします。Android バージョンが異なるのでそのせいかもしれません。

Zenfone 5 には大きな不満が2つあって(「充電中は通知ランプが機能しない」「ナビゲーションバーが液晶外でボタンが絵なので暗いところで使うと見えない」)、それが解消したのはよかったです。

ホーム画面アプリはすぐに Nova Launcher に切り替えてしまったので使い勝手はわかりません。

モバイルSuica

iPhone 7 に Suica がついて、おそらくモバイルSuica が使えるようになるのに、「Suicaなんていらねー」と思っている人が結構いるようです。

個人的にはモバイルSuicaはチョー便利だと思ってるので解せません。

モバイルSuicaがあると、

  • JR東日本の新幹線の予約がスマホからできる。
  • 座席の指定もスマホから可能。
  • モバイルSuicaで指定席を買うと、普通に自由席を買うよりも安い。
  • Suicaのチャージがスマホからできる。

注意事項もあって、

  • モバイルSuicaの乗車券は新幹線下車駅まで。そこから先の乗車券は別料金。
  • たとえば、長野から品川まで行くとしたら、普通に切符を買ったら東京までの乗車券で品川まで行けるけど、モバイルSuicaの場合は、東京-品川間は別料金が掛かる(それでもモバイルSuicaの方が安いけど)。
  • ビューカード以外ではモバイルSuicaは年会費が掛かる。
  • ビューカードだとモバイルSuicaの年会費は無料なんだけど、ビューカード自体に年会費が掛かる。
  • 個人的におすすめなのは、ビックカメラSuicaカードで、前年度にカード利用が1回でもあれば年会費無料。モバイルSuicaで新幹線に乗ったり、Suicaのチャージにカードを使っても実績になるので、実質年会費無料。

書いてて思ったんですけど、JR東日本の新幹線に乗らないような人にはあんまりメリットないかもしれない…。

[追記]

二週間ほど使ってみたんですが、CPU性能の問題なのかメモリが少ないからなのか、ヒジョーに遅いです。

ハードウェア的なスペックは前に使ってた Zenfone5 と同じくらいなので、同じような感じで使えるのかと思ってましたが、Android のバージョンが 4.4 から 6.0 になっているせいもあるのかもしれません。

ちなみに、ポケモンGOを起動してみると50秒もかかりました。

まあ最近はほとんどポケモンGOやってないんからいいんですけど、ポケモンGO以外もかなり遅くなることがあります。

Suicaが使えるSIMフリー端末ということで SHARP の「AQUOS mini SH-M03」と迷って、値段で arrows M03 にしてしまったんですけど、AQUOS にしておけばよかったかなぁ…。

MySQL で utf8 と utf8mb4 の混在で起きること

MySQL を UTF-8 で使おうと思ってハマりがちなのは charset utf8 を指定してしまうことです。

MySQL の UTF-8 には歴史的事情により utf8 と utf8mb4 の二つあります。

UTF-8 は1バイト〜4バイトで1文字が構成される文字コードですが、MySQL の utf8 は4バイト文字を扱うことができません。ハマりたくなければ utf8mb4 を使いましょう。

utf8 を使ってしまった場合に4バイト文字がどのように扱われるか、自分でもうろ覚えだったのでメモしておきます。

登録

接続が utf8mb4 でカラムが utf8mb4

あたりまえですが、そのまま登録されます。

mysql> insert into utf8mb4 (c) values ('美味しい?と?');
mysql> select * from utf8mb4;
+-------------------------+
| c                       |
+-------------------------+
| 美味しい?と?              |
+-------------------------+

接続が utf8 でカラムが utf8mb4

4バイト文字が「????」になります。

mysql> insert into utf8mb4 (c) values ('美味しい?と?');
Warning (Code 1300): Invalid utf8 character string: 'F09F8D'
Warning (Code 1366): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1
mysql> select * from utf8mb4;
+-------------------------+
| c                       |
+-------------------------+
| 美味しい????と????      |
+-------------------------+

utf8 の接続から送られてくるデータでは4バイト文字は不正な4バイトデータなので、4つの「?」に置き換えられます。

なお、sql_mode の設定によってはエラーになります。MySQL 5.7 のデフォルトではエラーになります。安全ですね。

mysql> insert into utf8mb4 (c) values ('美味しい?と?');
ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1

接続が utf8mb4 でカラムが utf8

4バイト文字が「?」になります。

mysql> insert into utf8 (c) values ('美味しい?と?');
Warning (Code 1366): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1
mysql> select * from utf8;
+-------------------+
| c                 |
+-------------------+
| 美味しい?と?      |
+-------------------+

utf8mb4 の接続上は4バイト文字は正しい1文字ですが、utf8 に対応する文字がないため、1つの「?」に置き換えられます。

sql_mode の設定によってエラーになるのは同上です。

接続が utf8 でカラムが utf8

4バイト文字が現れるとそこで文字列が切られてしまいます!

mysql> insert into utf8 (c) values ('美味しい?と?');
Warning (Code 1300): Invalid utf8 character string: 'F09F8D'
Warning (Code 1366): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1
mysql> select * from utf8;
+--------------+
| c            |
+--------------+
| 美味しい     |
+--------------+

utf8 の接続で4バイト文字は4バイトの不正データなので「????」になっても良さそうなのですが、MySQL の気持ちはよくわかりません。 接続もカラムも utf8 なので文字コードの変換が行われず、そのまま登録しようとしたらおかしな文字があったからそこで打ち切り…ということなのかもしれません。

まあ、これも sql_mode をちゃんと設定しておけばいいのですけど…。

参照

utf8mb4 接続で参照

あたりまえですが、そのまま参照できます。

mysql> select * from utf8mb4;
+-------------------------+
| c                       |
+-------------------------+
| 美味しい?と?              |
+-------------------------+

utf8 接続で参照

4バイト文字は「?」に置換されます。

mysql> select * from utf8mb4;
+-------------------+
| c                 |
+-------------------+
| 美味しい?と?      |
+-------------------+

文字「?」そのものが入っているのか、参照時に置換されたのかは HEX() 関数で確認できます。

mysql> select right(c,1),hex(right(c,1)) from utf8mb4;
+------------+-----------------+
| right(c,1) | hex(right(c,1)) |
+------------+-----------------+
| ?          | F09F8DBA        |
+------------+-----------------+

右端1文字の文字コードを16進で出力すると「?」の文字コード 3F ではなく「?」の F09F8DBA になっていることがわかります。

Ruby の Timeout の仕組み

Ruby で長い時間掛かるかも知れない処理のタイムアウトを行うにはこんな感じにします。

require 'timeout'

begin
  Timeout.timeout(3) do # 3秒でタイムアウト
    hoge                # 何かの処理
  end
rescue Timeout::Error
  puts 'なげーよ'       # タイムアウト発生時の処理
end

Timeout.timeout はブロック開始時にスレッドを作成し、そのスレッドで指定された秒数だけ sleep して、sleep から復帰してもまだブロックが終わってなければ作成元のスレッドに対して Timeout::Error 例外を発生させます。

指定時間以内に処理が終わる場合:

timeout(X)
    │
スレッド作成 ─┐
    │         │
ブロック実行  sleep X
    │         │
スレッドkill→ ?
    │
timeout復帰

指定時間以内に処理が終わらない場合:

timeout(X)
    │
スレッド作成 ─┐
    │         │
ブロック実行  sleep X
    :          │
    :  ← 元スレッドに Timeout::Error
    :

仕組みはシンプルです。ですが、実際に timeout.rb を読んでみると中では結構複雑なことをしてました。

たとえば、次のスクリプトを実行すると、'hoge' ではなく 'main' が出力されます。

require 'timeout'

def hoge
  sleep 5
rescue Timeout::Error
  p 'hoge'              # こっちは表示されない
end

begin
  Timeout.timeout(1) do
    hoge
  end
rescue Timeout::Error
  p 'main'              # こっちが表示される
end

hoge 内の sleep 中で Timeout::Error が発生したなら、普通は hoge 内の rescue で処理されるはずなのに、実際には main 側の rescue で処理されています。

確かに使う側からしたらこっちの方が便利なんですけど、Timeout は内部的にどのようにしてこのような処理を実現しているのでしょうか。

例外オブジェクトを作成して、それを元スレッドに対して raise してるのは合ってるんですけど、元スレッドで例外が発生しようとしたら無理やり throw に切り替えてました。

timeout.rb のやってることを簡単に書くとこんな感じです(実際の処理とは異なります):

class Timeout::Error < RuntimeError
  def exception(*)    # 例外発生時には Exception#exception が呼ばれるので
    throw self        # それをフックして代わりに throw する
  end
end

def timeout(sec)
  begin
    x = Thread.current  # 元のスレッド(timeout を呼び出したのと同じ)
    err = Timeout::Error.new
    y = Thread.new do
      sleep sec
      x.raise err       # 元のスレッドに対して例外発生(実際には throw される)
    end
    catch(err) do
      return yield      # ブロックが復帰したら return
    end
    raise err           # タイムアウト時はここで改めて例外発生
  ensure
    y.kill              # タイムアウト用スレッド終了
  end
end

raise - rescue と違って、throw - catch は まったく同じオブジェクト(object_id が同じもの)でないと catch しないので、別の timeout メソッドが作った例外オブジェクトはスルーするんですね。若干トリッキー感が否めませんが。

今回の学び

  • Thread#raise で別のスレッドに対して例外を発生させることができる。
  • Exception#exception を上書きすれば例外発生時に任意の処理を実行できる。