データベース設計と最適化
インデックス設計
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True) # unique=True で自動インデックス
status = models.CharField(max_length=10, db_index=True) # 単独インデックス
created_at = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE) # FK自動インデックス
class Meta:
indexes = [
models.Index(fields=['status', '-created_at']), # 複合インデックス
models.Index(
fields=['status'],
condition=models.Q(status='published'),
name='published_idx' # 部分インデックス
),
]
N+1問題と対策
# NG: N+1問題(記事ごとにSQLが発行される)
for post in Post.objects.all():
print(post.author.username) # 毎回SELECT
# OK: select_related(JOINで一括取得、ForeignKey用)
for post in Post.objects.select_related('author').all():
print(post.author.username) # 追加SQLなし
# OK: prefetch_related(別クエリで一括取得、M2M/逆引き用)
for author in User.objects.prefetch_related('posts').all():
print(author.posts.count()) # 追加SQLなし
クエリの最適化
# only(): 必要なフィールドだけ取得
posts = Post.objects.only('title', 'slug', 'created_at')
# defer(): 特定フィールドを遅延読み込み
posts = Post.objects.defer('body') # bodyは後でアクセス時に取得
# values(): 辞書で取得(ORMオブジェクトを作らない)
titles = Post.objects.values('title', 'created_at')
# values_list(): タプルやフラットリストで取得
ids = Post.objects.values_list('id', flat=True)
# exists(): 存在チェック(COUNTより高速)
if Post.objects.filter(author=user).exists():
pass
# count(): COUNT集約
total = Post.objects.filter(status='published').count()
# bulk_create(): 一括挿入
posts = [Post(title=f'記事{i}', author=user) for i in range(100)]
Post.objects.bulk_create(posts)
# bulk_update(): 一括更新
posts = Post.objects.filter(status='draft')
for p in posts:
p.status = 'published'
Post.objects.bulk_update(posts, ['status'])
SQLの確認
from django.db import connection
# クエリセットのSQL確認
qs = Post.objects.filter(status='published')
print(qs.query)
# 実行されたSQL一覧
print(connection.queries)
# django-debug-toolbar でGUIで確認
pip install django-debug-toolbar
トランザクション
from django.db import transaction
# デコレータ
@transaction.atomic
def transfer_money(from_acc, to_acc, amount):
from_acc.balance -= amount
from_acc.save()
to_acc.balance += amount
to_acc.save()
# コンテキストマネージャ
def complex_operation():
with transaction.atomic():
# この中の操作は全て成功するか、全て巻き戻される
obj1.save()
obj2.save()
まとめ
- よく検索するフィールドにインデックスを設定
select_related / prefetch_related でN+1問題を防止
only() / defer() / values() で不要なデータ取得を抑制
bulk_create / bulk_update で大量データ操作を高速化
transaction.atomic でデータ整合性を保証