CASE式で条件分岐をSQL文に任せる

条件分岐と言えば、プログラム言語にあるif文が代表格です。ところが、SQL文の中でも条件分岐を実現できます。CASE式を使うのですが、最初に知ったときは感動しました。プログラムのif文とSQLのCASE式、使い分けることで全体的にコードをスッキリさせることができます。

# CASE式とは

以下の書式で書かれた文法をCASE式と呼びます。
CASE式には単純CASE式と検索CASE式があります。
単純CASE式は等価条件が真であるかの評価しかできませんがクエリをシンプルにできます。
検索CASE式の方が応用が効きますし、検索CASE式で単純CASE式の内容を表すことができます。
私もほとんど検索CASE式を使用しています。
ただし、可読性のために単純CASE式を採用するのはアリでしょう。

以下の単純CASE式と検索CASE式の意味は同じです。

```sql
--単純CASE式
CASE gender
    WHEN '男' THEN 1
    WHEN '女' THEN 2
    ELSE 99
END

--検索CASE式
CASE
    WHEN gender = '男' THEN 1
    WHEN gender = '女' THEN 2
    ELSE 99
END
```

検索CASE式は、「=」以外も表現できるため柔軟です。
不等号でも、下記のようなIN句でも。

```sql
CASE
    WHEN prefecture IN ('北海道') THEN '北海道'
    WHEN prefecture IN ('青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県') THEN '東北'
    ...
    WHEN prefecture IN ('沖縄県') THEN '沖縄'
    ELSE NULL
END
```

CASE式を使うときには

* 条件は排他的に書く
* ELSEも書く
* 真（true）がマッチ対象であり、偽（false）と**不明（unknown）**にはマッチしないことに注意する

べきです。下記のような書き方は混乱の元なのでやめた方がよいでしょう。

```sql
-- この書き方は絶対1や２に評価されない
CASE
    WHEN gender = '男' OR gender = '女' THEN 0
    WHEN gender = '男' THEN 1
    WHEN gender = '女' THEN 2
    ELSE 99
END

-- '男'、'女'いずれにも設定されていない場合はNULLになる
-- NULLを期待する場合でも ELSE NULLを明示的に書いた方がよい
CASE
    WHEN gender = '男' THEN 1
    WHEN gender = '女' THEN 2
END

-- '×'には絶対に評価されない。これは「cleared = NULL」が真になることがないから。
CASE cleared
    WHEN 1 THEN '○'
    WHEN NULL THEN '×'
END
```

