Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

自己紹介 LAPRAS株式会社(2022/01~) Djangoアプリケーション(LAPRAS)開発 データアナリスト 医療者向けプロトタイピングスクール「もいせん」 の講師 Windows, WSL, VSCode, NeoVim Nizキーボード 前職でAzure, C#, .Netをやっていた名残で、 MS系への親しみが深い

Slide 3

Slide 3 text

はじめに

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

ユーザ Djangoアプリ DB クエリ実⾏ Request Request クエリ発⾏ クエリ発⾏ 結果返却 結果返却 モデルに変換 モデルに変換 Response Response ユーザ Djangoアプリ DB

Slide 9

Slide 9 text

ユーザ Djangoアプリ DB Request Request Response Response ユーザ Djangoアプリ DB

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

こんなテーブルを想定 CUSTOMER string name string address ORDER int orderNumber string deliveryAddress created_at datetime places

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

回避策① 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';

Slide 14

Slide 14 text

N+1問題を起こすコード② customers = Customer.objects.filter(address='tokyo').all() for customer in customers: for order in customer.order_set.all(): print(order.orderNumber)

Slide 15

Slide 15 text

回避策② 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);

Slide 16

Slide 16 text

回避策③ 子レコードの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;

Slide 17

Slide 17 text

N+1は知らないうちに紛れ込む みんな悪いのはわかっているけど… 気づける仕組みを作っておくのが大切

Slide 18

Slide 18 text

ローカルで発行される SQLを確認する Django Extensions の shell_plus で、--print- sqlオプションをつける の LOGGING を設定する Django Debug Toolbar の SQL パネルを使う settings.py

Slide 19

Slide 19 text

Production でパフォーマンスを監視する Datadogなどのツールを使う

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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)

Slide 24

Slide 24 text

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'])

Slide 25

Slide 25 text

delete()するたびにDELETE文が飛ぶ 回避策 customers = Customer.objects.filter(address='Tokyo').all() for customer in customers: customer.delete() Customer.objects.all().filter(address='Tokyo').delete()

Slide 26

Slide 26 text

「どこでDBにクエリが飛ぶか」 を押さえ、 できるだけ少ないクエリで目的を 達成するのが基本

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

テーブル構造 ROOT string label string root_type CATEGORY string label string parent_id Entity string label string parent_id has has

Slide 30

Slide 30 text

愚直に書くと… 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

Slide 31

Slide 31 text

ROOT, CATEGORY も bulk_create() すると、 ノードの紐づきを 管理するのが大変!

Slide 32

Slide 32 text

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)

Slide 33

Slide 33 text

高々30件程度のデータなら、 愚直に書いても大きな問題にはな らない

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

まとめ

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

宣伝

Slide 38

Slide 38 text

https://lapras.com