MySQLユーザーがPostgreSQLを触ってみたメモ

最近なぜか MySQL を使う Ruby アプリを PostgreSQL に対応する羽目になっているのですが、今までほとんど MySQL 以外の RDBMS を触ってなかったので、色々ハマったりしたのでメモっときます。

なお PostgreSQL 歴が浅いので間違ってること書いてるかもしれません。

API

プログラムから MySQL にアクセスするには Ruby/MySQL を使っていたのですが、PostgreSQL 用の API を新たに覚えるのは面倒だったので、Sequel を使って書き直しました。

mysql.query("select col1, col2 from table where col3='xxx'")
↓
db[:table].where(col3: 'xxx').select(:col1, :col2)

…みたいな感じです。

今までプログラム中に突然 SQL が現れていて読みにくかったのが、Ruby プログラムとして読みやすくなるという効果もありました。

Sequel については前に記事を書いたので興味があれば見てください。

ユーザーとデータベース

MySQL はデータベースを指定せずにサーバーに接続することができます。その場合データベース未選択状態になります。接続後に use を使用して自由にデータベースを切り替えることができます。また、カレントのデータベースとは異なるデータベースのテーブルを指定することもできます。

PostgreSQL の場合はデータベースを指定しないで接続することはできません。psql コマンドはデータベースを省略すると、ユーザー名と同じ名前のデータベースに接続しようとします。また、接続後はデータベースを変更することはできません。psql の \c を使えばデータベースを変更できますが、これは内部的には新しいデータベースに接続して元のデータベースとの接続を切断しています。

Ubuntu の場合は postgres という名前のデータベースが最初から用意されています。 誰でも使用できるので MySQL の test データベースのようなものでしょうか。

ユーザー名と認証

MySQL では UNIX ドメインソケット経由での接続であってもパスワードは必要です。パスワードを設定しないこともできますが、その場合は誰でもそのユーザーでログインできてしまいます。また OS のユーザー名と MySQL 上のユーザー名はほとんど関係ありません。mysql コマンドでユーザー名省略時に OS のユーザー名が使用される程度です。

PostgreSQL の場合は UNIX ドメインソケット経由の場合は、OS のユーザー名と PostgreSQL 上のユーザー名が一致していればパスワードを聞かれません。また OS のユーザー名と異なるユーザー名を指定しても接続できません。ネットワーク経由の場合はユーザー名とパスワードが必要です。

Ubuntu の場合はあらかじめ postgres というユーザーがスーパーユーザーとして用意されています。MySQL の root と同じようなもんだと思います。

認証まわりの設定は pg_hba.conf ファイルで行います。どのクライアントからの接続で、どの認証方式を使用するかを指定することができます。

UNIX ドメインソケットの場合にパスワードを聞かれないのは、pg_hba.conf ファイル中に次の行があるためです。

# TYPE  DATABASE        USER            ADDRESS                 METHOD
# "local" is for Unix domain socket connections only
local   all             all                                     peer

ユーザー作成

PostgreSQL に接続するユーザーを作るには、PostgreSQL のスーパーユーザーで createuser コマンドを実行します。

Ubuntu の場合は postgres ユーザーがスーパーユーザーです。

% sudo -u postgres -i
[sudo] password for tommy: 
postgres$ createuser -P hoge
Enter password for new role: 
Enter it again: 

この例では hoge ユーザーをパスワードつきで作成しています。 localhost からしか使用しない場合はパスワードをつけなくてもいいと思います。

データベース作成

PostgreSQL のスーパーユーザーで createdb コマンドを実行します。

postgres$ createdb hoge

接続

クライアントからの接続は MySQL に比べると PostgreSQL は重いです。

MySQL でコネクションプールを使わずに接続切断を繰り返してもなんとかなっていた場合でも、PostgreSQL ではコネクションプールを使わないといけなくなるかもしれません。

MySQL は1プロセスで動作し接続毎にスレッドを生成するのに対し、PostgreSQL は接続毎にプロセスを生成するためだと思います。

調べてませんが、もしかすると認証プロトコル自体の処理も関係あるのかもしれません。

自動変換等

MySQL は型が一致しなくてもテキトーに変換して処理してくれるのですが(かなり余計なお世話)、PostgreSQL は厳密に型をチェックするので、MySQL でエラーにならなかったクエリがエラーになることがあります。

以下は MySQL ではエラーになりませんが、PostgreSQL でエラーになる例です。

  • 数値カラムへの文字列の登録
hoge=> insert into t (i) values ('hoge');
ERROR:  invalid input syntax for integer: "hoge"
LINE 1: insert into t (i) values ('hoge');
                                  ^
  • 数値カラムと文字列の比較
hoge=> select * from t where i='hoge';
ERROR:  invalid input syntax for integer: "hoge"
LINE 1: select * from t where i='hoge';
                                ^
  • 文字列カラムと数値の比較
hoge=> select * from t where s=123;
ERROR:  operator does not exist: character = integer
LINE 1: select * from t where s=123;
                               ^
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
  • INSERT 時にデフォルト値を持たない NOT NULL カラムを省略
hoge=> insert into t (col1) values (123);
ERROR:  null value in column "col2" violates not-null constraint
DETAIL:  Failing row contains (123, null).
  • SELECT に指定していないカラムを GROUP BY に指定
hoge=> select col1 from t group by col2;
ERROR:  column "t.col1" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: select col1 from t group by col2;
               ^
  • CHAR カラムにサイズ超過文字列を登録
hoge=> insert into t (col1) values ('0123456789a');
ERROR:  value too long for type character(10)
  • DATE カラムに 0000-00-00 を登録
