この記事は以下のターゲットを対象としています。
★5 Djangoの開発経験が3年以上。
★4 Djangoの開発経験が1年以上。
★3 WEBサイト開発経験あり。これからDjangoを学習します。
★2 Python初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。
こんにちは、グローバルウェイの清家です。
前回に引き続き、グローバルウェイの開発標準とするDjangoに関するノウハウを紹介します。
DjangoのORMに触れてみよう
皆さん、普段からORMは使っていますか。
SQLが苦手な人でもプログラミングベースで記述できます。また、簡潔に表現できるとても便利な機能です。
では、実際にどんなクエリが発行されているのか意識して使っていますか。正しいデータが取得できることに納得して、知らないうちに無駄なクエリが発行されていることはないでしょうか。今回の記事では、実際にどのようなクエリが発行されているか確認していきたいと思います。
DjangoでSQLを表示する
Djangoではsettings.pyの設定によりクエリセットの実行結果をコンソールやログファイルに出力されることができます。
以下の設定をsettings.pyに記述すると、コンソール上にSQLが出力されるようになります。
# settings.py LOGGING = { “version”: 1, “disable_existing_loggers”: False, “formatters”: { “verbose”: { “format”: “[%(asctime)s][pid:%(process)d][%(levelname)s] %(message)s” }, }, “handlers”: { “console”: { “level”: “DEBUG”, “class”: “logging.StreamHandler”, “formatter”: “verbose”, }, }, “loggers”: { “django.db.backends”: { “handlers”: [“console”], “level”: “DEBUG”, }, }, } |
実際にORMを実行してSQLの表示を確認します。
まず以下のようなテーブルを用意します。
accountsテーブルに外部キーを設定しpeopleテーブルを参照できるようにします。
※それぞれのテーブルに3レコードずつデータを用意します。
以下にaccountsテーブルをすべて取得して、peopleテーブルのnameを参照するサンプルコードを示します。
# Python account_list = Accounts.objects.all() for account in account_list: # ① account.person.name # ② |
①では、accountsテーブルのデータを取得するクエリが発行されます。
SELECT “accounts”.”id”, “accounts”.”person”, “accounts”.”login_id”, “accounts”.”password”, FROM “accounts” LIMIT 21; args=(); alias=default
②では、accountsテーブルの1レコードごとにpeopleテーブルのデータを取得するクエリが発行されます。3レコードあるため、3回のクエリが発行されることになります。
SELECT “people”.”id”, “people”.”name”, “people”.”email” FROM “people” WHERE “people”.”id” = 1 LIMIT 21; args=(1,); alias=default
SELECT “people”.”id”, “people”.”name”, “people”.”email” FROM “people” WHERE “people”.”id” = 2 LIMIT 21; args=(2,); alias=default
SELECT “people”.”id”, “people”.”name”, “people”.”email” FROM “people” WHERE “people”.”id” = 3 LIMIT 21; args=(3,); alias=default
N + 1 問題
ここで問題提起です。
通常このようにすべてのレコードを取得して、1件ずつ処理をするケースでは、内部でどのような処理がされているか、開発者によっては、2テーブルを結合して1回のSQLを発行するであろうと想定している方もいるのではないでしょうか。
今回のサンプルコードでは、一覧を取得するクエリ1回、1レコードごとにpeopleテーブルを取得するクエリ3回で合計4回のクエリが発行されることになります。レコードが何万件、何十万件と増えるたびに、発行されるクエリも増えることになり、とても冗長な結果になります。
レコードN件に対して、N + 1回のクエリが発行されることをN + 1問題といい、パフォーマンスの低下を招きます。
最適なクエリを発行する
このようなケースでは1回のクエリ発行によるデータ取得が望まれます。
以下にサンプルコードを示します。
select_related関数を設定することで指定された外部キーのテーブルを結合してデータ取得できます。
# Python account_list = Accounts.objects.select_related(“person”).all() for account in account_list: # ① account.person.name |
①では、1回のクエリ発行でaccountsテーブル、peopleテーブルをまとめて取得できます。
SELECT “accounts”.”id”, “accounts”.”person”, “accounts”.”login_id”, “accounts”.”password”, “people”.”id”, “people”.”name”, “people”.”email” FROM “accounts” INNER JOIN “people” ON (“accounts”.”person” = “people”.”id”); args=(); alias=default
まとめ
ORMによる実装でどのようなクエリが発行されているか確認できました。
SQL文を確認しないまま、プログラムだけの記載に頼ってしまうと、ブラックボックスな箇所になってしまい、パフォーマンス劣化を招いたり、想定しない結果に気づかないままバグの温床になってしまうことも少なくはありません。
私の場合、設計時にSQL文を構築し、ORMによる実装後の確認で設計通りにクエリが発行されているか確認することを心掛けています。
今回はsettings.pyの設定による方法を採りあげましたが、django-debug-toolbarというライブラリを使うことでブラウザ上で確認することもできます。
次回以降では、複数テーブルを参照するときにどういったORMを実装すればいいか、紹介していきたいと思います。