SQLのJOINとGROUP BYを試す

リレーショナルデータベースではテーブルを結び付けるJOINが大事なのですが、「プロになるJava」ではページの都合で取り上げていなかったので補足。

追記 2023/8/11: H2コンソールでは自動でデータベースは作成されないので、とりあえずコードを追加しています。

データベースの準備

ここではSpring BootにH2データベースを導入してH2コンソールのWebインタフェースを使って進めていきます。

「プロになるJava」に従ってすでにH2コンソールでSQLを試している場合は、テーブルの作成から進めてください。

CodespacesやVS Codeである必要はないですが、Spring Bootの環境はこちらを参照。
GitHub CodespacesでSpring Bootの開発を始める - きしだのHatena

H2を導入

まずH2データベースを使える状態にします。

pom.xmlのdependenciesに次のdependencyを追加します。

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

今回はH2コンソールを使うだけなので、Spring Data JDBCdependencyは追加していません。

DBの作成

H2ではJDBC接続時にデータベースが自動作成されますが、H2コンソールからは自動作成されません。

Javaプログラムで接続したときには自動生成されるので、次のコードを実行してください。

package com.example.tasklist;

import java.sql.DriverManager;
import java.sql.SQLException;

public class ConDB {
    public static void main(String[] args) throws SQLException {
        var d = DriverManager.getConnection("jdbc:h2:~/taskdb", 
            "projava", "projavadb");
    }
}

Spring BootでH2コンソールのWebインタフェースを有効にする

src/main/resourcesの下のapplication.propertiesを編集します。

次の1行を追加します。

spring.h2.console.enabled=true

H2コンソールを使うだけなのでspring.datasource関連は追加していません。

H2コンソールへのアクセス

Spring Bootを起動して、URLに/h2-console/を追加します。

ログイン画面が表示されたら、左上の「English」をクリックして、「日本語」を選んでおきましょう。

各項目に次のような値を入力して「接続」をクリックします。

項目
ドライバクラス org.h2.Driver
JDBC URL jdbc:h2:~/taskdb
ユーザー名 projava
パスワード projavadb

H2コンソールが表示されるはずです。

ただ、この画面で作業しているとCodespaceのほうで操作がないとみなされて切断されることがあります。反応がなさそうなときはCodespaceの画面を確認してください。

Codespacesでの注意

Codespacesを使っている場合、VS Code内のシンプルブラウザではうまく動かないようです。

また、アクセス時に最後のスラッシュのない/h2-consoleにアクセスしようとするとスラッシュのついた/h2-console/へのリダイレクトが発生しますが、Codespacesを使っているとき8080ポートが追加されてしまってうまく表示されなくなります。URLから「:8080」を削除してください。

テーブルの作成

それではテーブルを作成します。ここではshohinテーブルとtradeテーブルのふたつのテーブルを作ります。

shohinテーブル

CREATE TABLE shohin(
id INT PRIMARY KEY,
name VARCHAR(256),
price int
);

左側のツリーにSHOHINが追加されています。

データを追加しましょう。

INSERT INTO shohin
values
(1, 'APPLE', 300),
(2, 'BANANA', 200)

tradeテーブル

もうひとつのtradeテーブルを作成します。

CREATE TABLE trade (
  id INT PRIMARY KEY,
  shohin_id INT,
  quantity INT,
  date VARCHAR(16),
  FOREIGN KEY(shohin_id) REFERENCES shohin(id)
)

ここでこの一行はshohin_idカラムをshohinテーブルのidカラムと結び付けるものです。

FOREIGN KEY(shohin_id) REFERENCES shohin(id)

データを追加します。

INSERT INTO trade
values
(1, 1, 3, '2024-07-24'),
(2, 1, 2, '2024-07-25'),
(3, 2, 4, '2024-07-25')

ここで2番目の値がshohin_idになりますが、次のようにshohinテーブルに存在しない値を追加しようとするとエラーになります。

INSERT INTO trade
values
(4, 3, 1, '2024-07-26')

FOREIGN KEYを設定すると、対応するテーブルのデータがなければエラーが出るようになり、確実にデータの対応をとることができるようになります。

データベースでは、このようにデータの整合性を保つ機能を持っています。

JOIN