hoge=> insert into t (d) values ('0000-00-00');
ERROR:  date/time field value out of range: "0000-00-00"
LINE 1: insert into t (d) values ('0000-00-00');
                                  ^
  • DATE カラムに不正な日付を登録
hoge=> insert into t (d) values ('2014-10-32');
ERROR:  date/time field value out of range: "2014-10-32"
LINE 1: insert into t (d) values ('2014-10-32');
                                  ^
  • 0 除算
hoge=> select 1/0;
ERROR:  division by zero

MySQL でも sql_mode を設定することで、これらのうちのいくつかをエラーにすることができます。

mysql> set sql_mode='STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY';

なお、ERROR_FOR_DIVISION_BY_ZERO を指定することで、UPDATE,INSERT 時のゼロ除算はエラーになりますが、SELECT の時はエラーではなくワーニングになります。

UNSIGNED

MySQL の整数型には UNSIGNED を指定することができますが、PostgreSQL には UNSIGNED 型はありません。

大文字小文字

MySQL の文字列型はデフォルトでは大文字小文字を区別しませんが、PostgreSQL は大文字小文字を区別します。

PostgreSQL で大文字小文字を区別しないようにするには、citext 拡張をデータベースに導入して、文字列型カラム作成時に char, varchar ではなく citext を使うのが良いようです。

hoge=> create table t (s varchar);
CREATE TABLE
hoge=> insert into t (s) values ('abc');
INSERT 0 1
hoge=> select * from t where s='ABC';
 s 
---
(0 rows)

citext 拡張を導入するにはスーパーユーザーでないとできません。

hoge=# create extension citext;
CREATE EXTENSION
hoge=> create table t (s citext);
CREATE TABLE
hoge=> insert into t (s) values ('abc');
INSERT 0 1
hoge=> select * from t where s='ABC';
  s  
-----
 abc
(1 row)

LIKE も大文字小文字を区別します。大文字小文字を区別したくない場合は ILIKE を使います。

hoge=> select 'abc' like 'ABC';
 ?column? 
----------
 f
(1 row)

hoge=> select 'abc' ilike 'ABC';
 ?column? 
----------
 t
(1 row)

スキーマ

MySQL には無いものですが、PostgreSQL にはデータベースとテーブルの間にスキーマというものがあります。 データベース内でテーブルの名前空間をわけられるようです。 特に指定しない場合はデフォルトのスキーマが使われるので意識しなくても問題ないです。

MySQL のデータベースは PostgreSQL のデータベースとスキーマの両方が混ざったものなのかもしれません。

トランザクション

PostgreSQL はトランザクション中で何かエラーが発生した場合は、以降のクエリはロールバックするまですべてエラーになります。

この状態でコミットするとエラーにはなりませんが、実際にはロールバックされます。これはちょっと罠っぽいです。

hoge=> begin;
BEGIN
hoge=> insert into t (i) values (123);
INSERT 0 1
hoge=> hoge;
ERROR:  syntax error at or near "hoge"
LINE 1: hoge;
        ^
hoge=> select * from t;
ERROR:  current transaction is aborted, commands ignored until end of transaction block
hoge=> commit;
ROLLBACK
hoge=> select * from t;
 i 
---
(0 rows)

MySQL と違い、PostgreSQL はトランザクション中で CREATE TABLE や DROP TABLE しても、ロールバックすると無かったことになります。これは結構嬉しいです。MySQL はトランザクション中で CREATE TABLE, DROP TABLE すると、その時点で勝手にコミットされてしまうという罠があるので…。

その他の構文

MySQL は文字列リテラルの表記は「'」でも「"」でも良いですが、PostgreSQL では「'」だけです。「"」はテーブルやカラムの識別子リテラルを表します(MySQL での「`」に相当)。

MySQL の「||」は論理和を表しますが、PostgreSQL では「||」は文字列結合です。

これらも MySQL 側で sql_mode を指定することで PostgreSQL に合わせることができますが、Sequel を使ってれば構文の差異はある程度吸収してくれるので、あまり気にしませんでした。

おわりに

はじめは「MySQLユーザーのためのPostgreSQLガイド」というタイトルにしようと思ったのですが、そんな大層なことは書けなかったので、「MySQLユーザーがPostgreSQLを触ってみたメモ」にしました。

MySQL と PostgreSQL は色々違いがありますが、構文まわりは MySQL の方が特殊だと思いました。勝手に余計な変換はしない方がみんな幸せになれると思います。

MySQL から PostgreSQL に移行するには、まず sql_mode を設定して、その状態で MySQL でエラーにならないようにしてから、PostgreSQL に移行するのがいいと思います。

自分の場合は、数値と文字列の比較とか、数値カラムに数字文字列入れようとしてたりとか、SELECT で指定してないカラムを GROUP BY に指定してたりとか色々ありました。

あと、最近 MySQL も sql_mode のデフォルト値を厳し目にするようになってるので、PostgreSQL とは関係なく、可能であれば厳し目に設定しておいた方が何かといいと思います。

Ubuntu の MySQL を標準以外のディレクトリで動かす

Ubuntu の MySQL の datadir は /var/lib/mysql です。これを別のディレクトリに変更して動かそうとするとエラーになります。

# mkdir /hoge
# mysql_install_db --datadir=/hoge
Installing MySQL system tables...
140614  8:57:23 [Warning] Can't create test file /hoge/hostname.lower-test
140614  8:57:23 [Warning] Can't create test file /hoge/hostname.lower-test

Installation of system tables failed!  Examine the logs in
/hoge for more information.
...

これは AppArmor の設定で、mysqld に対して /hoge へのアクセスが許可されていないためです。

