SQLAlchemyでカラムの値を遅延ロードする方法

どんなO/Rマッパでもそうだと思うけど、モデルを取得すると基本的に全てのカラムから値を取り出そうとする。
1レコードのサイズが少なければ問題にならないが、BLOBのようにサイズの大きなカラムを追加するととたんにパフォーマンスが悪くなってしまう。
SQLAlchemyもそのあたりは同じ。
でもどのカラムが必要で不要なのか細かく指示できるので、うまく調節すれば取り出す値を最小限にとどめることができる。

適用例

userテーブルはBLOB型のiconカラムを持っている。

# テーブル定義
user_table = sqlalchemy.Table(
    'user', meta_data,
    sqlalchemy.Column('id',   sqlalchemy.Integer, sqlalchemy.Sequence('user_id_seq'), primary_key = True),
    sqlalchemy.Column('name', sqlalchemy.Unicode(32), nullable = False),
    sqlalchemy.Column('icon', sqlalchemy.BLOB, nullable = False))

# マッピング定義
sqlalchemy.orm.mapper(User, user_table,
    properties = {
        # ここのdeferred()で遅延ロードを指定している
        'icon': sqlalchemy.orm.deferred(user_table.c.icon)
    })

deferred()を利用することで、iconプロパティは参照されるまでロードしなくなる。

users = db_session.query(User).all()  # id、nameはロードされるがiconはロードされない

for user in users:
    user.id      # idはすでにロードされている
    user.icon  # ここでようやくiconをロード

実行されるSQLの比較

deferredなし

SELECT "user".id AS user_id, "user".name AS user_name, "user".icon AS user_icon
FROM "user"

当たり前だけどiconをロードしてる。

deferredあり

SELECT "user".id AS user_id, "user".name AS user_name
FROM "user"

おぉ、ちゃんとiconだけ除外されてる!

問い合わせ時に遅延するか決める

iconのデータも利用することが分かっているのなら、遅延させずに取ってきたいこともある。
そんな時はQuery.options()を使おう。
undefer()を設定すれば、その問い合わせに限り遅延しなくなる。

from sqlalchemy.orm import undefer
query = query.options(undefer('icon'))

逆に普段は遅延しない(deferred()なし)のに、今回だけ遅延させたい場合はdefer()を設定する。

from sqlalchemy.orm import defer
query = query.options(defer('icon'))

まとめ

O/RマッパはSQLを隠蔽できて便利な反面、どんなSQLが実行されるのか意識しづらい。
知らないうちに非効率なSQLになっていることがあるので注意が必要だ。
幸いSQLAlchemyには、deferred以外にもいろいろチューニングできそうなのでもっと調べていきたい。

というわけで、BLOBのようにサイズの大きなカラムはdeferredしましょう、というお話でした。

参考: http://www.sqlalchemy.org/docs/05/reference/orm/mapping.html#sqlalchemy.orm.deferred