SQL学習入門｜サブクエリを演習で習得

※学習中のため間違っている箇所があるかもしれません。
# 初めに
今回はサブクエリを解説していきます。
サブクエリは名前だけは聞いたことはあるけど使ったことはない、という人も多いと思います。
サブクエリに関して、演習形式で学習していきましょう。

# 環境
以下に今回使用している環境をまとめておきます。
- A5M2
- MySQL
#### SQL
usersテーブル
```sql
CREATE TABLE users (
  id int AUTO_INCREMENT not null primary key,
  name varchar(50),
  email varchar(50),
  role ENUM('USER', 'ADMIN'),
  created_by varchar(50),
  updated_by varchar(50),
  created_at timestamp default current_timestamp,
  updated_at timestamp default current_timestamp,
  delete_flg boolean 
);
```
ordersテーブル
```sql
CREATE TABLE orders (
    id int AUTO_INCREMENT PRIMARY KEY NOT NULL,
    name VARCHAR(50) NOT NULL,
    price int NOT NULL,
    purchase_date timestamp default current_timestamp,
    user_id int NOT NULL
);
```
usersデータ挿入
```sql
INSERT INTO users (name) VALUES ('Taro');
INSERT INTO users (name) VALUES ('Hanako');
INSERT INTO users (name) VALUES ('Jiro');
INSERT INTO users (name) VALUES ('Miki');
```
ordersデータ挿入
```sql
INSERT INTO orders (user_id, name, price, purchase_date) VALUES (1, 'コーラ', 1000, '2025-04-01 10:00:00');
INSERT INTO orders (user_id, name, price, purchase_date) VALUES (1, 'サイダー', 2000, '2025-04-02 11:30:00');
INSERT INTO orders (user_id, name, price, purchase_date) VALUES (2, 'デカビタ', 3000, '2025-04-01 09:00:00');
INSERT INTO orders (user_id, name, price, purchase_date) VALUES (3, 'ドデカミン', 1500, '2025-04-02 14:00:00');
INSERT INTO orders (user_id, name, price, purchase_date) VALUES (5, 'ZONE', 1200, '2025-04-03 16:00:00');
```

# サブクエリ
`サブクエリ（副問い合わせ）`とは、SELECT文の中で入れ子になった別のSELECT文です。
構文を以下に記載します。

```
SELECT * FROM テーブル名 WHERE カラム名 = (
    SELECT 単一カラム FROM 他テーブル WHERE 条件
);
```

#### サブクエリの使用場所
サブクエリは以下の場所で使用することができます。
- WHERE 句：値の条件に使う
- FROM 句：サブクエリの結果を「仮想テーブル」として使う
- SELECT 句：単一値を取得して、表示用のカラムとして扱うことができます

このようにサブクエリは別のステートメントから取得した値を条件として使用することができます。
#### サブクエリの特徴
サブクエリの特徴をいかに記載します。
- サブクエリは「1つの値」だけを返すもの（スカラー）や、
  「1列・複数行」、「複数列・複数行」など、さまざまな形の結果を返すことができます。
- サブクエリは`GROUP BY`や`ORDER BY`、`LIMIT`など多くの句を使用することができる

実際に簡単なSQLを構築してみましょう。
```sql
SELECT * FROM users WHERE id=(
    SELECT user_id FROM orders WHERE id = 1
);
```

#### 結果
usersテーブルのid=1のレコードが取得できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155918/f58c9d42-48e4-491c-9ca2-12aa54ed0ec3.png)

#### サブクエリ
これをわかりやすく分解していきましょう
まず、サブクエリである以下を実行します
```sql
SELECT user_id FROM orders WHERE id = 1
```

`user_id`が取得できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155918/07b7b400-23b8-448e-b811-12eaed57829b.png)

つまり、これをsqlに反映すると以下のようになります。
```sql
SELECT * FROM users WHERE id=1
```
結果を確認すると、先ほどの結果と同じになることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155918/064391a3-1c82-42e5-9eed-4065c4d10f00.png)

