PostgreSQL UNION で複数SELECT文の結果を一つにまとめる方法

はい、承知いたしました。PostgreSQLのUNION句を使って複数SELECT文の結果を一つにまとめる方法について、詳細な記事を作成します。約5000語で記述し、直接ここに表示します。


PostgreSQL UNION句詳解:複数SELECT文の結果を一つにまとめる方法

はじめに:なぜデータを一つにまとめる必要があるのか?

データベースを操作していると、複数のテーブルや、同じテーブルでも異なる条件で取得したデータをまとめて表示したり、分析したりしたい場面が頻繁に発生します。例えば、以下のようなケースが考えられます。

  1. 異なるテーブルに分散した類似データの集約:

    • 顧客情報が「個人顧客テーブル」と「法人顧客テーブル」に分かれているが、まとめて顧客リストを作成したい。
    • 製品情報が「在庫品テーブル」と「受注生産品テーブル」に分かれているが、製品カタログとして統合して表示したい。
    • 過去の注文履歴が年ごとに分割されたテーブル(例: orders_2022, orders_2023)に格納されており、全期間の注文履歴を見たい。
  2. 同じテーブルから異なる条件で取得したデータの結合:

    • 「アクティブなユーザー」と「過去にアクティブだったユーザー」を一つのリストで表示したい。
    • 「直近1週間の売上データ」と「直近1ヶ月間の売上データ」を(重複を排除または保持して)結合し、特定の分析に利用したい。
  3. レポート作成やデータ分析の前処理:

    • 複数のソース(異なる部門のデータベース、外部データなど)から収集したデータを一時的に結合し、統合されたビューで分析したい。

このようなデータ結合のニーズに応えるために、SQLには「集合演算子」と呼ばれる機能が用意されています。集合演算子は、複数のSELECT文の結果セット(テーブル形式のデータ)を、あたかも数学的な集合のように扱い、和集合、差集合、積集合などを求めることができます。

SQLの標準的な集合演算子には以下の4つがあります。

  • UNION: 複数の結果セットの和集合を求めます。重複する行は一つにまとめられます。
  • UNION ALL: 複数の結果セットの和集合を求めます。重複する行もすべて含みます。
  • INTERSECT: 複数の結果セットの積集合(共通する行)を求めます。重複する行は一つにまとめられます。
  • EXCEPT (MINUSとして実装されているデータベースシステムもあります): 最初の結果セットから、2番目以降の結果セットに含まれる行を除外した差集合を求めます。重複する行は一つにまとめられます。

この記事では、これらの中でも最も一般的で広く利用されるUNIONUNION ALLに焦点を当て、PostgreSQLにおけるその使い方、注意点、応用例について詳細に解説します。PostgreSQLはこれらの標準的な集合演算子を完全にサポートしており、強力なデータ操作を可能にします。

対象読者は、リレーショナルデータベースの基本的な概念とSQLのSELECT文を理解している方を想定しています。この記事を読むことで、PostgreSQLで複数のデータセットを効率的かつ正確に結合し、目的に応じたデータ集合を作成できるようになることを目指します。

それでは、まずはUNION句の基本的な使い方から見ていきましょう。

1. UNION句の基本

UNION句は、2つ以上のSELECT文の結果セットを縦方向に連結し、一つの結果セットとして返す集合演算子です。最大の特長は、結果セットに含まれる重複行を自動的に排除することです。

1.1. 基本構文

UNION句の基本的な構文は非常にシンプルです。

“`sql
SELECT column1, column2, …
FROM table1
WHERE condition1

UNION [DISTINCT] — DISTINCTはデフォルトなので省略可

SELECT column1, column2, …
FROM table2
WHERE condition2;
“`

複数のSELECT文をUNIONキーワードで繋げるだけです。3つ以上のSELECT文を結合したい場合は、さらにUNIONキーワードを繋げていきます。

sql
SELECT ... FROM ... UNION SELECT ... FROM ... UNION SELECT ... FROM ...;

1.2. UNIONの挙動:重複行の排除

UNION句は、結合されるすべてのSELECT文の結果を合わせた後、全く同じ内容の行が存在する場合は、それらを一つにまとめます。これは、UNIONがデフォルトでDISTINCT(重複排除)の動作をするためです。

例えば、以下の2つのSELECT文があるとします。

SELECT 1:
| name | age |
| :—- | :– |
| Alice | 30 |
| Bob | 25 |
| Charlie | 35 |

SELECT 2:
| name | age |
| :—- | :– |
| Bob | 25 |
| David | 28 |
| Alice | 30 |

これらの結果セットをUNIONで結合すると、結果は以下のようになります。

SELECT 1 UNION SELECT 2:
| name | age |
| :—- | :– |
| Alice | 30 |
| Bob | 25 |
| Charlie | 35 |
| David | 28 |

Alice, 30Bob, 25は両方の結果セットに存在しましたが、UNIONによってそれぞれ一つの行にまとめられました。Charlie, 35David, 28は片方の結果セットにしか存在しないため、そのまま結果に含まれます。

1.3. UNIONを使う際の必須条件

