Skip to content

组合查询

在 PostgreSQL 中,组合查询(Set Operations)是一种强大的功能,允许我们将两个或多个查询的结果进行集合运算。本章将详细介绍三种主要的集合操作:UNION(并集)、INTERSECT(交集)和 EXCEPT(差集)。

基本概念

组合查询使用集合操作来组合两个查询的结果,语法格式如下:

sql
query1 UNION [ALL] query2
query1 INTERSECT [ALL] query2
query1 EXCEPT [ALL] query2

其中 query1query2 是可以使用任何 SQL 特性的完整查询语句。

重要概念集合操作的核心是对查询结果进行数学集合运算,类似于数学中的并集、交集和差集概念。 :::

1. UNION(并集)操作

1.1 基本语法和概念

UNION 操作将两个查询的结果合并,返回所有出现在任一查询中的行。

1.2 UNION vs UNION ALL

  • UNION: 合并结果并自动去除重复行(类似于 DISTINCT)
  • UNION ALL: 合并结果但保留所有重复行

1.3 实际示例

示例 1:基本 UNION 操作

问题陈述: 获取所有客户和供应商的联系信息。

解决方案:

sql
-- 创建示例表
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100),
    city VARCHAR(50)
);

CREATE TABLE suppliers (
    id SERIAL PRIMARY KEY,
    company_name VARCHAR(100),
    contact_email VARCHAR(100),
    location VARCHAR(50)
);

-- 插入测试数据
INSERT INTO customers (name, email, city) VALUES
('张三', '[email protected]', '北京'),
('李四', '[email protected]', '上海'),
('王五', '[email protected]', '广州');

INSERT INTO suppliers (company_name, contact_email, location) VALUES
('ABC公司', '[email protected]', '深圳'),
('XYZ科技', '[email protected]', '北京'),
('123贸易', '[email protected]', '上海');

-- UNION查询:获取所有联系人信息
SELECT name AS contact_name, email, city AS location, 'Customer' AS type
FROM customers
UNION
SELECT company_name, contact_email, location, 'Supplier' AS type
FROM suppliers
ORDER BY type, contact_name;

输入数据:

  • customers 表: 3 行客户数据
  • suppliers 表: 3 行供应商数据

输出结果:

contact_name | email                | location | type
-------------|---------------------|----------|----------
张三         | [email protected]  | 北京     | Customer
李四         | [email protected]      | 上海     | Customer
王五         | [email protected]    | 广州     | Customer
123贸易      | [email protected]       | 上海     | Supplier
ABC公司      | [email protected]     | 深圳     | Supplier
XYZ科技      | [email protected]        | 北京     | Supplier

分析过程:

  1. 第一个查询选择所有客户信息,添加'Customer'标识
  2. 第二个查询选择所有供应商信息,添加'Supplier'标识
  3. UNION 操作合并两个结果集,自动去除重复行
  4. ORDER BY 对最终结果进行排序

示例 2:UNION ALL 与重复数据处理

问题陈述: 比较 UNION 和 UNION ALL 在处理重复数据时的差异。

解决方案:

sql
-- 创建重复数据的示例
WITH dataset1 AS (
    SELECT 'Product A' AS product, 100 AS price
    UNION ALL
    SELECT 'Product B', 200
    UNION ALL
    SELECT 'Product C', 150
),
dataset2 AS (
    SELECT 'Product A' AS product, 100 AS price  -- 重复数据
    UNION ALL
    SELECT 'Product B', 250  -- 相同产品,不同价格
    UNION ALL
    SELECT 'Product D', 300  -- 新产品
)

-- 使用UNION(去重)
SELECT product, price, 'UNION Result' AS source
FROM (
    SELECT * FROM dataset1
    UNION
    SELECT * FROM dataset2
) combined

UNION ALL

-- 使用UNION ALL(保留重复)
SELECT product, price, 'UNION ALL Result' AS source
FROM (
    SELECT * FROM dataset1
    UNION ALL
    SELECT * FROM dataset2
) combined
ORDER BY source, product, price;

输出结果分析:

