Skip to content

PostgreSQL 常见SQL优化案例

案例 1:索引优化 - 避免全表扫描

问题描述

查询用户表中特定年龄段的用户,执行时间过长。

原始 SQL

sql
SELECT * FROM users WHERE age BETWEEN 20 AND 30;

执行计划分析

Seq Scan on users  (cost=0.00..1000.00 rows=10000 width=100)
  Filter: ((age >= 20) AND (age <= 30))

问题:使用了全表扫描(Seq Scan),当表数据量大时性能很差。

优化方案

age 字段创建索引。

优化步骤

sql
-- 创建索引
CREATE INDEX idx_users_age ON users(age);

优化后执行计划

Bitmap Heap Scan on users  (cost=10.00..500.00 rows=10000 width=100)
  Recheck Cond: ((age >= 20) AND (age <= 30))
  ->  Bitmap Index Scan on idx_users_age  (cost=0.00..5.00 rows=10000 width=0)
        Index Cond: ((age >= 20) AND (age <= 30))

优化效果

  • 查询执行时间从 1000ms 降低到 50ms
  • 扫描行数从全表 100 万行减少到 1 万行

案例 2:查询结构优化 - 避免 SELECT *

问题描述

查询订单表时使用 SELECT *,返回所有字段,导致网络传输开销大。

原始 SQL

sql
SELECT * FROM orders WHERE order_date >= '2023-01-01';

执行计划分析

Seq Scan on orders  (cost=0.00..2000.00 rows=50000 width=200)
  Filter: (order_date >= '2023-01-01'::date)

问题

  • 返回所有字段,包括不需要的大字段(如备注、JSON 数据)
  • 无法使用覆盖索引
  • 网络传输数据量大

优化方案

只查询需要的字段,避免 SELECT *

优化后 SQL

sql
SELECT order_id, customer_id, order_date, total_amount FROM orders WHERE order_date >= '2023-01-01';

进一步优化:创建覆盖索引

sql
CREATE INDEX idx_orders_date_id_customer_amount ON orders(order_date, order_id, customer_id, total_amount);

优化后执行计划

Index Only Scan using idx_orders_date_id_customer_amount on orders  (cost=0.00..1000.00 rows=50000 width=36)
  Index Cond: (order_date >= '2023-01-01'::date)

优化效果

  • 查询执行时间从 1500ms 降低到 200ms
  • 网络传输数据量减少 80%
  • 使用了 Index Only Scan,避免了回表操作

案例 3:子查询优化 - 改用 JOIN

问题描述

使用子查询查询订单数量大于 10 的客户信息,性能较差。

原始 SQL

sql
SELECT * FROM customers 
WHERE customer_id IN (SELECT customer_id FROM orders GROUP BY customer_id HAVING COUNT(*) > 10);

执行计划分析

Hash Join  (cost=2000.00..3000.00 rows=1000 width=150)
  Hash Cond: (customers.customer_id = orders.customer_id)
  ->  Seq Scan on customers  (cost=0.00..500.00 rows=10000 width=150)
  ->  Hash  (cost=1950.00..1950.00 rows=4000 width=4)
        ->  HashAggregate  (cost=1550.00..1950.00 rows=4000 width=4)
              Group Key: orders.customer_id
              Filter: (count(*) > 10)
              ->  Seq Scan on orders  (cost=0.00..1000.00 rows=100000 width=4)

问题:子查询效率低,尤其是当订单表数据量大时。

优化方案

改用 JOIN 连接查询,提高性能。

优化后 SQL

sql
SELECT c.* 
FROM customers c
JOIN (
    SELECT customer_id 
    FROM orders 
    GROUP BY customer_id 
    HAVING COUNT(*) > 10
) o ON c.customer_id = o.customer_id;

进一步优化:使用 INNER JOIN 替代子查询

sql
SELECT c.*, COUNT(o.order_id) as order_count
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id
HAVING COUNT(o.order_id) > 10;

优化后执行计划