UNION(およびUNION ALLINTERSECTEXCEPT)を使う上で、結合するすべてのSELECT文は以下の2つの重要な条件を満たす必要があります。

  1. 選択する列の数が同じであること:
    それぞれのSELECT文で指定する列(または式)の数は、完全に一致している必要があります。例えば、一方のSELECTが2列を選んでいるのに、もう一方が3列を選んでいる場合、UNIONすることはできません。

    sql
    -- これはエラーになる
    SELECT column1, column2 FROM table1
    UNION
    SELECT column1, column2, column3 FROM table2;

  2. 対応する列のデータ型が互換性を持っていること:
    SELECT文で選択される列は、同じ順番で、データ型が互換性を持っている必要があります。例えば、最初のSELECT文の1列目が整数型であれば、2番目のSELECT文の1列目も整数型や、PostgreSQLが暗黙的に整数型に変換可能な型(例: 数値型、文字列型の数字など)である必要があります。厳密な型一致ではなく、PostgreSQLが結果セットの型を決定できる互換性が必要です。

    “`sql
    — 型が互換性を持つ例 (INTEGERとBIGINT)
    SELECT id, name FROM table1 — idはINTEGER
    UNION
    SELECT user_id, username FROM table2; — user_idはBIGINT

    — 型が互換性を持つ例 (VARCHARとTEXT)
    SELECT code, description FROM products — codeはVARCHAR
    UNION
    SELECT item_code, details FROM archive_products; — item_codeはTEXT

    — これはエラーになる可能性が高い (INTEGERとDATEなど、互換性がない型)
    SELECT user_id, registration_date FROM users — user_idはINTEGER, registration_dateはDATE
    UNION
    SELECT product_id, price FROM products; — product_idはINTEGER, priceはNUMERIC
    “`
    データ型の互換性については、後ほど「注意点」のセクションで詳しく解説します。

1.4. 簡単なサンプルコード

実際のテーブルを使ってUNIONの基本を見てみましょう。簡単なテーブルを作成し、データを挿入します。

“`sql
— テーブル1の作成
CREATE TABLE students (
student_id SERIAL PRIMARY KEY,
name VARCHAR(100),
major VARCHAR(100)
);

— データ挿入
INSERT INTO students (name, major) VALUES
(‘Alice’, ‘Computer Science’),
(‘Bob’, ‘Physics’),
(‘Charlie’, ‘Mathematics’);

— テーブル2の作成
CREATE TABLE staff (
staff_id SERIAL PRIMARY KEY,
name VARCHAR(100),
department VARCHAR(100)
);

— データ挿入
INSERT INTO staff (name, department) VALUES
(‘Bob’, ‘Physics’), — Bobは学生かつスタッフという設定
(‘David’, ‘Administration’),
(‘Eve’, ‘Computer Science’);

— テーブルの内容確認
SELECT * FROM students;
SELECT * FROM staff;
“`

studentsテーブルとstaffテーブルから、name列だけを抽出してUNIONで結合してみましょう。

sql
SELECT name FROM students
UNION
SELECT name FROM staff;

このクエリの出力は以下のようになります。

name
Alice
Bob
Charlie
David
Eve

Bobstudentsテーブルとstaffテーブルの両方に存在しますが、UNIONによって重複が排除され、結果には一度だけ表示されています。

もし、nameと所属(学生の場合はmajor、スタッフの場合はdepartment)を一緒に表示したい場合は、それぞれのSELECT文で対応する列を選択し、データ型を合わせる必要があります。この例では、majordepartmentはどちらもVARCHAR型なので互換性があります。

sql
SELECT name, major FROM students
UNION
SELECT name, department FROM staff;

このクエリの出力は以下のようになります。

name major/department
Alice Computer Science
Bob Physics
Charlie Mathematics
David Administration
Eve Computer Science

結果セットの列名は、デフォルトでは最初のSELECT文の列名(この場合はnamemajor)が採用されます。後続のSELECT文の列名は無視されます。結果セットの列名を制御する方法については、「注意点」のセクションで詳しく解説します。

このように、UNIONは複数の異なるソースからデータを取得し、重複を排除して一つのリストを作成するのに便利です。

2. UNION ALL句

UNION ALL句もUNIONと同様に、2つ以上のSELECT文の結果セットを縦方向に連結します。しかし、UNIONと決定的に異なる点があります。それは、重複行を排除しないという点です。UNION ALLは、単純にすべての結果セットを結合し、取得した行をすべてそのまま返します。

2.1. UNION ALLの挙動:重複行を保持

UNION ALL句は、結合されるすべてのSELECT文の結果を単純に連結します。全く同じ内容の行であっても、元の結果セットに存在した数だけ結果に含まれます。

先ほどのSELECT 1SELECT 2の例を再度見てみましょう。

SELECT 1:
| name | age |
| :—- | :– |
| Alice | 30 |
| Bob | 25 |
| Charlie | 35 |

SELECT 2:
| name | age |
| :—- | :– |
| Bob | 25 |
| David | 28 |
| Alice | 30 |

これらの結果セットをUNION ALLで結合すると、結果は以下のようになります。

SELECT 1 UNION ALL SELECT 2:
| name | age |
| :—- | :– |
| Alice | 30 |
| Bob | 25 |
| Charlie | 35 |
| Bob | 25 |
| David | 28 |
| Alice | 30 |

Alice, 30が2回、Bob, 25が2回含まれています。これは、それぞれの元の結果セットに存在した回数だけ結果に含まれるためです。