以下の記事が非常に分かりやすくおすすめです。上記の例も載っています。
[CASE式のススメ（前編） \(1/3\)：CodeZine（コードジン）](https://codezine.jp/article/detail/404)

また、この本を読んでCASE式を使いこなせるようになりました。
[達人に学ぶ SQL徹底指南書 \| ミック \| 工学 \| Kindleストア \| Amazon](https://www.amazon.co.jp/dp/B00DIM6330/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)

# CASE式の実例

## フラグを付ける

既存のデータに対してCASE式を使ってフラグを設定する場合によく使います。

```sql
SELECT
    id,
    title,
    publish_date,
    CASE WHEN publish_date > CURRENT_TIMESTAMP() - INTERVAL 7 DAY THEN 1 ELSE 0 END AS is_new
FROM news
ORDER BY publish_date DESC
LIMIT 20
```

例えばお知らせの新しい記事に「NEW」マークをつけたい場合にこの方法を使います。日付が現在から７日以内のときは1、そうでない場合は0を設定しています。これにis_newとでも名前を付けておきます。こうすると、

```php
<!-- 先のSQLの実行結果が$newsに入っているものとする -->
<?php foreach ($news as $id => $data): ?>
<li>
    <?php echo $data['publish_date']; ?> <?php echo $data['title']; ?>　<?php if ($data['is_new']): ?><span class="icon">NEW</span><?php     endif; ?>
</li>
<?php endforeach; ?>
```

view側ではif文の条件を簡潔にできます。CASE式を使わない場合は

```php
<?php     if ($data['publish_date']が最新から７日以内かどうか): ?>
```

のようなことをするか、DBからの取得結果に対して、foreachなどでフラグを設定することになるでしょう。
※CASE式にしないと絶対に効率が悪いと言っているわけではありません。
実際私も下記のようなことをすることもあります。

```php
foreach ($news as $id => $data) {
    if ($data['publish_date']が最新から７日以内)　{
        $data['is_new'] = 1;
    } else {
        $data['is_new'] = 0;
    }
}
```

余談ですが、SELECT句の各フィールドの計算結果はMVCフレームワークのモデルでも表現できます。  
CASE式も例外ではなく、この例はLaravelだと以下のように表現できます。

```php
// Model
class NewsModel extends Model
{
    public function getIsNewAttribute()
    {
        if ($this->publish_dateが最新から7日以内かどうか) {
            $is_new = 1;
        } else {
            $is_new = 0;
        }

        return $is_new;
    }
}

// View
@foreach ($news as $data)
<li>
    {{ $data->publish_date }} {{ $data->title }}　{{ ($data->is_new) ? : '<span class="icon">NEW</span>' : '' }}
</li>
@endforeach
```

CASE式を使わない場合でも、概念を理解しておくと、Modelのメソッドも作りやすくなると思います。



例が単純ですが、高齢者をSQL側で定義しています。
条件を組み合わせて、例えば「高齢者割引対象者」のような式も作れそうです。

```sql
SELECT
    id,
    name,
    CASE WHEN age >= 60 THEN 1 ELSE 0 END AS senior
FROM member
LIMIT 20
```


## 階級に分ける
先の例は1or0でしたが、もちろん３段階以上の場合分けも可能です。

```sql
SELECT
    CASE
        WHEN score >= 80 THEN 'S'
        WHEN score >= 60 THEN 'A'
        WHEN score >= 40 THEN 'B'
        ELSE 'C'
    END
FROM exam;
```

## 区分ごとの集計を一発で出す

memberテーブルのgenderフィールドに
男性:1、女性:2
で登録されているとします。

男性、女性それぞれの人数はwhere句を2回使えば求められます。

```sql
SELECT COUNT(*) FROM member WHERE gender = 1;
SELECT COUNT(*) FROM member WHERE gender = 2;
```

ただ、GROUP BYの方がスマートでしょう。

```sql
SELECT gender, COUNT(gender) FROM member GROUP BY gender;
```

実は、CASE式を使っても求められます。
CASE文で条件に一致するレコードを1,しないレコードを0と評価しておいてそれらを足し合わせています。

```sql
SELECT
    SUM(CASE WHEN gender = 1 THEN 1 ELSE 0 END) AS male_count,
    SUM(CASE WHEN gender = 2 THEN 1 ELSE 0 END) AS female_count
FROM member
```
この例だとGROUP BYの方がSQLのコード的には読みやすいですが、
区分が増えたり、条件が複雑になったときには使えることがあります。
この手法の肝は、CASE式の評価を集計しているところです。
覚えておいて損はありません。


## 指定の商品全てが含まれた購入情報を求める

- 購入情報 sales
- 購入明細情報 sales_item（sales_item.sales_id = sales.idでリレーション）
- 商品情報 item（sales_item.sales_id = sales.idでリレーション）

という、買い物かごにありがちなテーブル構成を想定します。
下図の購入明細情報から、商品ID1と2全てが含まれている購入情報を求めたいときはどうすればよいでしょう？
つまり購入情報ID110と111を出したい（112や113は全てではないので除外）場合です。

| id | sales_id | item_id |
|:--:|:--:|:--:|
| 1  | 110  | 1 |
| 2  | 110  | 2 |
| 3  | 111  | 1 |
| 4  | 111  | 2 |
| 5  | 111  | 3 |
| 6  | 112  | 1 |
| 7  | 113  | 2 |
| 8  | 114  | 3 |

CASE式とHAVING句を使って求めることができます。
購入情報を出したいので、購入情報idでグループ化します。
集約の中身をCASE式で評価します。
CASE式での評価方法ですが、商品ID1,2が含まれているレコードは1、含まれていないレコードは0を割り当てます。
CASE式の評価合計 = 商品の点数（ここでは2種類の商品IDなので「2」）となる購入情報を求められます。

```sql
SELECT sales_id FROM sales_item GROUP BY sales_id HAVING SUM(CASE WHEN item_id IN (1, 2) THEN 1 ELSE 0 END) = 2
```

もちろん全てではなくても応用できます。

購入明細情報から、商品ID6〜10の商品の中から1種類だけ購入されている購入情報を求める

```sql
SELECT sales_id FROM sales_item GROUP BY sales_id HAVING SUM(CASE WHEN item_id IN (6, 7, 8, 9, 10) THEN 1 ELSE 0 END) = 1
```

購入明細情報から、商品ID11〜20の商品の中から3種類以上含まれている購入情報を求める

```sql
SELECT sales_id FROM sales_item GROUP BY sales_id HAVING SUM(CASE WHEN item_id >= 11 OR item_id <= 20 THEN 1 ELSE 0 END) >= 3
```


## 順序を変更する
ORDER句にCASE式を指定することで、順序を自在にコントロールできます。

```sql
SELECT
    *
FROM recipe
ORDER BY
    CASE
        WHEN category_id = 1 THEN 2
        WHEN category_id = 2 THEN 1
        WHEN category_id = 3 THEN 3
        WHEN category_id = 4 THEN 5
        WHEN category_id = 5 THEN 4
        ELSE category_id
    END;
```

式の内容そのままですが、実際の表示順は以下になります。
「CASE」〜「END」までが数値として評価されます。
category_idが1だったら2と評価され、2だったら1と評価され、・・・
最終的に評価された数値で並べ替えています。

※上記はMySQLなら、FIELD関数でも実現できます。

```sql
SELECT * FROM recipe ORDER BY FIELD(category_id, 2, 1, 3, 5, 4)
```

| カテゴリーID | 実際の表示順 |
|:--:|:--:|
| 1  | 2  |
| 2  | 1  |
| 3  | 3  |
| 4  | 5  |
| 5  | 4  |
| その他| カテゴリーIDと同じ |

レシピをカテゴリー順に表示させる仕様になっていて、例えば
1:和食
2:洋食
3:中華
...
と割り当てていたとしましょう。
「洋食を一番上に持ってきてくれ」と急に言われた場合なんかには使えます。
カッチリと対応する場合はカテゴリー情報を別テーブルにしてその中に順序情報用のフィールドを持たせることになりますが、応急処置として一例です。

## 数値を入れ替える

| ID | カテゴリーID |
|:--:|:--:|
| 1  | 1  |
| 2  | 1  |
| 3  | 1  |
| 4  | 3  |
| 5  | 2  |
| 6  | 5  |
| 7  | 5  |
| 8  | 5  |
| 9  | 4  |
| 10 | 4  |
| 11 | 4  |
| 12 | 4  |
| 13 | 4  |
| 14 | 4  |
| 15 | 4  |
| 16 | 4  |
| 17 | 4  |
| 18 | 4  |
| 19 | 4  |
| 20 | 4  |

例えば上記のようなテーブル（itemsテーブルとします。）でカテゴリーIDの1と4を入れ替えたい場合があります。

```sql
UPDATE items SET category = 4 WHERE category = 1;
UPDATE items SET category = 1 WHERE category = 4;
```

とやっては詰んでしまいます。（結果もとの4は4のまま。）

```sql
UPDATE items SET category = -1 WHERE category = 1;
UPDATE items SET category = 1 WHERE category = 4;
UPDATE items SET category = 1 WHERE category = -1;
```

存在しないカテゴリーIDでいったんUPDATEしておくとうまくいきます。
ただし、クエリを3回実行させていますし、上記でいえば-1が存在しないことを確認しておかないといけません。

CASE式を使えば、1クエリで完結します。

```sql
UPDATE items SET category = CASE WHEN category = 1 THEN 4 WHEN category = 4 THEN 1 ELSE category
WHERE category IN (1, 4);
```
UPDATEする値をCASE式で評価しています。
**「ELSE category」もしくはWHERE句を忘れない**ようにしましょう。
そうしないと、1と4以外全部NULLになってしまいます。
私は不用意な全件書き換えはしたくないのでWHERE句はつけますし、ELSEもいつも明示的に書く派なので両方記述しています。


## 住所フィールドに都道府県名が含まれていたときに、バラして都道府県番号に変換する

| ID（id） | 名前（name） | 住所（address） |
|:--:|:--:|:--:|
| 1  | 田中 | 大阪府大阪市〜  |
| 2  | 鈴木 | 東京都港区〜  |
| 3  | 佐藤 | 兵庫県神戸市〜  |
| 4  | 吉田 | 北海道札幌市〜  |

| ID（id） | 名前（name） | 都道府県（prefecture） | 住所（address） |
|:--:|:--:|:--:|:--:|
| 1  | 田中 | 27 | 大阪市〜  |
| 2  | 鈴木 | 13 | 港区〜  |
| 3  | 佐藤 | 28 | 神戸市〜  |
| 4  | 吉田 | 1 | 札幌市〜  |

上のテーブルを下のテーブルのような形にしたいときもUPDATE文の中でCASE式を使えば1クエリで可能です。
都道府県に対応する都道府県番号をWHEN句の1つ1つに割り当てていった上で評価します。
仮に住所が含まれていない場合はprefectureフィールドはNULLのままです。
同様にaddressフィールドからは都道府県名を取り除きます。
REPLACE関数（MySQLで操作）など、文字列を置換（都道府県名を空文字に置換）することで実現できます。

WHERE句がなくても実行結果は同じですが、安全配慮の癖づけとして入れています。
また、addressフィールドのELSE句は必ずそのままの値を設定します。
そうしないと、都道府県が含まれていない住所が全て消えてしまいます。
（下記はWHERE句を設定しているのでその事態は免れますが。。）

余談ですが、47行の編集が大変に見える場合は

- 連番を作成する手法
- 矩形編集

を覚えておくことをおすすめします。思ったより大変ではありません。

```sql
UPDATE member SET
prefecture = 
CASE
  WHEN address LIKE '%北海道%'   THEN  1
  WHEN address LIKE '%青森県%'   THEN  2
  WHEN address LIKE '%岩手県%'   THEN  3
  WHEN address LIKE '%宮城県%'   THEN  4
  WHEN address LIKE '%秋田県%'   THEN  5
  WHEN address LIKE '%山形県%'   THEN  6
  WHEN address LIKE '%福島県%'   THEN  7
  WHEN address LIKE '%茨城県%'   THEN  8
  WHEN address LIKE '%栃木県%'   THEN  9
  WHEN address LIKE '%群馬県%'   THEN 10
  WHEN address LIKE '%埼玉県%'   THEN 11
  WHEN address LIKE '%千葉県%'   THEN 12
  WHEN address LIKE '%東京都%'   THEN 13
  WHEN address LIKE '%神奈川県%' THEN 14
  WHEN address LIKE '%新潟県%'   THEN 15
  WHEN address LIKE '%富山県%'   THEN 16
  WHEN address LIKE '%石川県%'   THEN 17
  WHEN address LIKE '%福井県%'   THEN 18
  WHEN address LIKE '%山梨県%'   THEN 19
  WHEN address LIKE '%長野県%'   THEN 20
  WHEN address LIKE '%岐阜県%'   THEN 21
  WHEN address LIKE '%静岡県%'   THEN 22
  WHEN address LIKE '%愛知県%'   THEN 23
  WHEN address LIKE '%三重県%'   THEN 24
  WHEN address LIKE '%滋賀県%'   THEN 25
  WHEN address LIKE '%京都府%'   THEN 26
  WHEN address LIKE '%大阪府%'   THEN 27
  WHEN address LIKE '%兵庫県%'   THEN 28
  WHEN address LIKE '%奈良県%'   THEN 29
  WHEN address LIKE '%和歌山県%' THEN 30
  WHEN address LIKE '%鳥取県%'   THEN 31
  WHEN address LIKE '%島根県%'   THEN 32
  WHEN address LIKE '%岡山県%'   THEN 33
  WHEN address LIKE '%広島県%'   THEN 34
  WHEN address LIKE '%山口県%'   THEN 35
  WHEN address LIKE '%徳島県%'   THEN 36
  WHEN address LIKE '%香川県%'   THEN 37
  WHEN address LIKE '%愛媛県%'   THEN 38
  WHEN address LIKE '%高知県%'   THEN 39
  WHEN address LIKE '%福岡県%'   THEN 40
  WHEN address LIKE '%佐賀県%'   THEN 41
  WHEN address LIKE '%長崎県%'   THEN 42
  WHEN address LIKE '%熊本県%'   THEN 43
  WHEN address LIKE '%大分県%'   THEN 44
  WHEN address LIKE '%宮崎県%'   THEN 45
  WHEN address LIKE '%鹿児島県%' THEN 46
  WHEN address LIKE '%沖縄県%'   THEN 47
  ELSE NULL
END,
address = 
CASE
  WHEN address LIKE '%北海道%'   THEN REPLACE(address, '北海道',   '')
  WHEN address LIKE '%青森県%'   THEN REPLACE(address, '青森県',   '')
  WHEN address LIKE '%岩手県%'   THEN REPLACE(address, '岩手県',   '')
  WHEN address LIKE '%宮城県%'   THEN REPLACE(address, '宮城県',   '')
  WHEN address LIKE '%秋田県%'   THEN REPLACE(address, '秋田県',   '')
  WHEN address LIKE '%山形県%'   THEN REPLACE(address, '山形県',   '')
  WHEN address LIKE '%福島県%'   THEN REPLACE(address, '福島県',   '')
  WHEN address LIKE '%茨城県%'   THEN REPLACE(address, '茨城県',   '')
  WHEN address LIKE '%栃木県%'   THEN REPLACE(address, '栃木県',   '')
  WHEN address LIKE '%群馬県%'   THEN REPLACE(address, '群馬県',   '')
  WHEN address LIKE '%埼玉県%'   THEN REPLACE(address, '埼玉県',   '')
  WHEN address LIKE '%千葉県%'   THEN REPLACE(address, '千葉県',   '')
  WHEN address LIKE '%東京都%'   THEN REPLACE(address, '東京都',   '')
  WHEN address LIKE '%神奈川県%' THEN REPLACE(address, '神奈川県', '')
  WHEN address LIKE '%新潟県%'   THEN REPLACE(address, '新潟県',   '')
  WHEN address LIKE '%富山県%'   THEN REPLACE(address, '富山県',   '')
  WHEN address LIKE '%石川県%'   THEN REPLACE(address, '石川県',   '')
  WHEN address LIKE '%福井県%'   THEN REPLACE(address, '福井県',   '')
  WHEN address LIKE '%山梨県%'   THEN REPLACE(address, '山梨県',   '')
  WHEN address LIKE '%長野県%'   THEN REPLACE(address, '長野県',   '')
  WHEN address LIKE '%岐阜県%'   THEN REPLACE(address, '岐阜県',   '')
  WHEN address LIKE '%静岡県%'   THEN REPLACE(address, '静岡県',   '')
  WHEN address LIKE '%愛知県%'   THEN REPLACE(address, '愛知県',   '')
  WHEN address LIKE '%三重県%'   THEN REPLACE(address, '三重県',   '')
  WHEN address LIKE '%滋賀県%'   THEN REPLACE(address, '滋賀県',   '')
  WHEN address LIKE '%京都府%'   THEN REPLACE(address, '京都府',   '')
  WHEN address LIKE '%大阪府%'   THEN REPLACE(address, '大阪府',   '')
  WHEN address LIKE '%兵庫県%'   THEN REPLACE(address, '兵庫県',   '')
  WHEN address LIKE '%奈良県%'   THEN REPLACE(address, '奈良県',   '')
  WHEN address LIKE '%和歌山県%' THEN REPLACE(address, '和歌山県', '')
  WHEN address LIKE '%鳥取県%'   THEN REPLACE(address, '鳥取県',   '')
  WHEN address LIKE '%島根県%'   THEN REPLACE(address, '島根県',   '')
  WHEN address LIKE '%岡山県%'   THEN REPLACE(address, '岡山県',   '')
  WHEN address LIKE '%広島県%'   THEN REPLACE(address, '広島県',   '')
  WHEN address LIKE '%山口県%'   THEN REPLACE(address, '山口県',   '')
  WHEN address LIKE '%徳島県%'   THEN REPLACE(address, '徳島県',   '')
  WHEN address LIKE '%香川県%'   THEN REPLACE(address, '香川県',   '')
  WHEN address LIKE '%愛媛県%'   THEN REPLACE(address, '愛媛県',   '')
  WHEN address LIKE '%高知県%'   THEN REPLACE(address, '高知県',   '')
  WHEN address LIKE '%福岡県%'   THEN REPLACE(address, '福岡県',   '')
  WHEN address LIKE '%佐賀県%'   THEN REPLACE(address, '佐賀県',   '')
  WHEN address LIKE '%長崎県%'   THEN REPLACE(address, '長崎県',   '')
  WHEN address LIKE '%熊本県%'   THEN REPLACE(address, '熊本県',   '')
  WHEN address LIKE '%大分県%'   THEN REPLACE(address, '大分県',   '')
  WHEN address LIKE '%宮崎県%'   THEN REPLACE(address, '宮崎県',   '')
  WHEN address LIKE '%鹿児島県%' THEN REPLACE(address, '鹿児島県', '')
  WHEN address LIKE '%沖縄県%'   THEN REPLACE(address, '沖縄県',   '')
  ELSE address
END
WHERE 
address LIKE '%北海道%'   OR
address LIKE '%青森県%'   OR
address LIKE '%岩手県%'   OR
address LIKE '%宮城県%'   OR
address LIKE '%秋田県%'   OR
address LIKE '%山形県%'   OR
address LIKE '%福島県%'   OR
address LIKE '%茨城県%'   OR
address LIKE '%栃木県%'   OR
address LIKE '%群馬県%'   OR
address LIKE '%埼玉県%'   OR
address LIKE '%千葉県%'   OR
address LIKE '%東京都%'   OR
address LIKE '%神奈川県%' OR
address LIKE '%新潟県%'   OR
address LIKE '%富山県%'   OR
address LIKE '%石川県%'   OR
address LIKE '%福井県%'   OR
address LIKE '%山梨県%'   OR
address LIKE '%長野県%'   OR
address LIKE '%岐阜県%'   OR
address LIKE '%静岡県%'   OR
address LIKE '%愛知県%'   OR
address LIKE '%三重県%'   OR
address LIKE '%滋賀県%'   OR
address LIKE '%京都府%'   OR
address LIKE '%大阪府%'   OR
address LIKE '%兵庫県%'   OR
address LIKE '%奈良県%'   OR
address LIKE '%和歌山県%' OR
address LIKE '%鳥取県%'   OR
address LIKE '%島根県%'   OR
address LIKE '%岡山県%'   OR
address LIKE '%広島県%'   OR
address LIKE '%山口県%'   OR
address LIKE '%徳島県%'   OR
address LIKE '%香川県%'   OR
address LIKE '%愛媛県%'   OR
address LIKE '%高知県%'   OR
address LIKE '%福岡県%'   OR
address LIKE '%佐賀県%'   OR
address LIKE '%長崎県%'   OR
address LIKE '%熊本県%'   OR
address LIKE '%大分県%'   OR
address LIKE '%宮崎県%'   OR
address LIKE '%鹿児島県%' OR
address LIKE '%沖縄県%';
```

## item1, item2, item3...のようなテーブル（列持ちを行持ちに変換する）

売上テーブル（sales）

| ID（id） | 商品1（item1） | 金額1（price1） | 商品2（item2） | 金額2（price2） | 商品3（item3） | 金額3（price3） |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| 1  | 1 | 3000 | 2 | 1500 | 3 | 1000 |
| 2  | 4 | 1000 | 3 | 1000 |  |  |
| 3  | 2 | 2000 |  |  |  |  |
| 4  | 5 | 1000 | 1 | 500 | 3 | 5000 |

商品を複数フィールドに登録でき、それぞれの商品に対する売上も同様に複数フィールドを持ちます。
「列持ち」と言われている構成で、実務では度々出てきます。
列持ち構成で集計をしたいときに困ったことになります。
例えば上記のようなテーブルで各商品ごとの金額合計や商品が出てきた回数を出したいときはどうしましょう？

列ごとに集計してホスト言語側でループを回して合計する、という方法も考えられます。
その場合は上記ならば3回SQLを発行することになり、さらにホスト言語側のコードも増えます。

CASE式を使って、列持ちを行持ちに変換することで1SQLで解決できます。
変換のためには、別にピボットテーブルを用意します。
連番を格納しただけのテーブルです。
※今回は3までしか必要ないですが、使い回しができるように適当な数まで作っています。

```sql
CREATE TABLE pivot (
  seq INT NOT NULL AUTO_INCREMENT COMMENT '連番',
-- ---------------------------------------------------------
  PRIMARY KEY(seq)
) COMMENT = 'ピボットテーブル';

INSERT INTO pivot (seq) VALUES (1);
INSERT INTO pivot (seq) VALUES (2);
INSERT INTO pivot (seq) VALUES (3);
INSERT INTO pivot (seq) VALUES (4);
INSERT INTO pivot (seq) VALUES (5);
INSERT INTO pivot (seq) VALUES (6);
INSERT INTO pivot (seq) VALUES (7);
INSERT INTO pivot (seq) VALUES (8);
INSERT INTO pivot (seq) VALUES (9);
INSERT INTO pivot (seq) VALUES (10);
```

ピボットテーブル（pivot）

|seq|
|:--:|
| 1  |
| 2  |
| 3  |
| 4  |
| 5  |
| ...  |
| 10  |

列持ちテーブルとピボットテーブルをクロス結合します。
クロス結合しただけだと、以下のような結果になります。

| ID（id） | 商品1（item1） | 金額1（price1） | 商品2（item2） | 金額2（price2） | 商品3（item3） | 金額3（price3） | seq |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| 1  | 1 | 3000 | 2 | 1500 | 3 | 1000 | 1 |
| 1  | 1 | 3000 | 2 | 1500 | 3 | 1000 | 2 |
| 1  | 1 | 3000 | 2 | 1500 | 3 | 1000 | 3 |
| 2  | 4 | 1000 | 3 | 1000 |  |  | 1 |
| 2  | 4 | 1000 | 3 | 1000 |  |  | 2 |
| 2  | 4 | 1000 | 3 | 1000 |  |  | 3 |
| 3  | 2 | 2000 |  |  |  |  | 1 |
| 3  | 2 | 2000 |  |  |  |  | 2 |
| 3  | 2 | 2000 |  |  |  |  | 3 |
| 4  | 5 | 1000 | 1 | 500 | 3 | 5000 | 1 |
| 4  | 5 | 1000 | 1 | 500 | 3 | 5000 | 2 |
| 4  | 5 | 1000 | 1 | 500 | 3 | 5000 | 3 |

上記結果の「seq」の値によってCASE式で対象の商品番号、売上を選ぶイメージです。
例えば一番上の行はseqが1なので、商品1と売上1を集計対象にします。

今までの話を踏まえると下記の回答になります。

```sql
SELECT
    CASE pivot.seq
        WHEN 1 THEN sales.item1
        WHEN 2 THEN sales.item2
        WHEN 3 THEN sales.item3
    END AS fund,
    COUNT(CASE pivot.seq WHEN 1 THEN sales.item1 WHEN 2 THEN sales.item2 WHEN 3 THEN sales.item3 END) AS count,
    SUM(CASE pivot.seq WHEN 1 THEN sales.price1 WHEN 2 THEN sales.price2 WHEN 3 THEN sales.price3 END) AS price
FROM
    sales
    CROSS JOIN
        pivot
    ON  pivot.seq <= 3
GROUP BY
    CASE pivot.seq
        WHEN 1 THEN sales.item1
        WHEN 2 THEN sales.item2
        WHEN 3 THEN sales.item3
    END
```

今回は例示しませんが、逆に行持ちから列持ちへの変換も可能です。



# 注意事項
CASE式を使えると楽しくなってきますが、**何でもかんでもCASE式で処理するのはやめましょう**。
使い方を誤ると、逆にSQL文が解読不能になってしまいます。
また、SQL文はプログラム言語と比べてデバッグが難しいです。
当然プログラム側で処理を書いた方がシンプルになる場合もあるわけで、ケースバイケースで使い分けるべきと思います。