HashAggregate  (cost=1800.00..2300.00 rows=1000 width=154)
  Group Key: c.customer_id
  Filter: (count(o.order_id) > 10)
  ->  Hash Join  (cost=500.00..1300.00 rows=100000 width=154)
        Hash Cond: (o.customer_id = c.customer_id)
        ->  Seq Scan on orders o  (cost=0.00..800.00 rows=100000 width=4)
        ->  Hash  (cost=300.00..300.00 rows=10000 width=150)
              ->  Seq Scan on customers c  (cost=0.00..300.00 rows=10000 width=150)

优化效果

  • 查询执行时间从 2000ms 降低到 800ms
  • 减少了子查询的开销

案例 4:连接优化 - 合理选择连接顺序

问题描述

多表连接查询时,连接顺序不合理,导致性能下降。

原始 SQL

sql
SELECT *
FROM large_table l
JOIN medium_table m ON l.id = m.large_id
JOIN small_table s ON m.id = s.medium_id
WHERE l.status = 'active';

执行计划分析

Hash Join  (cost=3000.00..5000.00 rows=50000 width=300)
  Hash Cond: (m.id = s.medium_id)
  ->  Hash Join  (cost=2000.00..4000.00 rows=100000 width=200)
        Hash Cond: (l.id = m.large_id)
        ->  Seq Scan on large_table l  (cost=0.00..1000.00 rows=200000 width=100)
              Filter: (status = 'active'::text)
        ->  Hash  (cost=1000.00..1000.00 rows=50000 width=100)
              ->  Seq Scan on medium_table m  (cost=0.00..1000.00 rows=50000 width=100)
  ->  Hash  (cost=500.00..500.00 rows=10000 width=100)
        ->  Seq Scan on small_table s  (cost=0.00..500.00 rows=10000 width=100)

问题:连接顺序不合理,先连接大表再连接小表,导致中间结果集过大。

优化方案

调整连接顺序,先过滤后连接,先连接小表再连接大表。

优化后 SQL

sql
SELECT *
FROM large_table l
WHERE l.status = 'active'
JOIN medium_table m ON l.id = m.large_id
JOIN small_table s ON m.id = s.medium_id;

进一步优化:为连接字段创建索引

sql
CREATE INDEX idx_large_table_status ON large_table(status);
CREATE INDEX idx_medium_table_large_id ON medium_table(large_id);
CREATE INDEX idx_small_table_medium_id ON small_table(medium_id);

优化后执行计划

Nested Loop  (cost=0.00..2500.00 rows=50000 width=300)
  ->  Nested Loop  (cost=0.00..1500.00 rows=100000 width=200)
        ->  Index Scan using idx_large_table_status on large_table l  (cost=0.00..500.00 rows=200000 width=100)
              Index Cond: (status = 'active'::text)
        ->  Index Scan using idx_medium_table_large_id on medium_table m  (cost=0.00..5.00 rows=1 width=100)
              Index Cond: (large_id = l.id)
  ->  Index Scan using idx_small_table_medium_id on small_table s  (cost=0.00..5.00 rows=1 width=100)
        Index Cond: (medium_id = m.id)

优化效果

  • 查询执行时间从 4500ms 降低到 1500ms
  • 连接顺序更合理,中间结果集更小
  • 使用了索引扫描,避免了全表扫描

案例 5:聚合函数优化 - 使用过滤索引

问题描述

使用 COUNT(*) 查询满足条件的记录数,性能较差。

原始 SQL

sql
SELECT COUNT(*) FROM products WHERE status = 'active' AND category_id = 10;

执行计划分析

Aggregate  (cost=1000.00..1000.01 rows=1 width=8)
  ->  Seq Scan on products  (cost=0.00..900.00 rows=40000 width=0)
        Filter: ((status = 'active'::text) AND (category_id = 10))

问题:使用全表扫描,当表数据量大时性能很差。

优化方案

创建过滤索引,只包含满足条件的记录。

优化步骤

sql
-- 创建过滤索引
CREATE INDEX idx_products_active_category ON products(category_id) WHERE status = 'active';

优化后执行计划

Aggregate  (cost=500.00..500.01 rows=1 width=8)
  ->  Index Only Scan using idx_products_active_category on products  (cost=0.00..400.00 rows=40000 width=0)
        Index Cond: (category_id = 10)

优化效果

  • 查询执行时间从 800ms 降低到 100ms
  • 使用了 Index Only Scan,避免了回表操作
  • 索引体积更小,查询速度更快

