はい、承知いたしました。SQLiteで学ぶSQLのJOINについて、約5000語の詳細な解説記事を作成します。
SQL超基本:SQLiteで学ぶJOINの使い方
リレーショナルデータベースの世界へようこそ!データを複数のテーブルに分けて管理することは、データベース設計の基本中の基本です。しかし、それぞれのテーブルに分散された情報を組み合わせて「意味のある情報」を取り出すには、どうすれば良いのでしょうか?そこで登場するのが、SQLの強力な機能である「JOIN」です。
この記事では、「SQLは全く初めて」「JOINって聞くけどよく分からない」という方を対象に、データベースの基礎から始め、JOINが必要な理由、そして最も基本的なJOINのタイプ(INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN, SELF JOIN)の使い方までを、ハンズオン形式で学んでいきます。データベースには、セットアップが簡単で初心者向けのSQLiteを使用します。
約5000語のボリュームで、各JOINタイプの仕組み、構文、使用例、そしてSQLite特有の注意点まで、徹底的に掘り下げて解説します。この記事を読めば、JOINの基本をマスターし、複雑なデータも自在に扱えるようになるはずです。
さあ、一緒にJOINの世界を探検しましょう!
1. なぜJOINが必要なのか? リレーショナルデータベースの基礎
JOINを学ぶ前に、まずリレーショナルデータベースの基本的な考え方を理解しておく必要があります。
1.1. リレーショナルデータベースとは
リレーショナルデータベースは、データを「テーブル」と呼ばれる二次元の表形式で管理します。それぞれのテーブルは、特定の種類の情報(例:顧客情報、注文情報、商品情報など)を格納します。
- テーブル (Table): データの集合。Excelのスプレッドシートのようなもの。
- 行 (Row) / レコード (Record): テーブルの1つのエントリ。例えば、ある一人の顧客の情報、またはある1つの注文の情報。
- 列 (Column) / フィールド (Field): 行の各項目。例えば、顧客の名前、顧客の住所、注文の日付など。
1.2. データの正規化とテーブル分割
リレーショナルデータベース設計の重要な考え方に「正規化」があります。正規化とは、データの重複を避け、一貫性を保つためにデータを複数のテーブルに分割するプロセスです。
例えば、顧客とその注文情報を一つの巨大なテーブルに格納することを考えてみましょう。
注文ID | 注文日 | 顧客ID | 顧客名 | 顧客住所 | 顧客電話 | 商品ID | 商品名 | 価格 | 数量 |
---|---|---|---|---|---|---|---|---|---|
101 | 2023-10-26 | 1 | 山田 太郎 | 東京都新宿区 | 03-… | P001 | ノートPC A | 120000 | 1 |
102 | 2023-10-26 | 2 | 佐藤 花子 | 大阪府梅田区 | 06-… | P002 | スマートフォンB | 80000 | 1 |
103 | 2023-10-27 | 1 | 山田 太郎 | 東京都新宿区 | 03-… | P003 | マウス C | 3000 | 2 |
このテーブルでは、山田太郎さんの情報(名前、住所、電話)が2回重複しています。もし山田さんの住所が変わった場合、全ての関連する行を更新する必要があります。これはデータの更新を複雑にし、誤ったデータが生まれる可能性を高めます。
そこで、データを分割します。
テーブルA: 顧客情報 (customers
)
customer_id (PK) | name | address | phone |
---|---|---|---|
1 | 山田 太郎 | 東京都新宿区 | 03-… |
2 | 佐藤 花子 | 大阪府梅田区 | 06-… |
3 | 田中 一郎 | 福岡県博多区 | 092-… |
テーブルB: 注文情報 (orders
)
order_id (PK) | order_date | customer_id (FK) | product_id (FK) | quantity |
---|---|---|---|---|
101 | 2023-10-26 | 1 | P001 | 1 |
102 | 2023-10-26 | 2 | P002 | 1 |
103 | 2023-10-27 | 1 | P003 | 2 |
104 | 2023-10-28 | 3 | P001 | 1 |
105 | 2023-10-28 | 1 | P002 | 1 |
テーブルC: 商品情報 (products
)
product_id (PK) | name | price |
---|---|---|
P001 | ノートPC A | 120000 |
P002 | スマートフォンB | 80000 |
P003 | マウス C | 3000 |
P004 | キーボード D | 5000 |
このようにデータを分割することで、データの重複を防ぎ、更新や管理が容易になります。
1.3. キー (Key) の役割
テーブル間の関連性を保つために、「キー」という概念が重要になります。
- 主キー (Primary Key: PK): そのテーブルの各行を一意に識別するための列(または列の組み合わせ)。上記例では
customers
テーブルのcustomer_id
、orders
テーブルのorder_id
、products
テーブルのproduct_id
が主キーです。主キーは重複せず、NULLであってはいけません。 - 外部キー (Foreign Key: FK): 別のテーブルの主キーを参照する列。上記例では、
orders
テーブルのcustomer_id
はcustomers
テーブルのcustomer_id
を参照しており、orders
テーブルのproduct_id
はproducts
テーブルのproduct_id
を参照しています。外部キーは、テーブル間の関連性(リレーションシップ)を定義し、データの整合性を保つ役割を果たします。
1.4. JOINが必要になる瞬間
データがこのように分割されていると、「山田太郎さんが購入した商品のリストが見たい」「各注文がどの顧客によるもので、どんな商品を含んでいるかを知りたい」といった要求が出てきます。
これらの情報は、customers
、orders
、products
という複数のテーブルにまたがって存在しています。単一の SELECT
文で1つのテーブルからだけでは、これらの情報をまとめて取得することはできません。
ここで、異なるテーブルにある関連する行を、指定した共通の列(通常は主キーと外部キー)を基に結合する必要があるわけです。この「結合」こそが、JOIN の役割です。
2. SQLiteを準備しよう
学習を進めるために、まずはSQLiteを使える環境を準備しましょう。SQLiteはサーバーをインストールする必要がなく、ファイル一つでデータベースを扱えるため、学習用として非常に便利です。
2.1. SQLiteのインストール(または使用方法)
- Windows/macOS/Linux: 多くのOSにはSQLiteコマンドラインツールがプリインストールされているか、簡単にインストールできます。ターミナル(コマンドプロンプト)を開いて
sqlite3
と入力し、コマンドプロンプトが表示されれば準備OKです。- Windows: 公式サイトからダウンロード&インストールが必要な場合があります。
- macOS: ほとんどの場合プリインストールされています。
- Linux: パッケージマネージャー(apt, yumなど)でインストールできます。
- GUIツール: 初心者の方には、GUIツールの利用がおすすめです。「DB Browser for SQLite」のようなツールを使うと、SQLを書くだけでなく、テーブル構造を見たり、データを直感的に操作したりできます。この記事では主にSQLクエリを説明しますが、GUIツールがあると学習効率が格段に上がります。公式サイトからダウンロードしてインストールしておきましょう。
2.2. 作業用データベースファイルの作成
SQLiteコマンドラインツールを使う場合:
ターミナルを開き、以下のコマンドを入力します。
bash
sqlite3 my_database.db
これで my_database.db
という名前のデータベースファイルが作成(または開かれ)し、SQLiteのプロンプト(sqlite>
)が表示されます。
DB Browser for SQLiteを使う場合:
「新しいデータベース」を選択し、ファイル名を指定して保存します。
2.3. サンプルテーブルの作成とデータの挿入
JOINを学ぶためのサンプルデータを作成しましょう。先ほど考えた customers
, products
, orders
の3つのテーブルを作成し、データを投入します。
SQLiteコマンドラインツールまたはDB Browser for SQLiteの「SQL実行」タブで、以下のSQL文を実行してください。
まず、テーブルを作成します。
“`sql
— customers テーブル作成
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
address TEXT,
phone TEXT
);
— products テーブル作成
CREATE TABLE products (
product_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL
);
— orders テーブル作成
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
order_date TEXT NOT NULL,
customer_id INTEGER, — customers テーブルの customer_id を参照 (NULL可とする)
product_id TEXT, — products テーブルの product_id を参照 (NULL可とする)
quantity INTEGER NOT NULL,
— 外部キー制約 (ON DELETE CASCADEなどを付けることも多いが、ここではシンプルに)
FOREIGN KEY (customer_id) REFERENCES customers(customer_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
“`
次に、データを挿入します。
“`sql
— customers データ挿入
INSERT INTO customers (customer_id, name, address, phone) VALUES
(1, ‘山田 太郎’, ‘東京都新宿区’, ’03-xxxx-xxxx’),
(2, ‘佐藤 花子’, ‘大阪府梅田区’, ’06-xxxx-xxxx’),
(3, ‘田中 一郎’, ‘福岡県博多区’, ‘092-xxxx-xxxx’),
(4, ‘山本 次郎’, ‘北海道札幌市’, ‘011-xxxx-xxxx’),
(5, ‘中村 綾子’, ‘愛知県名古屋市’, ‘052-xxxx-xxxx’); — 注文のない顧客
— products データ挿入
INSERT INTO products (product_id, name, price) VALUES
(‘P001’, ‘ノートPC A’, 120000),
(‘P002’, ‘スマートフォンB’, 80000),
(‘P003’, ‘マウス C’, 3000),
(‘P004’, ‘キーボード D’, 5000),
(‘P005’, ‘モニター E’, 35000); — 注文されたことのない商品
— orders データ挿入
INSERT INTO orders (order_id, order_date, customer_id, product_id, quantity) VALUES
(101, ‘2023-10-26’, 1, ‘P001’, 1),
(102, ‘2023-10-26’, 2, ‘P002’, 1),
(103, ‘2023-10-27’, 1, ‘P003’, 2),
(104, ‘2023-10-28’, 3, ‘P001’, 1),
(105, ‘2023-10-28’, 1, ‘P002’, 1),
(106, ‘2023-10-29’, 2, ‘P003’, 1),
(107, ‘2023-10-30’, 4, ‘P004’, 1),
(108, ‘2023-10-30’, 4, ‘P003’, 3); — 山本さんの注文
— customer_id が存在しない注文 (外部キー制約があればエラーになるが、ここではNULLを許容)
— INSERT INTO orders (order_id, order_date, customer_id, product_id, quantity) VALUES (109, ‘2023-10-31’, 99, ‘P001’, 1); — 無効な顧客ID
— product_id が存在しない注文
— INSERT INTO orders (order_id, order_date, customer_id, product_id, quantity) VALUES (110, ‘2023-10-31’, 1, ‘P999’, 1); — 無効な商品ID
— 両方NULLの注文 (実用的ではないがJOINの挙動確認用に)
— INSERT INTO orders (order_id, order_date, customer_id, product_id, quantity) VALUES (111, ‘2023-11-01’, NULL, NULL, 5);
“`
これで、JOINの学習に使えるサンプルデータが準備できました。
それぞれのテーブルの中身を確認してみましょう。
sql
SELECT * FROM customers;
SELECT * FROM products;
SELECT * FROM orders;
これらのクエリを実行すると、それぞれのテーブルに挿入したデータが表示されるはずです。
3. JOINの基本構文
JOINを使う基本的な構文は以下のようになります。
sql
SELECT column1, column2, ...
FROM table1
[JOIN_TYPE] table2 ON table1.common_column = table2.common_column;
SELECT column1, column2, ...
: 取得したい列を指定します。結合後のテーブルから、必要な列を選択します。どのテーブルの列か明確にするため、table_name.column_name
の形式で指定することが推奨されます(特に同じ名前の列がある場合)。FROM table1
: 結合の基点となる最初のテーブルを指定します。[JOIN_TYPE]
: 結合の種類を指定します(INNER JOIN
,LEFT JOIN
,RIGHT JOIN
,FULL OUTER JOIN
,CROSS JOIN
など)。単にJOIN
と書くと、通常INNER JOIN
と同じ意味になります。table2
:table1
と結合する2つ目のテーブルを指定します。ON table1.common_column = table2.common_column
: これがJOINの最も重要な部分です。どの列を使って、どのようにテーブルを関連付けるかを指定します。ほとんどの場合、一方のテーブルの外部キーと、もう一方のテーブルの主キー(または一意なキー)を結びつけます。=
演算子が最も一般的ですが、他の比較演算子(<
,>
,<=
,>=
,<>
) やAND
を使って複数の条件を指定することも可能です。
注意: 以前の古いSQL構文では、FROM table1, table2 WHERE table1.column = table2.column
のように、FROM
句でテーブルをカンマ区切りで指定し、WHERE
句で結合条件を書く形式もありました。しかし、これは結合の種類が分かりにくく、条件を書き忘れると意図しない結果(後述するCROSS JOIN
)になるため、明示的なJOIN ... ON
構文を使用することが強く推奨されています。
4. 様々なJOINの種類
ここから、具体的なJOINの種類と使い方を見ていきましょう。先ほど作成したサンプルデータを使って、それぞれのJOINがどのような結果を返すのかを確認していきます。
4.1. INNER JOIN (内部結合)
INNER JOIN
は最も一般的で基本的な結合です。指定した結合条件に両方のテーブルで一致する行だけを結果として返します。一致しない行は結果に含まれません。
イメージとしては、二つのテーブルの「共通部分」を取り出すようなものです。Venn図で言えば、二つの円が重なる部分に当たります。
構文:
sql
SELECT ...
FROM table1
INNER JOIN table2 ON table1.common_column = table2.common_column;
または、単に JOIN
と書いても同じです。
sql
SELECT ...
FROM table1
JOIN table2 ON table1.common_column = table2.common_column;
例1: 注文を行った顧客の名前と注文日を取得する
customers
テーブルと orders
テーブルを customer_id
で結合します。customer_id
が両方のテーブルに存在する行だけが結果に含まれます。
sql
SELECT
customers.name,
orders.order_date,
orders.order_id -- どの注文か分かりやすいようにIDも表示
FROM
customers
INNER JOIN
orders ON customers.customer_id = orders.customer_id;
結果:
name | order_date | order_id |
---|---|---|
山田 太郎 | 2023-10-26 | 101 |
佐藤 花子 | 2023-10-26 | 102 |
山田 太郎 | 2023-10-27 | 103 |
田中 一郎 | 2023-10-28 | 104 |
山田 太郎 | 2023-10-28 | 105 |
佐藤 花子 | 2023-10-29 | 106 |
山本 次郎 | 2023-10-30 | 107 |
山本 次郎 | 2023-10-30 | 108 |
解説:
customers
テーブルの customer_id
と orders
テーブルの customer_id
が一致する行だけが結合されています。
* 顧客ID 5 の「中村 綾子」さんは注文をしていないため (orders
テーブルに customer_id=5 の行がない)、結果に含まれません。
* もし orders
テーブルに存在しない customer_id
(例えば99)を持つ注文があったとしても、それも結果に含まれません。
例2: 注文された商品名と数量、価格を取得する
orders
テーブルと products
テーブルを product_id
で結合します。
sql
SELECT
products.name AS product_name, -- 列名に別名を付けると分かりやすい
orders.quantity,
products.price,
orders.order_id -- どの注文か分かりやすいようにIDも表示
FROM
orders
INNER JOIN
products ON orders.product_id = products.product_id;
結果:
product_name | quantity | price | order_id |
---|---|---|---|
ノートPC A | 1 | 120000 | 101 |
スマートフォンB | 1 | 80000 | 102 |
マウス C | 2 | 3000 | 103 |
ノートPC A | 1 | 120000 | 104 |
スマートフォンB | 1 | 80000 | 105 |
マウス C | 1 | 3000 | 106 |
キーボード D | 1 | 5000 | 107 |
マウス C | 3 | 3000 | 108 |
解説:
orders
テーブルの product_id
と products
テーブルの product_id
が一致する行だけが結合されています。
* 商品ID ‘P005’ の「モニター E」は注文されたことがないため (orders
テーブルに product_id=’P005′ の行がない)、結果に含まれません。
* もし orders
テーブルに存在しない product_id
(例えば’P999’)を持つ注文があったとしても、それも結果に含まれません。
例3: 3つのテーブルを結合する
customers
, orders
, products
の3つのテーブルを結合して、「どの顧客が、いつ、どの商品を、いくつ購入したか」を知りたい場合。JOIN句を複数つなげて書きます。
sql
SELECT
c.name AS customer_name, -- テーブルに別名(エイリアス)を付けると更に簡潔に書ける
o.order_date,
p.name AS product_name,
o.quantity
FROM
customers AS c -- customers テーブルに c という別名
INNER JOIN
orders AS o ON c.customer_id = o.customer_id -- orders テーブルに o という別名
INNER JOIN
products AS p ON o.product_id = p.product_id; -- products テーブルに p という別名
テーブルの別名 (Alias): AS
キーワードを使ってテーブルに短い別名を付けることができます (customers AS c
)。これにより、customers.name
と書く代わりに c.name
と書くことができ、クエリが短く読みやすくなります。特に複数のテーブルを結合したり、同じテーブルを複数回結合したりする場合に非常に役立ちます。AS
は省略することも可能です(例: customers c
)。
結果:
customer_name | order_date | product_name | quantity |
---|---|---|---|
山田 太郎 | 2023-10-26 | ノートPC A | 1 |
佐藤 花子 | 2023-10-26 | スマートフォンB | 1 |
山田 太郎 | 2023-10-27 | マウス C | 2 |
田中 一郎 | 2023-10-28 | ノートPC A | 1 |
山田 太郎 | 2023-10-28 | スマートフォンB | 1 |
佐藤 花子 | 2023-10-29 | マウス C | 1 |
山本 次郎 | 2023-10-30 | キーボード D | 1 |
山本 次郎 | 2023-10-30 | マウス C | 3 |
解説:
このクエリは、customers
と orders
を customer_id
で結合した結果と、products
テーブルを orders
テーブルの product_id
で結合しています。すべての結合条件に一致する行だけが結果に含まれます。注文をしていない顧客、または注文されていない商品は結果に含まれません。
4.2. LEFT JOIN (左外部結合)
LEFT JOIN
は、FROM
句で指定した最初のテーブル(左側のテーブル)の全ての行を含み、JOIN
句で指定した2番目のテーブル(右側のテーブル)から、結合条件に一致する行を結合します。
もし右側のテーブルに一致する行がない場合でも、左側のテーブルの行は結果に含まれます。このとき、右側のテーブルから取得しようとした列には NULL が入ります。
構文:
sql
SELECT ...
FROM table1 -- 左側のテーブル
LEFT JOIN table2 ON table1.common_column = table2.common_column; -- 右側のテーブル
LEFT OUTER JOIN
と書いても同じ意味です。通常 OUTER
は省略されます。
イメージとしては、左側のテーブルの全ての円と、右側のテーブルのうち左側のテーブルと重なる部分を合わせたものです。
例1: 全ての顧客と、もしあればその注文情報を取得する
customers
テーブルを左側、orders
テーブルを右側にして LEFT JOIN
します。
sql
SELECT
c.name AS customer_name,
o.order_id, -- 注文がなければ NULL
o.order_date, -- 注文がなければ NULL
o.product_id -- 注文がなければ NULL
FROM
customers AS c
LEFT JOIN
orders AS o ON c.customer_id = o.customer_id;
結果:
customer_name | order_id | order_date | product_id |
---|---|---|---|
山田 太郎 | 101 | 2023-10-26 | P001 |
山田 太郎 | 103 | 2023-10-27 | P003 |
山田 太郎 | 105 | 2023-10-28 | P002 |
佐藤 花子 | 102 | 2023-10-26 | P002 |
佐藤 花子 | 106 | 2023-10-29 | P003 |
田中 一郎 | 104 | 2023-10-28 | P001 |
山本 次郎 | 107 | 2023-10-30 | P004 |
山本 次郎 | 108 | 2023-10-30 | P003 |
中村 綾子 | NULL | NULL | NULL |
解説:
customers
テーブル(左)の全ての顧客が結果に含まれています。顧客ID 5 の「中村 綾子」さんは注文をしていないため、orders
テーブルから取得されるべき列(order_id
, order_date
, product_id
)は NULL
になっています。注文した顧客については、それぞれの注文行と結合されています。
例2: 注文をしていない顧客をリストアップする
LEFT JOIN
の結果を利用して、「右側のテーブルに一致する行がなかった」という条件で絞り込むことで、一方のテーブルにだけ存在する行を抽出できます。
先の例1の結果で、order_id
が NULL
になっている行は、注文情報が見つからなかった(つまり、注文をしていない)顧客です。
sql
SELECT
c.name AS customer_name
FROM
customers AS c
LEFT JOIN
orders AS o ON c.customer_id = o.customer_id
WHERE
o.order_id IS NULL; -- orders テーブルの主キーが NULL である行を絞り込む
結果:
customer_name |
---|
中村 綾子 |
解説:
このように、LEFT JOIN
を使い、右側のテーブルの主キーなどが NULL
になる行を絞り込むことで、「左側のテーブルには存在するが、右側のテーブルには対応する行が存在しない」データを簡単に抽出できます。これは非常によく使われるテクニックです。
例3: 全ての商品と、もしあればそれが含まれる注文情報を取得する
今度は products
テーブルを左側、orders
テーブルを右側にして LEFT JOIN
します。
sql
SELECT
p.name AS product_name,
o.order_id, -- 注文がなければ NULL
o.order_date, -- 注文がなければ NULL
o.customer_id -- 注文がなければ NULL
FROM
products AS p
LEFT JOIN
orders AS o ON p.product_id = o.product_id;
結果:
product_name | order_id | order_date | customer_id |
---|---|---|---|
ノートPC A | 101 | 2023-10-26 | 1 |
スマートフォンB | 102 | 2023-10-26 | 2 |
マウス C | 103 | 2023-10-27 | 1 |
ノートPC A | 104 | 2023-10-28 | 3 |
スマートフォンB | 105 | 2023-10-28 | 1 |
マウス C | 106 | 2023-10-29 | 2 |
キーボード D | 107 | 2023-10-30 | 4 |
マウス C | 108 | 2023-10-30 | 4 |
モニター E | NULL | NULL | NULL |
解説:
products
テーブル(左)の全ての商品が結果に含まれています。商品ID ‘P005’ の「モニター E」は注文されたことがないため、orders
テーブルから取得されるべき列は NULL
になっています。注文されたことのある商品については、それぞれの注文行と結合されています。
4.3. RIGHT JOIN (右外部結合)
RIGHT JOIN
は、LEFT JOIN
の鏡のようなものです。JOIN
句で指定した2番目のテーブル(右側のテーブル)の全ての行を含み、FROM
句で指定した最初のテーブル(左側のテーブル)から、結合条件に一致する行を結合します。
もし左側のテーブルに一致する行がない場合でも、右側のテーブルの行は結果に含まれます。このとき、左側のテーブルから取得しようとした列には NULL が入ります。
構文:
sql
SELECT ...
FROM table1 -- 左側のテーブル
RIGHT JOIN table2 ON table1.common_column = table2.common_column; -- 右側のテーブル (こちらが全て含まれる)
RIGHT OUTER JOIN
と書いても同じ意味です。通常 OUTER
は省略されます。
イメージとしては、右側のテーブルの全ての円と、左側のテーブルのうち右側のテーブルと重なる部分を合わせたものです。
SQLiteにおけるRIGHT JOINの注意点:
重要なことですが、SQLiteは標準では RIGHT JOIN
を直接サポートしていません。 RIGHT JOIN
構文を使おうとするとエラーになるか、意図しない結果になる可能性があります。
しかし、RIGHT JOIN
と同じ結果は、LEFT JOIN
を使ってテーブルの順序を入れ替えることで実現できます。
例えば、table1 RIGHT JOIN table2 ON ...
という結合は、table2 LEFT JOIN table1 ON ...
と書き換えることで同じ結果になります。右側のテーブルだった table2
を左側 (FROM
句の直後) に持ってきて、左側のテーブルだった table1
を右側 (LEFT JOIN
の直後) に持ってくれば良いだけです。
例1 (SQLiteでのRIGHT JOIN相当): 全ての商品と、もしあればそれが含まれる注文情報を取得する
これは、先ほどの LEFT JOIN
の例3と全く同じ結果になります。
products
を右側にする RIGHT JOIN
は、products
を左側にする LEFT JOIN
と同じだからです。
“`sql
— 概念的なRIGHT JOIN (SQLiteでは直接実行できない)
— SELECT
— p.name AS product_name,
— o.order_id,
— o.order_date,
— o.customer_id
— FROM
— orders AS o — 左側(こちらが全て含まれない方)
— RIGHT JOIN
— products AS p ON o.product_id = p.product_id; — 右側(こちらが全て含まれる方)
— SQLiteで同じ結果を得る方法 (LEFT JOINでテーブルを入れ替え)
SELECT
p.name AS product_name,
o.order_id, — 注文がなければ NULL
o.order_date, — 注文がなければ NULL
o.customer_id — 注文がなければ NULL
FROM
products AS p — 左側(こちらが全て含まれる方)
LEFT JOIN
orders AS o ON p.product_id = o.product_id; — 右側(こちらが全て含まれない方)
“`
結果は LEFT JOIN
の例3と同じです。
product_name | order_id | order_date | customer_id |
---|---|---|---|
ノートPC A | 101 | 2023-10-26 | 1 |
スマートフォンB | 102 | 2023-10-26 | 2 |
マウス C | 103 | 2023-10-27 | 1 |
ノートPC A | 104 | 2023-10-28 | 3 |
スマートフォンB | 105 | 2023-10-28 | 1 |
マウス C | 106 | 2023-10-29 | 2 |
キーボード D | 107 | 2023-10-30 | 4 |
マウス C | 108 | 2023-10-30 | 4 |
モニター E | NULL | NULL | NULL |
例2 (SQLiteでのRIGHT JOIN相当): 注文に関連付けられていない商品をリストアップする
これは、先ほどの LEFT JOIN
の例2で「注文していない顧客」を抽出したのと同様に、「右側のテーブル(orders)に一致する行がなかった」商品を抽出します。
sql
SELECT
p.name AS product_name
FROM
products AS p -- 左側(全て含まれる方)
LEFT JOIN
orders AS o ON p.product_id = o.product_id
WHERE
o.order_id IS NULL; -- orders テーブルの主キーが NULL である行を絞り込む
結果:
product_name |
---|
モニター E |
解説:
products
テーブル(左)の全商品と orders
テーブル(右)を結合し、orders
側の情報が NULL
になる行だけを絞り込んでいます。これにより、「注文には一度も登場しない商品」を抽出できます。
SQLiteでは RIGHT JOIN
を使う機会はありませんが、他のデータベースシステム(PostgreSQL, MySQL, SQL Serverなど)では普通に使われるため、概念として理解しておくことは重要です。そして、「LEFT JOINでテーブル順序を逆にすればRIGHT JOINと同じ」という書き換えのテクニックは、様々なデータベースシステムで役立ちます。
4.4. FULL OUTER JOIN (完全外部結合)
FULL OUTER JOIN
は、LEFT JOIN
と RIGHT JOIN
を合わせたような結合です。どちらかのテーブルで結合条件に一致する行があれば、その行は結果に含まれます。
- 左側のテーブルにのみ存在する行は、右側のテーブルの列が
NULL
になって含まれます (LEFT JOIN
のような挙動)。 - 右側のテーブルにのみ存在する行は、左側のテーブルの列が
NULL
になって含まれます (RIGHT JOIN
のような挙動)。 - 両方のテーブルに一致する行は、通常の
INNER JOIN
のように結合されて含まれます。
イメージとしては、二つの円の合計(和集合)です。
構文:
sql
SELECT ...
FROM table1
FULL OUTER JOIN table2 ON table1.common_column = table2.common_column;
SQLiteにおけるFULL OUTER JOINの注意点:
RIGHT JOIN
と同様に、SQLiteは標準では FULL OUTER JOIN
を直接サポートしていません。
しかし、FULL OUTER JOIN
と同じ結果は、LEFT JOIN
と UNION ALL
を組み合わせて実現できます。
具体的には:
1. table1 LEFT JOIN table2 ON ...
の結果を取得します。これにより、table1
の全ての行(対応する table2
の行があれば結合、なければ table2
側は NULL
)が得られます。
2. table2 LEFT JOIN table1 ON ...
の結果を取得します。これにより、table2
の全ての行(対応する table1
の行があれば結合、なければ table1
側は NULL
)が得られます。
3. 上記の2つの結果を UNION ALL
で結合します。
ただし、このままだと INNER JOIN
に相当する部分(両方のテーブルに一致する行)が重複してしまいます。そこで、2つ目の table2 LEFT JOIN table1
の結果からは、table1
側が NULL
になる行(つまり、table1
には存在しない table2
の行)だけを抽出する必要があります。
table1 FULL OUTER JOIN table2 ON A = B
は、以下のSQLで再現できます。
sql
SELECT column_list -- 結合後のテーブルで取得したい列
FROM table1 LEFT JOIN table2 ON join_condition -- table1の全て + 一致するtable2
UNION ALL
SELECT column_list -- 同上。ただしtable1の列はNULLとなる行が含まれる
FROM table2 LEFT JOIN table1 ON join_condition -- table2の全て + 一致するtable1
WHERE table1.common_column IS NULL; -- table1に一致する行がなかったtable2の行のみ抽出
column_list
は、両方の SELECT
文で全く同じ列リスト(順番とデータ型も)にする必要があります。NULLになる可能性がある列も指定します。
例1 (SQLiteでのFULL OUTER JOIN相当): 全ての顧客と全ての注文、全ての商品の関連を取得する
この例では、customers
テーブルと orders
テーブル、そして products
テーブルを全て関連付けて、それぞれのテーブルに存在しないデータも漏らさず表示したいとします。
ただし、3つまとめて FULL OUTER JOIN
を再現するのは非常に複雑になるため、まずは最もシンプルな「2つのテーブルの FULL OUTER JOIN
」として、「全ての顧客」と「全ての注文」を関連付ける例を見てみましょう。注文されていない顧客も、顧客に関連付けられていない注文(もしあれば)も表示します。
“`sql
— SQLiteでの FULL OUTER JOIN customers と orders の再現
SELECT
c.name AS customer_name, — customersの列
o.order_id, — ordersの列 (NULLの可能性あり)
o.order_date, — ordersの列 (NULLの可能性あり)
o.product_id, — ordersの列 (NULLの可能性あり)
‘Left Join Part’ AS source — どのクエリ由来か確認用
FROM
customers AS c
LEFT JOIN
orders AS o ON c.customer_id = o.customer_id
UNION ALL
SELECT
c.name AS customer_name, — customersの列 (NULLの可能性あり)
o.order_id, — ordersの列
o.order_date, — ordersの列
o.product_id, — ordersの列
‘Right Join Part’ AS source — どのクエリ由来か確認用
FROM
orders AS o — FROMとLEFT JOINを入れ替える
LEFT JOIN
customers AS c ON o.customer_id = c.customer_id
WHERE
c.customer_id IS NULL; — customersに一致する行がなかったordersの行のみ抽出
— 注意: ordersテーブルのcustomer_idがNULLの行もここでは表示される (結合条件を満たさないため)
— もしorders.customer_idがNULLの行を除外したい場合は、WHERE o.customer_id IS NOT NULL を追加する
“`
結果:
customer_name | order_id | order_date | product_id | source |
---|---|---|---|---|
山田 太郎 | 101 | 2023-10-26 | P001 | Left Join Part |
山田 太郎 | 103 | 2023-10-27 | P003 | Left Join Part |
山田 太郎 | 105 | 2023-10-28 | P002 | Left Join Part |
佐藤 花子 | 102 | 2023-10-26 | P002 | Left Join Part |
佐藤 花子 | 106 | 2023-10-29 | P003 | Left Join Part |
田中 一郎 | 104 | 2023-10-28 | P001 | Left Join Part |
山本 次郎 | 107 | 2023-10-30 | P004 | Left Join Part |
山本 次郎 | 108 | 2023-10-30 | P003 | Left Join Part |
中村 綾子 | NULL | NULL | NULL | Left Join Part |
NULL | NULL | 2023-11-01 | NULL | Right Join Part |
NULL | 109 | 2023-10-31 | P001 | Right Join Part |
解説:
最初の LEFT JOIN
は、全ての顧客と、彼らがした注文を結合します。注文がない「中村 綾子」さんの行が含まれ、注文側の列は NULL
になります。
UNION ALL
の後の2つ目の LEFT JOIN
(実質 orders
を左とした RIGHT JOIN
) は、全ての注文と、それらをした顧客を結合します。ここで WHERE c.customer_id IS NULL
を追加することで、最初のクエリの結果に含まれていない行だけ(つまり、customers
テーブルに一致する行がなかった orders
の行)を抽出しています。
もしデータ挿入時に存在しない顧客IDや商品IDを持つ注文(例:order_id
109, 110)や、customer_idがNULLの注文(例:order_id
111)を挿入していれば、それらの注文は customers
テーブルとの結合条件 c.customer_id = o.customer_id
に一致しないため、2つ目のクエリで c.customer_id IS NULL
の条件を満たし、結果に customer_name
が NULL
の行として表示されます。今回はこれらの無効な注文は挿入していないため、2つ目の SELECT
の結果は空となり、「中村 綾子」さん以外の行は最初の SELECT
の結果のみが表示されます。
UNION
と UNION ALL
:
UNION
は結合する結果セットから重複する行を削除しますが、UNION ALL
は重複を削除しません。ここでは FULL OUTER JOIN
の再現として、LEFT JOIN
の結果に RIGHT JOIN
の結果(ただし INNER JOIN
部分を除く)を加えているため、重複を削除する必要がなく、UNION ALL
を使用するのが適切です。UNION ALL
の方が一般的に処理が高速です。
FULL OUTER JOIN
は、一方のテーブルにしか存在しないデータを漏らさずに表示したい場合に便利ですが、SQLiteでは上記のような少し複雑なクエリで再現する必要があるため、他のデータベースシステムほど手軽には使えません。
4.5. CROSS JOIN (デカルト積)
CROSS JOIN
は、結合条件を指定せずに2つのテーブルを結合します。その結果は、最初のテーブルの全ての行と、2番目のテーブルの全ての行の全ての組み合わせになります。これを「デカルト積 (Cartesian Product)」と呼びます。
構文:
sql
SELECT ...
FROM table1
CROSS JOIN table2;
または、古い構文で WHERE
句なしでテーブルをカンマ区切りで指定する形式も CROSS JOIN
と同じ結果になります。
sql
SELECT ...
FROM table1, table2; -- 結合条件がない場合、これはCROSS JOINと同じ
例1: customers テーブルと products テーブルを CROSS JOIN する
sql
SELECT
c.name AS customer_name,
p.name AS product_name
FROM
customers AS c
CROSS JOIN
products AS p;
結果:
customer_name | product_name |
---|---|
山田 太郎 | ノートPC A |
山田 太郎 | スマートフォンB |
山田 太郎 | マウス C |
山田 太郎 | キーボード D |
山田 太郎 | モニター E |
佐藤 花子 | ノートPC A |
佐藤 花子 | スマートフォンB |
佐藤 花子 | マウス C |
… | … |
中村 綾子 | キーボード D |
中村 綾子 | モニター E |
(合計 5 (顧客数) × 5 (商品数) = 25 行) |
解説:
customers
テーブルには5行、products
テーブルには5行あります。CROSS JOIN
の結果は 5 * 5 = 25 行になります。それぞれの顧客と、それぞれの商品の全ての組み合わせが生成されます。
CROSS JOIN
は、意図的に使うことは稀です。例えば、全ての顧客に対して全ての商品の価格をリストアップしたい、といった特殊なケースで使うことがあります。しかし、ほとんどの場合、これは INNER JOIN
などで結合条件を書き忘れた場合に意図せず発生し、膨大な結果セットを生み出してしまう「バグ」の原因となります。そのため、結合条件は必ず ON
句で明示的に指定する習慣をつけましょう。
4.6. SELF JOIN (自己結合)
SELF JOIN
は、JOIN
の特定のタイプというよりは、テーブルをそれ自身と結合するというパターンです。同一のテーブルを、異なる別名を使って2回 FROM
句や JOIN
句に登場させ、その別名を使って結合条件を指定します。
これは、テーブル内の行同士が関連を持っている場合に便利です。例えば:
* 従業員テーブルで、各従業員とその上司(上司も同じ従業員テーブルにいる)の関係を取得する。
* 商品テーブルで、ある商品の関連商品を、同じテーブル内の別の行として取得する。
* 地理情報テーブルで、ある都市から特定の距離内にある他の都市を取得する。
構文:
sql
SELECT ...
FROM table_name AS alias1
JOIN_TYPE table_name AS alias2 ON alias1.column = alias2.column; -- 同じテーブルだが別名で区別
例1: 従業員とその上司の名前を取得する
この例のために、新しい employees
テーブルを作成しましょう。
“`sql
— employees テーブル作成
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
manager_id INTEGER, — 上司の emp_id を参照 (NULL可)
FOREIGN KEY (manager_id) REFERENCES employees(emp_id) — 自分自身を参照する外部キー
);
— employees データ挿入
INSERT INTO employees (emp_id, name, manager_id) VALUES
(1, ‘社長’, NULL), — 社長は上司がいない
(2, ‘部長A’, 1), — 部長Aの上司は社長
(3, ‘部長B’, 1), — 部長Bの上司も社長
(4, ‘課長C’, 2), — 課長Cの上司は部長A
(5, ‘課長D’, 2), — 課長Dの上司も部長A
(6, ‘社員E’, 4), — 社員Eの上司は課長C
(7, ‘社員F’, 4); — 社員Fの上司も課長C
— 従業員テーブルの中身を確認
SELECT * FROM employees;
“`
employees
テーブルには、各従業員の情報と、その上司の emp_id
が格納されています。社員Eの上司の名前を知るには、社員Eの manager_id
(4) を使って、同じテーブルの中で emp_id
が4である行(課長C)を探す必要があります。
ここで SELF JOIN
を使います。employees
テーブルを「部下側」と「上司側」という役割で2回使用します。
sql
SELECT
e.name AS employee_name, -- 部下の名前
m.name AS manager_name -- 上司の名前
FROM
employees AS e -- e: employee (部下側)
INNER JOIN
employees AS m ON e.manager_id = m.emp_id; -- m: manager (上司側), 部下のmanager_idと上司のemp_idを結合
結果:
employee_name | manager_name |
---|---|
部長A | 社長 |
部長B | 社長 |
課長C | 部長A |
課長D | 部長A |
社員E | 課長C |
社員F | 課長C |
解説:
employees
テーブルを e
(部下) と m
(上司) という別名で参照しています。結合条件 e.manager_id = m.emp_id
は、「部下側の行の manager_id
が、上司側の行の emp_id
と一致する」という条件です。これにより、各従業員とその直属の上司の名前を組み合わせた結果が得られます。
この例では INNER JOIN
を使ったため、上司がいない「社長」は結果に含まれません。もし社長も含めて表示し、上司がいない場合は「上司なし」などと表示したい場合は、LEFT JOIN
を使用します。
例2: 全ての従業員と、もし上司がいればその名前を取得する (LEFT JOINを使用)
sql
SELECT
e.name AS employee_name, -- 従業員の名前
m.name AS manager_name -- 上司の名前 (いなければ NULL)
FROM
employees AS e -- e: employee (左側、全て含める)
LEFT JOIN
employees AS m ON e.manager_id = m.emp_id; -- m: manager (右側)
結果:
employee_name | manager_name |
---|---|
社長 | NULL |
部長A | 社長 |
部長B | 社長 |
課長C | 部長A |
課長D | 部長A |
社員E | 課長C |
社員F | 課長C |
解説:
employees
テーブルを左側 (e
)、同じテーブルを右側 (m
) として LEFT JOIN
を使っています。これにより、左側のテーブルである e
(全ての従業員) が結果に含まれます。e.manager_id
が NULL
の行 (社長) の場合、右側の m
テーブルで一致する m.emp_id
が存在しないため、m.name
は NULL
になります。上司がいる従業員については、通常通り結合されます。
5. JOINとWHERE句
JOIN
はテーブルを結合しますが、取得した結合結果からさらに特定の条件で絞り込みたい場合は、WHERE
句を使用します。WHERE
句は FROM
句や JOIN
句の後に記述します。
例1: 東京都の顧客が注文した商品名をリストアップする
customers
, orders
, products
を結合し、customers
テーブルの address
列で絞り込みます。
sql
SELECT
c.name AS customer_name,
p.name AS product_name,
o.order_date
FROM
customers AS c
INNER JOIN
orders AS o ON c.customer_id = o.customer_id
INNER JOIN
products AS p ON o.product_id = p.product_id
WHERE
c.address LIKE '東京都%'; -- 東京都で始まる住所で絞り込み
結果:
customer_name | product_name | order_date |
---|---|---|
山田 太郎 | ノートPC A | 2023-10-26 |
山田 太郎 | マウス C | 2023-10-27 |
山田 太郎 | スマートフォンB | 2023-10-28 |
解説:
3つのテーブルを結合した結果から、さらに WHERE c.address LIKE '東京都%'
という条件を満たす行だけがフィルタリングされて表示されます。
ON句とWHERE句の違い (特に外部結合の場合):
INNER JOIN
の場合、結合条件 (ON
) で指定する条件と WHERE
句で指定する条件は、基本的にどちらに書いても結果は同じになります(ただし、パフォーマンスに影響する場合がある)。
しかし、LEFT JOIN
や RIGHT JOIN
(または SQLiteでのその代替) の場合は、ON
句の条件と WHERE
句の条件は結果に大きな違いをもたらすことがあります。
- ON句の条件: JOINを行う前に適用される条件です。この条件を満たさない行は、
INNER JOIN
では排除されますが、LEFT JOIN
やRIGHT JOIN
では、片方のテーブルの行は残され、もう一方はNULL
となります。 - WHERE句の条件: JOINを行った後に、結合結果全体に対して適用される条件です。
INNER JOIN
、LEFT JOIN
、RIGHT JOIN
いずれの場合でも、WHERE
句の条件を満たさない行は結果から排除されます。
例2: 全ての顧客と、2023年10月28日以降の注文情報を取得する
これを ON
句で書いてみます。
sql
SELECT
c.name AS customer_name,
o.order_id,
o.order_date,
o.product_id
FROM
customers AS c
LEFT JOIN
orders AS o ON c.customer_id = o.customer_id AND o.order_date >= '2023-10-28'; -- JOIN条件の一部として日付条件
結果:
customer_name | order_id | order_date | product_id |
---|---|---|---|
山田 太郎 | 105 | 2023-10-28 | P002 |
佐藤 花子 | NULL | NULL | NULL |
田中 一郎 | 104 | 2023-10-28 | P001 |
山本 次郎 | 107 | 2023-10-30 | P004 |
山本 次郎 | 108 | 2023-10-30 | P003 |
中村 綾子 | NULL | NULL | NULL |
解説:
ON
句に日付条件を追加しました。この条件は「結合する前に」適用されます。customers
テーブルの各行について、orders
テーブルの中から customer_id
が一致し、かつ order_date >= '2023-10-28'
という条件を満たす行を探します。条件を満たす注文があれば結合され、なければ orders
側の列は NULL
になります。
山田太郎さんの注文101, 103 (10月26日, 10月27日) は日付条件を満たさないため、結合されず、結果に出てきません。佐藤花子さんの注文102, 106 も同様です。注文のない中村綾子さんの行も、日付条件を満たす注文がないため、注文側の列は NULL
になります。
次に、同じ条件を WHERE
句で書いてみます。
sql
SELECT
c.name AS customer_name,
o.order_id,
o.order_date,
o.product_id
FROM
customers AS c
LEFT JOIN
orders AS o ON c.customer_id = o.customer_id -- 通常のJOIN条件
WHERE
o.order_date >= '2023-10-28' OR o.order_id IS NULL; -- WHERE句で日付条件 OR 注文がない行
結果:
customer_name | order_id | order_date | product_id |
---|---|---|---|
山田 太郎 | 105 | 2023-10-28 | P002 |
田中 一郎 | 104 | 2023-10-28 | P001 |
山本 次郎 | 107 | 2023-10-30 | P004 |
山本 次郎 | 108 | 2023-10-30 | P003 |
中村 綾子 | NULL | NULL | NULL |
解説:
まず LEFT JOIN
が実行され、全ての顧客と、彼らの全ての注文が結合されます(注文がない顧客は NULL
)。その後、WHERE
句が適用されます。WHERE o.order_date >= '2023-10-28'
という条件だけだと、order_date
が NULL
の行(つまり注文がなかった顧客)は結果から排除されてしまいます。LEFT JOIN
の意味(左側のテーブルを全て残す)を維持するためには、WHERE
句の条件に OR o.order_id IS NULL
を追加する必要があります。これにより、「日付が2023-10-28以降の注文がある行」または「そもそも注文がない行(注文側の列がNULLの行)」が結果に含まれます。
このように、特に外部結合 (LEFT JOIN
, RIGHT JOIN
, FULL OUTER JOIN
) では、結合条件の一部を ON
句に書くか、結合後の結果を WHERE
句で絞り込むかで、結果が変わってきます。ON
句は「どのようにテーブルを関連付けるか」、WHERE
句は「結合された結果からどの行を取り出すか」という目的の違いを理解することが重要です。
一般的に、結合するテーブル間の関連性を定義する条件は ON
句に書き、結合された結果に対する追加のフィルタリング条件は WHERE
句に書くのが良いプラクティスとされています。ただし、特定の集計(例: 特定の期間の注文合計 ただし注文がない顧客も表示)など、複雑なケースでは ON
句でのフィルタリングが有効な場合もあります。
6. より実践的なJOINの利用例とヒント
6.1. 複数のテーブルを結合する
既に例3で示しましたが、JOIN句は複数つなげて書くことができます。これにより、3つ以上のテーブルを一度に結合できます。
sql
SELECT ...
FROM table1
JOIN table2 ON table1.col1 = table2.col2
JOIN table3 ON table2.col3 = table3.col4 -- 前のJOIN結果と次のテーブルを結合
JOIN table4 ON table3.col5 = table4.col6
...;
重要なのは、それぞれのJOIN句が、その時点での結合結果と次のテーブルをどのように関連付けるかを定義している点です。通常、関連するテーブルを順番に結合していきます(例: 顧客 -> 注文 -> 商品)。
6.2. 集計関数とJOIN
COUNT()
, SUM()
, AVG()
, MAX()
, MIN()
といった集計関数は、JOINの結果に対しても適用できます。GROUP BY
句と組み合わせることで、「顧客ごとの注文数」「商品ごとの売上合計」などを計算できます。
例: 顧客ごとの注文数を取得する (注文がない顧客も表示)
sql
SELECT
c.name AS customer_name,
COUNT(o.order_id) AS order_count -- 注文がない顧客の order_id は NULL なので COUNT() は 0 になる
FROM
customers AS c
LEFT JOIN
orders AS o ON c.customer_id = o.customer_id
GROUP BY
c.customer_id, c.name -- GROUP BY には SELECT で指定した非集約列を含める
ORDER BY
c.customer_id;
結果:
customer_name | order_count |
---|---|
山田 太郎 | 3 |
佐藤 花子 | 2 |
田中 一郎 | 1 |
山本 次郎 | 2 |
中村 綾子 | 0 |
解説:
LEFT JOIN
を使うことで、注文がない「中村 綾子」さんも結果に含まれます。COUNT(o.order_id)
は、NULLでない order_id
の数を数えるため、注文がない顧客の行では0になります。GROUP BY
句で顧客ごとにグループ化することで、それぞれの顧客の注文数を集計しています。
6.3. パフォーマンスの考慮 (インデックス)
大規模なデータベースでは、JOINのパフォーマンスが重要になります。JOIN条件として使用する列(通常は主キーと外部キー)にインデックスが作成されていると、データベースシステムは高速に一致する行を探すことができます。
SQLiteでは、主キーには自動的にインデックスが作成されます。外部キーにも手動でインデックスを作成することが推奨されます。
例えば、orders
テーブルの customer_id
列にインデックスを作成するには、以下のSQLを実行します。
sql
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_orders_product_id ON orders (product_id);
これにより、orders
テーブルを customer_id
や product_id
でJOINする際の検索効率が向上します。
7. まとめ:JOINのタイプと使い分け
これまで学んだJOINのタイプをまとめます。
JOINタイプ | 含まれる行 | 使いどころ | SQLiteのサポート |
---|---|---|---|
INNER JOIN (JOIN) | 両方のテーブルで結合条件に一致する行だけ。 | 関連する情報が両方のテーブルに存在する行だけを見たい場合。最も一般的。 | Yes |
LEFT JOIN | 左側のテーブルの全ての行と、右側のテーブルで一致する行。右側に一致がなければ右側はNULL。 | 左側のテーブルの全ての要素をリストし、関連する情報があれば表示したい場合。「左側には存在するが右側には存在しないデータ」を抽出したい場合。 | Yes |
RIGHT JOIN | 右側のテーブルの全ての行と、左側のテーブルで一致する行。左側に一致がなければ左側はNULL。 | 右側のテーブルの全ての要素をリストし、関連する情報があれば表示したい場合。「右側には存在するが左側には存在しないデータ」を抽出したい場合。 | No (LEFT JOINで代替) |
FULL OUTER JOIN | どちらかのテーブルで結合条件に一致する行があれば含む。どちらか一方にしかない場合は他方はNULL。両方に一致すれば結合。 | 両方のテーブルの全ての要素をリストし、関連があれば表示、なければNULLとして表示したい場合。「どちらか一方にしか存在しないデータ」や「両方に存在するデータ」をまとめて見たい場合。 | No (UNION ALLで代替) |
CROSS JOIN | 結合条件なし。左側の全行と右側の全行の全ての組み合わせ (デカルト積)。 | 意図的に使うことは稀。結合条件を忘れた場合に発生しやすい。特定の組み合わせ計算などで使うことがある。 | Yes |
SELF JOIN | 特定のJOINタイプではなく、同じテーブルを複数回参照し、自身と結合するパターン。INNER JOINやLEFT JOINを使って実現する。 | テーブル内の行同士に関連がある場合(例: 組織構造、関連商品など)。 | Yes |
8. 練習問題
学んだことを実践してみましょう。以下の問いに答えるSQLクエリを書いて、SQLiteで実行してみてください。
- 全ての商品の名前と価格、そしてその商品が注文された回数を取得してください。注文されたことがない商品も結果に含めてください。(ヒント:
products
を左側にしたLEFT JOIN
とCOUNT
,GROUP BY
を使います) - 2023年10月27日以降に注文を行った顧客の名前と注文日をリストアップしてください。(ヒント:
INNER JOIN
とWHERE
を使います) - 注文はしたが、
customer_id
が登録されていない顧客ID(サンプルデータにはありませんが、もしあった場合を想定)を持つ注文のリストを取得してください。(ヒント:LEFT JOIN
を使い、WHERE
で左側がNULLになる行を絞り込みます) orders
テーブルをorder_date
で自身と結合し、同日に2件以上の注文が入っている日と注文数を特定してください。(ヒント:SELF JOIN
(INNER JOINを使用) とGROUP BY
,HAVING
を使います)
練習問題解答 (参考):
sql
SELECT
p.name AS product_name,
p.price,
COUNT(o.order_id) AS order_count
FROM
products AS p
LEFT JOIN
orders AS o ON p.product_id = o.product_id
GROUP BY
p.product_id, p.name, p.price
ORDER BY
p.product_id;sql
SELECT DISTINCT -- 重複を避ける
c.name AS customer_name,
o.order_date
FROM
customers AS c
INNER JOIN
orders AS o ON c.customer_id = o.customer_id
WHERE
o.order_date >= '2023-10-27'
ORDER BY
o.order_date;sql
SELECT
o.order_id,
o.order_date,
o.customer_id,
o.product_id,
o.quantity
FROM
orders AS o
LEFT JOIN
customers AS c ON o.customer_id = c.customer_id
WHERE
c.customer_id IS NULL AND o.customer_id IS NOT NULL; -- customersに一致する行がなく、かつordersのcustomer_id自体はNULLでない行
-- もしorders.customer_idがNULLの行も含めるなら AND o.customer_id IS NOT NULL は不要sql
SELECT
o1.order_date,
COUNT(o1.order_id) AS daily_order_count
FROM
orders AS o1
INNER JOIN
orders AS o2 ON o1.order_id <> o2.order_id AND o1.order_date = o2.order_date
GROUP BY
o1.order_date
HAVING
COUNT(o1.order_id) >= 2;
-- 別解(よりシンプル):日付でグループ化してHAVINGで数えるだけでも良い
-- SELECT order_date, COUNT(order_id) AS daily_order_count
-- FROM orders
-- GROUP BY order_date
-- HAVING COUNT(order_id) >= 2;
9. まとめと次のステップ
この記事では、SQLにおけるJOINの基本的な概念と主要な種類について、SQLiteを使った具体的な例を通して詳しく解説しました。
- なぜリレーショナルデータベースではテーブル分割が必要で、その結果としてJOINが不可欠になるのかを理解しました。
INNER JOIN
で両方に存在するデータを取得する方法を学びました。LEFT JOIN
で左側のテーブルを基準にデータを取得し、存在しない場合はNULLになることを学びました。- SQLiteでは直接使えない
RIGHT JOIN
やFULL OUTER JOIN
を、LEFT JOIN
やUNION ALL
で代替する方法を学びました。 - 意図しない結合結果を生みやすい
CROSS JOIN
について触れました。 - 同じテーブルを結合する
SELF JOIN
のパターンを学びました。 WHERE
句を使った結合結果の絞り込みや、ON
句との違いを確認しました。- 集計関数やインデックスといった、より実践的なヒントを得ました。
JOINは、SQLで複数のテーブルを扱う上で最も重要かつ頻繁に使用される機能です。最初は複雑に感じるかもしれませんが、それぞれのJOINタイプが「どのテーブルのどの行を結果に含めるか」というルールに基づいていることを理解すれば、使い分けることができるようになります。
この記事で紹介したサンプルデータを使って、様々なクエリを試してみてください。列の選択を変えたり、WHERE
句の条件を変えたり、3つ以上のテーブルを結合してみたりすることで、理解が深まるはずです。
次のステップとしては、
* 他のSQLコマンド (UPDATE
, DELETE
, INSERT
) を学ぶ。
* サブクエリ(副問い合わせ)や共通テーブル式 (CTE) など、より複雑なデータ操作方法を学ぶ。
* ウィンドウ関数など、高度な集計方法を学ぶ。
* データベース設計(正規化の深いレベル、インデックス戦略など)について学ぶ。
などがあります。
この記事が、あなたのSQL学習の強固な基礎となり、データベースを自在に操るための一歩となることを願っています。頑張ってください!