Djangoでメモリに乗らないサイズのDBを扱うときに気をつける点
はじめに
Djangoでメモリに乗らないサイズのデータベースを扱うときに、気をつけるべきポイントをまとめます。メモリを大量に消費していつまで経っても処理が終わらなかったり、OOM Killerに殺されたりといった悲しい結末を回避できたら幸いです。
データ量としては、レコード数が数十万から数百万ぐらいで、サイズにして数GB〜十数GBぐらいのイメージです。インデックスを適切に張るといった、Django特有でないポイントは取り上げません。Djangoのバージョンは1.5系を対象にしています。
バッチ処理のDEBUGに気をつける
症状
DEBUG = True
の場合、バッチ処理で大量のクエリを発行するとメモリを食いつぶすことがあります。
原因
実行したすべてのSQLが記録されるためです。default
のデータベースを利用している場合は、django.db.connections['default'].queries
に実行されたSQLが記録されます。HTTPリクエストを処理する際に発行したクエリは、リクエストごとにリセットされますが、バッチ処理では自動でリセットされません。
対策
このように大量のクエリを発行する処理を行う場合はDEBUG = False
にするか、適度にリセットしましょう。後述の restordb
や South のData Migrationを実行するときなども注意しましょう。
settings.py
で以下のようにして、DEBUG
の値を環境変数で簡単に切り替えられるようにするのがおすすめです。
DEBUG = os.environ.get('DEBUG') == '1'
参考リンク
テーブル全件の処理に気をつける
症状
以下のようなコードでメモリを大量に消費してしまいます。
for blog in Blog.objects.all(): # do something
for blog in Blog.objects.iterator(): # do something
原因
all()
を使うと確実に全件が取得され、メモリを食いつぶします。
iterator()
を使うとメモリ消費が減りそうな気がしますが、データベースに接続するモジュールの実装によっては役に立ちません。少なくともMySQLやPostgreSQLでは、iterator()
はサーバーサイドのカーソルではなく、クライアント側のカーソルが使われます。クライアント側では一度全件が読み込まれるため、メモリの消費量は all()
とほとんど変わりません。
対策
バッチ処理などで不用意に上記のようなコードを書かないようにしましょう。代わりに、1000件ずつなどチャンクに区切って取得します。具体的なコードは参考リンクをご参照ください。
他にも以下のようなことを気をつけることでメモリ消費や性能を改善できる場合があります。
参考リンク
- python - Limiting Memory Use in a *Large* Django QuerySet - Stack Overflow
- Memory-efficient Django queries | Jan Pöschko's space
- Database access optimization | Django documentation | Django - Very Large Result Sets in Django using PostgreSQL
dumpdataとloaddataに気をつける
症状
アプリケーションを作る際、最初はSQLiteを使い、ある程度の規模になったときにMySQLなど他のデータベースに移行することがあります。
このようなときに、データを manage.py dumpdata
でエクスポートして、 manage.py loaddata
でインポートする方法がありますが、 dumpdata
/ loaddata
がメモリを大量に消費してしまいデータを移行できないことがあります。
原因
dumpdata
はiterator()
を使って全件を取得するため、メモリを大量に消費します。 loaddata
もファイルをすべてメモリに読み込んで処理するため、メモリに乗らないファイルを読み込めません。
1.5から dumpdata
のJSON出力が改善されましたが、全件を取得するという根本的な問題は残ったままです。
対策
このようなときは、django-dumpdb *1 を使うと良いです。INSTALLED_APPS
に追加すると dumpdb
/ restoredb
というコマンドが使えるようになります。 dumpdb
は、1レコードごとにJSONで出力してくれるため、メモリの消費が増大しません。
restoredb
するときにはDEBUG=False
にするのを忘れないようにしましょう。
参考リンク
Adminサイトの外部キーに気をつける(リストページ)
症状
Adminサイトで、list_display
に外部キーを指定した場合、リストページの表示に時間がかかることがあります。
原因
list_display
に外部キーを指定した場合、select_related
(SQLのJOIN)が使われます。select_related
で取得するテーブル数が増えると、データ量によっては取得に時間がかかります。
対策
この問題については、list_display
に含めないようにする以外に効果的な対策を知りません。
参考リンク
Adminサイトの外部キーに気をつける(詳細ページ)
症状
要素数が多いテーブルへのForeignKeyがある場合、Adminサイトの詳細ページの表示に非常に時間がかかることがあります。
原因
これは、選択肢がリストボックスで作られるためです。10万個の選択肢があるリストボックスとかゾッとしますね。
対策
この場合、以下のいずれかで対策ができます。通常は1で問題ないと思います。 raw_id_fields
に含まれるフィールドは、リストボックスではなくidの数字をテキストボックスで指定できるようになります。
- ModelAdminで
raw_id_fields
にフィールドを指定する。 - ModelAdminで
readonly_fields
にフィールドを指定する。 - ModelのFieldで
editable=False
にする。
参考リンク
まとめ
これらのポイントは完全なリストではなく、私が遭遇したことのあるものだけです。新しく見つけたら追加していこうと思います。こんな問題もあるよとか、対策はこうしたほうがいいよなどありましたら教えていただけると助かります。
あとは全件処理がもう少し簡単に書けるようになると嬉しいですね。次のような議論はあるみたいですが。