SQL超基本:SQLiteで学ぶJOINの使い方

はい、承知いたしました。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_idorders テーブルの order_idproducts テーブルの product_id が主キーです。主キーは重複せず、NULLであってはいけません。
  • 外部キー (Foreign Key: FK): 別のテーブルの主キーを参照する列。上記例では、orders テーブルの customer_idcustomers テーブルの customer_id を参照しており、orders テーブルの product_idproducts テーブルの product_id を参照しています。外部キーは、テーブル間の関連性(リレーションシップ)を定義し、データの整合性を保つ役割を果たします。

1.4. JOINが必要になる瞬間

データがこのように分割されていると、「山田太郎さんが購入した商品のリストが見たい」「各注文がどの顧客によるもので、どんな商品を含んでいるかを知りたい」といった要求が出てきます。

これらの情報は、customersordersproducts という複数のテーブルにまたがって存在しています。単一の 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_idorders テーブルの 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_idproducts テーブルの 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

解説:
このクエリは、customersorderscustomer_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_idNULL になっている行は、注文情報が見つからなかった(つまり、注文をしていない)顧客です。

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 JOINRIGHT 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 JOINUNION 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_nameNULL の行として表示されます。今回はこれらの無効な注文は挿入していないため、2つ目の SELECT の結果は空となり、「中村 綾子」さん以外の行は最初の SELECT の結果のみが表示されます。

UNIONUNION 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_idNULL の行 (社長) の場合、右側の m テーブルで一致する m.emp_id が存在しないため、m.nameNULL になります。上司がいる従業員については、通常通り結合されます。

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 JOINRIGHT JOIN (または SQLiteでのその代替) の場合は、ON 句の条件と WHERE 句の条件は結果に大きな違いをもたらすことがあります。

  • ON句の条件: JOINを行う前に適用される条件です。この条件を満たさない行は、INNER JOIN では排除されますが、LEFT JOINRIGHT JOIN では、片方のテーブルの行は残され、もう一方は NULL となります。
  • WHERE句の条件: JOINを行った後に、結合結果全体に対して適用される条件です。INNER JOINLEFT JOINRIGHT 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_dateNULL の行(つまり注文がなかった顧客)は結果から排除されてしまいます。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_idproduct_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で実行してみてください。

  1. 全ての商品の名前と価格、そしてその商品が注文された回数を取得してください。注文されたことがない商品も結果に含めてください。(ヒント:products を左側にした LEFT JOINCOUNT, GROUP BY を使います)
  2. 2023年10月27日以降に注文を行った顧客の名前と注文日をリストアップしてください。(ヒント:INNER JOINWHERE を使います)
  3. 注文はしたが、customer_id が登録されていない顧客ID(サンプルデータにはありませんが、もしあった場合を想定)を持つ注文のリストを取得してください。(ヒント:LEFT JOIN を使い、WHERE で左側がNULLになる行を絞り込みます)
  4. orders テーブルをorder_dateで自身と結合し、同日に2件以上の注文が入っている日と注文数を特定してください。(ヒント:SELF JOIN (INNER JOINを使用) と GROUP BY, HAVING を使います)

練習問題解答 (参考):

  1. 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;
  2. 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;
  3. 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 は不要
  4. 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 JOINFULL OUTER JOIN を、LEFT JOINUNION ALL で代替する方法を学びました。
  • 意図しない結合結果を生みやすい CROSS JOIN について触れました。
  • 同じテーブルを結合する SELF JOIN のパターンを学びました。
  • WHERE 句を使った結合結果の絞り込みや、ON 句との違いを確認しました。
  • 集計関数やインデックスといった、より実践的なヒントを得ました。

JOINは、SQLで複数のテーブルを扱う上で最も重要かつ頻繁に使用される機能です。最初は複雑に感じるかもしれませんが、それぞれのJOINタイプが「どのテーブルのどの行を結果に含めるか」というルールに基づいていることを理解すれば、使い分けることができるようになります。

この記事で紹介したサンプルデータを使って、様々なクエリを試してみてください。列の選択を変えたり、WHERE 句の条件を変えたり、3つ以上のテーブルを結合してみたりすることで、理解が深まるはずです。

次のステップとしては、
* 他のSQLコマンド (UPDATE, DELETE, INSERT) を学ぶ。
* サブクエリ(副問い合わせ)や共通テーブル式 (CTE) など、より複雑なデータ操作方法を学ぶ。
* ウィンドウ関数など、高度な集計方法を学ぶ。
* データベース設計(正規化の深いレベル、インデックス戦略など)について学ぶ。

などがあります。

この記事が、あなたのSQL学習の強固な基礎となり、データベースを自在に操るための一歩となることを願っています。頑張ってください!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール