未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合在新建的隔离测试库中开展多类型索引、执行计划、查询改写和分区实验,不是可直接部署的生产监控或自动维护方案;本次仅作全文结构与代码静态审阅,未执行SQL或进行外部事实核验。当前统计监控查询、监控视图和维护函数仍误用统计视图字段,列分布查询也存在字段名错误;慢查询示例使用旧版pg_stat_statements字段,尚未明确适用版本、扩展启用及访问权限前置。物化视图的90天汇总不能替代原查询的30天统计,现有索引不满足所示并发刷新要求;维护函数内的VACUUM及浮点格式化写法也不能按原样执行。订单数据生成依赖新建products表产生的1至100000主键;BRIN示例的固定2024年区间与默认当前时间数据不匹配,不适合据此展示有代表性的范围查询效果。多个竞争索引是否被采用及优化是否有效,须结合实际数据分布、统计信息和执行计划判断;GiST的POINT示例仅表示平面距离。
⚡ PostgreSQL索引与性能优化
📚 学习目标
通过本章节的学习,你将能够:
- 理解PostgreSQL的各种索引类型和使用场景
- 掌握查询计划分析和性能诊断技巧
- 学会创建和优化索引策略
- 理解查询优化器的工作原理
- 掌握实际的性能调优方法和最佳实践
tip 💡 学习建议
性能优化是一门艺术,也是一门科学。不要追求完美的理论,而要关注实际效果。就像给汽车调优一样,需要在不同的场景下测试和调整。每个优化都要有数据支撑,使用EXPLAIN ANALYZE来验证你的假设。
🎯 PostgreSQL索引类型详解
1. B-tree索引(默认索引)
B-tree索引是最常用的索引类型,适合等值查询和范围查询。
-- 创建测试表
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(200),
category VARCHAR(100),
price DECIMAL(10,2),
stock_quantity INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
tags TEXT[]
);
-- 插入测试数据
INSERT INTO products (name, category, price, stock_quantity, tags)
SELECT
'Product ' || generate_series,
CASE (generate_series % 5)
WHEN 0 THEN 'Electronics'
WHEN 1 THEN 'Clothing'
WHEN 2 THEN 'Books'
WHEN 3 THEN 'Home'
ELSE 'Sports'
END,
ROUND((RANDOM() * 1000 + 10)::NUMERIC, 2),
FLOOR(RANDOM() * 1000),
ARRAY['tag' || (generate_series % 10), 'category' || (generate_series % 3)]
FROM generate_series(1, 100000);
-- 创建B-tree索引
CREATE INDEX idx_products_name ON products(name);
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_products_created_at ON products(created_at);
-- 复合B-tree索引
CREATE INDEX idx_products_category_price ON products(category, price);
CREATE INDEX idx_products_price_stock ON products(price, stock_quantity);
-- 测试B-tree索引性能
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE name = 'Product 1000';
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE category = 'Electronics' AND price BETWEEN 100 AND 500;
-- 部分索引(条件索引)
CREATE INDEX idx_products_active_price ON products(price) WHERE is_active = TRUE;
CREATE INDEX idx_products_expensive ON products(name) WHERE price > 500;
-- 表达式索引
CREATE INDEX idx_products_upper_name ON products(UPPER(name));
CREATE INDEX idx_products_year ON products(EXTRACT(YEAR FROM created_at));
2. Hash索引
Hash索引适合等值查询,但不支持范围查询。
-- 创建Hash索引
CREATE INDEX idx_products_id_hash ON products USING HASH(id);
CREATE INDEX idx_products_category_hash ON products USING HASH(category);
-- 测试Hash索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE id = 5000;
-- Hash索引不适合范围查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE id > 5000; -- 不会使用Hash索引
3. GIN索引(通用倒排索引)
GIN索引适合包含查询,如数组、JSON、全文搜索等。
-- 为数组创建GIN索引
CREATE INDEX idx_products_tags_gin ON products USING GIN(tags);
-- 测试数组查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE tags @> ARRAY['tag1'];
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE tags && ARRAY['tag1', 'tag2'];
-- 为JSON创建GIN索引
ALTER TABLE products ADD COLUMN attributes JSONB;
UPDATE products SET attributes = json_build_object(
'brand', 'Brand ' || (id % 10),
'model', 'Model ' || (id % 20),
'features', ARRAY['feature' || (id % 5), 'feature' || (id % 7)]
);
CREATE INDEX idx_products_attributes_gin ON products USING GIN(attributes);
-- 测试JSON查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE attributes @> '{"brand": "Brand 1"}';
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE attributes ? 'model';
-- 全文搜索GIN索引
ALTER TABLE products ADD COLUMN search_vector TSVECTOR;
UPDATE products SET search_vector = to_tsvector('english', name || ' ' || category);
CREATE INDEX idx_products_search_gin ON products USING GIN(search_vector);
-- 测试全文搜索
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE search_vector @@ to_tsquery('english', 'Product & Electronics');
4. GiST索引(通用搜索树)
GiST索引适合几何数据、范围查询等。
-- 添加几何数据列
ALTER TABLE products ADD COLUMN location POINT;
UPDATE products SET location = POINT(
RANDOM() * 180 - 90, -- 平面x坐标
RANDOM() * 90 - 45 -- 平面y坐标
);
-- 创建GiST索引
CREATE INDEX idx_products_location_gist ON products USING GiST(location);
-- 测试几何查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products
ORDER BY location <-> POINT(0, 0)
LIMIT 10; -- 平面坐标最近邻查询,可用于观察GiST距离排序计划
-- 为范围类型创建GiST索引
ALTER TABLE products ADD COLUMN price_range NUMRANGE;
UPDATE products SET price_range = NUMRANGE(price, price * 1.2);
CREATE INDEX idx_products_price_range_gist ON products USING GiST(price_range);
-- 测试范围查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products
WHERE price_range @> 100.0; -- 包含查询
5. SP-GiST索引(空间分区通用搜索树)
SP-GiST索引适合分层数据和空间数据。
-- 为TEXT类型创建SP-GiST索引
CREATE INDEX idx_products_name_spgist ON products USING SPGIST(name);
-- 测试前缀查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE name ^@ 'Product 1'; -- 前缀匹配
6. BRIN索引(块范围索引)
BRIN索引适合大表中的有序数据,占用空间小。
-- 为时间序列数据创建BRIN索引
CREATE INDEX idx_products_created_brin ON products USING BRIN(created_at);
CREATE INDEX idx_products_id_brin ON products USING BRIN(id);
-- 测试BRIN索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products
WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01';
📊 查询计划分析
1. EXPLAIN命令详解
-- 基本EXPLAIN
EXPLAIN SELECT * FROM products WHERE category = 'Electronics';
-- EXPLAIN ANALYZE(实际执行)
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE category = 'Electronics' AND price > 100;
-- 详细的EXPLAIN选项
EXPLAIN (
ANALYZE TRUE, -- 实际执行查询
BUFFERS TRUE, -- 显示缓冲区信息
TIMING TRUE, -- 显示执行时间
COSTS TRUE, -- 显示成本估算
VERBOSE TRUE, -- 详细输出
FORMAT JSON -- JSON格式输出
)
SELECT p.name, p.price, p.category
FROM products p
WHERE p.category = 'Electronics'
AND p.price BETWEEN 100 AND 500
ORDER BY p.price DESC
LIMIT 10;
2. 读懂查询计划
-- 创建更复杂的查询来分析
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(id),
customer_id INTEGER,
quantity INTEGER,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'pending'
);
-- 插入订单数据
INSERT INTO orders (product_id, customer_id, quantity)
SELECT
FLOOR(RANDOM() * 100000)::INTEGER + 1,
(RANDOM() * 10000 + 1)::INTEGER,
(RANDOM() * 10 + 1)::INTEGER
FROM generate_series(1, 50000);
-- 创建索引
CREATE INDEX idx_orders_product_id ON orders(product_id);
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_order_date ON orders(order_date);
-- 分析复杂查询
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT
p.name,
p.category,
p.price,
COUNT(o.id) as order_count,
SUM(o.quantity) as total_quantity,
AVG(o.quantity) as avg_quantity
FROM products p
LEFT JOIN orders o ON p.id = o.product_id
WHERE p.category IN ('Electronics', 'Books')
AND p.price > 50
AND (o.order_date IS NULL OR o.order_date >= '2024-01-01')
GROUP BY p.id, p.name, p.category, p.price
HAVING COUNT(o.id) > 0
ORDER BY total_quantity DESC
LIMIT 20;
-- 查看查询计划中的关键信息
/*
关键指标解读:
1. Seq Scan:顺序扫描,读取较大比例数据时可能是合理选择
2. Index Scan:索引扫描,效果取决于选择率和回表成本
3. Index Only Scan:仅索引扫描,是否避免堆访问还取决于可见性映射;三者没有固定优劣顺序
4. Nested Loop:嵌套循环连接
5. Hash Join:哈希连接
6. Merge Join:合并连接
7. cost:估算成本
8. rows:估算行数
9. actual time:实际执行时间
10. buffers:缓冲区使用情况
*/
3. 性能监控查询
-- 查看慢查询
SELECT
query,
calls,
total_time,
mean_time,
rows,
100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;
-- 查看表的统计信息
SELECT
schemaname,
tablename,
seq_scan, -- 顺序扫描次数
seq_tup_read, -- 顺序扫描读取的行数
idx_scan, -- 索引扫描次数
idx_tup_fetch, -- 索引扫描获取的行数
n_tup_ins, -- 插入的行数
n_tup_upd, -- 更新的行数
n_tup_del -- 删除的行数
FROM pg_stat_user_tables
WHERE tablename IN ('products', 'orders');
-- 查看索引使用情况
SELECT
schemaname,
tablename,
indexname,
idx_scan, -- 索引被使用的次数
idx_tup_read, -- 索引读取的行数
idx_tup_fetch -- 索引获取的行数
FROM pg_stat_user_indexes
WHERE tablename IN ('products', 'orders')
ORDER BY idx_scan DESC;
🚀 性能优化策略
1. 索引优化
-- 分析表的大小和索引
SELECT
tablename,
pg_size_pretty(pg_total_relation_size(tablename::regclass)) as table_size,
pg_size_pretty(pg_relation_size(tablename::regclass)) as table_only_size,
pg_size_pretty(pg_indexes_size(tablename::regclass)) as index_size
FROM pg_tables
WHERE tablename IN ('products', 'orders');
-- 查找未使用的索引
SELECT
schemaname,
tablename,
indexname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
-- 查找列名相同的索引候选;还需比较访问方法、唯一性、谓词、表达式、操作符类及包含列,不能直接认定重复或删除
WITH index_columns AS (
SELECT
i.indexrelid,
i.indrelid,
array_agg(a.attname ORDER BY a.attnum) as columns
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indexrelid
WHERE a.attnum > 0
GROUP BY i.indexrelid, i.indrelid
)
SELECT
c1.relname as table_name,
c2.relname as index1,
c3.relname as index2,
ic1.columns
FROM index_columns ic1
JOIN index_columns ic2 ON ic1.indrelid = ic2.indrelid
AND ic1.columns = ic2.columns
AND ic1.indexrelid < ic2.indexrelid
JOIN pg_class c1 ON c1.oid = ic1.indrelid
JOIN pg_class c2 ON c2.oid = ic1.indexrelid
JOIN pg_class c3 ON c3.oid = ic2.indexrelid;
-- 创建覆盖索引(包含索引)
CREATE INDEX idx_products_category_covering
ON products(category)
INCLUDE (name, price);
-- 测试覆盖索引效果
EXPLAIN (ANALYZE, BUFFERS)
SELECT name, price
FROM products
WHERE category = 'Electronics';
2. 查询优化
-- 优化前的查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT DISTINCT p.category, COUNT(*)
FROM products p
JOIN orders o ON p.id = o.product_id
WHERE p.price > 100
GROUP BY p.category;
-- 改为统计至少有一笔订单的产品数(使用EXISTS,与上面的订单行数统计不等价)
EXPLAIN (ANALYZE, BUFFERS)
SELECT p.category, COUNT(*)
FROM products p
WHERE p.price > 100
AND EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.id)
GROUP BY p.category;
-- 使用CTE优化复杂查询
EXPLAIN (ANALYZE, BUFFERS)
WITH popular_products AS (
SELECT product_id, COUNT(*) as order_count
FROM orders
GROUP BY product_id
HAVING COUNT(*) > 5
),
high_value_products AS (
SELECT id, name, category, price
FROM products
WHERE price > 100
)
SELECT
hvp.category,
COUNT(*) as product_count,
AVG(hvp.price) as avg_price
FROM high_value_products hvp
JOIN popular_products pp ON hvp.id = pp.product_id
GROUP BY hvp.category;
-- 使用窗口函数计算同类均价;以下SELECT *还会返回category_avg_price,若需保持原输出结构,应显式选择原表列
-- 优化前
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM products p
WHERE p.price > (
SELECT AVG(price)
FROM products p2
WHERE p2.category = p.category
);
-- 优化后
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM (
SELECT *,
AVG(price) OVER (PARTITION BY category) as category_avg_price
FROM products
) t
WHERE price > category_avg_price;
3. 分区表优化
-- 创建分区表
CREATE TABLE sales_partitioned (
id SERIAL,
sale_date DATE NOT NULL,
product_id INTEGER,
amount DECIMAL(10,2),
region VARCHAR(50)
) PARTITION BY RANGE (sale_date);
-- 创建分区
CREATE TABLE sales_2024_q1 PARTITION OF sales_partitioned
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE sales_2024_q2 PARTITION OF sales_partitioned
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
CREATE TABLE sales_2024_q3 PARTITION OF sales_partitioned
FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');
CREATE TABLE sales_2024_q4 PARTITION OF sales_partitioned
FOR VALUES FROM ('2024-10-01') TO ('2025-01-01');
-- 以下仅在2024年第一季度分区上创建索引
CREATE INDEX idx_sales_2024_q1_date ON sales_2024_q1(sale_date);
CREATE INDEX idx_sales_2024_q1_product ON sales_2024_q1(product_id);
-- 插入测试数据
INSERT INTO sales_partitioned (sale_date, product_id, amount, region)
SELECT
'2024-01-01'::date + (generate_series % 365),
(RANDOM() * 1000 + 1)::INTEGER,
ROUND((RANDOM() * 1000 + 10)::NUMERIC, 2),
CASE (generate_series % 4)
WHEN 0 THEN 'North'
WHEN 1 THEN 'South'
WHEN 2 THEN 'East'
ELSE 'West'
END
FROM generate_series(1, 100000);
-- 测试分区剪裁
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM sales_partitioned
WHERE sale_date BETWEEN '2024-02-01' AND '2024-02-28';
4. 配置优化
-- 查看当前配置
SHOW shared_buffers;
SHOW work_mem;
SHOW maintenance_work_mem;
SHOW effective_cache_size;
SHOW random_page_cost;
-- 查看统计信息更新情况
SELECT
schemaname,
tablename,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze,
vacuum_count,
autovacuum_count,
analyze_count,
autoanalyze_count
FROM pg_stat_user_tables
WHERE tablename IN ('products', 'orders');
-- 手动更新统计信息
ANALYZE products;
ANALYZE orders;
-- 查看表的膨胀情况
SELECT
schemaname,
tablename,
n_dead_tup,
n_live_tup,
ROUND(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) as dead_tuple_percent
FROM pg_stat_user_tables
WHERE n_dead_tup > 0
ORDER BY dead_tuple_percent DESC;
🔧 实际优化案例
1. 慢查询优化案例
-- 问题查询:查找热门产品和相关订单
EXPLAIN (ANALYZE, BUFFERS)
SELECT
p.name,
p.category,
p.price,
COUNT(o.id) as order_count,
SUM(o.quantity * p.price) as total_revenue
FROM products p
JOIN orders o ON p.id = o.product_id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY p.id, p.name, p.category, p.price
ORDER BY total_revenue DESC
LIMIT 10;
-- 优化步骤1:创建复合索引
CREATE INDEX idx_orders_date_product ON orders(order_date, product_id);
-- 优化步骤2:创建覆盖索引
CREATE INDEX idx_products_covering_revenue
ON products(id)
INCLUDE (name, category, price);
-- 优化步骤3:重新分析
EXPLAIN (ANALYZE, BUFFERS)
SELECT
p.name,
p.category,
p.price,
COUNT(o.id) as order_count,
SUM(o.quantity * p.price) as total_revenue
FROM products p
JOIN orders o ON p.id = o.product_id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY p.id, p.name, p.category, p.price
ORDER BY total_revenue DESC
LIMIT 10;
-- 优化步骤4:使用物化视图(对于复杂的聚合查询)
CREATE MATERIALIZED VIEW mv_product_revenue AS
SELECT
p.id,
p.name,
p.category,
p.price,
COUNT(o.id) as order_count,
SUM(o.quantity * p.price) as total_revenue,
MAX(o.order_date) as last_order_date
FROM products p
LEFT JOIN orders o ON p.id = o.product_id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY p.id, p.name, p.category, p.price;
-- 为物化视图创建索引
CREATE INDEX idx_mv_product_revenue_total ON mv_product_revenue(total_revenue DESC);
CREATE INDEX idx_mv_product_revenue_category ON mv_product_revenue(category);
-- 使用物化视图的查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM mv_product_revenue
WHERE last_order_date >= CURRENT_DATE - INTERVAL '30 days'
ORDER BY total_revenue DESC
LIMIT 10;
-- 刷新物化视图
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_product_revenue;
2. JOIN优化案例
-- 创建客户表
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
city VARCHAR(50),
registration_date DATE
);
INSERT INTO customers (name, email, city, registration_date)
SELECT
'Customer ' || generate_series,
'customer' || generate_series || '@example.com',
CASE (generate_series % 5)
WHEN 0 THEN 'Beijing'
WHEN 1 THEN 'Shanghai'
WHEN 2 THEN 'Guangzhou'
WHEN 3 THEN 'Shenzhen'
ELSE 'Hangzhou'
END,
'2020-01-01'::date + (generate_series % 1460)
FROM generate_series(1, 10000);
-- 更新orders表添加客户关联
UPDATE orders SET customer_id = (id % 10000) + 1;
-- 问题查询:三表连接
EXPLAIN (ANALYZE, BUFFERS)
SELECT
c.name as customer_name,
c.city,
p.name as product_name,
p.category,
o.quantity,
o.order_date
FROM customers c
JOIN orders o ON c.id = o.customer_id
JOIN products p ON o.product_id = p.id
WHERE c.city = 'Beijing'
AND o.order_date >= '2024-01-01'
AND p.category = 'Electronics';
-- 优化:创建合适的索引
CREATE INDEX idx_customers_city ON customers(city);
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);
CREATE INDEX idx_orders_product_date ON orders(product_id, order_date);
-- 重新测试查询性能
EXPLAIN (ANALYZE, BUFFERS)
SELECT
c.name as customer_name,
c.city,
p.name as product_name,
p.category,
o.quantity,
o.order_date
FROM customers c
JOIN orders o ON c.id = o.customer_id
JOIN products p ON o.product_id = p.id
WHERE c.city = 'Beijing'
AND o.order_date >= '2024-01-01'
AND p.category = 'Electronics';
📊 性能监控和维护
1. 自动化监控
-- 创建性能监控视图
CREATE VIEW v_table_stats AS
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch,
n_tup_ins + n_tup_upd + n_tup_del as total_writes,
CASE
WHEN seq_scan + idx_scan = 0 THEN 0
ELSE COALESCE(ROUND(seq_tup_read * 100.0 / NULLIF(seq_tup_read + idx_tup_fetch, 0), 2), 0)
END as seq_tuple_read_ratio
FROM pg_stat_user_tables;
-- 查看表统计
-- 此比例为累计元组读取比例,不是扫描次数比例;顺序与索引统计的计数口径也存在差异
SELECT * FROM v_table_stats ORDER BY seq_tuple_read_ratio DESC;
-- 创建索引效率监控
CREATE VIEW v_index_efficiency AS
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
CASE
WHEN idx_scan = 0 THEN 0
ELSE ROUND(idx_tup_fetch * 1.0 / idx_scan, 2)
END as avg_tuples_per_scan,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
-- 查看索引效率
SELECT * FROM v_index_efficiency;
2. 维护任务
-- 创建维护函数
CREATE OR REPLACE FUNCTION maintain_table_stats(schema_name TEXT DEFAULT 'public')
RETURNS TABLE(
table_name TEXT,
action TEXT,
result TEXT
) AS $$
DECLARE
table_record RECORD;
dead_ratio NUMERIC;
BEGIN
FOR table_record IN
SELECT schemaname, tablename, n_dead_tup, n_live_tup
FROM pg_stat_user_tables
WHERE schemaname = schema_name
LOOP
-- 计算死亡元组比例
IF table_record.n_live_tup + table_record.n_dead_tup > 0 THEN
dead_ratio := table_record.n_dead_tup * 100.0 /
(table_record.n_live_tup + table_record.n_dead_tup);
ELSE
dead_ratio := 0;
END IF;
-- 根据死亡元组比例决定维护策略
IF dead_ratio > 20 THEN
EXECUTE format('VACUUM ANALYZE %I.%I',
table_record.schemaname, table_record.tablename);
RETURN QUERY SELECT
table_record.tablename::TEXT,
'VACUUM ANALYZE'::TEXT,
format('Dead ratio: %.2f%%', dead_ratio)::TEXT;
ELSIF dead_ratio > 10 THEN
EXECUTE format('ANALYZE %I.%I',
table_record.schemaname, table_record.tablename);
RETURN QUERY SELECT
table_record.tablename::TEXT,
'ANALYZE'::TEXT,
format('Dead ratio: %.2f%%', dead_ratio)::TEXT;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- 执行维护
SELECT * FROM maintain_table_stats();
🎯 性能优化最佳实践
1. 索引设计原则
-- 1. 观察列的数据分布;选择性不能单独决定复合索引列顺序
SELECT
column_name,
n_distinct,
correlation
FROM pg_stats
WHERE tablename = 'products'
AND schemaname = 'public'
ORDER BY n_distinct DESC;
-- 2. 复合索引的列顺序很重要
-- 复合索引候选:列顺序应结合等值条件、范围条件、排序需求及实际执行计划确定
CREATE INDEX idx_products_good ON products(category, price, name);
-- 另一复合索引候选:低选择性列在前不必然无效,需结合查询条件和执行计划判断
-- CREATE INDEX idx_products_bad ON products(is_active, category, price);
-- 3. 避免过多的索引
SELECT
tablename,
COUNT(*) as index_count,
pg_size_pretty(SUM(pg_relation_size(indexrelid))) as total_index_size
FROM pg_stat_user_indexes
GROUP BY tablename
ORDER BY index_count DESC;
2. 查询优化技巧
-- 1. 使用LIMIT避免大结果集
-- 不好的查询
-- SELECT * FROM products ORDER BY price DESC;
-- 好的查询
SELECT * FROM products ORDER BY price DESC LIMIT 10;
-- 2. 避免SELECT *
-- 不好的查询
-- SELECT * FROM products WHERE category = 'Electronics';
-- 好的查询
SELECT id, name, price FROM products WHERE category = 'Electronics';
-- 3. 合理使用EXISTS vs IN
-- EXISTS与IN应先比较结果语义,再结合数据分布和执行计划评估性能,不能仅凭表大小判断
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products p
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.product_id = p.id
AND o.order_date >= '2024-01-01'
);
-- 4. 使用适当的JOIN类型
-- 内连接 vs 外连接的性能差异
EXPLAIN (ANALYZE, BUFFERS)
SELECT p.name, COUNT(o.id)
FROM products p
INNER JOIN orders o ON p.id = o.product_id
GROUP BY p.id, p.name;
EXPLAIN (ANALYZE, BUFFERS)
SELECT p.name, COUNT(o.id)
FROM products p
LEFT JOIN orders o ON p.id = o.product_id
GROUP BY p.id, p.name;
🎯 小结
通过本章节的学习,我们掌握了:
- 索引类型:B-tree、Hash、GIN、GiST、SP-GiST、BRIN的特点和用途
- 查询计划分析:使用EXPLAIN分析和优化查询性能
- 索引优化:创建、维护和监控索引的策略
- 查询优化:各种查询优化技巧和最佳实践
- 性能监控:监控数据库性能和自动化维护
abstract 📚 下一步学习
现在你已经掌握了PostgreSQL的索引和性能优化技巧,接下来我们将学习07-PostgreSQL事务与并发控制|PostgreSQL事务与并发控制,了解如何在并发环境中保证数据一致性。
🔗 相关资源
- PostgreSQL高级查询与函数
- PostgreSQL
- SQL - 通用索引概念
💡 记住:性能优化是一个持续的过程,不是一次性的任务。总是基于实际的性能数据进行优化,避免过早优化。索引不是越多越好,每个索引都有维护成本。定期监控和维护数据库,保持统计信息的准确性对查询优化器很重要!