自動化プロジェクトで扱うデータは、メトリクスデータ・分析結果・パフォーマンス記録など、全てが時系列データです。当初はSQLiteやCSVファイルで管理していましたが、以下の限界に直面しました。
- 並行アクセス: 複数のlaunchdジョブが同時にデータを書き込む → SQLiteのロック競合
- クエリ性能: 数百万行の時系列データに対する範囲クエリが遅い
- 型安全性: CSVでは型が曖昧、SQLiteでは型制約が弱い
本記事では、PostgreSQL 17 + TimescaleDB を採用した経緯と、個人開発での設計判断を記録します。
なぜPostgreSQLか
SQLiteの限界
SQLiteは「1プロセスからの読み書き」には優れていますが、本番システムでは複数のジョブが同時にDBにアクセスします。
109:00 ジョブA: メトリクスデータ書き込み209:00 ジョブB: 分析結果書き込み(ジョブAと同時実行)309:00 ジョブC: パフォーマンス読み取り(↑と同時実行)4→ SQLite: "database is locked" エラーWALモードを有効にしても、書き込みの同時実行は1つに制限されます。90ジョブをlaunchdで運用する環境では、この制約は致命的でした。
PostgreSQLを選んだ3つの理由
| 観点 | SQLite | PostgreSQL |
|---|---|---|
| 並行書き込み | 1プロセスのみ | MVCC対応(複数同時書き込み可) |
| 型安全性 | 型アフィニティ(弱い型) | 厳密な型制約 |
| 時系列拡張 | なし | TimescaleDB対応 |
| セットアップ | ファイル1つ | Homebrewでインストール |
個人開発で「PostgreSQLはオーバースペック」と思われがちですが、並行アクセスと型安全性が必要な時点で、SQLiteの方がむしろリスクが高いです。
macOSでのセットアップ
Homebrew インストール
1# PostgreSQL 17 インストール2brew install postgresql@173
4# サービス起動5brew services start postgresql@176
7# TimescaleDB インストール8brew install timescaledb9
10# TimescaleDB 有効化11timescaledb-tune --yes12brew services restart postgresql@17データベース作成
1# データベース作成2createdb metrics_analysis3
4# TimescaleDB拡張を有効化5psql metrics_analysis -c "CREATE EXTENSION IF NOT EXISTS timescaledb;"テーブル設計
daily_metrics: 日次メトリクスデータ
1CREATE TABLE daily_metrics (2 id BIGSERIAL PRIMARY KEY,3 datetime TIMESTAMPTZ NOT NULL,4 source VARCHAR(20) NOT NULL,5 timeframe VARCHAR(10) NOT NULL,6 value DOUBLE PRECISION NOT NULL,7 min_value DOUBLE PRECISION NOT NULL,8 max_value DOUBLE PRECISION NOT NULL,9 avg_value DOUBLE PRECISION NOT NULL,10 sample_count DOUBLE PRECISION,11 created_at TIMESTAMPTZ DEFAULT NOW(),12 UNIQUE (datetime, source, timeframe)13);14
15-- インデックス2 collapsed lines
16CREATE INDEX idx_metrics_source_time17 ON daily_metrics (source, datetime DESC);必須カラムはDateTime, Value, MinValue, MaxValue, AvgValue, SampleCountの6つです。これは時系列メトリクスの標準的なフォーマットです。
analysis_results: 分析結果
1CREATE TABLE analysis_results (2 id BIGSERIAL PRIMARY KEY,3 datetime TIMESTAMPTZ NOT NULL,4 source VARCHAR(20) NOT NULL,5 timeframe VARCHAR(10) NOT NULL,6 metric_name VARCHAR(50) NOT NULL,7 metric_value DOUBLE PRECISION,8 reference_date DATE NOT NULL,9 composite_id VARCHAR(100) NOT NULL,10 created_at TIMESTAMPTZ DEFAULT NOW()11);12
13-- BIツール対応: 複合IDインデックス14CREATE INDEX idx_analysis_composite15 ON analysis_results (composite_id);performance_snapshots: パフォーマンス記録(TimescaleDB hypertable)
1CREATE TABLE performance_snapshots (2 datetime TIMESTAMPTZ NOT NULL,3 metric_name VARCHAR(50) NOT NULL,4 metric_value DOUBLE PRECISION NOT NULL,5 source VARCHAR(20),6 timeframe VARCHAR(10),7 metadata JSONB8);9
10-- TimescaleDB hypertable に変換(自動パーティション)11SELECT create_hypertable(12 'performance_snapshots',13 'datetime',14 chunk_time_interval => INTERVAL '1 month'15);TimescaleDBのhypertableは、時系列データを月単位で自動パーティションします。古いデータの削除やアーカイブが、パーティション単位で高速に行えます。
Pythonからの接続
共有モジュール: mgmtDB
DB接続は共有モジュールで抽象化します。各スクリプトが個別に接続情報を持つと、接続先の変更時に全スクリプトの修正が必要になります。
1from myModules.mgmtDB import get_db_connection2
3
4def save_analysis_results(df: pd.DataFrame, table_name: str) -> None:5 """分析結果をDBに保存する。6
7 Args:8 df: 保存するDataFrame9 table_name: 保存先テーブル名10 """11 with get_db_connection() as conn:12 df.to_sql(table_name, conn, if_exists="append", index=False)時系列クエリの例
1import pandas as pd2from myModules.mgmtDB import get_db_connection3
4
5def get_recent_performance(6 days: int = 30,7 source: str | None = None,8) -> pd.DataFrame:9 """直近N日のパフォーマンスデータを取得する。"""10 query = """11 SELECT datetime, metric_name, metric_value, source12 FROM performance_snapshots13 WHERE datetime >= NOW() - INTERVAL '%s days'14 """15 params = [days]9 collapsed lines
16
17 if source is not None:18 query += " AND source = %s"19 params.append(source)20
21 query += " ORDER BY datetime DESC"22
23 with get_db_connection() as conn:24 return pd.read_sql(query, conn, params=params)TimescaleDB hypertable の利点
自動パーティション
通常のPostgreSQLでは、テーブルのパーティショニングを手動で設定する必要があります。TimescaleDBはcreate_hypertable()を呼ぶだけで、指定した時間間隔で自動パーティションされます。
1-- 月単位の自動パーティション2SELECT create_hypertable(3 'performance_snapshots',4 'datetime',5 chunk_time_interval => INTERVAL '1 month'6);7
8-- 古いデータの一括削除(パーティション単位で高速)9SELECT drop_chunks('performance_snapshots', older_than => INTERVAL '1 year');時系列専用の集約関数
1-- 直近30日の日次平均を取得2SELECT time_bucket('1 day', datetime) AS day,3 avg(metric_value) AS avg_value4FROM performance_snapshots5WHERE metric_name = 'daily_throughput'6 AND datetime >= NOW() - INTERVAL '30 days'7GROUP BY day8ORDER BY day;time_bucket()はTimescaleDB固有の関数で、任意の時間間隔でのグルーピングが簡潔に書けます。
BIツール対応:複合IDカラム
BIツール(Metabase等)との連携のため、各レコードに複合IDを付与しました。
1from datetime import datetime2
3
4def generate_composite_id(5 source: str,6 timeframe: str,7 sequence: int,8) -> str:9 """複合IDを生成する。10
11 形式: {YYYYMMDD}_{Source}_{TimeFrame}_{YYYYMMDDHHMM}_{00001-99999}12 例: 20260327_server01_4H_202603271500_0000113 """14 today = datetime.now().strftime("%Y%m%d")15 now = datetime.now().strftime("%Y%m%d%H%M")1 collapsed line
16 return f"{today}_{source}_{timeframe}_{now}_{sequence:05d}"この複合IDにより、BIツールから「特定日・特定データソース・特定タイムフレーム」のデータを一意に参照できます。
学んだこと
1. 個人開発でもPostgreSQLを選ぶべき場面がある
「個人開発ならSQLite」という定石は、並行アクセスと型安全性が必要になった時点で見直すべきです。Homebrewで簡単にインストールでき、運用コストも大きくありません。
2. TimescaleDBの自動パーティションは時系列データの必須機能
手動パーティションは設定ミスの温床になります。create_hypertable()一行で自動パーティションが有効になるTimescaleDBは、時系列データを扱うなら必須の拡張です。
3. 共有モジュール(mgmtDB)でDB接続を抽象化すべき
各スクリプトが個別に接続情報を持つと、接続先変更時に全スクリプトを修正する羽目になります。get_db_connection()で接続を一元管理することで、環境切り替えも1箇所の変更で済みます。
まとめ
個人開発でのDB設計で重要なのは以下の3点です。
- 並行アクセスが必要ならPostgreSQL: SQLiteのロック競合は90ジョブ運用で致命的です。PostgreSQLのMVCCが解決します
- TimescaleDBで自動パーティション:
create_hypertable()一行で月単位のパーティションが有効になります。古いデータの削除も高速です - DB接続は共有モジュールで抽象化:
mgmtDB.get_db_connection()で接続を一元管理し、環境依存を排除します