さて、それではこれらのふたつのテーブルを結び付けて見ましょう。

まずはtradeテーブルのデータを全部表示します。

SELECT *
FROM trade

しかしこれではshouhin_idが実際にどの商品を表しているかわかりませんね。

FROM句にshohinも付け加えてみます。

SELECT *
FROM trade, shohin

たしかにtradeテーブルとshohinテーブルが並んで表示されていますが、すべての組み合わせが表示されてしまっています。

WHERE句にtradeテーブルのshohin_idとshohinテーブルのidが一致するような条件を書いてみましょう。

SELECT *
FROM trade, shohin
WHERE trade.shohin_id=shohin.id

tradeとshohinが対応しているものだけが表示されました。

ところで次のようにshohin_idに値を設定せずにtradeテーブルにデータを追加してみましょう。INSERT文ではテーブル名のあとに追加先のカラムを指定することができます。

INSERT INTO trade(id, quantity, date)
VALUES(4, 1, '2024-07-26')

それでは先ほどのSQLを実行してみましょう。「オートコンプリート」の左のボタンで履歴が表示されるので、再実行したいSQLの左の鉛筆ボタンで編集領域に表示されます。バグがあるようで10がついてくるので削除してください。

実行しても、先ほどと変わりませんね。

tradeテーブルだけを見てみると、確かにID=4のデータが入っています。

このようなとき、shohinに対応したデータがなくてもtradeは全部表示したい、ということがあります。そういうときに使うのがleft joinです。

SELECT *
FROM trade LEFT JOIN shohin ON trade.shohin_id = shohin.id

そうすると、shohin_idに値がないものも表示されるようになります。

leftのないJOINの場合、WHERE句での連結と同じになります。

また、RIGHT JOINにすると右に書いたshohinテーブルが全て表示されるようになります。

最後に、項目を見やすいようにしておきましょう。SELECT句に表示するカラムを指定しておきます。ここで、idについてはtradeテーブルにもshohinテーブルにもあるのでtrade.idとしてtradeテーブルのものを指定しています。また、as totalとしてquantity * priceにtotalという名前をつけています。

SELECT trade.id, name, quantity, quantity * price AS total, date
FROM trade LEFT JOIN shohin ON trade.shohin_id = shohin.id

GROUP BY

では、データをグループ化させるGROUP BYを見ておきましょう。

SELECT shohin_id, name, SUM(quantity * price) AS total
FROM trade LEFT JOIN shohin ON trade.shohin_id = shohin.id
GROUP BY shohin_id

GROUP BYをつけると、SELECT句に*は使えなくなります。GROUP BYで指定したカラムか、そのカラムに対応付いたテーブルのカラム、そしてSUMやCOUNTなどの集計関数だけを指定できます。

集計関数には次のようなものがあります。

集計関数 用途
SUM 合計
COUNT 個数
AVG 平均
MIN 最小
MAX 最大

集計関数は、GROUP BYがなくても使えます。

SELECT COUNT(*)
FROM trade

WHERE句をつかって集計対象を指定することができます。

SELECT shohin_id, name, SUM(quantity * price) AS total
FROM trade LEFT JOIN shohin ON trade.shohin_id = shohin.id
WHERE date='2024-07-25'
GROUP BY shohin_id

ORDER BYを使うと並べ替えができます。ORDER BYはGROUP BYがなくても使えます。ASCをつけるか省略した場合は昇順、DESCを付けると降順になります。

SELECT shohin_id, name, SUM(quantity * price) AS total
FROM trade LEFT JOIN shohin ON trade.shohin_id = shohin.id
WHERE date='2024-07-25'
GROUP BY shohin_id
ORDER BY total DESC

ORDER BYがない場合の出力順は決まっていないので、一覧表示などの場合には必ず指定するようにしましょう。

集計した結果から絞り込みを行う場合はHAVINGを使います。

SELECT shohin_id, name, SUM(quantity * price) AS total
FROM trade LEFT JOIN shohin ON trade.shohin_id = shohin.id
WHERE date='2024-07-25'
GROUP BY shohin_id
HAVING total > 700
ORDER BY total DESC

これでSELECT文で書ける句が出そろいました。順番もこの通りで書く必要があります。