PostgreSQLで外部キーのCASCADE削除連鎖を可視化する

## はじめに
こんにちは。データベース設計やマイグレーションに関わる方なら、外部キー制約のCASCADE削除の影響範囲を把握することの重要性を感じたことがあるかと思います。

PostgreSQLを採用しているプロダクトでは、特定のテーブルのレコードを削除した際に、CASCADE制約によって連鎖的に削除されるレコードの範囲を事前に把握することは非常に重要です。これは特に以下のような場面で役立ちます：

- データ削除前のリスク評価
- データベース設計の見直し
- マイグレーション計画の策定

本記事では、PostgreSQLの`pg_constraint`テーブルを活用して、CASCADE削除の連鎖を可視化するクエリを紹介します。このクエリは任意のテーブルから始まるCASCADE削除の影響範囲を再帰的に特定できる汎用的なものです。

## CASCADE削除の連鎖を可視化するクエリ

```sql
WITH RECURSIVE cascade_graph AS (
  -- アンカー: 指定テーブルを参照している外部キー制約を取得
  SELECT
    con.conrelid::regclass::text AS child_table,
    con.confrelid::regclass::text AS parent_table,
    con.conname                  AS constraint_name
  FROM pg_constraint con
  WHERE
    con.contype   = 'f'
    AND con.confdeltype = 'c'
    AND con.confrelid = 'public.shops'::regclass

  UNION ALL

  -- 再帰部: 子テーブルをさらに参照しているテーブルを探索（自己参照は除外）
  SELECT
    c2.conrelid::regclass::text,
    c2.confrelid::regclass::text,
    c2.conname
  FROM pg_constraint c2
  JOIN cascade_graph cg
    ON c2.confrelid::regclass::text = cg.child_table
  WHERE
    c2.contype   = 'f'
    AND c2.confdeltype = 'c'
    AND c2.conrelid::regclass::text <> c2.confrelid::regclass::text
)

SELECT DISTINCT
  parent_table AS 親テーブル,
  child_table  AS 子テーブル,
  constraint_name AS 制約名
FROM cascade_graph
ORDER BY 親テーブル, 子テーブル;
```

このクエリを実行すると、特定のテーブル（この例では`public.shops`）を削除した際に連鎖的に削除される可能性のあるテーブルの一覧が表示されます。

### 出力結果例

例えば、ECサイトのデータベースで`shops`テーブルに対してこのクエリを実行した場合、以下のような結果が得られます：

```
      親テーブル     |       子テーブル       |      制約名
--------------------+-----------------------+--------------------
 public.shops       | public.shop_staff     | staff_shop_fk
 public.shops       | public.products       | products_shop_fk
 public.shops       | public.shop_reviews   | reviews_shop_fk
 public.products    | public.product_images | image_product_fk
 public.products    | public.inventory      | inventory_product_fk
 public.products    | public.order_items    | order_items_product_fk
```

この結果から、`shops`テーブルのレコードを削除すると、以下の影響が発生することがわかります：

1. 最初のレベル（直接的な影響）:
   - `shop_staff`テーブルの関連レコードが削除される（店舗スタッフ情報）
   - `products`テーブルの関連レコードが削除される（店舗の商品情報）
   - `shop_reviews`テーブルの関連レコードが削除される（店舗レビュー）

2. 二次的な影響（一段階先の連鎖）:
   - `products`経由で`product_images`テーブルのレコードが削除される（商品画像）
   - `products`経由で`inventory`テーブルのレコードが削除される（在庫情報）
   - `products`経由で`order_items`テーブルのレコードが削除される（注文商品詳細）

このように、複数の階層にわたる削除の連鎖を一目で把握できます。

## クエリの解説

このクエリは再帰的共通テーブル式（WITH RECURSIVE）を使用して、CASCADE削除の連鎖を追跡します。詳細な解説は以下の通りです：

### 1. アンカー部分