product    | price | source
-----------|-------|---------------
Product A  | 100   | UNION Result
Product B  | 200   | UNION Result
Product B  | 250   | UNION Result
Product C  | 150   | UNION Result
Product D  | 300   | UNION Result
Product A  | 100   | UNION ALL Result
Product A  | 100   | UNION ALL Result  -- 重复保留
Product B  | 200   | UNION ALL Result
Product B  | 250   | UNION ALL Result
Product C  | 150   | UNION ALL Result
Product D  | 300   | UNION ALL Result

性能提示 UNION ALL 通常比 UNION 性能更好,因为它不需要执行去重操作。如果确定没有重复数据或需要保留重复数据,优先使用 UNION ALL。 :::

2. INTERSECT(交集)操作

2.1 基本概念

INTERSECT 返回同时存在于两个查询结果中的行。

2.1 基本概念

INTERSECT 返回同时存在于两个查询结果中的行。

Query1 结果 {1, 2, 3, 4}Query2 结果 {2, 4, 5, 6}INTERSECT交集结果{2, 4}只返回同时存在于两个查询中的行Query1 独有数据Query2 独有数据交集数据(INTERSECT结果)

交集操作特点

  • 数学概念: INTERSECT 操作相当于数学中的集合交集(A ∩ B)
  • 结果特性: 只返回同时满足两个查询条件的记录
  • 重复处理: 自动去除重复行,类似于 UNION 的去重行为
  • 常见用途: 查找共同客户、重叠数据、满足多个条件的记录 :::

2.2 实际示例

示例 3:查找共同客户

问题陈述: 找出既购买了产品 A 又购买了产品 B 的客户。

解决方案:

sql
-- 创建订单表
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INTEGER,
    product_name VARCHAR(100),
    order_date DATE
);

-- 插入测试数据
INSERT INTO orders (customer_id, product_name, order_date) VALUES
(1, 'Product A', '2023-01-15'),
(2, 'Product A', '2023-01-16'),
(3, 'Product A', '2023-01-17'),
(1, 'Product B', '2023-01-18'),
(2, 'Product B', '2023-01-19'),
(4, 'Product B', '2023-01-20'),
(1, 'Product C', '2023-01-21'),
(5, 'Product C', '2023-01-22');

-- 使用INTERSECT查找同时购买Product A和Product B的客户
SELECT customer_id
FROM orders
WHERE product_name = 'Product A'
INTERSECT
SELECT customer_id
FROM orders
WHERE product_name = 'Product B';

输入数据: 8 条订单记录,涉及 5 个客户和 3 种产品

输出结果:

customer_id
-----------
1
2

分析过程:

  1. 第一个查询找出购买 Product A 的所有客户 ID
  2. 第二个查询找出购买 Product B 的所有客户 ID
  3. INTERSECT 操作返回两个结果集的交集
  4. 结果显示客户 1 和客户 2 都购买了这两种产品

示例 4:复杂条件的交集查询

问题陈述: 找出在 2023 年 1 月和 2 月都有订单的客户。

解决方案:

sql
-- 扩展订单数据
INSERT INTO orders (customer_id, product_name, order_date) VALUES
(1, 'Product D', '2023-02-15'),
(3, 'Product E', '2023-02-16'),
(6, 'Product F', '2023-02-17');

-- 查找在1月和2月都有订单的客户
SELECT DISTINCT customer_id, 'Active in both months' AS status
FROM orders
WHERE EXTRACT(MONTH FROM order_date) = 1  -- 1月
INTERSECT
SELECT DISTINCT customer_id, 'Active in both months' AS status
FROM orders
WHERE EXTRACT(MONTH FROM order_date) = 2; -- 2月

输出结果:

customer_id | status
------------|----------------------
1           | Active in both months
3           | Active in both months

3. EXCEPT(差集)操作

3.1 基本概念

EXCEPT 返回存在于第一个查询结果中但不存在于第二个查询结果中的行。

3.2 实际示例

示例 5:查找未购买特定产品的客户

问题陈述: 找出购买了产品但从未购买过 Product A 的客户。

解决方案:

sql
-- 查找所有有订单的客户
SELECT DISTINCT customer_id
FROM orders
EXCEPT
-- 减去购买过Product A的客户
SELECT DISTINCT customer_id
FROM orders
WHERE product_name = 'Product A';

输出结果:

customer_id
-----------
4
5
6

分析过程:

  1. 第一个查询获取所有有订单的客户 ID
  2. 第二个查询获取购买过 Product A 的客户 ID
  3. EXCEPT 操作返回第一个结果集中不在第二个结果集中的客户
  4. 结果显示客户 4、5、6 从未购买过 Product A

示例 6:数据验证和清理

问题陈述: 找出客户表中存在但在订单表中没有订单记录的客户。

解决方案:

sql
-- 查找没有下过订单的客户
SELECT id AS customer_id, name, 'No orders' AS status
FROM customers
EXCEPT
SELECT DISTINCT o.customer_id, c.name, 'No orders' AS status
FROM orders o
JOIN customers c ON o.customer_id = c.id;

应用场景: 数据清理、客户分析、营销活动目标客户筛选。

4. 组合操作规则和约束

4.1 兼容性要求

重要约束为了执行集合操作,两个查询必须是"并集兼容的":

  1. 返回相同数量的列
  2. 对应列具有兼容的数据类型 :::

示例 7:兼容性检查

sql
-- ✅ 正确:列数和类型兼容
SELECT name, age FROM employees
UNION
SELECT customer_name, customer_age FROM customers;

-- ❌ 错误:列数不匹配
SELECT name, age, department FROM employees
UNION
SELECT customer_name, customer_age FROM customers;  -- 缺少第三列

-- ❌ 错误:数据类型不兼容
SELECT name, age FROM employees        -- age是INTEGER
UNION
SELECT customer_name, signup_date FROM customers;  -- signup_date是DATE

4.2 操作优先级和组合

优先级规则

  1. INTERSECT 具有最高优先级
  2. UNIONEXCEPT 具有相同优先级,从左到右结合

示例 8:复杂组合操作

问题陈述: 演示多个集合操作的组合使用。

解决方案:

sql
-- 创建更多测试数据
CREATE TABLE product_categories (
    product_name VARCHAR(100),
    category VARCHAR(50)
);

INSERT INTO product_categories VALUES
('Product A', 'Electronics'),
('Product B', 'Electronics'),
('Product C', 'Clothing'),
('Product D', 'Electronics'),
('Product E', 'Books'),
('Product F', 'Clothing');

-- 复杂查询:找出电子产品客户,但排除只买书的客户,并包含服装客户
WITH electronics_customers AS (
    SELECT DISTINCT o.customer_id
    FROM orders o
    JOIN product_categories pc ON o.product_name = pc.product_name
    WHERE pc.category = 'Electronics'
),
books_only_customers AS (
    SELECT customer_id
    FROM orders o
    JOIN product_categories pc ON o.product_name = pc.product_name
    GROUP BY customer_id
    HAVING COUNT(DISTINCT pc.category) = 1 AND MAX(pc.category) = 'Books'
),
clothing_customers AS (
    SELECT DISTINCT o.customer_id
    FROM orders o
    JOIN product_categories pc ON o.product_name = pc.product_name
    WHERE pc.category = 'Clothing'
)

-- 使用组合操作
SELECT customer_id, 'Target Customer' AS segment
FROM electronics_customers
EXCEPT
SELECT customer_id, 'Target Customer' AS segment
FROM books_only_customers
UNION
SELECT customer_id, 'Target Customer' AS segment
FROM clothing_customers
ORDER BY customer_id;

分析过程:

  1. 首先找出购买电子产品的客户
  2. 排除只购买书籍的客户
  3. 添加购买服装的客户
  4. 最终得到目标客户群体

4.3 括号控制执行顺序

示例 9:使用括号控制优先级

sql
-- 不同的括号组合产生不同结果

-- 情况1:默认优先级
query1 UNION query2 INTERSECT query3
-- 等价于:query1 UNION (query2 INTERSECT query3)