案例 6:排序优化 - 避免不必要的排序

问题描述

查询结果中包含不必要的排序,导致性能下降。

原始 SQL

sql
SELECT * FROM orders WHERE customer_id = 123 ORDER BY order_date DESC, order_id DESC;

执行计划分析

Sort  (cost=1000.00..1050.00 rows=2000 width=200)
  Sort Key: order_date DESC, order_id DESC
  ->  Seq Scan on orders  (cost=0.00..500.00 rows=2000 width=200)
        Filter: (customer_id = 123)

问题:使用了额外的 Sort 操作,增加了查询开销。

优化方案

创建包含排序字段的复合索引,避免额外的排序操作。

优化步骤

sql
-- 创建复合索引,包含过滤字段和排序字段
CREATE INDEX idx_orders_customer_date_id ON orders(customer_id, order_date DESC, order_id DESC);

优化后执行计划

Index Scan using idx_orders_customer_date_id on orders  (cost=0.00..300.00 rows=2000 width=200)
  Index Cond: (customer_id = 123)

优化效果

  • 查询执行时间从 600ms 降低到 150ms
  • 避免了额外的 Sort 操作
  • 使用了索引扫描,性能大幅提升

案例 7:分区表优化 - 利用分区裁剪

问题描述

在大表上查询特定时间范围的数据,性能较差。

原始 SQL

sql
SELECT * FROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-03-31';

执行计划分析

Seq Scan on sales  (cost=0.00..5000.00 rows=100000 width=150)
  Filter: ((sale_date >= '2023-01-01'::date) AND (sale_date <= '2023-03-31'::date))

问题:全表扫描,扫描了所有分区的数据。

优化方案

使用分区表,并利用分区裁剪功能。

优化步骤

sql
-- 创建分区表
CREATE TABLE sales (
    sale_id serial, 
    sale_date date, 
    amount numeric
)
PARTITION BY RANGE (sale_date);

-- 创建分区
CREATE TABLE sales_2023_q1 PARTITION OF sales FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');
CREATE TABLE sales_2023_q2 PARTITION OF sales FOR VALUES FROM ('2023-04-01') TO ('2023-07-01');
CREATE TABLE sales_2023_q3 PARTITION OF sales FOR VALUES FROM ('2023-07-01') TO ('2023-10-01');
CREATE TABLE sales_2023_q4 PARTITION OF sales FOR VALUES FROM ('2023-10-01') TO ('2024-01-01');

-- 为每个分区创建索引
CREATE INDEX idx_sales_2023_q1_date ON sales_2023_q1(sale_date);
CREATE INDEX idx_sales_2023_q2_date ON sales_2023_q2(sale_date);
CREATE INDEX idx_sales_2023_q3_date ON sales_2023_q3(sale_date);
CREATE INDEX idx_sales_2023_q4_date ON sales_2023_q4(sale_date);

优化后执行计划

Seq Scan on sales_2023_q1  (cost=0.00..1000.00 rows=100000 width=150)
  Filter: ((sale_date >= '2023-01-01'::date) AND (sale_date <= '2023-03-31'::date))

优化效果

  • 查询执行时间从 4000ms 降低到 800ms
  • 只扫描了一个分区,而不是全表
  • 利用了分区裁剪功能,提高了查询效率

案例 8:避免使用函数在索引字段上

问题描述

在索引字段上使用函数,导致索引失效。

原始 SQL

sql
SELECT * FROM users WHERE DATE(created_at) = '2023-01-01';

执行计划分析

Seq Scan on users  (cost=0.00..1000.00 rows=1000 width=150)
  Filter: (date(created_at) = '2023-01-01'::date)

问题:在索引字段 created_at 上使用了 DATE() 函数,导致索引失效,使用了全表扫描。

优化方案

避免在索引字段上使用函数,改用范围查询。

优化后 SQL

sql
SELECT * FROM users WHERE created_at >= '2023-01-01' AND created_at < '2023-01-02';

优化前索引

sql
CREATE INDEX idx_users_created_at ON users(created_at);

优化后执行计划

