2012年10月2日

Python の MySQL ドライバはどれを利用すれば良いのか

概要

Python から MySQL に接続するためのドライバは複数存在している。どれを使うのが一番良いのか確認してみる。

Python の MySQLドライバ

Python の MySQLドライバの主な物は「MySQL - PythonInfo Wiki」に記載されている。

以下のような物がある。記事を書いた時点でのpypiでの最終リリース日の新しい順。

リンク Python3対応 DB API v2.0対応 ライセンス メンテナー 最終リリース日 概要
MySQL-python × GPL or CNRI Python License Andy Dustman 2012-09-27 比較的初期から存在したため有名なドライバ
mysql-connector-python GNU GPLv2 (with FOSS License Exception) Oracle社および協力者 2012-09-07 MySQL公式配布物
oursql BSD Aaron Gallagher 2012-06-05 MYSQL_STMT API のラッピングにフォーカスしたドライバ
umysql 不明 × BSD License Jonas Tarnstrom 2012-04-20 C/C++のみで実装されている。gevent対応と記載されている。
mysql-connector-repackaged GNU GPLv2 (with FOSS License Exception) Sun Microsystems社 2012-03-11 mysql-connector-python」の古いバージョン
PyMySQL3 MIT Pete Hunt 2011-11-08 Pythonのみで実装されている。Python2 対応版はPyMySQLで別配布。
PyMySQL × MIT Pete Hunt 2011-11-08 Pythonのみで実装されている。Python3 対応版はPyMySQL3で別配布。

とりあえず、Python 2.7.3 で動作を確認する。また DB API v2.0 対応は必須としたい。
この条件に合致する 「MySQL-python」、 「mysql-connector-python」、 「oursql」、 「PyMySQL」、 を比較してみる。

準備

利用した環境のバージョンは以下とした。今回の MySQL は localhost の物を利用する。
比較観点は、「実行速度」、「メモリ効率」、「使い勝手」。「使い勝手」に関しては「戻り値」、「with」、「例外」あたりを見ることにする。

  • Mac OS X 10.8.3 Mountain Lion
  • Python 2.7.3
  • MySQL Community Server 5.5.27
    • ホスト:localhost
    • データベース名:example
    • ユーザ名:username
    • パスワード:password
  • 利用ドライバ
    • MySQL-python 1.2.4b2
    • mysql-connector-python 1.0.6b2
    • oursql 0.9.3.1
    • PyMySQL 0.5

Python に今回利用するライブラリをインストール。

# ドライバ類のインストール
pip install MySQL-python
pip install mysql-connector-python
pip install oursql
pip install PyMySQL
# 計測ツール
easy_install Benchmarker
pip install psutil
pip install memory_profiler

データベースとテーブルを作成。

> mysql -u root
mysql> CREATE DATABASE example DEFAULT CHARACTER SET utf8;

mysql> GRANT ALL PRIVILEGES ON example.*
    -> TO username@localhost
    -> IDENTIFIED BY 'password';

mysql> exit;

> mysql -u username -ppassword example

mysql> CREATE TABLE tweets (
    ->  id serial PRIMARY KEY,
    ->  status_id VARCHAR(255) UNIQUE NOT NULL,
    ->  from_user_id VARCHAR(255) NOT NULL,
    ->  text VARCHAR(140) NOT NULL,
    ->  created_at VARCHAR(50) NOT NULL,
    ->  datetime DATETIME NOT NULL
    ->) engine=innodb default charset=utf8;

ソースコード

単純な INSERT、SELECT、DELETE のコードを以下のように書いてみた。

MySQL-pythonのサンプル