2.2. UNION ALLを使う利点:パフォーマンス

UNION ALLの最大の利点は、そのパフォーマンスです。UNIONが重複排除のために結果セット全体に対してソートやハッシュ処理を行う必要があるのに対し、UNION ALLは単に結果を連結するだけなので、この追加処理が不要です。

したがって、重複行を排除する必要がない場合や、重複行が存在しないことが分かっている場合は、必ずUNION ALLを使用するべきです。UNIONは重複排除のコストがかかるため、データ量が多い場合にパフォーマンスが著しく低下する可能性があります。

2.3. 基本構文

UNION ALL句の構文は、UNION句とほぼ同じです。

“`sql
SELECT column1, column2, …
FROM table1
WHERE condition1

UNION ALL

SELECT column1, column2, …
FROM table2
WHERE condition2;
“`

2.4. 簡単なサンプルコード

先ほどのstudentsテーブルとstaffテーブルの例でUNION ALLを使ってみましょう。

sql
SELECT name FROM students
UNION ALL
SELECT name FROM staff;

このクエリの出力は以下のようになります。

name
Alice
Bob
Charlie
Bob
David
Eve

Bobが2回表示されていることがわかります。

nameと所属を一緒に表示する例でもUNION ALLを使ってみます。

sql
SELECT name, major FROM students
UNION ALL
SELECT name, department FROM staff;

このクエリの出力は以下のようになります。

name major/department
Alice Computer Science
Bob Physics
Charlie Mathematics
Bob Physics
David Administration
Eve Computer Science

2.5. UNIONとUNION ALLの使い分け

UNIONUNION ALLの使い分けは、以下の点を考慮して行います。

  • 重複行を排除する必要があるか?: 必要があればUNION。不要であればUNION ALL
  • パフォーマンスが重要か?: 重複排除の必要がない、または重複が少ないことが分かっている場合は、パフォーマンスの観点からUNION ALLを強く推奨。

ほとんどの場合、UNION ALLの方が高速に実行されます。したがって、特別な理由(重複排除が必要な場合)がない限り、UNION ALLを選択するのが良いプラクティスと言えます。

3. UNION句を使う上での注意点

UNIONUNION ALLを使う際には、いくつか気をつけるべき重要な点があります。これらを理解していないと、意図しない結果になったり、エラーが発生したり、パフォーマンス問題に直面したりする可能性があります。

3.1. 列の数とデータ型の互換性に関する詳細

前述の必須条件について、もう少し詳しく掘り下げます。

列の数:
これは非常に厳格なルールです。UNIONまたはUNION ALLで結合するすべてのSELECT文は、全く同じ数の列を選択する必要があります。そうでなければ構文エラーになります。

sql
-- エラー例: 列数が異なる
SELECT col1, col2 FROM tableA
UNION
SELECT col1, col2, col3 FROM tableB; -- 列数が違う!

データ型の互換性:
こちらは列数ほど厳格ではありませんが、重要です。対応する位置の列(1番目のSELECTの1列目と、2番目のSELECTの1列目など)は、PostgreSQLが互換性があると判断できるデータ型である必要があります。PostgreSQLは、結果セットの列の最終的なデータ型を、結合されるすべてのSELECT文の対応する列のデータ型から決定します。これは、型の昇格(Promote)ルールに従って行われます。例えば、INTEGERBIGINTUNIONした場合、結果の列はより広い範囲を扱えるBIGINT型になります。VARCHARTEXTUNIONした場合は、TEXT型になることが多いです。

PostgreSQLは多くの組み込み型変換を提供していますが、すべての型の組み合わせが互換性を持つわけではありません。例えば、INTEGERDATEVARCHARBOOLEANなどは通常、互換性がありません。

もし、デフォルトの型変換では互換性が確保できない場合や、明示的に結果セットの型を指定したい場合は、キャスト演算子 :: を使用してデータ型を変換する必要があります。

“`sql
— 例: NUMERIC型とINTEGER型を結合し、すべてNUMERIC型として扱う
SELECT col1::NUMERIC, col2 FROM tableA — col1はINTEGER
UNION ALL
SELECT col1, col2 FROM tableB; — col1はNUMERIC

— 例: 日付を文字列として結合する(あまり一般的ではないが、型互換性の例として)
SELECT name, registration_date::VARCHAR FROM users
UNION ALL
SELECT company_name, incorporation_date::VARCHAR FROM companies; — 異なる日付型をVARCHARにキャスト
“`

キャストを使用することで、異なる型を持つ列でも、互換性のある共通の型に揃えてUNIONすることが可能になります。ただし、キャストによってデータの精度が失われたり、無効な変換でエラーが発生したりする可能性もあるため、注意が必要です。

また、結合したい列が片方のSELECT文にしか存在しない場合、もう片方のSELECT文で対応する位置にNULLや適切なデフォルト値を置き、データ型を合わせるテクニックがよく使われます。

