【Django】DjangoでN+1を防いで高速化を行うテクニック

こんにちはバックエンド担当の山田です。

自分の担当回では処理負荷軽減のテクニックを数回に渡って記述していこうと思います。

弊社のサービス「クロスオーダー」ではPythonフレームワークであるDjangoを利用しています。 Djangoに限らずORMの機能が充実しているフレームワークでは注意を払わないと、簡単にN+1問題が発生してパフォーマンスに影響が生じてしまうケースがあります。

今回は以下の点に絞って記載します

  • Django環境でN+1発生箇所の見つけ方
    • django-debug-toolbarを利用する
    • debugsqlshellを利用する

N+1発生箇所の見つけ方

「いやいや、発生箇所を見つけるってことは、もうN+1が発生しているって事だよね?最初からN+1を発生させないように書こうよ…!」というツッコミ。ありがとうございます。

結論から言うと僕は一発ではそのようなコーディングをすることは出来ないです。はい。

しかし、そんなソースコードもネットの海に公開しなければ問題ありません。

「またN+1を書いてしまったぜ、ガハハ」と笑っていられる内に発生箇所を特定してしまいましょう。

django-debug-toolbarを利用する

django-debug-toolbarはSQLの実行だけではなく、各種ログ、負荷状況が確認出来るデバッグ用のツールです。

使い方は簡単。

  1. パッケージをインストール
$ pip install django-debug-toolbar
  1. settings.pyにINSTALLED_APPSに”debug_toolbar”を追加。 MIDDLEWAREに”debug_toolbar.middleware.DebugToolbarMiddleware”を追加。 INTERNAL_IPSに”REMOTE_ADDR”の値を追加(各環境に合わせて値を設定。今回の場合は’127.0.0.1’を指定)
INSTALLED_APPS = ['debug_toolbar']

MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware']

INTERNAL_IPS = ['127.0.0.1']
  1. config URLに定義を追加
if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [path("__debug__/", include(debug_toolbar.urls))]

これらを設定した後、Djangoをrunserver で起動すると画面右側にパネルが出てきます。

殺伐とした画面にこのようなメニューが!

 

その後は実際に画面を操作し、「SQL」のパネルを開いて内容を確認するだけです。

(あー!これはいけません!612回もクエリが実行されています!!)

 

N+1が発生している箇所には”xxx similar queries.” や “Duplicated xxx times.”と回数付きで教えてくれます。

プラスアイコンを押下するとクエリ実行箇所とtracebackまで丁寧に表示してくれるので、問題箇所を修正します。

 

”xxx similar queries.” や “Duplicated xxx times.”と表示されている箇所を全て修正することで…



実行回数を14回に減らすことが出来ました!

debugsqlshellを利用する

django-debug-toolbarを導入すると’debugsqlshell’コマンドが使えるようになります。

$ python manage.py debugsqlshell

このコマンドを実行すると対話型シェルが起動するので、後はDjangoの記述をそのままシェルで実行すると、このようにクエリを実行するタイミングに合わせて実際のクエリが表示されます。

>>> from xmart.webapi.models import Product
>>> Product.objects.values('id').all()
SELECT `product`.`id`
FROM `product`
WHERE `product`.`is_active`
LIMIT 21 [2.01ms]
<QuerySet [{'id': 1}, {'id': 2}, {'id': 3}, {'id': 4}, {'id': 5}, {'id': 6}, {'id': 7}, {'id': 8}, {'id': 9}, {'id': 10}, {'id': 11}, {'id': 12}, {'id': 13}, {'id': 14}, {'id': 15}, {'id': 16}, {'id': 17}, {'id': 18}, {'id': 19}, {'id': 20}, '...(remaining elements truncated)...']>
>>>

例えばN+1になっている場合は…。

>>> products = Product.objects.filter(id__in=[1,2,3])
>>> for p in products:
...   print('カテゴリ : ' + p.category.name)
...
SELECT `product`.`id`,
# 省略
FROM `product`
WHERE (`product`.`id` IN (1,
                          2,
                          3)) [1.98ms]

SELECT `product_category`.`id`,
# 省略
FROM `product_category`
WHERE `product_category`.`id` = 1
LIMIT 21 [1.37ms]

カテゴリ : 野菜

SELECT `product_category`.`id`,
# 省略
FROM `product_category`
WHERE `product_category`.`id` = 1
LIMIT 21 [1.16ms]

カテゴリ : 野菜

SELECT `product_category`.`id`,
# 省略
FROM `product_category`
WHERE `product_category`.`id` = 2
LIMIT 21 [1.02ms]

カテゴリ: 肉

このように同じようなクエリが何度も実行されている事がわかります。

簡単に実行させ確認が出来るので、開発中に気になる記述があればすぐに叩いて確認することが出来るので、気軽に確認を行いましょう。