$30 off During Our Annual Pro Sale. View Details »

Django ORM パフォーマンスTips

takeaship
November 10, 2022

Django ORM パフォーマンスTips

みんなのPython勉強会 #87 での発表資料です。
https://startpython.connpass.com/event/263565/

実務でDjangoアプリケーション開発をするなかで、パフォーマンス上の問題に対処する機会がよくあったので、その知見を共有しました。
N + 1問題を中心に、効率的なクエリを発行できるORMのコードを書くためのポイントをまとめています。

takeaship

November 10, 2022
Tweet

More Decks by takeaship

Other Decks in Programming

Transcript

  1. Django ORM パフォーマンスTips LAPRAS, Inc. 増川武志 @takeaship

  2. 自己紹介 LAPRAS株式会社(2022/01~) Djangoアプリケーション(LAPRAS)開発 データアナリスト 医療者向けプロトタイピングスクール「もいせん」 の講師 Windows, WSL, VSCode, NeoVim

    Nizキーボード 前職でAzure, C#, .Netをやっていた名残で、 MS系への親しみが深い
  3. はじめに

  4. 話すこと よくある問題のあるクエリの類型とその解決策 問題のあるクエリの見つけ方

  5. 話さないこと Django ORMの限界 DBのインデックス作成 DBのチューニング DB固有のTips

  6. こんな人に役立つかも Pythonは書けて、 これからDjangoで開発を始める人 Djangoで開発をしていて、 ORMが裏でしていることを詳しく知らない人 ORMにある程度精通している人には、 物足りない内容かも。

  7. ORMの問題のある クエリの類型 DBとのやりとりの回数が無駄に多い 発行されるSQLが重い DBから取得するデータ量が無駄に多い モデルへの変換コストが重い

  8. ユーザ Djangoアプリ DB クエリ実⾏ Request Request クエリ発⾏ クエリ発⾏ 結果返却 結果返却

    モデルに変換 モデルに変換 Response Response ユーザ Djangoアプリ DB
  9. ユーザ Djangoアプリ DB Request Request Response Response ユーザ Djangoアプリ DB

  10. DBとのやりとりの 回数が多い N+1問題

  11. こんなテーブルを想定 CUSTOMER string name string address ORDER int orderNumber string

    deliveryAddress created_at datetime places
  12. N+1問題を起こすコード① orders = Order.objects.filter(created_at__gte='2022-11-01').all() for order in orders: print(order.customer.name)

  13. 回避策① N:1や1:1の関係の場合: select_related()を使う 発行されるSQL orders = Order.objects .filter(created_at__gte='2022-11-01') .select_related('customer').all() for

    order in orders: print(order.customer.name) SELECT * FROM ORDER INNER JOIN CUSTOMER ON ORDER.customer_id = CUSTOMER.id WHERE ORDER.created_at >= '2022-11-01';
  14. N+1問題を起こすコード② customers = Customer.objects.filter(address='tokyo').all() for customer in customers: for order

    in customer.order_set.all(): print(order.orderNumber)
  15. 回避策② 1:NやN:Nの関係の場合、prefetch_related()を使う 発行されるSQL customers = Customer.objects .filter(address='Tokyo') .prefetch_related('order_set').all() for customer

    in customers: for order in customer.order_set.all(): print(order.orderNumber) SELECT * FROM CUSTOMER WHERE CUSTOMER.address = 'Tokyo'; SELECT * FROM ORDER WHERE ORDER.customer_id IN (1, 2, 3, 4, 5);
  16. 回避策③ 子レコードのcountだけ欲しい、 などならannotateを使う 発行されるSQL customers = Customer.objects .filter(address='Tokyo') .annotate(order_count=models.Count('order')).all() for

    customer in customers: print(customer.order_count) SELECT CUSTOMER.*, COUNT(ORDER.id) AS order_count FROM CUSTOMER LEFT OUTER JOIN ORDER ON CUSTOMER.id = ORDER.customer_id WHERE CUSTOMER.address = 'Tokyo' GROUP BY CUSTOMER;
  17. N+1は知らないうちに紛れ込む みんな悪いのはわかっているけど… 気づける仕組みを作っておくのが大切

  18. ローカルで発行される SQLを確認する Django Extensions の shell_plus で、--print- sqlオプションをつける の LOGGING

    を設定する Django Debug Toolbar の SQL パネルを使う settings.py
  19. Production でパフォーマンスを監視する Datadogなどのツールを使う

  20. パフォーマンスにコミットする しくみをつくる プロダクションフリーズ DSでパフォーマンスを監視し、閾値を下回ったら 開発を止めて改善に集中する

  21. 参考: エラーバジェット枯渇時のプロダクションフリーズ運用マニュアル

  22. DBとのやりとりの 回数が多い ループ内でcreate() update() delete()

  23. create()するたびにINSERT文が飛ぶ 回避策 names = ['hoge','fuga','piyo'] for name in names: Customer.objects.create(name=name)

    customer_objecs = [Customer(name=name) for name in names] Customer.objects.bulk_create(customer_objecs)
  24. save()するたびにUPDATE文が飛ぶ 回避策 customers = Customer.objects.all() for customer in customers: customer.name

    = 'new name' customer.save() customers = Customer.objects.all() for customer in customers: customer.name = 'new name' Customer.objects.bulk_update(customers, fields=['name'])
  25. delete()するたびにDELETE文が飛ぶ 回避策 customers = Customer.objects.filter(address='Tokyo').all() for customer in customers: customer.delete()

    Customer.objects.all().filter(address='Tokyo').delete()
  26. 「どこでDBにクエリが飛ぶか」 を押さえ、 できるだけ少ないクエリで目的を 達成するのが基本

  27. とは言っても… パフォーマンスと可読性のジレン マ

  28. Skills & Wills Map の場合 職務経歴書の内容から自動生成されるマップ

  29. テーブル構造 ROOT string label string root_type CATEGORY string label string

    parent_id Entity string label string parent_id has has
  30. 愚直に書くと… for root_label, categories in roots: root = Root.objects.create(label=root_label) for

    category_label, entities in categories: category = Category.objects.create(label=category_label, for entity_label in entities: Entity.objects.create(label=entity_label, category=ca
  31. ROOT, CATEGORY も bulk_create() すると、 ノードの紐づきを 管理するのが大変!

  32. entityだけまとめて bulk_create することに落ち着いた entity_obj_list = [] for root_label, categories in

    roots: root = Root.objects.create(label=root_label) for category_label, entities in categories: category = Category.objects.create(label=category_label, for entity_label in entities: entity_obj_list.append(Entity(label=entity_label, cat Entity.objects.bulk_create(entity_obj_list)
  33. 高々30件程度のデータなら、 愚直に書いても大きな問題にはな らない

  34. プロダクトの特性と相談して、 どこまでパフォーマンスを 追求するか決めよう!

  35. まとめ

  36. どこでDBにクエリが飛ぶかを意識し、 クエリの回数を減らすことが ORMパフォーマンス改善の第一歩 パフォーマンスは知らないうちに劣化する。 気づける仕組みを作るのが大切 プロダクトが扱うデータ量によって、 パフォーマンスより可読性を優先することもある

  37. 宣伝

  38. https://lapras.com