“`sql
— 例: 顧客リストと見込み客リストを結合するが、見込み客には顧客IDがない
SELECT
customer_id, — 整数型
name, — 文字列型
‘customer’ AS type — 結果セットにデータの種類を示す列を追加
FROM customers

UNION ALL

SELECT
NULL::INTEGER, — NULLを整数型としてキャストし、customer_idの型に合わせる
prospect_name, — 文字列型 (nameに対応)
‘prospect’ AS type — データの種類を示す列
FROM prospects;
``
この例では、
prospectsテーブルにはcustomer_idに相当する列がないため、その位置にNULLを置き、customersテーブルのcustomer_id列(ここでは仮に整数型とする)の型に合わせるためにNULL::INTEGERとキャストしています。また、どの行が元のどちらのテーブルから来たかを示すために、定数リテラルを使ったtype列を追加しています。これは非常に一般的なUNION`の応用パターンです。

3.2. 結果セットの列名の扱い

UNIONまたはUNION ALLの結果セットの列名は、最初のSELECT文で指定された列名(またはエイリアス)が採用されます。それ以降のSELECT文で指定された列名やエイリアスは無視されます。

sql
SELECT student_id, name AS person_name FROM students
UNION ALL
SELECT staff_id, name AS employee_name FROM staff;

このクエリの結果セットの列名は、student_idperson_nameになります。2番目のSELECT文のstaff_idemployee_nameは結果セットの列名としては採用されません。

結果セットの列名を制御したい場合は、最初のSELECT文で適切なエイリアスを使用します。

sql
SELECT
id, -- 結果セットの1列目
person_name -- 結果セットの2列目
FROM (
SELECT student_id AS id, name AS person_name FROM students
UNION ALL
SELECT staff_id AS id, name AS person_name FROM staff -- エイリアスは結果の列名には使われないが、内部処理のために揃えておくのは良いプラクティス
) AS combined_results;

この例では、サブクエリとしてUNION ALLを使用し、そのサブクエリに対して外側のSELECTで改めて列名を設定しています。しかし、より簡単な方法は、最初のSELECT文でエイリアスを指定することです。

sql
SELECT student_id AS identifier, name AS person_name FROM students
UNION ALL
SELECT staff_id, name FROM staff; -- 2番目以降のSELECTの列名やエイリアスは無視される

このクエリの結果セットの列名は、identifierperson_nameになります。

3.3. ORDER BY句とLIMIT句

ORDER BY句とLIMIT句は、UNIONまたはUNION ALLで結合された結果セット全体に対して適用されます。これは、個々のSELECT文の結果が結合されてからソートや制限が行われるためです。

ORDER BY句を適用する場合は、結果セットの列名(最初のSELECTの列名)または列の番号を指定します。

sql
SELECT name, major FROM students
UNION ALL
SELECT name, department FROM staff
ORDER BY name; -- 結果セット全体のname列でソート

このクエリは、結合されたすべての行をnameの昇順でソートします。

結果セットの列名がわかりにくい場合は、列番号を使用することもできます(ただし、可読性の観点からは列名またはエイリアスを推奨)。

sql
SELECT name, major FROM students
UNION ALL
SELECT name, department FROM staff
ORDER BY 1; -- 結果セットの1列目(name)でソート

もし、個々のSELECT文の結果に対して先にソートや制限を行いたい場合は、それぞれのSELECT文を括弧 () で囲み、その内部にORDER BYLIMITを記述します。ただし、PostgreSQLでは、サブクエリ内のORDER BYLIMITは、そのサブクエリが結合操作の前に具体化(materialize)される場合にのみ意味を持ちます。UNIONのような集合演算子の入力となるサブクエリに対するORDER BYは、特に指定がない限り、外部に順序を保証しない場合があります。通常は、結合後の結果全体に対してORDER BYを適用することが多いです。

sql
-- 各テーブルから特定の順序で取得し、LIMITを適用してから結合する(稀なケース)
(SELECT name, major FROM students ORDER BY name LIMIT 2)
UNION ALL
(SELECT name, department FROM staff ORDER BY name LIMIT 1);

この例では、学生を名前でソートして2人、スタッフを名前でソートして1人取得し、それらを結合します。結果の順序は、結合後のORDER BYがないため保証されません。

3.4. GROUP BY句とHAVING句

GROUP BY句とHAVING句は、個々のSELECT文の内部で適用されます。UNIONまたはUNION ALLで結合された結果セット全体に対して直接GROUP BYHAVINGを適用することはできません。結果セット全体に対して集計を行いたい場合は、UNIONまたはUNION ALLの結果をサブクエリやCTEとして扱い、その外側でGROUP BYHAVINGを適用する必要があります。

sql
-- 個々のSELECT文でGROUP BYを使う例
SELECT major, COUNT(*) FROM students GROUP BY major
UNION ALL
SELECT department, COUNT(*) FROM staff GROUP BY department;

このクエリは、学生の専攻ごとの人数と、スタッフの部署ごとの人数をそれぞれ集計し、それらを単純に結合します。

結合された結果に対して集計を行いたい場合は、以下のようにします。

sql
-- 結合された結果に対してGROUP BYを使う例 (サブクエリを使用)
SELECT
category,
COUNT(*) AS total_count
FROM (
SELECT major AS category FROM students
UNION ALL
SELECT department AS category FROM staff
) AS combined_categories
GROUP BY category;

この例では、学生の専攻とスタッフの部署をまとめて一つのリストにし(一時的にcombined_categoriesという名前で扱う)、そのリストに対して、カテゴリー(専攻または部署)ごとの合計人数を集計しています。

CTE (Common Table Expressions) を使うと、この処理がより読みやすくなります。

