
目次
- 1 user@sinyblog:~/article ❯ 01_motivation.mdなぜ自動削除が必要か — 手動運用の限界
- 2 user@sinyblog:~/article ❯ 02_architecture.md設計の全体像
- 3 user@sinyblog:~/article ❯ 03_sample_model.mdサンプルモデル(パフォーマンス履歴テーブル)
- 4 user@sinyblog:~/article ❯ 04_filter_delete.md削除ロジックの基本 — filter + lte で日付絞り込み
- 5 user@sinyblog:~/article ❯ 05_custom_command.mdDjango カスタムコマンドの実装
- 6 user@sinyblog:~/article ❯ 06_scheduling.mdスケジュール実行 — cron / タスクスケジューラ
- 7 user@sinyblog:~/article ❯ 07_operations.md運用 Tips — ログ・通知・トラブル対処
- 8 user@sinyblog:~/article ❯ 99_summary.mdまとめ
Django · Maintenance Automation · 2026 Edition
Django アプリで「ログテーブル」「履歴テーブル」「監査テーブル」を運用していると、データ量が日々肥大化して DB 容量を圧迫します。手動削除では運用負荷が高く、削除漏れも起きがちです。本記事では Django の カスタムコマンド + cron(or タスクスケジューラ) を使って、保持期間を超えた古いレコードを 定期自動削除 するパイプラインを、ログ出力・エラー時メール通知込みで実装します。所要時間 25 分。
user@sinyblog:~/article ❯ 01_motivation.mdなぜ自動削除が必要か — 手動運用の限界
運用フェーズに入った Django アプリでは、以下のような「増え続けるテーブル」が必ず登場します。
- サーバーのパフォーマンス履歴(CPU / メモリ / ディスク使用率)
- ユーザー操作のアクセスログ・監査ログ
- API 呼び出し履歴・エラーログ
- バッチ処理の実行履歴
これらは 直近のデータだけが価値を持ち、古いものは保持コストの方が高くなる 性質があります。データ保持期間を「過去 6 ヶ月分」のように決めて、超えたものは順次削除するのが定石です。
「気が向いたときに削除」「忘れた頃に容量警告」は典型的な失敗パターン。週次・月次の自動削除パイプラインを組んでおけば、運用担当者の頭から「DB メンテナンス」の重荷が消えます。
「テーブルそのものを削除したい」「全データをリセットしたい」など、削除操作の基本パターンは姉妹記事 【Django】モデル・テーブル・レコードを削除する完全ガイド で解説しています。
user@sinyblog:~/article ❯ 02_architecture.md設計の全体像
本記事で構築するパイプラインは、以下のシンプルな構成です。
- Django カスタムコマンド —
python manage.py <コマンド名>で実行可能なバッチ処理を定義 - 削除ロジック —
filter(update_time__lte=<N日前>).delete()で日付絞り込み一括削除 - ログ出力 —
loggingモジュールで実行履歴を日時付きファイルに記録 - エラー通知 —
try/exceptで例外捕捉、smtplibでメール送信 - スケジュール実行 — Linux: cron、Windows: タスクスケジューラ で定期起動
user@sinyblog:~/article ❯ 03_sample_model.mdサンプルモデル(パフォーマンス履歴テーブル)
本記事の例では、サーバーの CPU / メモリ使用率を時系列で記録する PerfHist モデルを題材にします。過去 180 日(約 6 ヶ月)以前のレコードを定期削除する という想定です。
from django.db import models
from datetime import datetime
class PerfHist(models.Model):
class Meta:
verbose_name = 'パフォーマンスデータ履歴情報'
verbose_name_plural = 'パフォーマンスデータ履歴情報'
server_name = models.ForeignKey(
Server, on_delete=models.CASCADE, verbose_name="サーバ名"
)
cpu = models.IntegerField(help_text="単位は%", verbose_name="CPU使用率")
memory = models.IntegerField(help_text="単位は%", verbose_name="メモリ使用率")
update_time = models.DateTimeField(default=datetime.now, verbose_name="更新日時")
def __str__(self):
return str(self.server_name)
update_time フィールドが 削除判定の基準日 になります。
user@sinyblog:~/article ❯ 04_filter_delete.md削除ロジックの基本 — filter + lte で日付絞り込み
「N 日前以前のレコードを全削除」は Django ORM の filter() + ルックアップ __lte(less than or equal) + delete() の組み合わせで一発です。
from datetime import date, timedelta
from app1.models import PerfHist
# 180 日前の日付を取得
before_180_days = date.today() - timedelta(days=180)
# update_time が 180 日前以前のレコードを全削除
PerfHist.objects.filter(update_time__lte=before_180_days).delete()
| ルックアップ | 意味 |
|---|---|
__lt |
未満(less than) |
__lte |
以下(less than or equal) — 本記事はこれを使用 |
__gt |
より大きい(greater than) |
__gte |
以上(greater than or equal) |
__range=(d1, d2) |
2 つの日付の間 |
初回実行前に必ず .count() で削除対象件数を確認してください。timedelta の単位ミスや基準日の設定ミスで意図しない大量削除を起こす事故が多発します。
qs = PerfHist.objects.filter(update_time__lte=before_180_days)
print(f"削除対象: {qs.count()} 件")
# 想定通りなら実行
# qs.delete()
user@sinyblog:~/article ❯ 05_custom_command.mdDjango カスタムコマンドの実装
削除ロジックを python manage.py rotate_perfhist のように呼び出せるカスタムコマンドにラップします。
ディレクトリ構成
app1/
├── __init__.py
├── models.py
├── management/ ← 新規作成
│ ├── __init__.py ← 空ファイルでOK
│ └── commands/ ← 新規作成
│ ├── __init__.py ← 空ファイルでOK
│ └── rotate_perfhist.py ← 本記事のカスタムコマンド
カスタムコマンド本体
# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand
from app1.models import PerfHist
from datetime import date, datetime, timedelta
from email.mime.text import MIMEText
import smtplib
import sys
import os
import logging
# ===== 初期設定 =====
LOG_DIR = "/var/log/django_rotate/" # ログ出力先(環境に合わせて変更)
RETENTION_DAYS = 180 # データ保持期間(日)
# ===== ログファイル設定 =====
date_name = datetime.now().strftime("%Y%m%d-%H%M%S")
file_name = os.path.join(LOG_DIR, f"{date_name}_ROTATE_PERFHIST.log")
logging.basicConfig(
filename=file_name,
level=logging.DEBUG,
format='%(asctime)s %(message)s',
datefmt='%Y/%m/%d %H:%M:%S',
)
def send_mail(subject, content):
"""異常終了時の通知メール送信"""
message = MIMEText(content)
message['Subject'] = subject
message['From'] = 'noreply@example.com'
message['To'] = 'admin@example.com'
with smtplib.SMTP('mail.example.com') as sender:
sender.sendmail(message['From'], message['To'], message.as_string())
class Command(BaseCommand):
help = '180 日以前の PerfHist レコードを削除する'
def handle(self, *args, **options):
logging.info('[正常] パフォーマンス履歴データのハウスキープ処理を開始')
try:
before_n_days = date.today() - timedelta(days=RETENTION_DAYS)
qs = PerfHist.objects.filter(update_time__lte=before_n_days)
count = qs.count()
logging.info(f'[情報] 削除対象件数: {count} 件')
deleted_count, _ = qs.delete()
logging.info(f'[正常] {deleted_count} 件のレコードを削除しました')
except Exception as e:
logging.exception('[異常] ハウスキープ処理でエラー発生')
send_mail(
subject='【異常終了】PerfHist ハウスキープジョブ',
content=f'PerfHist ハウスキープでエラー発生。\n詳細: {e}\nログファイル: {file_name}',
)
sys.exit(1)
logging.info('[正常] ハウスキープ処理が正常終了しました')
実行方法
cd /path/to/your/project
python manage.py rotate_perfhist
user@sinyblog:~/article ❯ 06_scheduling.mdスケジュール実行 — cron / タスクスケジューラ
カスタムコマンドができたので、あとは OS 側のスケジューラから定期起動するだけです。
Linux / macOS — cron
# 毎日 午前 3 時に実行
0 3 * * * cd /path/to/your/project && /path/to/venv/bin/python manage.py rotate_perfhist
# 毎週日曜午前 4 時に実行
0 4 * * 0 cd /path/to/your/project && /path/to/venv/bin/python manage.py rotate_perfhist
Windows — タスクスケジューラ
GUI からの設定が一般的ですが、コマンドラインで一発登録するなら以下:
schtasks /Create /SC DAILY /TN "DjangoRotatePerfhist" /ST 03:00 ^
/TR "C:\path\to\venv\Scripts\python.exe C:\path\to\project\manage.py rotate_perfhist" ^
/RL HIGHEST
大量レコード削除は DB に負荷をかけるので、ユーザーアクセスが少ない深夜〜早朝(午前 3〜5 時)が定石です。
user@sinyblog:~/article ❯ 07_operations.md運用 Tips — ログ・通知・トラブル対処
ログファイルの確認
本記事の実装では {日時}_ROTATE_PERFHIST.log 形式でログが出力されます。正常時の例:
2026/05/02 03:00:00 [正常] パフォーマンス履歴データのハウスキープ処理を開始
2026/05/02 03:00:00 [情報] 削除対象件数: 14523 件
2026/05/02 03:00:01 [正常] 14523 件のレコードを削除しました
2026/05/02 03:00:01 [正常] ハウスキープ処理が正常終了しました
異常時のメール通知
例外発生時は登録メールアドレスに件名「【異常終了】PerfHist ハウスキープジョブ」で通知されます。本文には例外内容とログファイルパスが含まれるので、すぐに調査できます。
よくあるトラブル
| 症状 | 原因 | 対処 |
|---|---|---|
| cron で実行されるとエラー | 環境変数(DJANGO_SETTINGS_MODULE 等)が読まれてない |
cron に cd /path && /path/venv/bin/python ... 形式でフルパス指定 or source /path/venv/bin/activate |
| 削除に時間がかかりすぎる | 対象件数が多すぎて 1 トランザクションで処理しきれない | バッチサイズで分割: qs[:10000].delete() をループ |
| メールが届かない | SMTP 認証 / TLS 設定不足 | smtplib.SMTP_SSL + login() で認証付きに変更 |
| 関連レコードが残ってしまう | ForeignKey の on_delete が SET_NULL 等 |
CASCADE に変更 or 関連テーブル側でも別途削除コマンドを用意 |
user@sinyblog:~/article ❯ 99_summary.mdまとめ
本記事のポイントは 3 つです。
- 削除ロジックは
filter(update_time__lte=N日前).delete()の 1 行で書ける。本番では.count()で件数確認を必ず挟む - カスタムコマンド + ログ + try/except + メール通知 をセットで作っておけば、運用時の可観測性と安全性が一気に上がる
- cron(Linux) or タスクスケジューラ(Windows) で定期起動すれば、運用担当者の手を完全に離れる
ログテーブル・履歴テーブルが肥大化して頭を抱える前に、運用初期からこの仕組みを入れておくのがおすすめです。