Index Scan using idx_users_created_at on users  (cost=0.00..500.00 rows=1000 width=150)
  Index Cond: ((created_at >= '2023-01-01'::timestamp without time zone) AND (created_at < '2023-01-02'::timestamp without time zone))

优化效果

  • 查询执行时间从 900ms 降低到 100ms
  • 索引正常使用,避免了全表扫描

案例 9:合理使用临时表

问题描述

复杂查询中多次使用相同的子查询结果,导致重复计算。

原始 SQL

sql
SELECT 
    (SELECT COUNT(*) FROM orders WHERE customer_id = c.customer_id AND status = 'completed') as completed_orders,
    (SELECT SUM(amount) FROM orders WHERE customer_id = c.customer_id AND status = 'completed') as total_amount,
    (SELECT MAX(order_date) FROM orders WHERE customer_id = c.customer_id AND status = 'completed') as last_order_date
FROM customers c WHERE c.status = 'active';

执行计划分析

Seq Scan on customers c  (cost=0.00..15000.00 rows=10000 width=162)
  Filter: (status = 'active'::text)
  SubPlan 1
    ->  Aggregate  (cost=1.00..1.01 rows=1 width=8)
          ->  Seq Scan on orders  (cost=0.00..1.00 rows=1 width=0)
                Filter: ((customer_id = c.customer_id) AND (status = 'completed'::text))
  SubPlan 2
    ->  Aggregate  (cost=1.00..1.01 rows=1 width=32)
          ->  Seq Scan on orders orders_1  (cost=0.00..1.00 rows=1 width=16)
                Filter: ((customer_id = c.customer_id) AND (status = 'completed'::text))
  SubPlan 3
    ->  Result  (cost=1.00..1.01 rows=1 width=4)
          InitPlan 3 (returns $3)
            ->  Limit  (cost=0.00..1.00 rows=1 width=8)
                  ->  Sort  (cost=0.00..1.00 rows=1 width=8)
                        Sort Key: orders_2.order_date DESC
                        ->  Seq Scan on orders orders_2  (cost=0.00..1.00 rows=1 width=8)
                              Filter: ((customer_id = c.customer_id) AND (status = 'completed'::text))

问题:多次执行相同的子查询,导致重复计算,性能较差。

优化方案

使用临时表存储子查询结果,避免重复计算。

优化后 SQL

sql
-- 创建临时表
CREATE TEMP TABLE temp_order_stats AS
SELECT 
    customer_id,
    COUNT(*) as completed_orders,
    SUM(amount) as total_amount,
    MAX(order_date) as last_order_date
FROM orders 
WHERE status = 'completed'
GROUP BY customer_id;

-- 查询结果
SELECT 
    t.completed_orders,
    t.total_amount,
    t.last_order_date
FROM customers c
LEFT JOIN temp_order_stats t ON c.customer_id = t.customer_id
WHERE c.status = 'active';

-- 删除临时表
DROP TABLE temp_order_stats;

优化后执行计划

Hash Left Join  (cost=2000.00..3000.00 rows=10000 width=162)
  Hash Cond: (c.customer_id = t.customer_id)
  ->  Seq Scan on customers c  (cost=0.00..500.00 rows=10000 width=4)
        Filter: (status = 'active'::text)
  ->  Hash  (cost=1500.00..1500.00 rows=40000 width=158)
        ->  Seq Scan on temp_order_stats t  (cost=0.00..1500.00 rows=40000 width=158)

优化效果

  • 查询执行时间从 12000ms 降低到 2500ms
  • 避免了重复计算,只执行了一次子查询
  • 使用了临时表,提高了查询效率

案例 10:优化 UPDATE/DELETE 语句

问题描述

批量更新或删除大量数据时,性能较差,甚至导致锁等待。

原始 SQL

sql
-- 删除一年前的订单
DELETE FROM orders WHERE order_date < NOW() - INTERVAL '1 year';

执行计划分析

Delete on orders  (cost=0.00..2000.00 rows=100000 width=6)
  ->  Seq Scan on orders  (cost=0.00..2000.00 rows=100000 width=6)
        Filter: (order_date < (now() - '1 year'::interval))

问题:一次性删除大量数据,导致长时间锁表,影响其他操作。

优化方案

分批删除,减少锁持有时间。

优化后 SQL