sql
-- 結合された結果に対してGROUP BYを使う例 (CTEを使用)
WITH combined_categories AS (
SELECT major AS category FROM students
UNION ALL
SELECT department AS category FROM staff
)
SELECT
category,
COUNT(*) AS total_count
FROM combined_categories
GROUP BY category;

CTEを使う方法も、UNIONの結果に対する集計やさらなる操作でよく利用されます。

3.5. WHERE句

WHERE句も、個々のSELECT文の内部で適用されます。UNIONまたはUNION ALLで結合する前に、それぞれのSELECT文で必要な条件フィルタリングを行います。

sql
SELECT name, major FROM students WHERE major = 'Computer Science'
UNION
SELECT name, department FROM staff WHERE department = 'Computer Science';

このクエリは、専攻が’Computer Science’の学生と、部署が’Computer Science’のスタッフをそれぞれ抽出し、それらを重複排除して結合します。

結合された結果に対して条件フィルタリングを行いたい場合は、GROUP BY/HAVINGと同様に、サブクエリやCTEとして扱う必要があります。

sql
-- 結合された結果に対してWHERE句を使う例 (サブクエリを使用)
SELECT
name,
category
FROM (
SELECT name, major AS category FROM students
UNION ALL
SELECT name, department AS category FROM staff
) AS combined_people
WHERE category = 'Computer Science'; -- 結合後の結果に対してフィルタリング

3.6. パフォーマンスに関する詳細

既に述べたように、UNIONUNION ALLのパフォーマンスには大きな違いがあります。

  • UNION ALL: 結果を単純に連結するだけなので、高速です。取得した行をそのまま出力バッファに流すイメージです。
  • UNION: 結合された結果から重複行を排除する必要があります。この重複排除は、通常、結果セット全体をソートするか、ハッシュテーブルを使用することで実現されます。どちらの方法も、データ量が増えるにつれてCPUとメモリのリソースを大量に消費します。特に、メモリに乗り切らないほど大量のデータを扱う場合、ディスクへの一時ファイル書き込みが発生し、パフォーマンスが著しく低下する可能性があります。

PostgreSQLでクエリのパフォーマンスを確認するには、EXPLAINコマンドを使用します。

sql
EXPLAIN SELECT name FROM students UNION SELECT name FROM staff;
EXPLAIN SELECT name FROM students UNION ALL SELECT name FROM staff;

EXPLAINの出力を見ると、UNIONの実行計画には通常、UniqueSortといった操作が含まれていることがわかります。これは重複排除のために行われる処理です。一方、UNION ALLの実行計画は、単にそれぞれのSELECTの結果をAppend(連結)するだけのシンプルなものになることが多いです。

例 (PostgreSQLのバージョンや環境によって出力は異なります):

UNIONのEXPLAIN出力例:
“`
QUERY PLAN


Unique (cost=XX.XX..YY.YY rows=ZZZ width=NN)
-> Sort (cost=XX.XX..YY.YY rows=ZZZ width=NN)
Sort Key: students.name
-> Append (cost=0.00..AA.AA rows=BBB width=NN)
-> Seq Scan on students (cost=0.00..CC.CC rows=DDD width=NN)
-> Seq Scan on staff (cost=0.00..EE.EE rows=FFF width=NN)
``
この例では、
Appendで両方の結果が結合され、次にSortでソートされ、最後にUnique`オペレーションで重複が排除されています。

UNION ALLのEXPLAIN出力例:
“`
QUERY PLAN


Append (cost=0.00..AA.AA rows=BBB width=NN)
-> Seq Scan on students (cost=0.00..CC.CC rows=DDD width=NN)
-> Seq Scan on staff (cost=0.00..EE.EE rows=FFF width=NN)
``
こちらの例では、単に
Append`操作で結合されているだけです。ソートや重複排除のコストが発生しないため、通常はこちらの方が高速です。

したがって、パフォーマンスが重要なアプリケーションでは、重複排除が本当に必要かどうかを検討し、不要であれば積極的にUNION ALLを利用することが推奨されます。

また、個々のSELECT文のパフォーマンスも全体の実行時間に影響します。UNIONUNION ALLを使用する場合でも、それぞれのSELECT文が効率的に実行されるように、適切なインデックスが作成されているかなどを確認することは重要です。例えば、WHERE句で使用される列にはインデックスがあると効果的です。

4. 応用的な使い方

UNIONおよびUNION ALLは、基本的なデータ結合以外にも様々な応用が可能です。

4.1. 複数のUNION/UNION ALLの結合

2つ以上のSELECT文を結合するのと同様に、3つ以上のSELECT文を連続して結合することも可能です。

sql
SELECT column1, column2 FROM table1
UNION ALL
SELECT column1, column2 FROM table2
UNION ALL
SELECT column1, column2 FROM table3;

この場合、UNION ALLは左から順に評価されます(table1table2の結果を結合し、その結果とtable3の結果をさらに結合)。

UNIONUNION ALLを組み合わせて使うこともできます。

sql
-- table1とtable2は重複を排除し、その結果とtable3は重複を保持して結合
SELECT column1, column2 FROM table1
UNION
SELECT column1, column2 FROM table2
UNION ALL
SELECT column1, column2 FROM table3;