```sql
SELECT
  con.conrelid::regclass::text AS child_table,
  con.confrelid::regclass::text AS parent_table,
  con.conname                  AS constraint_name
FROM pg_constraint con
WHERE
  con.contype   = 'f'
  AND con.confdeltype = 'c'
  AND con.confrelid = 'public.shops'::regclass
```

この部分では：
- `pg_constraint`テーブルから外部キー制約（`contype = 'f'`）を検索
- CASCADE削除設定されている制約のみを抽出（`confdeltype = 'c'`）
- 指定テーブル（この例では`public.shops`）を参照している外部キーを取得

### 2. 再帰部分

```sql
SELECT
  c2.conrelid::regclass::text,
  c2.confrelid::regclass::text,
  c2.conname
FROM pg_constraint c2
JOIN cascade_graph cg
  ON c2.confrelid::regclass::text = cg.child_table
WHERE
  c2.contype   = 'f'
  AND c2.confdeltype = 'c'
  AND c2.conrelid::regclass::text <> c2.confrelid::regclass::text
```

この部分では：
- 前のステップで見つかった子テーブルを今度は親として捉え、それを参照している外部キーを検索
- 自己参照は除外（`c2.conrelid::regclass::text <> c2.confrelid::regclass::text`）
- CASCADE削除設定されている制約のみを抽出
- これにより、連鎖的にすべての影響範囲を特定

### 3. 結果の整形

```sql
SELECT DISTINCT
  parent_table AS 親テーブル,
  child_table  AS 子テーブル,
  constraint_name AS 制約名
FROM cascade_graph
ORDER BY 親テーブル, 子テーブル;
```

この部分では：
- 重複を排除（`DISTINCT`）
- わかりやすい列名に変更
- 親テーブル、子テーブルでソート

## 一般的なCASCADE連鎖の確認方法との比較

一般的に、CASCADE削除の連鎖を確認する方法としては以下のようなアプローチがあります：

1. **情報スキーマからの確認**
   ```sql
   SELECT
     tc.table_schema, tc.constraint_name,
     tc.table_name, kcu.column_name,
     ccu.table_schema AS foreign_table_schema,
     ccu.table_name AS foreign_table_name,
     ccu.column_name AS foreign_column_name,
     rc.delete_rule
   FROM information_schema.table_constraints AS tc
   JOIN information_schema.key_column_usage AS kcu
     ON tc.constraint_name = kcu.constraint_name
   JOIN information_schema.constraint_column_usage AS ccu
     ON ccu.constraint_name = tc.constraint_name
   JOIN information_schema.referential_constraints AS rc
     ON tc.constraint_name = rc.constraint_name
   WHERE constraint_type = 'FOREIGN KEY' AND rc.delete_rule = 'CASCADE';
   ```

2. **テーブル定義の確認**
   ```sql
   \d+ テーブル名
   ```

3. **サンプルデータでのテスト**
   テスト環境で実際に削除を試してみる

しかし、これらの方法には以下のような課題があります：
- 再帰的な連鎖を視覚化できない
- 深い階層の連鎖を追跡するのが難しい
- 大規模データベースでは全体像を把握しづらい

## CASCADEの連鎖を把握するための他の方法やツール

冒頭で紹介した再帰クエリ以外にも、CASCADE削除の連鎖を把握するための方法やツールがいくつか存在します。それぞれの特徴と適用場面を見ていきましょう。

### 1. データベース可視化ツール

**pgAdmin**や**DBeaver**などのデータベース管理ツールには、ERダイアグラムを生成する機能があります。これらのツールを使うと、テーブル間のリレーションシップを視覚的に確認できます。

```
- pgAdmin: PostgreSQL専用の管理ツールで、ERダイアグラム機能を提供
- DBeaver: マルチデータベース対応の管理ツールで、ERダイアグラムやデータベース依存関係を視覚化
- DataGrip: JetBrains社の有料データベース管理ツールで、依存関係の可視化が可能
```

これらのツールの利点：
- 視覚的に関係性を把握できる
- マウス操作で直感的に確認できる