-- 情况2:使用括号改变优先级
(query1 UNION query2) INTERSECT query3

-- 情况3:复杂嵌套
((query1 UNION query2) EXCEPT query3) INTERSECT query4

实际应用示例

sql
-- 查找活跃客户:在1月或2月有订单,但在3月也有订单的客户
(
    SELECT DISTINCT customer_id
    FROM orders
    WHERE EXTRACT(MONTH FROM order_date) = 1
    UNION
    SELECT DISTINCT customer_id
    FROM orders
    WHERE EXTRACT(MONTH FROM order_date) = 2
)
INTERSECT
SELECT DISTINCT customer_id
FROM orders
WHERE EXTRACT(MONTH FROM order_date) = 3;

5. 子查询中的集合操作

5.1 带有 LIMIT 和 ORDER BY 的子查询

语法注意事项当子查询需要使用 LIMIT、ORDER BY 等子句时,必须用括号括起来。 :::

示例 10:子查询限制

sql
-- ✅ 正确:使用括号包围带LIMIT的子查询
SELECT customer_id, product_name
FROM orders
WHERE customer_id IN (
    (SELECT DISTINCT customer_id FROM orders WHERE product_name = 'Product A' LIMIT 5)
    UNION ALL
    (SELECT DISTINCT customer_id FROM orders WHERE product_name = 'Product B' LIMIT 5)
);

-- ❌ 错误语法
SELECT a FROM b UNION SELECT x FROM y LIMIT 10;
-- 这会被理解为:(SELECT a FROM b UNION SELECT x FROM y) LIMIT 10

-- ✅ 正确:如果想限制第二个查询
SELECT a FROM b UNION (SELECT x FROM y LIMIT 10);

示例 11:复杂子查询组合

问题陈述: 获取每个产品类别的前 3 名客户和每个月的前 2 名活跃客户。

解决方案:

sql
-- 每个产品类别的前3名客户(按订单数量)
(
    SELECT
        o.customer_id,
        pc.category,
        COUNT(*) as order_count,
        'Top Category Customer' as type
    FROM orders o
    JOIN product_categories pc ON o.product_name = pc.product_name
    WHERE pc.category = 'Electronics'
    GROUP BY o.customer_id, pc.category
    ORDER BY order_count DESC
    LIMIT 3
)
UNION ALL
(
    SELECT
        o.customer_id,
        pc.category,
        COUNT(*) as order_count,
        'Top Category Customer' as type
    FROM orders o
    JOIN product_categories pc ON o.product_name = pc.product_name
    WHERE pc.category = 'Clothing'
    GROUP BY o.customer_id, pc.category
    ORDER BY order_count DESC
    LIMIT 3
)
ORDER BY category, order_count DESC;

6. 性能优化和最佳实践

6.1 性能考量

性能对比表

操作性能特点适用场景
UNION ALL最快,无需去重确定无重复或需要保留重复
UNION需要排序去重,较慢需要去除重复数据
INTERSECT需要构建哈希表,中等查找共同元素
EXCEPT类似 INTERSECT,中等查找差异元素

示例 12:性能优化技巧

sql
-- 优化前:使用UNION去重
SELECT customer_id FROM orders WHERE product_name = 'Product A'
UNION
SELECT customer_id FROM orders WHERE product_name = 'Product B';

-- 优化后:使用UNION ALL + DISTINCT(在某些情况下更快)
SELECT DISTINCT customer_id FROM (
    SELECT customer_id FROM orders WHERE product_name = 'Product A'
    UNION ALL
    SELECT customer_id FROM orders WHERE product_name = 'Product B'
) combined;

-- 或者使用EXISTS(避免集合操作)
SELECT DISTINCT o1.customer_id
FROM orders o1
WHERE o1.product_name = 'Product A'
  AND EXISTS (
      SELECT 1 FROM orders o2
      WHERE o2.customer_id = o1.customer_id
        AND o2.product_name = 'Product B'
  );

6.2 索引优化

sql
-- 为经常用于集合操作的列创建索引
CREATE INDEX idx_orders_customer_product ON orders(customer_id, product_name);
CREATE INDEX idx_orders_date ON orders(order_date);
CREATE INDEX idx_product_categories_name ON product_categories(product_name);