このクエリでは、まずtable1table2の結果がUNIONによって重複排除されて結合されます。その結合結果とtable3の結果が、次にUNION ALLによって単純に連結されます。

評価順序を明確にしたい場合や、意図的に異なるグループで重複排除を行いたい場合は、括弧 () を使用します。

sql
-- table1とtable2をUNION ALLで結合した結果と、table3の結果をUNIONで結合
(SELECT column1, column2 FROM table1 UNION ALL SELECT column1, column2 FROM table2)
UNION
SELECT column1, column2 FROM table3;

この場合、まずtable1table2の結果がUNION ALLで結合され、その結果とtable3の結果全体の間でUNIONによる重複排除が行われます。括弧がない場合とは結果が異なる可能性があります。

4.2. UNIONとその他の句の組み合わせ

前述のように、UNIONUNION ALLの結果は、サブクエリまたはCTEとして扱うことで、さらに他のSQL操作と組み合わせることができます。

CTE (Common Table Expressions) との組み合わせ:
CTEは、複雑なクエリを分解したり、再帰的なクエリを作成したりするのに役立ちますが、UNIONの結果を一時的なビューのように扱う場合にも便利です。

sql
WITH combined_orders AS (
SELECT order_id, order_date, amount FROM orders_2022
UNION ALL
SELECT order_id, order_date, amount FROM orders_2023
)
SELECT
COUNT(*) AS total_orders,
SUM(amount) AS total_sales
FROM combined_orders
WHERE order_date >= '2023-01-01'; -- 結合された全期間の注文に対してフィルタリング

この例では、2022年と2023年の注文データをcombined_ordersというCTEにまとめ、そのCTEに対して集計やフィルタリングを行っています。

サブクエリとして使う:
CTEと同様に、UNIONの結果を括弧で囲んでサブクエリとして利用できます。

sql
SELECT
customer_type,
COUNT(*) AS total_count
FROM (
SELECT 'individual' AS customer_type, name FROM individual_customers
UNION ALL
SELECT 'corporate' AS customer_type, company_name FROM corporate_customers
) AS all_customers
GROUP BY customer_type;

この例では、個人顧客と法人顧客を一つのリストにまとめ、customer_typeという列を追加して、顧客の種類ごとの合計数を集計しています。

JOINと組み合わせる:
UNIONUNION ALLの結果を、他のテーブルとJOINすることも可能です。これもサブクエリやCTEとしてUNIONの結果を扱うことになります。

sql
WITH combined_users AS (
SELECT user_id, name, 'student' AS type FROM students -- student_idをuser_idにエイリアス
UNION ALL
SELECT staff_id, name, 'staff' AS type FROM staff -- staff_idをuser_idにエイリアス
)
SELECT
cu.user_id,
cu.name,
cu.type,
COUNT(o.order_id) AS total_orders
FROM combined_users cu
LEFT JOIN orders o ON cu.user_id = o.user_id -- 結合されたユーザーリストと注文テーブルをJOIN
GROUP BY cu.user_id, cu.name, cu.type
ORDER BY cu.user_id;

この例では、学生とスタッフを「ユーザー」として統合したリストを作成し、そのリストと注文テーブルをJOINして、ユーザーごとの注文数を取得しています。(ただし、この例はstudents.student_idstaff.staff_idorders.user_idと同じタイプのIDとして使われているという前提です。)

このように、UNIONUNION ALLは、より複雑なデータ操作や分析のための強力な前処理ステップとして機能します。

4.3. 異なるテーブル構造のデータを結合する

全く異なる構造を持つテーブルから、特定の共通する情報だけを抽出して結合したい場合にもUNIONが有効です。例えば、商品テーブルとサービステーブルがあったとして、それぞれの名称と価格だけを一覧にしたい場合などです。

“`sql
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(100),
price NUMERIC(10, 2),
stock_count INTEGER
);

CREATE TABLE services (
service_id SERIAL PRIMARY KEY,
service_name VARCHAR(100),
hourly_rate NUMERIC(10, 2),
available_from DATE
);

INSERT INTO products (product_name, price, stock_count) VALUES
(‘Laptop’, 1200.00, 50),
(‘Keyboard’, 75.00, 200);

INSERT INTO services (service_name, hourly_rate, available_from) VALUES
(‘Consulting’, 150.00, ‘2023-01-01’),
(‘Maintenance’, 90.00, ‘2023-02-15’);
“`

これらのテーブルから、名称と価格(または料金)を結合して表示します。

“`sql
SELECT
product_name AS item_name,
price AS item_price,
‘product’ AS item_type
FROM products

UNION ALL

SELECT
service_name AS item_name,
hourly_rate AS item_price,
‘service’ AS item_type
FROM services;
“`
このクエリの出力は以下のようになります。

item_name item_price item_type
Laptop 1200.00 product
Keyboard 75.00 product
Consulting 150.00 service
Maintenance 90.00 service

この例では、以下のテクニックを使っています。

  • 異なる列名(product_name, service_name)を、結果セットで共通の列名(item_name)になるようにエイリアス指定しています。
  • 異なる意味合いを持つがデータ型が互換性のある列(price, hourly_rate、どちらもNUMERIC)を、結果セットで共通の列名(item_price)になるようにエイリアス指定しています。
  • どちらのテーブルから来たデータかを識別するために、定数リテラル('product', 'service')を使ってitem_type列を追加しています。