欠点：
- CASCADE削除の連鎖のみを抽出して表示することは難しい
- 大規模データベースではダイアグラムが複雑になりすぎる
- 完全に正確な依存関係までは把握しづらい場合がある

### 2. ORMツールによる検証

**Django ORM**や**SQLAlchemy**などのORMツールでは、モデル定義からCASCADE削除の連鎖を確認できることがあります。

例えば、Djangoの場合：
```python
# モデル間の依存関係とCASCADEの設定を確認
from django.db.models import GET_DEPENDENT_OBJECTS
from your_app.models import YourModel

def get_cascade_dependencies(model_instance):
    collector = Collector(using='default')
    collector.collect([model_instance], keep_parents=False)
    return collector.data
```

このアプローチの利点：
- アプリケーションコードベースから直接確認できる
- 実際の削除処理と同じロジックで確認できる

欠点：
- ORMを使用している場合のみ適用可能
- ORMレベルでの依存関係とデータベースレベルでの依存関係に差異がある可能性がある

### 3. PostgreSQLのエクステンションを活用

**pg_dependency**や**pg_stat_statements**などのエクステンションを使うことで、より詳細な依存関係情報を取得できる場合があります。

例えば、**pg_stat_statements**を使った例：
```sql
-- エクステンションのインストール
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- 依存関係のあるオブジェクトを確認するクエリ
SELECT DISTINCT
    d.classid::regclass AS dependent_class,
    d.objid AS dependent_id,
    d.refobjid AS referenced_id
FROM pg_depend d
WHERE d.refobjid = 'your_table'::regclass::oid;
```

利点：
- PostgreSQLの内部情報に直接アクセスできる
- データベース全体の依存関係を詳細に分析できる

欠点：
- 追加のエクステンションのインストールが必要
- 出力が低レベルでやや解釈が難しい

### 4. サードパーティのデータベース設計ツール

商用またはオープンソースのデータベース設計ツールを使用することも選択肢の一つです：

- **ERDPlus**: 無料のオンラインERダイアグラム作成ツール
- **Lucidchart**: 有料のダイアグラム作成ツールでデータベース設計もサポート
- **dbdiagram.io**: SQLからERダイアグラムを自動生成できるツール

これらのツールの利点：
- きれいなダイアグラムを作成できる
- チームでの共有や協業が容易

欠点：
- 手動でのメンテナンスが必要なケースが多い
- 実際のデータベースとの同期が必要
- CASCADE削除の連鎖に特化していない

### 5. PostgreSQLのメタコマンドを活用

PostgreSQLのメタコマンドを使用して、依存関係を調査することも可能です：

```sql
-- テーブルの詳細な定義を確認
\d+ table_name

-- 外部キー制約の一覧を確認
\d table_name

-- すべてのリレーションを確認
\drt
```

利点：
- 追加のツールやエクステンションが不要
- PostgreSQLに標準で搭載されている機能で確認できる

欠点：
- コマンドラインベースで視覚的ではない
- 再帰的な関係の把握が手動になる

## このクエリのメリット

本記事で紹介した再帰クエリには、上記の方法と比較して以下のようなメリットがあります：

1. **完全な連鎖の可視化**：再帰クエリにより、何段階にも渡るCASCADE削除の連鎖を一目で把握できます。

2. **特定テーブルからの影響範囲の特定**：任意のテーブルを起点として、そこから派生する削除連鎖を特定できます。

3. **自己参照の適切な処理**：自己参照テーブルによる無限ループを回避する処理が含まれています。

4. **PostgreSQLのシステムカタログを直接利用**：情報スキーマよりも直接的かつ効率的に情報を取得できます。

5. **汎用性**：テーブル名を変更するだけで、どのPostgreSQLデータベースでも利用可能です。

6. **追加ツール不要**：標準的なPostgreSQLインスタンスで実行でき、追加のツールやエクステンションをインストールする必要がありません。

7. **自動化可能**：スクリプトに組み込んで、データベースマイグレーションの前に自動チェックとして実行できます。

