SQL内でループ処理を実装してみよう（CURSORについて）

## 本記事について

業務にてストアドプロシージャを作成した際に、CURSORについて触れたので記事に起こしてみました。
SQLでループを使ってみよう！
## 対象者
・SQLでループ処理を実装したい人
・CURSORの使い方を知りたい人
## 環境

・SQLServer2022
・SQLServer Management Studio(SSMS)

:::note warn
実際に当記事の内容について検証する際は、汚れてもいい環境で行ってください。
:::
# 1. 「CURSOR」って何？
「CURSOR」はSQLの機能の一つです。
CURSORについて一番わかりやすいと感じた[記事](https://wa3.i-3-i.info/word11582.html)の方より引用させていただくと、
>データの「 **検索条件** 」と「**現在位置**」を保持して、複数の検索結果を**1件ずつ処理**するための仕組み

というものになります。

流れとして、
1. SELECTでデータを取得する（検索条件）
2. 取得したデータを上から1行ずつたどっていく（現在位置）

といった感じになります。
# 2. 実装してみよう
何かシナリオがあったほうが理解に繋がるかもしれないので、
**青果店の在庫管理**をしているシチュエーションを思い浮かべてください。
シナリオは以下の通りです。
```plaintext:シナリオ
この青果店では果物の在庫を確認して、数が少ない果物を補充予定としてリストアップしていきます。
・「在庫数」が「発注点」を下回っているものを補充対象とします。
・「補充対象の果物」と、補充する際の「発注数量」を確認できるようにします。
・各果物の「発注点」の数を倍にしたものを「発注数量」として設定します。
```
## 2-1.準備

はじめに必要なテーブルとデータを用意していきます。
### ◎果物在庫テーブル【FRUITS_STOCK】
名の通り果物の在庫を管理しているテーブルを用意し、
あらかじめパターン分けしたデータを入れておきます。

カラムについては以下を用意します。
| 物理名 | 論理名 | 説明 |
| --- | --- | --- |
| STOCK_ID | 在庫ID | 自動生成される主キー。 |
| FRUIT_NAME | 果物の名前 | 果物の名前が入る。 |
| STOCK_COUNT | 在庫数 | 果物の在庫数が入る。 |
| ORDER_POINT | 発注点 | 発注点となる数が入る。<br />在庫数 < 発注点 となっていれば入荷を行う。 |


```sql:FRUITS_STOCK
-- 果物在庫テーブル作成
CREATE TABLE FRUITS_STOCK (
    STOCK_ID	INT IDENTITY PRIMARY KEY,
    FRUIT_NAME	NVARCHAR(100) NOT NULL,
    STOCK_COUNT INT NOT NULL,
    ORDER_POINT INT NOT NULL
);

-- 作成したテーブルにデータを挿入
INSERT INTO FRUITS_STOCK (
	FRUIT_NAME, STOCK_COUNT, ORDER_POINT
)
VALUES 
    (N'りんご', 50, 10),   -- 在庫数が発注点を上回る
    (N'バナナ', 3, 5),     -- 在庫数が発注点を下回る
    (N'みかん', 30, 10),   -- 在庫数が発注点を上回る
    (N'ぶどう', 8, 8),     -- 在庫数が発注点と等しい
    (N'梨', 5, 7);         -- 在庫数が発注点を下回る
```
SELECTでこのような結果になります。
![cursor_img1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523852/d0f88606-3416-ffdb-97cb-d3d898c14313.png)

### ◎補充予定テーブル【RESTOCK_SCHEDULE】
補充がな必要な果物の名前と数量を管理するテーブルです。
カラムについては以下を用意します。
| 物理名 | 論理名 | 説明 |
| --- | --- | --- |
| RESTOCK_ID | 補充ID | 自動生成される主キー。 |
| FRUIT_NAME | 果物の名前 | 補充対象の果物の名前が入る。 |
| ORDER_QUANTITY | 発注数量 | 補充の際に発注する数量が入る。 |
```sql:RESTOCK_SCHEDULE
CREATE TABLE RESTOCK_SCHEDULE (
    RESTOCK_ID INT IDENTITY PRIMARY KEY,
    FRUIT_NAME NVARCHAR(100),
    ORDER_QUANTITY INT NOT NULL,
);
```

## 2-2.実装内容
処理の流れについては以下のようになります。
1. 果物在庫テーブルに入っているデータを取得する。
2. データを1行ずつ確認する。
3. 現在位置の果物が「在庫数」<「発注数量」であれば、補充予定とする。
4. 補充予定となった果物の「発注数量」は、「発注点」x2とする。
5. 補充予定テーブルに「果物の名前」、「発注数量」を登録する。
6. データが確認できなくなれば、処理を終了する。
```sql:SQL
-- 変数の宣言
DECLARE @CUR_FRUIT_NAME      NVARCHAR(30)
DECLARE @CUR_STOCK_COUNT     INT
DECLARE @CUR_ORDER_POINT     INT

-- カーソルの宣言
DECLARE FRUITS_STOCK_CURSOR CURSOR FOR
SELECT
     FRUIT_NAME
    ,STOCK_COUNT
    ,ORDER_POINT
FROM
    FRUITS_STOCK

-- カーソルをオープン
OPEN FRUITS_STOCK_CURSOR;

-- カーソルからデータを取得
FETCH NEXT FROM 
    FRUITS_STOCK_CURSOR 
INTO 
     @CUR_FRUIT_NAME
    ,@CUR_STOCK_COUNT
    ,@CUR_ORDER_POINT

WHILE @@FETCH_STATUS = 0
BEGIN
    -- 在庫数が発注点を下回っているか
    IF @CUR_STOCK_COUNT < @CUR_ORDER_POINT
    BEGIN
        -- 補充予定テーブルに登録
        INSERT INTO RESTOCK_SCHEDULE (
             FRUIT_NAME
            ,ORDER_QUANTITY
        ) VALUES (
             @CUR_FRUIT_NAME    -- 果物在庫.果物の名前
            ,@CUR_ORDER_POINT*2 -- 果物在庫.発注点x2
        );
    END;

    -- 次のレコードを取得
    FETCH NEXT FROM 
        FRUITS_STOCK_CURSOR 
    INTO 
         @CUR_FRUIT_NAME
        ,@CUR_STOCK_COUNT
        ,@CUR_ORDER_POINT
END

-- カーソルをクローズ
CLOSE FRUITS_STOCK_CURSOR

-- カーソルを解放
DEALLOCATE FRUITS_STOCK_CURSOR
```

# 3. 実装内容の解説
### 変数の定義
最初に定義しているものは、ループの中で使用する変数（イテレータ変数）です。
```sql:変数の宣言
-- 変数の宣言
DECLARE @CUR_FRUIT_NAME      NVARCHAR(30)
DECLARE @CUR_STOCK_COUNT     INT
DECLARE @CUR_ORDER_POINT     INT
```

Pythonでいうforの隣にいるやつにあたる役割を担います。
```python:Pythonのfor
for fruit in fruits:
    print(fruit)  #fruit←コレ
```
### カーソルの宣言とデータ取得
次に、カーソル本体の宣言と、果物在庫テーブルよりデータの抽出を行っています。
（カーソルを宣言する際は、「@」は不要です。）
```sql:カーソルの宣言
-- カーソルの宣言
DECLARE FRUITS_STOCK_CURSOR CURSOR FOR
SELECT
     FRUIT_NAME
    ,STOCK_COUNT
    ,ORDER_POINT
FROM
    FRUITS_STOCK
```
DECLAREしている行の下にはSELECT文がありますね。
このSELECTで取得した結果のセットが、丸ごと`FRUITS_STOCK_CURSOR`に入ります。

### カーソルオープン
次に、カーソルをオープンして取得した結果セットにアクセスできるようにします。
```sql:カーソルオープン
-- カーソルをオープン
OPEN FRUITS_STOCK_CURSOR;
```
### カーソル内のデータを取得し、変数に格納
次に、オープンしたカーソルからデータを取得し、一番最初に定義していた変数に格納します。
```sql:カーソルからデータを取得
-- カーソルからデータを取得
FETCH NEXT FROM 
    FRUITS_STOCK_CURSOR 
INTO 
     @CUR_FRUIT_NAME
    ,@CUR_STOCK_COUNT
    ,@CUR_ORDER_POINT

```
他のプログラミング言語では、ループ開始時点でイテレータ変数にはデータが格納されています。
しかし、CURSORについては、pythonのforのように現在位置が自動で移動しないため、
    **「現在位置を自分で移動させる」**
必要があります。

:::note
**Point**
CURSOR定義時、現在位置はデータの外にある
初回は自力で1行目のデータを取得する
:::

### ループ開始
次に本処理を行うループに入ります。
```sql:ループ開始
WHILE @@FETCH_STATUS = 0
BEGIN
```
WHILE条件の`@@FETCH_STATUS = 0`の部分についてです。
`@@FETCH_STATUS`とは、カーソルからデータを取得（`FETCH NEXT ~`）した際のステータスを保持するグローバル変数です。
ループの条件は、「ステータスの値が0」となっているため、この状態のときはループを継続します。

:::note 
**Point**
ループの継続 = `@@FETCH_STATUS`が0である = 次の行にデータがある
:::

データを取得できない、すなわち次の行に現在位置をずらしてもデータがないとき、
`@@FETCH_STATUS`は0ではなくなる→ループが終了します。

`@@FETCH_STATUS`の戻り値については[公式ドキュメント](https://learn.microsoft.com/ja-jp/sql/t-sql/functions/fetch-status-transact-sql?view=sql-server-ver16)にて以下のように記載されてます。
| 戻り値 | 説明 |
| --- | --- |
| 0 | FETCH ステートメントは正常に実行されました。 |
| -1 | FETCH ステートメントが失敗したか、または行が結果セットに収まりません。 |
| -2 | フェッチした行がありません。 |
| -9 | カーソルはフェッチ操作を実行しません。 |

### 本処理部分
次に、メインの処理を行う個所を見ていきます。
```sql:本処理部分
    -- 在庫数が発注点を下回っているか
    IF @CUR_STOCK_COUNT < @CUR_ORDER_POINT
    BEGIN
        -- 補充予定テーブルに登録
        INSERT INTO RESTOCK_SCHEDULE (
             FRUIT_NAME
            ,ORDER_QUANTITY
        ) VALUES (
             @CUR_FRUIT_NAME    -- 果物在庫.果物の名前
            ,@CUR_ORDER_POINT*2 -- 果物在庫.発注点x2
        );
    END;
```
`@CUR_STOCK_COUNT`、`@CUR_ORDER_POINT`、`@CUR_FRUIT_NAME`の変数を使用しています。
各変数には`FETCH NEXT`した際のデータが入っています。

ここで一度シナリオの内容を確認してみましょう。
```plaintext:シナリオ
この青果店では果物の在庫を確認して、数が少ない果物を補充予定としてリストアップしていきます。
・「在庫数」が「発注点」を下回っているものを補充対象とします。
・「補充対象の果物」と、補充する際の「発注数量」を確認できるようにします。
・各果物の「発注点」の数を倍にしたものを「発注数量」として設定します。
```
「在庫数」が「発注点」を下回っているものを補充対象としている部分は以下です。
```sql
IF @CUR_STOCK_COUNT < @CUR_ORDER_POINT
```

「補充対象の果物」と、補充する際の「発注数量」を確認できる、
各果物の「発注点」の数を倍にしたものを「発注数量」として設定している部分は以下となります。
```sql
    BEGIN
        -- 補充予定テーブルに登録
        INSERT INTO RESTOCK_SCHEDULE (
             FRUIT_NAME
            ,ORDER_QUANTITY
        ) VALUES (
             @CUR_FRUIT_NAME    -- 果物在庫.果物の名前
            ,@CUR_ORDER_POINT*2 -- 果物在庫.発注点x2
        );
    END
```
### 次のレコードを取得
ループのスコープの末端になります。
```sql:次のレコードを取得
    -- 次のレコードを取得
    FETCH NEXT FROM 
        FRUITS_STOCK_CURSOR 
    INTO 
         @CUR_FRUIT_NAME
        ,@CUR_STOCK_COUNT
        ,@CUR_ORDER_POINT
END
```
ここで気付いた方もいらっしゃると思いますが、この`FETCH NEXT~INTO~`部分、
```sql:カーソルオープン ~ データ取得
-- カーソルをオープン
OPEN FRUITS_STOCK_CURSOR;

-- カーソルからデータを取得
FETCH NEXT FROM 
    FRUITS_STOCK_CURSOR 
INTO 
     @CUR_FRUIT_NAME
    ,@CUR_STOCK_COUNT
    ,@CUR_ORDER_POINT
```
カーソルオープンしてすぐ、データを取得したときと全く同じ文を書いてます。

:::note
**Point**
`FETCH NEXT`でデータを取得する際は、ループの外側、内側共に同じものを書けばよい
:::
~~2回同じものを書くのはめんどくさいですが~~ここは楽ですね。

### カーソルクローズ・解放
ループを抜け、カーソルを使用することがなくなったら、クローズと解放（破棄）を行います。

```sql:カーソルクローズ・解放
-- カーソルをクローズ
CLOSE FRUITS_STOCK_CURSOR

-- カーソルを解放
DEALLOCATE FRUITS_STOCK_CURSOR
```
カーソルをオープンしたままだと、メモリリークやアプリケーションのパフォーマンス低下などに繋がるため、記述を忘れないようにしましょう。


# 4. SQL実行結果
実装したSQLを実行し、結果を見てみましょう。
### 実行前の状態
![cursor_img2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523852/df47c966-dd7f-fdd3-2423-8f2186f26baf.png)
上のデータが表示されている方のテーブルが「果物在庫テーブル（FRUITS_STOCK）」、
下のデータが空のテーブルが「補充予定テーブル（RESTOCK_SCHEDULE）」です。

「在庫数（STOCK_COUNT）」が「発注点(ORDER_POINT)」を下回っている果物は
「バナナ」と「梨」の二つです。
補充予定テーブルに追加するときに、
それぞれの発注点の数 x2が発注数量として設定されているか確認してみましょう。

### 実行後の状態
![cursor_img3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523852/32611052-2d7d-2f9c-9080-e5a5e36138c7.png)

「バナナ」の発注数量（ORDER_QUANTITY）が、10（発注点：5 x2）
「梨」の発注数量（ORDER_QUANTITY）が、14（発注点：7 x2）
となっていることを確認できました。

（STOCK_IDのカラムは今回要らなかったですね...）


# 5.まとめ
実装内容のスクショに直接ポイントを書き込んでみました。
![cursor_img4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523852/ae62b106-2937-441d-6add-bf152ad4d6e3.png)

SQLを組み立てる際は、行数が多くなりがちなので、
このようにブロックごとに分けることを意識して実装するとわかりやすいです。

# 最後に
この記事では、CURSORの使い方や注意点について解説しました。最後まで読んでいただき、ありがとうございます。この記事が少しでも皆様のお役に立てれば幸いです。今後のデータベース操作において、今回の内容が役立つことを願っています。

また、自分でも思い出しながら書いた部分も含まれているので、
不足している部分や誤っている個所がありましたらコメント頂けると幸いです。

# 参考
[カーソル【DB】とは |「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典](https://wa3.i-3-i.info/word11582.html)

https://learn.microsoft.com/ja-jp/sql/t-sql/language-elements/cursors-transact-sql?view=sql-server-ver16

https://learn.microsoft.com/ja-jp/sql/t-sql/functions/fetch-status-transact-sql?view=sql-server-ver16