このようにサブクエリはクエリの実行結果を条件式として使用することができます。

#### 例文
以下に`WHERE`、`FROM`、`SELECT`でサブクエリを使用する構文をそれぞれ記載します。
##### WHERE
単一の値、または複数行を返却するようにします。
```
--単一の値
SELECT * FROM テーブル名 WHERE カラム名 = (
    SELECT 単一カラム FROM 他テーブル WHERE 条件
);

--複数行(in句などで指定する場合)
--in句の場合
SELECT * FROM テーブル名 WHERE カラム名 in (
    SELECT 単一カラム FROM 他テーブル WHERE 条件
);
--EXISTSの場合
SELECT * FROM テーブル名 WHERE EXISTS (
    SELECT 単一カラム FROM 他テーブル WHERE 条件
);
```
##### FROM
テーブル形式で値を返却するようにします。
```
SELECT * FROM (
    SELECT * FROM テーブル名
) AS エイリアス(別名);
```
※`AS`を使用しないとエラーになるため注意しましょう。
##### SELECT
単一の値(スカラー)のみを返却するようにします。
```
SELECT name, (
    SELECT カラム名 FROM テーブル名 WHERE カラム名=値
) FROM テーブル名;
```

# 演習
## 演習１：最新の注文をしたユーザーの名前を取得せよ
- ordersテーブルの中で一番新しいpurchase_dateの注文をしたuserの名前を出力

## 演習2：Taroが購入した商品の名前一覧を取得せよ（サブクエリ + IN）
- Taroのuser_idをサブクエリで取得し、それを元にordersからname一覧を取得

## 演習3：価格が全体平均以上の注文だけを取得せよ（サブクエリ + 比較）
- orders.price >= (全体の平均価格) でフィルターする

## 演習4：全ユーザーの最新の注文日を一覧で取得せよ（サブクエリ + FROM）
- サブクエリで各ユーザーごとの最新purchase_dateを抽出

- 外側のSELECTで user_id と日付を一覧で取得

## 演習5：存在しないユーザーが行った注文の一覧を取得せよ（サブクエリ + NOT IN）
- orders.user_id が users.id に存在しないものだけを取得

# 回答
## 演習１：最新の注文をしたユーザーの名前を取得せよ
### 解説
それでは今回のSQLを解説していきます。

SQLの条件を見ていきましょう
- ordersテーブルの中で一番新しいpurchase_dateの注文をしたuserの名前を出力

まずはこの条件のうち、「ordersテーブルの中で一番新しいpurchase_date」を取得していきましょう。
そのために、最初に`ORDER BY`を使用し、購入日順に並べ替えます。
```sql
SELECT name, purchase_date FROM orders ORDER BY purchase_date DESC;
```
#### 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155918/4819807a-a206-4ae1-a008-be43419f8c6e.png)

`ORDER BY`を使用して、`purchase_date`を降順に並べ替えました。

次に、ユーザー名を取得していきましょう
```sql
SELECT name, purchase_date, 
    (
        SELECT name FROM users WHERE id=orders.user_id
    )
 FROM orders ORDER BY purchase_date DESC;
```
サブクエリで、order.user_idとusers.idが一致する値を取得しました。
SELECTのサブクエリは各行に対して実行され、一致する値を出力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155918/02bebd58-a216-497a-9e0b-2536d77ce2f4.png)

次に一番新しい値を取得してみましょう
```sql
SELECT name, purchase_date, 
    (
        SELECT name FROM users WHERE id=orders.user_id
    )
 FROM orders ORDER BY purchase_date DESC LIMIT 1;
```
LIMIT句を使用することで取得できる行数を制限できます。
今回は最新の1行だけ取得したので`LIMIT 1`とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155918/6150ac13-2fef-4af7-87e6-d9c4f363df95.png)

