CS/SE Fundamentals

データベース (Databases)

はじめに

データベースはアプリケーションの「記憶」だ。ユーザー情報、診療記録、予約データ -- すべてのビジネスデータはデータベースに永続化される。適切なデータベース設計と運用は、アプリケーションのパフォーマンス、信頼性、保守性に直結する。


リレーショナルデータベース (RDB) vs NoSQL

リレーショナルデータベース (RDB)

テーブル (行と列) でデータを表現し、SQL で操作する。PostgreSQL, MySQL が代表的。

-- テーブル定義の例
CREATE TABLE patients (
  id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name       VARCHAR(100) NOT NULL,
  birth_date DATE NOT NULL,
  email      VARCHAR(255) UNIQUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE appointments (
  id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  patient_id UUID NOT NULL REFERENCES patients(id),
  doctor_id  UUID NOT NULL REFERENCES doctors(id),
  start_time TIMESTAMP NOT NULL,
  status     VARCHAR(20) DEFAULT 'scheduled'
);

強み: データの整合性保証、複雑なクエリ、トランザクション、成熟したエコシステム。

NoSQL

RDB 以外のデータベースの総称。用途に応じて複数の種類がある。

種類使いどころ
ドキュメント DBMongoDB, DynamoDBスキーマが柔軟なデータ
キーバリューRedis, Memcachedキャッシュ、セッション
カラム指向Cassandra, HBase大量の書き込み、時系列データ
グラフ DBNeo4j関係性の深い分析

どちらを選ぶか

RDB を選ぶ場合 (大半のケース):

  • データ間に明確な関係性がある
  • トランザクションが必要 (医療データは基本これ)
  • データの整合性が重要
  • 複雑なクエリが必要

NoSQL を検討する場合:

  • 超大量の読み書きが必要
  • スキーマが頻繁に変わる
  • 水平スケーリングが必須
  • 特定のアクセスパターンに最適化したい

ACID 特性

トランザクションが満たすべき4つの性質。医療データを扱う上で極めて重要。

特性意味
Atomicity (原子性)トランザクション内の操作は全部成功か全部失敗予約作成と通知送信を一括で
Consistency (一貫性)トランザクション前後でデータの整合性が保たれる外部キー制約の維持
Isolation (分離性)並行するトランザクションが互いに干渉しない同時予約での二重予約防止
Durability (永続性)コミットされたデータは失われないサーバー障害後もデータが残る
-- トランザクションの例: 予約作成と空き枠の更新を一括で行う
BEGIN;
  INSERT INTO appointments (patient_id, doctor_id, start_time, status)
  VALUES ('patient-123', 'doctor-456', '2026-03-10 10:00', 'scheduled');

  UPDATE time_slots
  SET is_available = false
  WHERE doctor_id = 'doctor-456' AND start_time = '2026-03-10 10:00';
COMMIT;
-- どちらかが失敗した場合、両方がロールバックされる

インデックス (Index)

データベースの検索を高速化するための仕組み。書籍の索引と同じ概念。

インデックスの仕組み

インデックスなし: テーブル全体を走査 (Full Table Scan) → O(n) インデックスあり: B-Tree を使った探索 → O(log n)

-- インデックスの作成
CREATE INDEX idx_appointments_patient_id ON appointments(patient_id);
CREATE INDEX idx_appointments_start_time ON appointments(start_time);

-- 複合インデックス (よくセットで検索するカラム)
CREATE INDEX idx_appointments_doctor_time
  ON appointments(doctor_id, start_time);

インデックスを張るべき場所

  • WHERE 句で頻繁に使われるカラム
  • JOIN の結合キー
  • ORDER BY で使われるカラム
  • 外部キー (多くの DB は自動で張らない)

インデックスの注意点

  • 書き込みが遅くなる: INSERT/UPDATE のたびにインデックスも更新される
  • ストレージを消費する: インデックスもディスク上のデータ
  • 多すぎるとリスクが高い: 不要なインデックスは削除する
  • EXPLAIN で確認する: クエリが実際にインデックスを使っているか確認
-- クエリの実行計画を確認
EXPLAIN ANALYZE
SELECT * FROM appointments
WHERE doctor_id = 'doctor-456'
  AND start_time BETWEEN '2026-03-01' AND '2026-03-31';

クエリ最適化

よくあるパフォーマンス問題と対策

1. N+1 クエリ問題

-- Bad: 患者ごとにクエリを発行
SELECT * FROM patients;
-- ループ内で:
SELECT * FROM appointments WHERE patient_id = ?;  -- N回実行

-- Good: JOIN またはバッチ取得
SELECT p.*, a.*
FROM patients p
LEFT JOIN appointments a ON p.id = a.patient_id;

2. SELECT * の回避

-- Bad: 不要なカラムまで取得
SELECT * FROM patients;

-- Good: 必要なカラムだけ取得
SELECT id, name, email FROM patients;

3. 適切な WHERE 句

-- Bad: インデックスが効かないパターン
SELECT * FROM patients WHERE LOWER(name) = 'tanaka';

-- Good: 関数インデックスを使うか、アプリ側で正規化
CREATE INDEX idx_patients_name_lower ON patients(LOWER(name));

4. LIMIT の活用

-- ページネーション
SELECT * FROM appointments
WHERE doctor_id = 'doctor-456'
ORDER BY start_time DESC
LIMIT 20 OFFSET 40;

-- 大量データではカーソルベースのページネーションが効率的
SELECT * FROM appointments
WHERE doctor_id = 'doctor-456'
  AND start_time < '2026-03-01T10:00:00'
ORDER BY start_time DESC
LIMIT 20;

正規化 (Normalization)

データの冗長性を排除し、整合性を保つための設計手法。

主要な正規形

第1正規形 (1NF): 各カラムが原子的な値を持つ (繰り返しグループを排除)

-- Bad: 1NF 違反
| patient_id | phone_numbers          |
|------------|------------------------|
| 1          | 090-1234-5678, 03-... |

-- Good: 別テーブルに分離
patients: | id | name   |
phones:   | id | patient_id | number        |

第2正規形 (2NF): 1NF + 部分関数従属の排除 第3正規形 (3NF): 2NF + 推移的関数従属の排除

実務的なバランス

  • 通常は第3正規形までで十分
  • パフォーマンスのために意図的に非正規化することもある (レポーティング用のテーブルなど)
  • 非正規化する場合は理由をドキュメントに残す

マイグレーション (Migration)

データベーススキーマの変更を管理・追跡する仕組み。

マイグレーションの原則

  • バージョン管理する: マイグレーションファイルは Git で管理
  • 前方互換性を意識する: 旧バージョンのアプリケーションとの共存期間を考慮
  • ロールバック可能にする: updown の両方を定義
  • 破壊的変更を避ける: カラム削除やリネームは段階的に
-- マイグレーション例: カラム追加 (安全)
ALTER TABLE patients ADD COLUMN phone VARCHAR(20);

-- 危険な操作: カラム削除は段階的に行う
-- Step 1: アプリケーションからカラムの参照を削除してデプロイ
-- Step 2: しばらく経ってからカラムを削除
ALTER TABLE patients DROP COLUMN old_column;

大規模テーブルのマイグレーション

数百万行のテーブルへの ALTER TABLE はロックを取得し、サービスが停止する可能性がある。

対策:

  • pt-online-schema-change (MySQL) や pg_repack (PostgreSQL) の利用
  • 新テーブル作成 → データコピー → テーブル名変更 のパターン
  • NOT NULL 制約の追加は、DEFAULT 値と一緒に行う

ORM の利点と落とし穴

ORM (Object-Relational Mapping) は、プログラムのオブジェクトとデータベースのテーブルを対応付ける。

利点

  • 生産性: SQL を書かずにデータ操作ができる
  • 型安全性: TypeScript の ORM なら型チェックが効く
  • マイグレーション管理: 多くの ORM がマイグレーション機能を内蔵
  • DB 方言の吸収: 複数の DB に対応しやすい

落とし穴

  • N+1 問題: Lazy Loading のデフォルト設定で発生しがち
  • 非効率なクエリ: ORM が生成する SQL が最適でない場合がある
  • 学習コスト: ORM 固有の API を学ぶ必要がある
  • 複雑なクエリの限界: 集計や分析クエリは生 SQL の方が書きやすい
// ORM の使い方 (Prisma の例)
// Good: リレーションを明示的に include
const patient = await prisma.patient.findUnique({
  where: { id: "patient-123" },
  include: {
    appointments: {
      where: { status: "scheduled" },
      orderBy: { startTime: "asc" },
      take: 10,
    },
  },
});

// 複雑なクエリは生 SQL を使う
const stats = await prisma.$queryRaw`
  SELECT doctor_id, COUNT(*) as appointment_count
  FROM appointments
  WHERE start_time >= ${startDate}
  GROUP BY doctor_id
  ORDER BY appointment_count DESC
`;

医療 SaaS でのデータベース設計の考慮点

  • 監査ログ: 全てのデータ変更を追跡可能にする (誰が、いつ、何を変更したか)
  • 論理削除 (Soft Delete): deleted_at カラムで削除をマーク。物理削除は原則行わない
  • 暗号化: 機密性の高いカラムはアプリケーションレベルで暗号化
  • バックアップ: 定期的なバックアップとリストアのテスト
  • テナント分離: マルチテナント構成では、テナント間のデータ漏洩を防ぐ設計

Agent-first 開発においてこれが重要な理由

データベースはアプリケーションの最も重要な部分であり、エージェントに任せきりにすべきではない。

  • スキーマ設計をレビューできる: エージェントが提案するテーブル設計の正規化レベル、インデックス、制約を評価できる
  • パフォーマンス問題を予見できる: 「このクエリはインデックスが効かない」「N+1 が発生する」と事前に指摘できる
  • マイグレーションの安全性を判断できる: 「この ALTER TABLE は本番で実行すると数分のロックがかかる」といったリスクを評価できる
  • 適切なトランザクション境界を指示できる: 「この2つの操作はアトミックに実行する必要がある」と明確に伝えられる

データベースの変更は取り消しが難しい。エージェントが生成するマイグレーションは特に慎重にレビューすべきだ。


Further Reading