この記事はワイ記法です。やめ太郎@Yametaro氏にリスペクトをこめて。
ある日の我が家
ワイ「趣味でDjangoアプリを作っているんやが」
ワイ「Modelを書くだけでテーブルも作れるし」
ワイ「管理画面も作ってくれるなんてDjangoは太っ腹やな!」
DBの正規化をした
ワイ「外部キーの設定も楽々や」
ワイ「例えば選手のデータと選手ごとの成績はテーブルを分けた方がええな1」
Players
id | year | name | position |
---|---|---|---|
1 | 2019 | 田中 | 投手 |
2 | 2019 | 鈴木 | 一塁手 |
3 | 2020 | 高橋 | 外野手 |
4 | 2020 | 山田 | 二塁手 |
FielderTotalResults
id | player_id | at_bat | hit | homerun | 略 |
---|---|---|---|---|---|
1 | 1 | 4 | 1 | 1 | ・・・ |
2 | 2 | 3 | 3 | 0 | ・・・ |
3 | 3 | 5 | 2 | 1 | ・・・ |
4 | 4 | 4 | 1 | 0 | ・・・ |
※説明用のため簡易的に表しています |
ワイ「FielderTotalResultsのplayer_id
が外部キーで」
ワイ「Playersテーブルのid
と対応しているんや」
ワイ「この例だとFieldeTotalResultsのidが1が田中、2のレコードは鈴木の成績で」
ワイ「3が高橋、4が山田の成績だってはっきりわかんだね」
INNER JOINしたい
ワイ「FielderTotalResultsテーブルは選手ごとに成績の集計をした結果なんや」
ワイ「その結果をPlayersと結合して画面に表示したいんや」
year | name | position | at_bat | hit | homerun | 略 |
---|---|---|---|---|---|---|
2019 | 田中 | 投手 | 4 | 1 | 1 | ・・・ |
2019 | 鈴木 | 一塁手 | 3 | 3 | 0 | ・・・ |
2020 | 高橋 | 外野手 | 5 | 2 | 1 | ・・・ |
2020 | 山田 | 二塁手 | 4 | 1 | 0 | ・・・ |
ワイ「イメージ的にはこんな感じやな」
ワイ「Playersのid
とFielderTotalResultsのplayer_id
を一致させるように記述すればええんかな」
ワイ「例えば鈴木の選手情報と成績を結合したい場合なら」
from django.db import models
from eikan.models import Players, FielderTotalResults
p = Players.objects.get(pk=2)
f = FielderTotalResults.objects.get(player_id=p.id)
ワイ「ん?これじゃただ単にそれぞれのテーブルで同じ選手のレコードを取得しただけやんけ!」
ワイ「結合して1つのデータとしてTemplateに渡したいんやけど」
ワイ「うーん、公式ドキュメント見てもJOINの記述がないなぁ」
ワイ「こういう時はそれぞれ別に取得してTemplateでなんとかするんか?」
実はもうINNER JOINされている
娘(2歳)2「パパー!Djangoだと子テーブルを取得したら」
ワイ「なんやて!?」
ワイ「(2歳ですでにワイよりDjangoを理解している・・・!?)」
ワイ「娘ちゃん、どこでそんなの知ったんや」
娘「保育園でお友達と遊んでたら覚えたの」
ワイ「(保育園ってDjangoでも遊べるんか・・・?)」
ワイ「(ワイは幼稚園通いだったから知らんかったわ・・・)」
ワイ「で、でも娘ちゃん、ほんまに親テーブルと結合されてるんか?」
ワイ「f.name
って書いたら」
ワイ「そんなもんないで、って怒られるんや」
娘「パパったらマニュアル人間ね」
娘「こういう時はね、まず子テーブルのレコードを取得するでしょ?」
f = FielderTotalResults.objects.get(player_id=2)
娘「この状態で子テーブル.外部キー.親テーブルの列名
って書くと、値が取れるよ」
f.player_id.name
>>> 鈴木
ワイ「ほんまや!(マニュアル人間・・・?)」
娘「Templateでも同じように書けば」
娘「わざわざ親テーブルを渡さなくても親テーブルの列の値が使えるし」
娘「親->子->孫ってリレーションしている場合なら」
娘「孫テーブル.子の外部キー.親の外部キー.親の要素
」
娘「って書くことができるの」
ワイ「はぇ〜すっごい・・・・」
ワイ「わいはどこにもJoinできんのに、Djangoは簡単にJOINできるんやなぁ」
娘「パパは誰からも参照されないもんね」
ワイ「せ、せやな・・・」
ワイ「(なんて破壊力のある2歳児なんや・・・)」
以下、コメントをきっかけに追記
別の日
ワイ「ようやく完成したで!」
ワイ「寝る間も惜しんだから8時間しか寝れなかったわ!」
ヨッメ(それ十分寝てますわよね?)
ワイ「サンプルでHerokuにあげとこか」
ワイ「まぁ、内容が内容やからワイ以外ガチで使わないやろけどな!」
娘「やってしまいましたなぁ」
娘「これは大変なことやと思うよ」
娘「これは教育やろなぁ」
ワイ「ファッ!?」
ワイ(関西球団の元監督の様な重圧が・・・!)
ワイ「娘ちゃん、現代は盗塁にはそこまで得点価値は高くないって見解になってるんやで?」
娘「パパ、セイバーメトリクスじゃなくてDjangoのお話よ」
娘「いくらINNER JOINされている状態で取得できても」
娘「今の書き方じゃ値を使おうとするたびに」
娘「大量のクエリが発行されて、とっても重くなるの」
娘「いわゆるN+1問題っていうの」
娘「現にパパがHerokuにデプロイしたやつも大量のクエリが発行されているの」
ワイ「なんとなく重いと思ってたんやけどこんなもんかな・・・って・・・」
ワイ「娘ちゃんはどうやってクエリがたくさん発行されているってわかったんや?」
娘「django-debug-toolbarを使うと発行されたクエリが表示されるようになるの」
娘「ググれば使い方はすぐわかるから省くけど」
娘「パパのアプリで発行されているクエリはこんな感じね」
ページ名 | クエリ数 | 処理時間(ms) |
---|---|---|
現在のチーム | 48 | 959 |
チーム一覧 | 7 | 115 |
チーム詳細 | 324 | 4576 |
野手一覧 | 43 | 770 |
投手一覧 | 15 | 416 |
投手詳細 | 76 | 1426 |
打者詳細 | 66 | 1261 |
試合一覧 | 29 | 453 |
試合詳細 | 20 | 345 |
※登録されているレコード数により一部増減します |
ワイ「チーム詳細はいろいろなテーブルから取得してるから」
ワイ「めっちゃ重いんやな」
ワイ「これもっと軽くなったらええんやけど」
娘「いろんなところでリレーションしているレコードを取得して処理するなら」
娘「コメントであったとおり」
娘「select_related()を使うことで無駄なクエリを発行しなくなるの」
f = FielderTotalResults.objects.select_related('player_id').get(player_id=2)
f.player_id.name
>>> 鈴木
娘「書き方としてはこんな感じね」
娘「こうすることで、あと何回f.player_id.name
って使っても」
娘「最初の一回しかクエリが発行されないの」
ワイ「はぇ~すっごい・・・」
ワイ「じゃあひとまず該当するとこ全部直すやで~」
n週間後
ワイ「だいぶ軽くなってきたやで~」
ワイ「select_related()にするだけでも結構効果がでるんやな」
ワイ「django-debug-toolbarで確認して」
ワイ「全体的な処理の見直しも楽々や」
ワイ「大幅に手直しした結果がこれや」
| ページ名 | クエリ数(前) | クエリ数(後) | 処理時間(前)(ms) | 処理時間(後)(ms) |
|:-:|--:|--:|--:|--:|--:|
| 現在のチーム | 48 | 7 | 959 | 183 |
| チーム一覧 | 7 | 3 | 115 | 75 |
| チーム詳細 | 324 | 15 | 4576 | 110 |
| 野手一覧 | 43 | 3 | 770 | 92 |
| 打者詳細 | 66 | 6 | 1261 | 212 |
| 投手一覧 | 15 | 3 | 416 | 71 |
| 投手詳細 | 76 | 12 | 1426 | 137 |
| 試合一覧 | 29 | 3 | 453 | 1 |
| 試合詳細 | 20 | 6 | 345 | 2 |
※登録されているレコード数により一部増減します
ワイ「なるべくクエリを発行しないように直したから」
ワイ「どこ表示してもサックサクになったやで!」
ワイ「比べてみたら一目瞭然やな!」
ワイ「しかもさらにデータを追加しても」
ワイ「大してクエリ数は増えないようになったで」
ワイ「気になったら実際に触って比較してくれやで~」
重い方:https://trial-endb-v1.herokuapp.com/
軽い方:https://trial-endb-v2.herokuapp.com/
※同じデータが入っています
お風呂にて
ワイ「ワイ自身がこの機能に感動したからこうやって記事にしたんやけどな」
ワイ「ほんまはこのteratailの投稿を見つければ長々とこの記事を読まんでも」
ワイ「すぐ解決する話やったんや」
ワイ「この記事は多くの人にとってノイズになってまうかもしれんのや」
娘「パパ」
娘「teratailの回答を補足する記事も誰かの役には立つと思うよ?」
娘「それに、こうやってワイ記法で書くことによって」
娘「失敗から成功の追体験をすることで」
娘「みんなの記憶に定着していくの」
娘「実際、コメントもらってやってみて」
娘「比較することで身に染みたでしょ?」
ワイ「せやな」
ワイ「ワイ自身「こっちの方がええで!」といきなりベストプラクティスを言われても恩恵が感じられないこともあるから」
ワイ「誰かもこの記事を読んでうまあじを感じてくれたらええんやけど」
娘「うーまーみー!」
ワイ「風呂から上がったらアイスでも食べよか」
娘「わーい食べるー!」
ワイ「(この間までよちよち歩きやったのに)」
ワイ「(負うた子に教えられて、とはこのことやなぁ)」
〜〜(オチもヤマもなく)おしまい〜〜
Github
参考
SQL素人でも分かるテーブル結合(inner joinとouter join)
Django データベース操作 についてのまとめ
Djangoで生成されたクエリを確認したら、大量に発行されていた
【Django】クエリ数を減らすための工夫たち(随時追加予定)
Django逆引きチートシート(QuerySet編)
【Django2.2】Djangoでリレーション先はどう表示するの? 【ListView編】
Djangoで孫テーブルの検索
Djangoでの開発中にN+1問題を発見して根絶やしにするための方法