## 実際の使用例

例えば、ユーザー情報を含む`users`テーブルを削除する前に、このクエリを実行して影響範囲を確認できます：

```sql
WITH RECURSIVE cascade_graph AS (
  -- アンカー
  SELECT
    con.conrelid::regclass::text AS child_table,
    con.confrelid::regclass::text AS parent_table,
    con.conname                  AS constraint_name
  FROM pg_constraint con
  WHERE
    con.contype   = 'f'
    AND con.confdeltype = 'c'
    AND con.confrelid = 'public.users'::regclass

  UNION ALL

  -- 再帰部
  SELECT
    c2.conrelid::regclass::text,
    c2.confrelid::regclass::text,
    c2.conname
  FROM pg_constraint c2
  JOIN cascade_graph cg
    ON c2.confrelid::regclass::text = cg.child_table
  WHERE
    c2.contype   = 'f'
    AND c2.confdeltype = 'c'
    AND c2.conrelid::regclass::text <> c2.confrelid::regclass::text
)

SELECT DISTINCT
  parent_table AS 親テーブル,
  child_table  AS 子テーブル,
  constraint_name AS 制約名
FROM cascade_graph
ORDER BY 親テーブル, 子テーブル;
```

これにより、ユーザーを削除した場合に連鎖的に削除される可能性のあるデータを事前に把握できます。

### 実際の出力例

一般的なウェブアプリケーションのデータベースで`users`テーブルに対してクエリを実行した場合の出力例です：

```
      親テーブル   |      子テーブル       |      制約名
------------------+-----------------------+-------------------
 public.users     | public.user_profiles  | profile_user_fk
 public.users     | public.auth_tokens    | token_user_fk 
 public.users     | public.orders         | orders_user_fk
 public.orders    | public.order_items    | items_order_fk
 public.orders    | public.payments       | payment_order_fk
 public.user_profiles | public.addresses  | address_profile_fk
```

この結果から、`users`テーブルのレコードを削除すると：

1. ユーザーの基本情報（`user_profiles`）
2. 認証トークン（`auth_tokens`）
3. 注文履歴（`orders`）

が削除され、さらに連鎖的に：

4. 注文商品詳細（`order_items`）
5. 支払い情報（`payments`）
6. ユーザーの住所情報（`addresses`）

まで削除されることがわかります。

システム内で重要なデータに関しては、このような連鎖的な削除の影響を事前に把握しておくことで、誤った操作によるデータ損失を防ぐことができます。

## まとめ

PostgreSQLのシステムカタログと再帰クエリを組み合わせることで、CASCADE削除の連鎖を効果的に可視化できることを紹介しました。このクエリはデータベース設計の見直しやデータ削除前のリスク評価などに非常に役立ちます。

他にも様々な方法やツールが存在しますが、紹介した再帰クエリは追加ツールなしで正確な連鎖を把握できる点が大きな利点です。特にシステム規模が大きくなるほど、このような自動化された方法でCASCADE削除の影響範囲を把握することの重要性は増していきます。

クエリには多少の複雑さがありますが、テーブル名を変更するだけで任意のテーブルからの削除連鎖を追跡できる汎用性の高いツールとして、ぜひ活用してみてください。

## 参考資料

- [PostgreSQL公式ドキュメント: システムカタログ](https://www.postgresql.org/docs/current/catalogs.html)
- [PostgreSQL公式ドキュメント: pg_constraint](https://www.postgresql.org/docs/current/catalog-pg-constraint.html)
- [PostgreSQL公式ドキュメント: 再帰的クエリ](https://www.postgresql.org/docs/current/queries-with.html)
- [PostgreSQL公式ドキュメント: 外部キー制約](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK)
- [PostgreSQL: information_schema](https://www.postgresql.org/docs/current/information-schema.html)
- [pgAdmin ドキュメント](https://www.pgadmin.org/docs/)
- [DBeaver データベース図機能](https://dbeaver.io/docs/wiki/ER-Diagrams/)