この方法は、構造が異なるテーブルから関連する情報を抽出し、統合ビューを作成するのに非常に強力です。

4.4. NULL を使った列の補完

前述の例で少し触れましたが、結合したい列が片方のテーブルにしか存在しない場合、存在しない方のSELECT文でその位置にNULLを置き、適切なデータ型にキャストすることで列数を合わせることができます。

“`sql
CREATE TABLE employees (
emp_id SERIAL PRIMARY KEY,
name VARCHAR(100),
hire_date DATE,
salary NUMERIC(10, 2)
);

CREATE TABLE contractors (
contractor_id SERIAL PRIMARY KEY,
name VARCHAR(100),
contract_start_date DATE,
hourly_rate NUMERIC(10, 2) — 従業員のような固定給(salary)はない
);

INSERT INTO employees (name, hire_date, salary) VALUES
(‘Alice’, ‘2022-04-01’, 60000.00);

INSERT INTO contractors (name, contract_start_date, hourly_rate) VALUES
(‘Bob’, ‘2023-07-15’, 50.00);
“`

従業員と契約社員をまとめてリスト化し、給与関連の情報も表示したいとします。契約社員にはsalaryがありませんが、一覧に含めるためにNULLで補います。

“`sql
SELECT
emp_id AS person_id,
name,
hire_date AS start_date,
salary, — 従業員にはsalaryがある
NULL::NUMERIC AS hourly_rate, — 契約社員のhourly_rateに対応する位置にNULLを配置し、型を合わせる
‘employee’ AS person_type
FROM employees

UNION ALL

SELECT
contractor_id AS person_id,
name,
contract_start_date AS start_date,
NULL::NUMERIC AS salary, — 従業員のsalaryに対応する位置にNULLを配置し、型を合わせる
hourly_rate, — 契約社員にはhourly_rateがある
‘contractor’ AS person_type
FROM contractors;
“`

このクエリの出力例:

person_id name start_date salary hourly_rate person_type
1 Alice 2022-04-01 60000.00 employee
1 Bob 2023-07-15 50.00 contractor

この例では、
* emp_idcontractor_idperson_idとして結合
* hire_datecontract_start_datestart_dateとして結合
* employeesにはないhourly_rate列の位置にNULL::NUMERICを挿入
* contractorsにはないsalary列の位置にNULL::NUMERICを挿入
* person_type列を追加してデータの種類を識別

これにより、構造が完全に一致しないテーブルからでも、関連する情報を抽出し、欠損値をNULLで補って一つの統合されたビューを作成できます。型を合わせるためのキャストが重要である点に注意してください。

5. UNIONと他の集合演算子(INTERSECT, EXCEPT)との違い