def sample_mysql_python():
    import MySQLdb
    cur = MySQLdb.connect(
        host='localhost',
        db='example',
        user='username',
        passwd='password').cursor()

    print(cur.execute('SELECT * FROM tweets'))

    # 日本語でも u つけない
    cur.execute(
        'INSERT INTO tweets '
        '(status_id, from_user_id, text, created_at, datetime) '
        'VALUES (%s, %s, %s, %s, current_timestamp)',
        ('251298602096xxxxx1', '43172xxx6',
         'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

    print(cur.execute('SELECT * FROM tweets'))

    res = cur.fetchall()

    for row in res:
        print(row[3])

    cur.execute(
        'DELETE FROM tweets WHERE status_id = %s',
        ('251298602096xxxxx1',))

    cur.close()

oursqlのサンプル

def sample_oursql():
    import oursql
    with oursql.connect(
            host='localhost',
            db='example',
            user='username',
            passwd='password').cursor() as cur:

        print(cur.execute('SELECT * FROM tweets'))

        # 日本語は u つける
        cur.execute(
            'INSERT INTO tweets '
            '(status_id, from_user_id, text, created_at, datetime) '
            'VALUES (?, ?, ?, ?, current_timestamp)',
            ('251298602096xxxxx1', '43172xxx6',
             u'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

        print(cur.execute('SELECT * FROM tweets'))

        res = cur.fetchall()

        for row in res:
            print(row[3])

        cur.execute(
            'DELETE FROM tweets WHERE status_id = ?',
            ('251298602096xxxxx1',))

PyMySQLのサンプル。自分の環境では日本語が INSERT できなかった。

def sample_pymysql():
    import pymysql
    cur = pymysql.connect(
        host='localhost',
        db='example',
        user='username',
        passwd='password').cursor()

    print(cur.execute('SELECT * FROM tweets'))

    # TODO 日本語がうまく入らない?
    cur.execute(
        'INSERT INTO tweets '
        '(status_id, from_user_id, text, created_at, datetime) '
        'VALUES (%s, %s, %s, %s, current_timestamp)',
        ('251298602096xxxxx1', '43172xxx6',
         'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

    print(cur.execute('SELECT * FROM tweets'))

    res = cur.fetchall()

    for row in res:
        print(row[3])

    cur.execute(
        'DELETE FROM tweets WHERE status_id = %s',
        ('251298602096xxxxx1',))

    cur.close()

mysql-connector-pythonのサンプル。自分の環境では INSERT 文が実行できなかった。

def sample_mysql_connector():
    import mysql.connector
    con = mysql.connector.connect(
        host='localhost',
        db='example',
        user='username',
        passwd='password',
        buffered=True)

    # 書き方が他と違うが、connect().cursor() 形式だとエラーになる模様
    cur = con.cursor()

    cur.execute('SELECT * FROM tweets')

    # TODO 動作しない?「Unread result found」になる「buffered=True」で動作する
    cur.execute(
        'INSERT INTO tweets '
        '(status_id, from_user_id, text, created_at, datetime) '
        'VALUES (%s, %s, %s, %s, current_timestamp)',
        ('251298602096xxxxx1', '43172xxx6',
         'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

    cur.execute('SELECT * FROM tweets')

    res = cur.fetchall()

    for row in res:
        print(row[3])

    cur.execute(
        'DELETE FROM tweets WHERE status_id = %s',
        ('251298602096xxxxx1',))

    cur.close()
    con.close()

比較結果

「mysql-connector-python」は実行できなかったので、比較できないという結果になったが、どうやるのが正しいのだろう?このサンプルでは「buffered=True」が必要。

様々な条件で試した結果、速度は「MySQL-python」、「oursql」、「PyMySQL」の順。数値は載せないので、自分で試してほしい。
メモリ効率はこのサンプルだとそれほど違いがないようだ。

使い易さは「oursql」が圧倒的で、「with」が利用できるのはこのドライバだけ。いろいろ頑張っている分遅いと思われる。また 「oursql」は「libmysql」に依存しているのは欠点。

結論

速度を求める場合や、ORMを利用する場合は、「MySQL-python」を使うのが良い。Python 3 への対応は徐々に行なわれている模様。
ドライバだけで利用したい場合や Python 3 に対応したい場合は、「oursql」は良い選択肢になる。

「PyMySQL」と「mysql-connector-python」に関しては、今回上手く実行できていない部分があるので判断を保留とする。

2012/10/10追記:

「PyMySQL」と「mysql-connector-python」のエラーに関して補足しておく。

「PyMySQL」のエラー

「PyMySQL」は「"さんぷるでーた"」として日本語を INSERT して、SELECT しようとすると以下のようなエラーになる。

Traceback (most recent call last):
  File "sample.py", line xxx, in <module>
    main()
  File "sample.py", line xxx, in main
    sample_pymysql()
  File "sample.py", line xxx, in sample_pymysql
    print(cur.execute('SELECT * FROM tweets'))
  File "/path/to/lib/python2.7/site-packages/pymysql/cursors.py", line 117, in execute
    self.errorhandler(self, exc, value)
  File "/path/to/lib/python2.7/site-packages/pymysql/connections.py", line 187, in defaulterrorhandler
    raise Error(errorclass, errorvalue)
pymysql.err.Error: (<type 'exceptions.UnicodeEncodeError'>, UnicodeEncodeError('latin-1', u'\u3055\u3093\u3077\u308b\u3067\u30fc\u305f', 0, 7, 'ordinal not in range(256)'))

「u"さんぷるでーた"」として日本語を INSERT しようとすると以下のようなエラーになる。

Traceback (most recent call last):
  File "sample.py", line xxx, in <module>
    main()
  File "sample.py", line xxx, in main
    sample_pymysql()
  File "sample.py", line xx, in sample_pymysql
    u'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))
  File "/path/to/lib/python2.7/site-packages/pymysql/cursors.py", line 97, in execute
    escaped_args = tuple(conn.escape(arg) for arg in args)
  File "/path/to/lib/python2.7/site-packages/pymysql/cursors.py", line 97, in <genexpr>
    escaped_args = tuple(conn.escape(arg) for arg in args)
  File "/path/to/lib/python2.7/site-packages/pymysql/connections.py", line 579, in escape
    return escape_item(obj, self.charset)
  File "/path/to/lib/python2.7/site-packages/pymysql/converters.py", line 35, in escape_item
    val = val.encode(charset)
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 1-7: ordinal not in range(256)

つまり、日本語の INSERT は可能な模様。しかし、「SELECT」ができない。
このサンプルのままだと、エラーが解消されなかったのでそれ以上の原因の調査はしていない。

「mysql-connector-python」のエラー

「mysql-connector-python」は、INSERTで以下のようなエラーになる。

Traceback (most recent call last):
  File "sample.py", line xxx, in <module>
    main()
  File "sample.py", line xxx, in main
    sample_mysql_connector()
  File "sample.py", line xxx, in sample_mysql_connector
    'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))
  File "/path/to/lib/python2.7/site-packages/mysql/connector/cursor.py", line 365, in execute
    raise errors.InternalError("Unread result found.")
mysql.connector.errors.InternalError: Unread result found.

その後 ORM の調査をしているが、「MySQL-python」以外のドライバを利用しようとすると、結構面倒なので、「MySQL-python」以外のドライバに関して興味を喪失したため、その後調査していない。

2012/10/11追記:

「PyMySQL」charsetの設定をしたら通った。「PyMySQL」では明確に指定する必要があるのですね。

con = pymysql.connect(
    host='localhost',
    db='example',
    user='username',
    passwd='password',
    charset='utf8')

「mysql-connector-python」の方はcharsetやportを指定してもエラーになるので別の原因らしい。

2013/05/29追記:

「mysql-connector-python」はこのサンプルでは「buffered=True」が必要。

blog comments powered by Disqus