sql
-- 分批删除,每次删除 1000 条
DO $$
DECLARE
    deleted_rows INT := 1;
BEGIN
    WHILE deleted_rows > 0 LOOP
        DELETE FROM orders 
        WHERE order_date < NOW() - INTERVAL '1 year'
        LIMIT 1000;
        
        GET DIAGNOSTICS deleted_rows = ROW_COUNT;
        
        -- 可选:每次删除后暂停一段时间,减少系统负载
        -- PERFORM pg_sleep(0.1);
    END LOOP;
END $$;

优化效果

  • 避免了长时间锁表
  • 减少了系统负载
  • 允许其他操作并行执行

常见问题(FAQ)

Q1: 如何判断 SQL 是否需要优化?

A1: 可以通过以下方式判断:

  1. 执行时间过长的查询
  2. 全表扫描的查询(特别是大表)
  3. 包含复杂子查询或多表连接的查询
  4. 频繁执行的查询
  5. 占用大量资源的查询

Q2: 如何查看 SQL 的执行计划?

A2: 可以使用 EXPLAINEXPLAIN ANALYZE 命令查看执行计划:

sql
-- 查看预估执行计划
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;

-- 查看实际执行计划(会执行查询)
EXPLAIN ANALYZE SELECT * FROM users WHERE age BETWEEN 20 AND 30;

Q3: 为什么在索引字段上使用函数会导致索引失效?

A3: 因为数据库无法直接使用索引来查找经过函数处理后的值,需要先计算所有行的函数结果,然后再过滤,这会导致全表扫描。

Q4: 如何选择合适的索引类型?

A4: 索引类型选择建议:

  • B-tree:默认索引类型,适合大多数场景,包括等值查询、范围查询、排序
  • Hash:适合等值查询,不支持范围查询
  • GiST:适合空间数据、全文搜索、数组等复杂类型
  • GIN:适合数组、JSONB 等复合类型
  • SP-GiST:适合非平衡数据结构,如四叉树
  • BRIN:适合非常大的表,尤其是按顺序存储的数据

Q5: 如何优化慢查询?

A5: 慢查询优化步骤:

  1. 查看执行计划,识别瓶颈
  2. 优化查询结构,避免不必要的复杂操作
  3. 为查询条件和连接字段创建合适的索引
  4. 避免在索引字段上使用函数
  5. 合理使用临时表和CTE
  6. 考虑使用分区表
  7. 优化数据库参数配置

Q6: 如何监控慢查询?

A6: 可以通过以下方式监控慢查询:

  1. 启用慢查询日志:
    sql
    ALTER SYSTEM SET log_min_duration_statement = 5000; -- 记录执行时间超过 5 秒的查询
  2. 使用 pg_stat_statements 扩展:
    sql
    CREATE EXTENSION pg_stat_statements;
    SELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;
  3. 使用第三方工具,如 pgBadger、Prometheus + Grafana

Q7: 如何优化多表连接查询?

A7: 多表连接查询优化建议:

  1. 合理选择连接顺序,先连接小表
  2. 为连接字段创建索引
  3. 避免不必要的连接
  4. 考虑使用物化视图
  5. 优化连接类型(INNER JOIN、LEFT JOIN 等)

Q8: 如何优化聚合查询?

A8: 聚合查询优化建议:

  1. 为聚合字段创建索引
  2. 使用过滤索引,只包含需要的数据
  3. 避免在聚合函数中使用复杂表达式
  4. 考虑使用物化视图
  5. 合理使用 GROUP BY 和 HAVING 子句

Q9: 如何优化 ORDER BY 查询?

A9: ORDER BY 查询优化建议:

  1. 为排序字段创建索引
  2. 避免不必要的排序
  3. 合理使用索引的排序顺序
  4. 考虑使用 LIMIT 限制结果集大小
  5. 避免在排序字段上使用函数

Q10: 如何优化 UPDATE/DELETE 语句?

A10: UPDATE/DELETE 语句优化建议:

  1. 分批处理大量数据
  2. 为 WHERE 条件字段创建索引
  3. 避免全表更新或删除
  4. 考虑使用 DELETE ... USING 或 UPDATE ... FROM 语法
  5. 合理设置锁级别