# vi /etc/apparmor.d/local/usr.sbin.mysqld

   # Site-specific additions and overrides for usr.sbin.mysqld.
   # For more details, please see /etc/apparmor.d/local/README.
   /hoge/ r,
   /hoge/** rwk,

# service apparmor reload
 * Reloading AppArmor profiles
Skipping profile in /etc/apparmor.d/disable: usr.bin.firefox
Skipping profile in /etc/apparmor.d/disable: usr.sbin.rsyslogd

これで /hoge にデータベースを作ることができるようになりました。

# mysql_install_db --datadir=/hoge
Installing MySQL system tables...
OK
Filling help tables...
OK
...

以下駄文

  • テスト用に tmpfs 上にデータベースを作りたかったのにエラーになった。
  • /tmp とか /var/tmp とかだとエラーにならないので、ディレクトリが異なるためじゃなくて、tmpfs のせいだと思い込んだ(実は AppArmor で /tmp, /var/tmp が許可されていた)。
  • tmpfs の制限を調べても見つからない。
  • 当然 SELinux は真っ先に疑ったけど入ってなかった。
  • Ubuntu の deb じゃなくて MySQL の公式バイナリを使用したらできた(これは /usr/sbin/mysqld じゃなくて /usr/local/mysql/bin/mysqld だったため)。
  • システムコールトレースとってみたけど違いがわからない。
  • deb ファイルのパッチ読んでみたけど怪しいところはなかった。
  • Twitter でつぶやいてたら教えてもらえた。

  • AppArmor なんてものがあるのは知らなかった。SELinux がないと思って油断してた。

  • 解決

MySQL Talk in 長野を開催しました

もう二週間も経ってしまいましたが、3/15 に NSEG 第49回勉強会として「MySQL Talk in 長野」を開催しました。

本当は 2/15 に開催する予定だったのですが、例の大雪でひと月延伸となりました。

会場はいつも NSEG 勉強会で使わせていただいているケイケンさんの会議室です。長野駅から近くてありがたいです。

今回はオラクルの梶山さんをメインスピーカーに招いて MySQL をテーマに開催しました。

当日のツイートのまとめはこちら http://togetter.com/li/642666

前説 / とみた

MySQLの始め方 / とみた

実は内容は一昨年の MySQL Beginners Talk の発表の焼き直しでした。

MySQL 最新動向 / 梶山さん

MySQL の最新動向として、5.6 や 5.7 DMR の新機能を紹介していただきました。

Transactd / 矢口さん

Transactd という NoSQL プラグインの説明でした。

自分は Transactd というプラグインは初めて知ったのですが、Handler Socket と同じような仕組みのプラグインのようです。

NoSQL ながらトランザクションも使用可能で Windows にも対応しているようです。 Action PSQL という製品の Btrieve 互換の API で使えるとのこと。

Dart + MySQL / 小山さん

自分は全然知らないのですが Dart という言語用の MySQL Connector の説明でした。

その他

今回はイベント管理に Doorkeeper を使ってみました。 編集したページを公開前にプレビューできるのと、今回は使いませんでしたが、メンバーに一斉に連絡できる機能があったり、チケットをバーコードで管理できたり、こくちーずより使いやすかったです。

MySQL の SQL エスケープ

この記事は MySQL Casual Advent Calendar 2013 の15日目の記事です。

今、空前の SQL エスケープブームみたいなので、このビッグウェーブに乗っかってみます。

でも面倒なのでセキュリティについての話はしません。カジュアル!

文字列リテラルとエスケープ

MySQL では SQL 中の文字列リテラルは次のように表現します。

'abc'      -- シングルクォートで括る
"abc"      -- ダブルクォートで括る
0x616263   -- 16進数
x'616263'  -- 16進数
0b011000010110001001100011  -- 2進数
b'011000010110001001100011' -- 2進数

各表記で charset を指定することができます

_utf8 'abc'
_utf8 "abc"
_utf8 0x616263
_utf8 x'616263'
_utf8 0b011000010110001001100011
_utf8 b'011000010110001001100011'

通常は16進数や2進数を使用することはあまりないでしょう。

シングルクォートとダブルクォートの表記では、「\」を使用することでで通常の文字として記述できないコードを指定できます。

\0   -- 0x00 (NUL)
\'   -- 「'」
\"   -- 「"」
\b   -- 0x08 (BS)
\n   -- 0x0A (LF)
\r   -- 0x0D (CR)
\t   -- 0x09 (TAB)
\Z   -- 0x1A (Windows EOF)
\\   -- 「\」
\%   -- 「\%」
\_   -- 「\_」

この「\」をエスケープ文字と言い、「\」を使用して文字を表記することを「文字をエスケープする」と言います。

なお、この他にも「'」中で「''」(シングルクォートを2つ)と記述すれば「'」になります。同様に「"」中で「""」(ダブルクォートを2つ)と記述すれば「"」になります。

変数埋め込み

普通に SQL を記述するときは上記に気をつけて文字列を記述すれば良いのですが、プログラム中で変数の値、特に何が入っているかわからない値を埋め込む時には注意が必要です。

char *value = "ほげ";
sprintf(sql, "SELECT '%s'", value);
//=> SELECT 'ほげ'

上の例の場合は問題ないのですが、value の値に「'」「"」「\」が入っていると正しい SQL が生成されません。

char *value = "ほ'げ";
sprintf(sql, "SELECT '%s'", value);
//=> SELECT 'ほ'げ'

value 中の「'」「"」「\」の前に「\」を置けば問題ないはずです。

char *escape_special_char(char *value)
{
  char *outbuf;
  int i, j;

  outbuf = malloc(strlen(value) * 2 + 1);
  for (i=0, j=0; i < strlen(value); i++) {
    if (value[i] == '\'' || value[i] == '"' || value[i] == '\\')
      outbuf[j++] = '\\';
    outbuf[j++] = value[i];
  }
  outbuf[j] = '\0';
  return outbuf;
}

...

char *value = "ほ'げ";
buf = escape_special_char(value);
sprintf(sql, "SELECT '%s'", buf);
//=> SELECT 'ほ\'げ'
free(buf);

しかしこれはやってはいけないダメな例です。

文字コード

文字コードが UTF-8 等の場合は上の例でも問題ないのですが、シフトJISみたいにマルチバイト文字中に「\」を含む文字があるような文字コードの場合は、もう少し考慮が必要です。

そのような文字コードで、マルチバイト文字を文字単位ではなくバイト単位で処理してしまうと、文字中の「\」まで置き換えてしまいます。

たとえば「表」という文字はシフトJISでは 0x95 0x5c の2バイトで表され、2バイト目の 0x5c が「\」です。 「表計算」という文字列を上のプログラムで処理すると「表\計算」となり、元々存在しないはずの「\」が現れてしまいます。

つまり、上のように escape_special_char() のような関数を独自に作って「\」を処理するのではなく、文字列の文字コードにあわせて処理を行う必要があります。

MySQL の C API には特殊文字をエスケープするための mysql_real_escape_string() という関数が用意されているので、それを使用すべきです。

MySQL の API は初期化時にどの文字コードを使用するか指定できます。mysql_real_escape_string() はその文字コードにあわせて処理を行います。

なお、MySQL の API には mysql_escape_string() という関数もありますが、これは使用してはいけません。文字コードを意識しないので適切な処理が行えないためです。

シフトJISで2バイト目に「\」が含まれる文字には以下のようなものがあります。割とよく使われる文字も含まれてますね。

ソ 噂 浬 欺 圭 構 蚕 十 申 曾 箪 貼 能 表 暴 予 禄 兔
喀 媾 彌 拿 杤 歃 濬 畚 秉 綵 臀 藹 觸 軆 鐔 饅 鷭

接続用の文字コード

もうひとつ気をつけないといけないのは、MySQL との接続に使われる文字コードです。

SET NAMES 命令で文字コードを変更すると、接続用文字コードは変更されますが、mysql_real_escape_string() が使用する文字コードは変更されません。

もし、接続中に文字コードを変更した場合は、SET NAMES ではなく mysql_set_character_set() を使用しなければいけません。エスケープ用の文字コードと接続用の文字コードが異なると正しく処理できません。

たとえ文字列がバイナリデータであってもエスケープする際には接続用の文字コードとしてエスケープする必要があります。 たとえば、バイナリデータ「0x95 0x5c 0x6e」を latin1 としてエスケープすると、「0x95 0x5c 0x5c 0x6e」になりますが、接続の文字コードがシフトJISだと、MySQL サーバーは「表\n」と解釈して、「0x95 0x5c 0x0a」として扱われてしまいます。

まとめ

  • 文字列中で使用するためにエスケープしないといけない文字があります。
  • エスケープする場合は文字コードを意識する必要があります。
  • エスケープする際の文字コードと MySQL の接続用文字コードを合わせる必要があります。

今回は MySQL の C API を例に使用しましたが、どのプログラミング言語であっても同様に注意しなければいけません。 面倒な人は、16進数表記リテラルとかプリペアドステートメントを使うのがいいと思います。

明日 12/16 は @ さんです。

Ruby/MySQLの速度

Ruby 2.1.0 preview1 がリリースされたので、Ruby/MySQL の速度を測ってみました。

測定環境

測定内容は https://github.com/tmtm/ruby-mysql/tree/master/bench 参照。

ruby-mysql

すべて Ruby で書かれたバージョンです。

f:id:tmtms:20130928104204p:plain

select_prepare に掛かる時間が 1.9.3, 2.0, 2.1 と増加傾向にあります。

それ以外は 1.9.3 以降はどれもほぼ同じです。

insertinsert_prepare の差は、オブジェクトを MySQL のネットワーク表現に変換している部分によるものでしょうか。 もしかしたら、整数なんかは律儀に変換せずに to_s して文字列として渡してしまった方が速くなるかもしれません。

ruby-mysql-ext

Ruby で遅い部分の処理を C で書いたバージョンです。

f:id:tmtms:20130928104220p:plain

1.8.7 も含め、各バージョンでほとんど差がありません。select は 1.8.7 の方が速いです。 データベースから取り出した文字列から Ruby の String オブジェクトを生成する際に、1.8.7 ではエンコーディングの処理がないからじゃないかと思います。

pure Ruby 版と比べて、insertinsert_prepare の差がほとんどありません。 グラフだとわかりにくいですが実は insert_prepare の方がほんの少しだけ速いです。 PREPARE だと、MySQL サーバー側のクエリのパース処理を省けるので、その分だけ速くなっているのかもしれません。

おまけ MySQL/Ruby

その昔、MySQL/Ruby という名前で MySQL の公式 C API の libmysqlclient を使ったライブラリがあったのですが、それでも測ってみました。

f:id:tmtms:20130928104236p:plain

さすがに速いです。けど、思ってたよりも ruby-mysql-ext との差はありませんでした。

まとめ

  • 1.9.3 以降はあまり速度に違いはない
  • C は速い
  • Ruby/MySQL もうちょっと頑張れ

MyNA会2013年7月 に行って来ました - MySQLのプロトコル解説

7/29 に MyNA会2013年7月 というイベントがあったので、行って喋って来ました。

MySQLのプロトコルという誰得なネタを発表しました。

何故か2ヶ月ほど前のスライドにアクセスが急増している昨今ですが、こっちも見てください。でも面白くはないです。

軽い気持ちで参加した人を置いてきぼりにして、淡々とプロトコルの中身を喋るというのも面白いかと思ったのですが、案の定会場が静まり返ってみんなポカンとしてる感じになったので、個人的には満足です。

あと極一部のマニアな方には好評だったようで良かったです。

今回は他の発表も割と深い話なものが多かったので、会全体のバランスとしてはちょっと良くなかったかもしれないなと少しは思いました。役に立たなさ度でダントツな私が言うのも何ですけど。

スライドを書くにあたって、MySQLソースコード中のマクロ名やコード片を極力排除することに気をつけました。 これは著作権の問題で、万が一、誰かが私のスライドを参考にしてプログラムを実装した時に、MySQL の著作権の影響を受けない方がいいだろうと思ったためです。

少なくとも日本ではプロトコルやアルゴリズムは著作権では保護されないはずなので、それらを示すことは問題ないのですが、マクロ名については怪しいところです。特に API にも著作権があると主張した某社の事例もあることですし。

MySQL のプロトコルについては、もっと詳しくちゃんと書いて電子書籍みたいな感じでまとめられたらいいなぁ…と思ってはいるんですが、思ってるだけなので、本当にやるかどうかはわかりません。

おまけ

早めに新宿駅についたので喫茶店で時間を潰してたのですが、いざ移動しようとしたら新宿駅で迷って結局遅刻してしまいました。新宿駅は何回行っても迷う…。

今回は節約のために高速バスで移動したのですが往復で9時間かかりました。新幹線使って宿泊することを考えたら3分の1くらいの値段で済むのはいいのですが、体力的にやばいので、高速バスはもうやめときます。

MyNAにお金があれば交通費もらってもいいのかもしれないのですけど、MyNAは会費も寄付も貰わないってことにしたのも自分なので何も言うまい…。

オラクルの青山ビルのロビーにセキュリティゲートがなくなってて、直接会場フロアまでいけるようになってました。楽で良いですね。

MySQL の "Illegal mix of collations" エラーについて

MySQL で「Illegal mix of collations」というエラーが出ることがあります。テーブルの charset と接続の charset 等、すべてを utf8 などで統一してれば出ないので、あまり見ることはないかもしれません。

私はカラム毎に charset を指定することがあるので、時々このエラーにハマります。

続きを読む

OSS貢献者賞を頂いたので昔話をします

この記事は、おっさんの自分語りと昔話なので、あまり読む価値はないです。

もう1ヶ月くらい前になるのですが、OSS貢献者賞を頂きました。

最近はあまりOSSに貢献するようなことをしてる自覚はなかったので、なぜ自分が候補にあがったのか謎でした。OSS貢献者賞のページによると、

影響力のある開発プロジェクトを創造、運営した開発者やグローバルプロジェクトにおいて活躍する卓越した開発者及び普及に貢献した方を表彰するもの

ということらしいです。創造や運営してないし、たぶん卓越した開発者でもないので、「普及に貢献した」だろうと思い、じゃあ最近のことじゃなくて過去の実績が評価されってことならいいかと、受賞させていただくことにしました。

ちゃんとした受賞理由は発表のページに載るまで知らされてなかったのですが、予想通り MySQL 関係でした。

MySQLの初期の日本語化パッチを作成し、国内での普及に大きく貢献した。またその後立ち上がった日本MySQLユーザ会の代表を務めるなどコミュニティの成長にも貢献している。

他の貢献者賞の受賞者の受賞理由に比べると 1/3 くらいで一番短くて、貢献度の少なさを物語ってますね。

ペンネームがひらがなのおかげで受賞者の表記順が先頭でした。「50音順」って書いてあるけど違いますね、これ。

授賞式には諸般の事情で残念ながら出席できませんでした。出席したら何か喋らされたらしいのですが、自分は人前で喋るのがあまり得意じゃないので、まあ良かったのかもしれません。

授賞式で喋らなかった代わりに、自分の MySQL との関わりをここに書いておきます。

続きを読む

Ruby/MySQL 2.9.9

Ruby/MySQL 2.9.9 を作りました。

Ruby/MySQL

Ruby/MySQLRuby から MySQL を使用するためのライブラリです。

特徴:

  • Ruby だけで書かれているためコンパイル不要
  • Ruby 1.9 の Encoding 対応
  • Ruby のスレッドでも動作
  • MySQL C API ライブラリ(libmysqlclient)を使用していない
  • 遅い

今回のリリースは主に速度改善です。

インストール

% gem install ruby-mysql

速度改善のため一部をCで書いた ruby-mysql-ext というのもあります。
こちらは、インストールするためにコンパイルが必要です。

% gem install ruby-mysql-ext

速度

簡単に速度を測ってみました。ちゃんと測ってないので雰囲気です。

f:id:tmtms:20120618232922p:image:w640

(select の結果が正しくなかったので取り直しました)

計測環境:

ThinkPad X220 Core i5-2410M
Memory 8GB 
Ubuntu Linux 12.04
MySQL 5.5.25
Ruby 1.9.3p194 / 1.8.7p352

グラフの縦軸は秒です。

処理内容はこんな感じです。詳細は https://github.com/tmtm/ruby-mysql/tree/master/bench を見てください。

insert
10万レコードのINSERT
insert(prepare)
10万レコードをプリペアドステートメントでINSERT
select
10万レコードのSELECT(10000レコードを10回)
select(prepare)
10万レコードをプリペアドステートメントでSELECT(〃)
many_query
結果を返さないクエリを10万回発行

グラフ中の MySQL/Ruby というのは MySQL の C APIライブラリ libmysqlclient を使用したものです。すべて C で書かれているためやっぱり速いです。

最速な環境を求めるのであれば MySQL/Ruby を使うのもいいと思いますが、今後はおそらくメンテされません。mysql2 というライブラリもあるのでそれを使うのもいいと思います。

マルチスレッド

Ruby/MySQLRuby で書かれているため、Ruby のスレッドを使用して、時間がかかるクエリ実行中でも別の処理を行うことができます。一部 C で書かれた Ext版でも、スレッド切り替えを妨げてるようなことはしていないつもりです。

たとえば次のスクリプトを Ruby/MySQL で実行すると、クエリ実行中の5秒の間でもスレッドが動いて数字を出力します。

require 'mysql'

my = Mysql.new('localhost', 'root', 'password')
Thread.new do
  10.times do |i|
    p i
    sleep 1
  end
end
my.query('select sleep(5)')   # 5秒掛かるクエリ

MySQL/Ruby を使用した場合は5秒間何も表示せずに終了してしまいます。

Ext版(一部 C版)の注意

内部でエンディアンに依存した箇所があるのですが、手元にビッグエンディアン環境がないため試せてません。

GCC でしかコンパイルしてないので、他のコンパイラでコンパイルできるかどうかわかりません。

Windows 環境では動かないような気がします。なんとなく。

MySQL Beginners Talk #mysqlbt で発表しました

MySQL Beginners Talk に行って喋ってきました。

匿名ユーザーとか不要なユーザーの削除まわりの話は、手で削除するんじゃなくて mysql_secure_installation を実行すればいいと Twitter で教えてもらいました。手で消すより簡単でミスもないのでいいですね。

Rabbit

私が発表で使っていたプレゼンツールは Rabbit です。

プレゼン中スライド上にツイートが流れていたと思いますが、これも Rabbit の機能です。詳しくは http://rabbit-shockers.org/ja/faq.html を見てください。

Rabbit は RD 形式のテキストファイルでスライドを作成しますが、PDF を表示することも可能です。PDF のスライドの上にウサギとカメを出したり、ツイートを流すこともできます。

Rabbit の作者は須藤さん(@)です。須藤さんは MySQL のストレージエンジン mroonga の作者でもあります。

その他

このイベントを企画して、諸事情により当日は会場ではなくて作戦本部にいた梶山さん(@)のところに発表スライドが集まってます。 http://d.hatena.ne.jp/rkajiyama/20120529

発表中に加山さん(@)に良い写真を撮ってもらったので、TwitterFacebook のプロフィール写真として使わせてもらってます。

Ruby/MySQL

最近 Rails でプログラム作ったりしてるのですが、Ruby/MySQLRails で使えないことがわかったので、1年以上放置していた Ruby/MySQL をいじって使えるようにしてみました。

Ruby/MySQL というのは Ruby スクリプトから MySQL を使用するためのライブラリです。

https://github.com/tmtm/ruby-mysql

ややこしいのですが MySQL/Ruby というのもあります。Ruby/MySQLRuby だけで書かれたライブラリで、MySQL/Ruby は C で書かれたライブラリです。Ruby/MySQLMySQL/Ruby とほぼ互換があります。

Ruby/MySQL はコンパイルの必要がないため Ruby さえ動けばどこでも動くというのと、libmysqlclient を使用しないのでアプリが GPL の影響を受けないというのが特徴です。

インストール

次のようにしてインストールできます。

% gem install ruby-mysql

Railsから使う場合

次のように Rails プロジェクトを作成します。

% rails new hoge --skip-bundle --database mysql

Gemfile を次のように変更します。

gem 'mysql2'
  ↓
gem 'ruby-mysql'

その後 bundle install。

config/database.yml の mysql2 を mysql に変更します。

Railsmysql アダプタがなぜかバージョンを 2.8.x に固定しているので、手で変更します(ダサい)。

gems/activerecord-3.2.3/lib/active_record/connection_adapters/mysql_adapter.rb の 5行目をコメントアウト:

# gem 'mysql', '~> 2.8.1'

あとは普通に使えます。

ちなみに MySQL/Ruby の最新は 2.8.2 です。2.8.1 は場合によっては致命的なバグがあるので使わない方がいいと思います。

速度

で、まあ Ruby で書かれているのでやっぱり遅いわけです。速度を測ってみました。Ruby のバージョンは 1.9.3 と 1.8.7 です。

結果:

f:id:tmtms:20120503171652p:image

縦軸は秒です(大きい方が遅い)。処理内容は https://github.com/tmtm/ruby-mysql/tree/master/bench を見てください。

遅っ! SELECT とプリペアドステートメントの遅さが際立ちますね。プリペアドステートメントの方が遅いとか、もうね…。

Ruby/MySQL(ext)」は、遅そうな処理を一部Cで書きなおしたバージョンです。使用するには gem で ruby-mysql の代わりに ruby-mysql-ext を指定します。

SELECT が格段に速くなりました。この際、全部 C で書きなおしちゃおうかと思ったり思わなかったり…。

まとめ

Ruby/MySQL を作るために MySQL のプロトコルをさんざん調べたので、一度ちゃんとまとめておきたいと思ったりしてます。

mysqldump --single-transaction に --flush-logs をつけてはいけない

(いまだに時々ブクマされていたりしますが、これはバグで MySQL 5.5.21 以降では修正されています。)

mysqldump は MySQL のデータのバックアップを取得するコマンドです。

mysqldump に --single-transaction を指定すると一貫性を保持したバックアップを取得することができます*1
この時に mysqldump が発行しているクエリは次のような感じです。

[mysqldump --single-transaction DB名]

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
START TRANSACTION WITH CONSISTENT SNAPSHOT
UNLOCK TABLES
DB選択
テーブルからデータの読み込み

「START TRANSACTION WITH CONSISTENT SNAPSHOT」を実行すると、その瞬間のデータのスナップショットが保持され、トランザクションが終了するまでの間は、たとえ他の接続からデータが更新されたとしても、この接続からはスナップショット時点のデータが読み込まれます。

--single-transaction をつけない場合、mysqldump があるテーブルをダンプしている間に、まだダンプしてない別のテーブルが他の接続により更新されると、取得したバックアップは更新前と後のテーブルが混在することになってしまいます。

さらに --master-data をつけると次のような感じになります。

[mysqldump --single-transaction --master-data DB名]

FLUSH LOCAL TABLES
FLUSH TABLES WITH READ LOCK
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
START TRANSACTION WITH CONSISTENT SNAPSHOT
SHOW MASTER STATUS
UNLOCK TABLES
DB選択
テーブルからデータの読み込み

「FLUSH TABLES WITH READ LOCK」により全テーブルがロックされます。既にロックされているテーブルがある場合はロックが解放されるのを待つため、一貫性を高めるのにおすすめです。その後「UNLOCK TABLES」でロックを解放していますが、「START TRANSACTION WITH CONSISTENT SNAPSHOT」しているので、その時点(ロックされている時)のデータをちゃんと読み込むことができます。

mysqldump に --flush-logs オプションをつけると、バイナリログがローテーションされます。バックアップ取得のタイミングでバイナリログが新しくなるので、データベースのリカバリ時に mysqldump で取得したバックアップを戻して、バイナリログを利用してロールフォワードリカバリを行うことができます。

[mysqldump --single-transaction --master-data --flush-logs DB名]

FLUSH LOCAL TABLES
FLUSH TABLES WITH READ LOCK
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
START TRANSACTION WITH CONSISTENT SNAPSHOT
FLUSH LOGS                    ← ログのローテーションが行われる
SHOW MASTER STATUS
UNLOCK TABLES
DB選択
テーブルからデータの読み込み

ところが MySQL 5.5 ではこの組み合わせはうまく動きません。MySQL 5.5 では「FLUSH LOGS*2」でトランザクションが暗黙的に終了(COMMIT)してしまうようになっているためです。
「START TRANSACTION WITH CONSISTENT SNAPSHOT」でせっかく一貫性を保持するようにトランザクションを開始したのに、直後にトランザクションを終了してしまってます。

MySQL 5.1 までは「FLUSH LOGS」でもトランザクションが終了しなかったため期待通りに動きます。

--single-transaction と --flush-logs は便利なので組み合わせて使われてることが多いと思いますが、MySQL 5.5 では注意が必要です。

[追記]

バグとして登録されてました(http://bugs.mysql.com/61854)が、MySQL 5.5.21 で修正されました。

*1:InnoDB のテーブルについてのみ

*2:実際には FLUSH LOGS クエリではなくて、mysql_refresh() が使用されてます

MySQL最新ロードマップセミナー2011

MySQL最新ロードマップセミナー2011 に行ってきました。講師は肩書き長すぎな 梶山さんでした。
「お客様講演」は都合がつかずに中止でした。残念。
以下適当なメモです。嘘書いてあるかもしれません。

MySQL State of The Dolphin

  • OracleMySQL に対して本気で取り組んでる
  • Oracle の雑誌に MySQL が巻頭特集として載った
  • MySQL 5.5 は非常に高品質
  • Sun & Oracle の厳しい品質基準
  • InnoDB の主要メンバーは一人もやめてない
  • Oracle のパフォーマンスのスペシャリストが MySQL を支援
  • MySQL 5.6 - Development = β版
  • Oracle になって MySQLInnoDB の開発チームがひとつの会社になった
  • 2009 IPA 調査で、MySQL 60.5%, PostgreSQL 51.9%
  • 国内で初めて MySQLPostgreSQL を抜いた
  • Facebook は数千台の MySQL
  • モバゲーは 700台
  • 大きな Web サイトでは、ほとんど MySQL を使っている
  • MySQL Enterprise Backup
    • mysqldump に比べて、バックアップも速いがリストアが非常に高速
    • バックアップデータの圧縮もされる
  • MySQL Enterprise Monitor
    • クエリの実行回数や時間の統計
    • グラフをドラッグすることでその範囲だけ表示できる
  • MySQL Workbench
    • OSS 版もあり。商用版とほぼ同じ機能
  • サポート
    • 日本語は 9〜17時, 英語で 24時間
    • 全サポートエンジニアが MySQL のソースにアクセス可
    • Premier Support が 5年間、Extended Support が 8年間
  • MySQL Cluster
  • MySQL Enterprise の価格は物理サーバー単位
  • 物理サーバー上でVMがいくつ動いていても、1台分の価格でいい

MySQL Technical Update

MySQL 5.5
  • InnoDB がデフォルトストレージエンジン
  • 5.1 に比べ、Linux で 360%, Windows で 1500% のパフォーマンス向上
  • 接続数をふやしても性能が落ちにくい
  • CPU スケーラビリティ ほぼリニアにスケール
  • MySQL on Windows
  • Windows 環境の改善
  • MySQL のユーザーの半数以上が Windows で MySQL を使用している
  • Windows 環境では 5.5 を強くおすすめ
レプリケーション
  • 準同期レプリケーション
  • コミット後に落ちてもスレーブに確実にデータがある
  • 非同期型にくらべて応答が長くなる
  • 「同期」じゃなく「準同期」なのは、コミット直後にスレーブを参照しても反映されていないかもしれないため
MySQL 5.6
  • Development Milestone Release
  • これがそのままメジャーバージョンになるかどうかはわからない。
  • もしかしたら、5.7 とか 5.8 とかになるかも。
  • http://dev.mysql.com/downloads/mysql からダウンロード
  • 性能改善
  • パーティショニング
    • 明示的にパーティションを指定できる
    • 特定パーティションのデータのみをインポート/エクスポート
  • PERFORMANCE_SCHEMA
  • InnoDB 改善
  • Replication 改善
5.6 には含まれてないもの
  • Multu-threaded Slaves
    • 複数スレッドでスレーブの更新を速く
  • MyISAM でできることは全部 InnoDB でもできるようにするという方向。まだ紹介できるレベルではない
MySQL Cluster
  • 大量の同時多発トランザクション向け
  • レプリケーションを使えばディザスタリカバリも可能。99.999% の可用性
  • MySQL Cluster 7.2
  • 7.1 ではシンプルなクエリ向き = JOIN がほとんどないクエリ。JOIN は SQLノードで処理していたため遅い
  • 7.2 では JOIN もデータノードで処理する。20倍以上速い
  • 7.2 は年内GA予定
  • カラム数の上限が128から512に増加
  • オンラインでテーブルに追加
  • 管理データをデータノードに集約
  • NDB を直接使うための Memcached API - Memcached Driver for NDB
その他
  • Oracle VM Template for MySQL
    • 7月中。もうちょっと早いかも
  • MySQLの15分ルール
    • ダウンロード/インストール/起動まで15分以内
    • Windows用 MySQLインストーラでもっと速く
  • MySQL Workbench プラグイン
    • Python でプラグインを書ける
  • 商用製品の今後
    • Oracle製品との連携強化
    • 設定とインスタンス管理のためのツール
    • バックアップ履歴管理
    • 直接メディアにバックアップ等

Q&A

Q: 以前は商用製品のリリースサイクルはGPL版と異なっていたが?
A: 今はほぼ同じ時期にリリースしている

Q: 5.1.x が去年で EOL だったと思うけど、まだリリースされてるのは?
A: サポートが8年間になったので、5.0.x, 5.1.x はまだバグフィックスリリースされる

上の質問は2つとも私です。もっと質問が出るかと思ってたんですけど、他には出ませんでした。なのに終了後に講演者に個別質問の行列ができてました。みんなQ&Aタイムに質問しましょうよ。

感想

他の OSS プロダクトとは違って、MySQL にとっては Oracle になったことが非常にプラスに働いているようです。コミュニティベースの開発か否かという違いなのかもしれません。
サポートが8年というのもすごいですね。5.0.x, 5.1.x が EOL だったはずなのに復活してますし。というか 5.0.x は既に 5.0.92 なので、更新が続くとバージョン番号が溢れる危険がw
5.6 はさらにパフォーマンスが向上するらしいし、Memcached API みたいな面白い機能も増えるかもしれないので、楽しみです。

UTF-8 のサニタイズ

UTF-8 文字列中に UTF-8 として正しくないコードが入っていた場合に、その文字を「?」などに置き換えたいことがあります。

たとえば MySQL に登録するときは不正な文字を消しとかないと、その文字以降すべて消えてしまいます。

mysql> insert into t (c) values (0x414243FF58595A);
Query OK, 1 row affected, 1 warning (0.06 sec)

Warning (Code 1366): Incorrect string value: '\xFFXYZ' for column 'c' at row 1
mysql> select * from t;
+------+
| c    |
+------+
| ABC  |
+------+
1 row in set (0.00 sec)

ということで、Ruby では Iconv を使ってこんな感じで対処してます。

require 'iconv'
def sanitize_utf8(str)
  ret = ''
  i = Iconv.open('UTF-8', 'UTF-8')
  begin
    ret << i.iconv(str)
  rescue Iconv::Failure => e
    ret << e.success << '?'
    str = e.failed[1..-1]
    retry
  end
  ret << i.iconv(nil)
  ret
end

sanitize_utf8("\xff") #=> "ほ?げ"

Iconv が UTF-8 として不正な文字をエラーにしているのを利用しています。

ですが、次のようなバイト列ではうまくいかないことがわかりました。

sanitize_utf8("\xf8\x90\x90\x90\x90") #=> "ほ\xf8\x90\x90\x90\x90げ"

この5バイトは実は UTF-8 としては正しいバイト列なのです。ですが今は UTF-8 は 4バイトまでしか使われないことになってますし、当然 MySQL の utf8mb4 charset のカラムにも入りません。

Iconv はこの文字を UTF-8 として正しい文字として扱っているので UTF-8 から UTF-8 への変換では対処できません。4バイト以下の UTF-8 のみが有効な文字である UTF-16 を一旦経由することでうまくいきました。

require 'iconv'
def sanitize_utf8(str)
  ret = []
  i = Iconv.open('UTF-16BE', 'UTF-8')
  unknown_char = "\x00?"
  begin
    ret << i.iconv(str)
  rescue Iconv::Failure => e
    ret << e.success << unknown_char
    str = e.failed[1..-1]
    retry
  end
  ret << i.iconv(nil)
  Iconv.iconv('UTF-8', 'UTF-16BE', *ret).join
end

sanitize_utf8("\xf8\x90\x90\x90\x90") #=> "ほ?????げ"

他にもっといい方法を知ってる人は教えて下さい。

なお、3バイト以下の UTF-8 しか有効でない MySQL の utf8 charset には対応できてません。

NSEG 14 - MySQLの文字コード

NSEG14 に参加しました。

はじめはそのつもりはなかったんですが、枠が空いてたので発表することにしました。急遽決めたので、当日の15時すぎまでスライド作ってました。

今回は、つい最近 MySQL文字コードまわりでハマったので、それをネタにしてみました。ま、知ってる人にとってはどうってことない話ですが。