AIが生成するSQLクエリで閉包テーブルを階層表示

## 背景

[閉包テーブルからパス文字列を得る](https://qiita.com/ssc-ynakamura/items/d288a195391df47b3c92)の記事を読んだことがきっかけです。
この記事のようにやりたいことは浮かんでいますが、SQLクエリを作るために少し工夫が必要そうなことは多々あるかと思います。

そこで、LLMを利用してSQLクエリを生成することで、考える作業を効率化できないかと考えました。
本記事では、AIを使って閉包テーブルの階層構造を表示するSQLクエリの生成を試みます。

:::note warn
AI利用時のデータ漏洩には注意が必要です
AIの学習に利用されないプラットフォームを選択するか、漏れてはいけない内容は入力しないようにしましょう。
:::

:::note warn
AIが生成したクエリには注意が必要です
どんなクエリが作成されるかわからないため、必ず実行前に確認をしましょう。
:::

## 目的

この記事では、AIを用いてSQLクエリを生成することを目的としています。
具体的には、閉包テーブルで表された部署を階層構造で出力するSQLクエリをLLMに生成させることを試みます。

## 環境

- クエリ生成: AzureのGPT-4o
- データベース: PostgreSQL 15.7

## 閉包テーブルの構造

テーブルは以下のように定義しています。
閉包テーブルで部署の構造を表現しています。

### departments

部署情報を格納するテーブル

| Column        | Type            | Constraints                          |
|---------------|-----------------|--------------------------------------|
| department_id | SERIAL          | PK                    |
| name          | VARCHAR(20)     | NOT NULL                             |

### department_tree_paths

部署の階層構造を保存するテーブル

| Column        | Type            | Constraints                          |
|---------------|-----------------|--------------------------------------|
| ancestor_id   | INTEGER         | PK, FK departments(department_id) |
| descendant_id | INTEGER         | PK, FK departments(department_id) |
| depth         | INTEGER         | NOT NULL                             |

## 使ったデータ

上記のテーブルには次のデータを挿入しました。
これらは、[閉包テーブルからパス文字列を得る](https://qiita.com/ssc-ynakamura/items/d288a195391df47b3c92)の記事で記載されたものを利用しています。

### departments

| **department_id** | **name** |
|---|---|
| 1 | 本社 |
| 2 | 営業部 |
| 3 | 営業1課 |
| 4 | 営業2課 |
| 5 | 東京支社 |
| 6 | 開発部 |
| 7 | 開発1課 |
| 8 | 開発2課 |

### department_tree_paths

| **ancestor_id** | **descendant_id** | **depth** |
|---|---|---|
| 1 | 1 | 0 |
| 1 | 2 | 1 |
| 1 | 3 | 2 |
| 1 | 4 | 2 |
| 1 | 5 | 1 |
| 1 | 6 | 2 |
| 1 | 7 | 3 |
| 1 | 8 | 3 |
| 2 | 2 | 0 |
| 2 | 3 | 1 |
| 2 | 4 | 1 |
| 3 | 3 | 0 |
| 4 | 4 | 0 |
| 5 | 5 | 0 |
| 5 | 6 | 1 |
| 5 | 7 | 2 |
| 5 | 8 | 2 |
| 6 | 6 | 0 |
| 6 | 7 | 1 |
| 6 | 8 | 1 |
| 7 | 7 | 0 |
| 8 | 8 | 0 |

## プロンプト

GPT-4oに投げたシステムプロンプト、ユーザプロンプトを紹介します。

### システムプロンプト

以下のシステムプロンプトを用いました。
どんなことをするのかの概要と簡単なルールを与えています。
念の為SELECT文のみ生成するようにお願いしています。

```md
あなたはユーザの入力からPostgreSQLのクエリを生成するAIです
以下の手順に従って段階的に考え、クエリを生成しましょう

1. `## question`に書かれた入力内容を確認してください
2. `## DB`に書かれたデータベースのテーブルとカラム情報を参考にして、クエリを生成してください

## クエリの生成ルール

- SELECT文のみを生成し、カラム名には適切な名前を考え別名をつけてください
- 回答はSQLのクエリのみ出力してください
```

### ユーザプロンプト

ユーザプロンプトは以下のようにしました。
作って欲しいクエリの詳細に加えて、DBの構造を与えています。
DBの構造部分はCREATE文を貼り付けているだけです。

今回は参考記事に倣い、部署を`/`で区切り、階層が高い順で表示してみようと思います。

````md
## question

部署IDが8の部署を階層構造で/で区切って表示してください
その際階層が高い方から順に表示してください

## DB

```
CREATE TABLE departments (
    department_id SERIAL PRIMARY KEY,
    name VARCHAR(20) NOT NULL
);
CREATE TABLE department_tree_paths (
    ancestor_id INTEGER REFERENCES departments(department_id),
    descendant_id INTEGER REFERENCES departments(department_id),
    depth INTEGER NOT NULL,
    PRIMARY KEY (ancestor_id, descendant_id)
);
```
````
## 生成結果

こちらが生成されたSQLクエリです。

```sql
SELECT   
    STRING_AGG(d.name, '/' ORDER BY dtp.depth DESC) AS department_hierarchy  
FROM   
    department_tree_paths dtp  
JOIN   
    departments d ON dtp.ancestor_id = d.department_id  
WHERE   
    dtp.descendant_id = 8  
GROUP BY   
    dtp.descendant_id;
```

このSQLクエリを実行すると、以下のような出力になります。
階層が高い順に`/`で区切られて出力されています。

|department_hierarchy|
|---|
|本社/東京支社/開発部/開発2課|

## 終わりに

今回の記事では、AIを利用して閉包テーブルの階層構造を表示するSQLクエリの生成ができました。
どの程度複雑なことができるのかの検証は必要ですが、プロンプトの工夫次第で様々なクエリを生成できる可能性があります。
ただし、冒頭で述べた通り、どんなものが生成されるかはわからないので、AIの生成結果を使う際は必ず内容を確認した上で使うようにしましょう