`NULL`が取得できました。
しかし、今回取得したいのは「一番新しいpurchase_dateの注文をしたuserの名前を出力」です。
そのため、NULLではなくユーザー名を取得する必要があります。

サブクエリを使用してusersテーブルにユーザーデータがある場合だけ表示してみましょう。

```sql
--IN句の場合
SELECT name, purchase_date, 
    (
        SELECT name FROM users WHERE id=orders.user_id
    ) AS 'ユーザー名'
 FROM orders WHERE user_id IN 
    (
        SELECT id FROM users
    )
ORDER BY purchase_date DESC LIMIT 1;
```
```sql
--EXISTS句の場合
SELECT name, purchase_date, 
    (
        SELECT name FROM users WHERE id=orders.user_id
    ) AS 'ユーザー名'
 FROM orders WHERE EXISTS 
    (
        SELECT id FROM users WHERE id=orders.user_id
    )
ORDER BY purchase_date DESC LIMIT 1;
```
※このEXISTSの使い方は「データが存在するかの判定」としては機能しますが、IN句と同様の結果になります。実務では、より複雑な絞り込み（相関サブクエリなど）でEXISTSが真価を発揮します。

この場合以下のように変換できます。

```
--IN句の場合
SELECT name, purchase_date, 
    (
        SELECT name FROM users WHERE id=orders.user_id
    ) AS 'ユーザー名'
 FROM orders WHERE user_id in 
    (
        1,2,3,4
    )
ORDER BY purchase_date DESC LIMIT 1;
```
```
--EXISTS句の場合
SELECT name, purchase_date, 
    (
        SELECT name FROM users WHERE id=orders.user_id
    ) AS 'ユーザー名'
 FROM orders WHERE EXISTS 
    (
        orders.user_idと一致するuser.idを取得
    )
ORDER BY purchase_date DESC LIMIT 1;
```
usersテーブルを確認すると、idは1,2,3,4が存在しています。
`WHERE`句で使用する場合、`in`句を使用することで複数の値を指定することができます。

実行すると、以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155918/1f7baf4e-b4f4-4ac9-b32b-cb0f2e2e8e81.png)

このように最新の注文をしたユーザーIDを取得することができました。
その他演習も同じように組み立てられます。

以下演習2以降も考え方自体は変わりません。
そのため、回答と補足のみ記載させていただきます

## 演習2
```sql
SELECT name FROM orders WHERE orders.user_id = 
    (
        SELECT id FROM users WHERE name='Taro'
    )
```

## 演習3

```sql
SELECT * FROM orders WHERE price >=
    (
        SELECT AVG(price) FROM orders
    )
```

## 演習4
```sql
SELECT user_id, (
        SELECT name FROM users WHERE id=orders.user_id
    ) AS username, MAX(purchase_date) 
FROM orders GROUP BY user_id HAVING (
        SELECT name FROM users WHERE id=orders.user_id
    ) IS NOT NULL
```

## 演習5
```sql
SELECT * FROM orders WHERE user_id NOT IN 
    (
        SELECT id FROM users
    )
```

# まとめ
今回はサブクエリにサブクエリを使用した演習を通じて
使い方を解説しました。

サブクエリには相関サブクエリと非相関サブクエリがあります。
今回学んだのは非相関サブクエリです。

非相関サブクエリに関してはまた次回以降に解説できればと考えています。

次回は非相関サブクエリ/パフォーマンスチューニングなどを解説していければと考えています。
引き続き、SQLの基礎力を高める学習記事を投稿していきます！

[前：SQL学習入門｜GROUP BY / SUM / COUNTを演習形式で習得](https://qiita.com/suica1010/items/019d5b29036a950047b5)

参考文献
[【22日間で学ぶ】SQL文、分析関数、テーブル設計、SQLチューニングまでMySQLで覚えるSQL実践講座](https://www.udemy.com/course/3sqlmysql/)