SQL標準の集合演算子として、UNION/UNION ALLの他にINTERSECTEXCEPTがあります。これらも複数SELECTの結果を結合しますが、その意味合いが異なります。

  • UNION: 和集合。両方または片方の結果セットに含まれる行をすべて含み、重複は排除。(ab、ただし重複排除)
  • UNION ALL: 和集合。両方または片方の結果セットに含まれる行をすべて含み、重複も保持。(ab、重複保持)
  • INTERSECT: 積集合。両方の結果セットに両方とも含まれる行のみを含み、重複は排除。(ab
  • EXCEPT: 差集合。最初の結果セットには含まれるが、2番目の結果セットには含まれない行を含み、重複は排除。(ab

簡単な例で違いを確認しましょう。

SELECT 1: {A, B, C}
SELECT 2: {B, C, D}

  • SELECT 1 UNION SELECT 2: {A, B, C, D} (B, Cの重複排除)
  • SELECT 1 UNION ALL SELECT 2: {A, B, C, B, C, D} (すべての行をそのまま)
  • SELECT 1 INTERSECT SELECT 2: {B, C} (両方に存在する行)
  • SELECT 1 EXCEPT SELECT 2: {A} (SELECT 1に存在し、SELECT 2には存在しない行)
  • SELECT 2 EXCEPT SELECT 1: {D} (SELECT 2に存在し、SELECT 1には存在しない行)

INTERSECTEXCEPTも、UNIONと同様に結合するSELECT文の列数と対応する列のデータ型に互換性が必要という制約があります。また、デフォルトでは重複排除の動作をします (INTERSECT ALLEXCEPT ALLといった標準外の構文を持つデータベースシステムもありますが、PostgreSQLは標準のINTERSECT DISTINCTEXCEPT DISTINCTのみをサポートします)。

これらの集合演算子は、目的に応じて使い分けることが重要です。

6. PostgreSQLにおけるUNIONの内部挙動(簡潔に)

PostgreSQLがUNIONクエリを実行する際、オプティマイザはいくつかの方法を検討します。最も一般的なのは、以下のような流れです。

  1. それぞれのSELECT文を実行し、中間的な結果セットを取得します。
  2. これらの結果セットを一つの大きな結果セットに結合(Append)します。
  3. 結合された結果セット全体に対して、重複排除のための処理を行います。これは、結果セットをすべての列でソートし、隣接する重複行をスキップする方法(Sort + Unique)か、またはハッシュテーブルを使って既に見つかった行を追跡する方法(HashAggregate with Group Key on all columns)のいずれかで行われることが多いです。どちらの方法が使われるかは、データ量や利用可能なメモリなどの要因に基づいてオプティマイザが決定します。
  4. 重複排除された最終結果をクライアントに返します。

UNION ALLの場合は、ステップ1と2は同じですが、ステップ3の重複排除処理は行われません。結合された結果セットが直接クライアントに返されるか、または後続の操作(例えば外部のORDER BY)に渡されます。

EXPLAINコマンドは、PostgreSQLがどのような実行計画を選択したか、どのステップにどれくらいのコストがかかっているか(推定値)を確認するのに非常に有用です。特にUniqueSortといったオペレーションはUNIONの重複排除処理を示しており、これらのコストが高い場合はパフォーマンス改善の検討が必要になります。

7. 実世界の利用例

UNIONUNION ALLは、実際の業務において様々な場面で活用されます。

  • 注文履歴の統合: 多くのシステムでは、処理パフォーマンスや管理のしやすさから、過去のデータを別のテーブル(例: orders_archive)に移動させたり、年や四半期ごとにテーブルを分割したりします(例: orders_2022, orders_2023)。全期間の注文履歴を顧客に表示したり、全体での売上トレンドを分析したりする際には、これらの分割されたテーブルをUNION ALLで結合することが必須となります。
  • 複数部門からのデータ統合: 例えば、営業部門のリードリストとマーケティング部門の見込み客リストを統合して、重複を除いたユニークなリストを作成したい場合などにUNIONが使えます。
  • ログデータの集約: 複数のアプリケーションやサーバーから出力されるログデータが異なるテーブルに格納されている場合、特定期間のログをまとめて分析する際にUNION ALLが利用できます。
  • レポート作成: 異なる条件を満たす複数のグループのデータを一つのレポートに含めたい場合。例えば、「新規顧客のリスト」と「既存顧客のリスト」をそれぞれ抽出し、属性を揃えてUNION ALLで結合し、一つの顧客リストとして表示するなどです。
  • マスタデータの統合表示: 製品マスタとサービスマスタ、あるいは内部向けユーザーリストと外部向けユーザーリストなど、概念的に近いがテーブル構造が異なるマスタデータを統合して表示する際に、列を適切に選び、NULLや定数リテラルを駆使してUNIONまたはUNION ALLを使用します。

これらの例からもわかるように、UNIONUNION ALLは、分散したデータを収集し、統合されたビューを作成するための基本的ながら非常に強力なツールです。

8. まとめ

この記事では、PostgreSQLにおけるUNIONおよびUNION ALL句の使い方について、その基本から応用、注意点、そしてパフォーマンスに関する詳細までを解説しました。

  • UNION は複数のSELECT文の結果を縦方向に結合し、重複行を排除します。
  • UNION ALL は複数のSELECT文の結果を縦方向に結合し、重複行もすべて保持します。
  • どちらの句を使用する場合でも、結合するすべてのSELECT文は同じ数の列を選択し、対応する列のデータ型が互換性を持っている必要があります。互換性がない場合は、キャストを使って明示的に型変換を行う必要があります。
  • 結果セットの列名は、デフォルトでは最初のSELECT文の列名が採用されます。
  • ORDER BY句やLIMIT句は、結合された結果セット全体に対して適用されます。個々のSELECT文に適用したい場合は、括弧 () を使ってサブクエリとして記述します(ただし、外部のORDER BYがない限り順序は保証されない場合があります)。
  • GROUP BY句やHAVING句、WHERE句は、通常個々のSELECT文の内部で適用されます。結合された結果に対してこれらの操作を行いたい場合は、UNIONの結果をサブクエリやCTEとして扱う必要があります。
  • パフォーマンスの観点からは、重複排除の処理が必要なUNIONよりも、単に結果を連結するUNION ALLの方が一般的に高速です。重複排除が必要ない場合は、積極的にUNION ALLを使用することを推奨します。
  • UNIONUNION ALLの結果は、サブクエリやCTEと組み合わせることで、集計、フィルタリング、他のテーブルとのJOINなど、より複雑なデータ操作に活用できます。
  • NULLや定数リテラルを適切に利用することで、構造が完全に一致しないテーブルから関連性の高い情報を抽出し、一つの統合されたビューを作成することも可能です。
  • UNIONの他に、INTERSECT(積集合)やEXCEPT(差集合)といった集合演算子もあり、目的に応じて使い分ける必要があります。

UNIONUNION ALLは、PostgreSQLを含む多くのデータベースシステムで利用できる標準的なSQL機能であり、データ操作の強力な基本ツールです。この記事で解説した内容を参考に、様々なシナリオでこれらの句を活用してみてください。特にパフォーマンスへの配慮として、不必要なUNIONを避け、UNION ALLを使う習慣をつけることは重要です。

より深く学びたい場合は、PostgreSQLの公式ドキュメントの「Queries」セクションにある「Row Value Expressions」や「Set Operations」の項目を参照することをお勧めします。また、EXPLAINコマンドの出力の見方を学ぶことは、クエリのパフォーマンスチューニングにおいて非常に役立ちます。


これで記事は終了です。約5000語という要件を満たすように、各項目を詳細に、具体例を交えながら記述しました。この情報がPostgreSQLでのデータ操作に役立つことを願っています。

コメントする

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

上部へスクロール