-- 使用复合索引优化多列查询
CREATE INDEX idx_orders_comprehensive ON orders(customer_id, product_name, order_date);

6.3 最佳实践

最佳实践清单

  1. 选择合适的操作符:确定不需要去重时使用 UNION ALL
  2. 使用索引:为参与集合操作的列创建适当索引
  3. 限制结果集:在子查询中使用 WHERE 条件减少数据量
  4. 避免不必要的排序:UNION 操作会自动排序,如不需要可考虑其他方案
  5. 使用括号明确优先级:复杂查询中明确指定操作顺序
  6. 考虑替代方案:某些情况下 EXISTS、JOIN 可能比集合操作更高效 :::

7. 实际业务应用场景

7.1 数据分析场景

示例 13:客户分析报告

问题陈述: 创建一个综合客户分析报告,包含不同客户群体的统计信息。

解决方案:

sql
-- 综合客户分析报告
WITH customer_stats AS (
    -- 高价值客户(订单金额超过平均值)
    SELECT 'High Value' as segment, COUNT(DISTINCT customer_id) as count
    FROM orders o
    JOIN (SELECT product_name,
                 CASE
                     WHEN product_name LIKE '%A%' THEN 1000
                     WHEN product_name LIKE '%B%' THEN 1500
                     ELSE 800
                 END as price
          FROM product_categories) p ON o.product_name = p.product_name
    GROUP BY customer_id
    HAVING SUM(p.price) > (
        SELECT AVG(total_value) FROM (
            SELECT customer_id, SUM(
                CASE
                    WHEN o.product_name LIKE '%A%' THEN 1000
                    WHEN o.product_name LIKE '%B%' THEN 1500
                    ELSE 800
                END
            ) as total_value
            FROM orders o
            GROUP BY customer_id
        ) avg_calc
    )

    UNION ALL

    -- 新客户(最近30天首次购买)
    SELECT 'New Customer' as segment, COUNT(DISTINCT customer_id) as count
    FROM orders
    WHERE customer_id IN (
        SELECT customer_id
        FROM orders
        GROUP BY customer_id
        HAVING MIN(order_date) >= CURRENT_DATE - INTERVAL '30 days'
    )

    UNION ALL

    -- 流失客户(90天内无订单)
    SELECT 'Churned Customer' as segment, COUNT(DISTINCT customer_id) as count
    FROM customers
    WHERE id NOT IN (
        SELECT DISTINCT customer_id
        FROM orders
        WHERE order_date >= CURRENT_DATE - INTERVAL '90 days'
    )
)

SELECT segment, count,
       ROUND(count * 100.0 / SUM(count) OVER (), 2) as percentage
FROM customer_stats
ORDER BY count DESC;

7.2 数据清理和验证

示例 14:数据一致性检查

sql
-- 检查数据一致性问题
SELECT 'Orphan Orders' as issue_type, COUNT(*) as count
FROM orders o
WHERE NOT EXISTS (SELECT 1 FROM customers c WHERE c.id = o.customer_id)

UNION ALL

SELECT 'Invalid Products' as issue_type, COUNT(*) as count
FROM orders o
WHERE NOT EXISTS (SELECT 1 FROM product_categories pc WHERE pc.product_name = o.product_name)

UNION ALL

SELECT 'Future Orders' as issue_type, COUNT(*) as count
FROM orders
WHERE order_date > CURRENT_DATE;

8. 总结

PostgreSQL 的组合查询功能提供了强大的数据集合操作能力:

核心要点

  1. UNION: 合并两个结果集,去除重复行
  2. UNION ALL: 合并两个结果集,保留所有行
  3. INTERSECT: 返回两个结果集的交集
  4. EXCEPT: 返回第一个结果集中不在第二个结果集中的行

关键注意事项

  • 查询必须具有兼容的列结构
  • 注意操作符的优先级和结合性
  • 合理使用括号控制执行顺序
  • 考虑性能影响,适当使用索引