Appearance
PostgreSQL 规划器统计信息详解
PostgreSQL 查询规划器需要准确估计查询将检索的行数,以便制定最优的查询执行计划。本文深入探讨 PostgreSQL 规划器使用的统计信息系统,包括单列统计信息和扩展统计信息的工作原理、实际应用场景和最佳实践。
1. 统计信息概述
查询规划器的核心任务是为每个 SQL 查询选择最高效的执行策略。这个决策过程严重依赖于对数据分布和查询选择性的准确估计。PostgreSQL 通过收集和维护详细的统计信息来支持这一过程。
INFO
统计信息的重要性
统计信息的准确性直接影响查询性能。不准确的统计信息可能导致:
- 选择错误的连接算法
- 不合理的索引使用策略
- 错误的内存分配决策
- 次优的执行顺序
2. 单列统计信息
2.1 基础统计信息
PostgreSQL 为每个表和索引维护基础的物理统计信息,存储在 pg_class
系统表中。
2.1.1 查看基础统计信息
sql
-- 查看表和索引的基础统计信息
SELECT relname, relkind, reltuples, relpages
FROM pg_class
WHERE relname LIKE 'tenk1%';
输出示例:
relname | relkind | reltuples | relpages
----------------------+---------+-----------+----------
tenk1 | r | 10000 | 345
tenk1_hundred | i | 10000 | 11
tenk1_thous_tenthous | i | 10000 | 30
tenk1_unique1 | i | 10000 | 30
tenk1_unique2 | i | 10000 | 30
(5 rows)
分析过程:
reltuples
:表或索引中的估计行数relpages
:占用的磁盘页数- 索引通常比表占用更少的磁盘页面
- 这些值由
VACUUM
、ANALYZE
和某些 DDL 命令更新
2.1.2 业务场景应用
在电商系统中,我们经常需要查询订单表:
sql
-- 创建一个模拟的订单表
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INTEGER,
order_date DATE,
total_amount DECIMAL(10,2),
status VARCHAR(20)
);
-- 插入测试数据
INSERT INTO orders (customer_id, order_date, total_amount, status)
SELECT
(random() * 10000)::INTEGER,
CURRENT_DATE - (random() * 365)::INTEGER,
(random() * 1000)::DECIMAL(10,2),
CASE WHEN random() < 0.8 THEN 'completed' ELSE 'pending' END
FROM generate_series(1, 100000);
-- 创建索引
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_date ON orders(order_date);
-- 更新统计信息
ANALYZE orders;
-- 查看统计信息
SELECT relname, relkind, reltuples, relpages
FROM pg_class
WHERE relname LIKE 'orders%' OR relname LIKE 'idx_orders%';
2.2 详细列统计信息
更详细的统计信息存储在 pg_statistic
系统表中,通过 pg_stats
视图可以更友好地查看。
2.2.1 查看列统计信息
sql
-- 查看订单表的列统计信息
SELECT attname, inherited, n_distinct,
array_to_string(most_common_vals, E'\n') as most_common_vals,
array_to_string(most_common_freqs, E'\n') as most_common_freqs
FROM pg_stats
WHERE tablename = 'orders';
输出示例:
attname | inherited | n_distinct | most_common_vals | most_common_freqs
-------------+-----------+------------+-------------------------+------------------
customer_id | f | 9876 | |
order_date | f | 365 | 2024-01-15 | 0.003
| | | 2024-02-20 | 0.0028
| | | 2024-03-10 | 0.0027
status | f | 2 | completed | 0.8
| | | pending | 0.2
total_amount| f | 45678 | |
2.2.2 统计信息配置
sql
-- 调整特定列的统计信息详细程度
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;
ALTER TABLE orders ALTER COLUMN customer_id SET STATISTICS 500;
-- 全局设置默认统计目标
SET default_statistics_target = 150;
-- 重新收集统计信息
ANALYZE orders;
TIP
统计信息优化建议
- 对于数据分布均匀的列,可以降低统计目标以节省空间
- 对于数据分布不规律的列,提高统计目标可以改善估计准确性
- 默认值 100 对大多数情况都足够
3. 扩展统计信息
当查询涉及多个相关联的列时,传统的单列统计信息往往无法提供准确的估计。PostgreSQL 的扩展统计信息功能可以捕获列间的相关性信息。
3.1 扩展统计信息类型
3.2 创建扩展统计信息对象
sql
-- 创建一个地址表示例
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
street VARCHAR(100),
city VARCHAR(50),
state VARCHAR(20),
zip_code VARCHAR(10),
country VARCHAR(30)
);
-- 插入测试数据(模拟真实的地址分布)
INSERT INTO addresses (street, city, state, zip_code, country)
SELECT
'Street ' || (random() * 1000)::INTEGER,
CASE
WHEN random() < 0.3 THEN 'New York'
WHEN random() < 0.5 THEN 'Los Angeles'
WHEN random() < 0.7 THEN 'Chicago'
ELSE 'Houston'
END,
CASE
WHEN city = 'New York' THEN 'NY'
WHEN city = 'Los Angeles' THEN 'CA'
WHEN city = 'Chicago' THEN 'IL'
ELSE 'TX'
END,
CASE
WHEN city = 'New York' THEN '100' || (random() * 99)::INTEGER
WHEN city = 'Los Angeles' THEN '900' || (random() * 99)::INTEGER
WHEN city = 'Chicago' THEN '606' || (random() * 99)::INTEGER
ELSE '770' || (random() * 99)::INTEGER
END,
'USA'
FROM generate_series(1, 50000);
-- 更新统计信息
ANALYZE addresses;
4. 函数依赖统计信息
4.1 概念理解
函数依赖描述了列之间的确定性关系。如果知道列 A 的值就能确定列 B 的值,那么 B 函数依赖于 A。
4.1.1 创建函数依赖统计信息
sql
-- 创建城市和邮政编码的函数依赖统计
CREATE STATISTICS addr_deps (dependencies) ON city, zip_code FROM addresses;
-- 收集统计信息
ANALYZE addresses;
-- 查看函数依赖关系
SELECT stxname, stxkeys, stxddependencies
FROM pg_statistic_ext
JOIN pg_statistic_ext_data ON (oid = stxoid)
WHERE stxname = 'addr_deps';
输出示例:
stxname | stxkeys | stxddependencies
----------+---------+---------------------------------------------
addr_deps| 2 4 | {"2 => 4": 1.000000, "4 => 2": 0.423130}
分析过程:
- 列 2(城市)完全决定列 4(邮政编码),系数为 1.0
- 列 4(邮政编码)在约 42% 的情况下决定列 2(城市)
- 这表明许多城市有多个邮政编码,但每个邮政编码只属于一个城市
4.1.2 查询优化实例
sql
-- 没有函数依赖统计信息的查询估计
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM addresses
WHERE city = 'New York' AND zip_code = '10001';
-- 创建函数依赖统计信息后的查询估计
-- 规划器会识别到 city 条件是冗余的,因为 zip_code 已经确定了 city
sql
-- 规划器独立估计每个条件的选择性
-- 可能严重低估结果集大小
SELECT * FROM addresses
WHERE city = 'New York' AND zip_code = '10001';
-- 估计: rows=50 (实际可能只有 1-2 行)
sql
-- 规划器理解函数依赖关系
-- 提供更准确的行数估计
SELECT * FROM addresses
WHERE city = 'New York' AND zip_code = '10001';
-- 估计: rows=1-2 (更接近实际值)
4.2 函数依赖的局限性
4.2.1 适用条件限制
函数依赖统计信息仅在以下情况下有效:
- 简单相等条件:
column = constant
- IN 子句:
column IN (const1, const2, ...)
sql
-- ✅ 支持的查询类型
SELECT * FROM addresses WHERE city = 'New York' AND zip_code = '10001';
SELECT * FROM addresses WHERE city IN ('New York', 'Chicago') AND zip_code = '10001';
-- ❌ 不支持的查询类型
SELECT * FROM addresses WHERE city = state; -- 列比较
SELECT * FROM addresses WHERE city LIKE 'New%'; -- 模式匹配
SELECT * FROM addresses WHERE zip_code > '10000'; -- 范围查询
4.2.2 兼容性假设问题
sql
-- 兼容条件 - 正确估计
SELECT * FROM addresses
WHERE city = 'New York' AND zip_code = '10001';
-- 不兼容条件 - 可能错误估计
SELECT * FROM addresses
WHERE city = 'New York' AND zip_code = '90210'; -- 洛杉矶的邮编
WARNING
函数依赖使用注意事项
规划器假设条件是兼容的,不会检测不兼容的条件组合。如果应用程序允许用户输入不兼容的条件,函数依赖可能不适用。
5. 多元 N-唯一值计数
5.1 概念和应用场景
N-唯一值统计信息帮助规划器准确估计多列组合的唯一值数量,这对 GROUP BY
和聚合查询的优化至关重要。
5.1.1 创建 N-唯一值统计信息
sql
-- 创建多列唯一值统计信息
CREATE STATISTICS addr_ndistinct (ndistinct) ON city, state, zip_code FROM addresses;
-- 收集统计信息
ANALYZE addresses;
-- 查看 N-唯一值统计信息
SELECT stxkeys AS k, stxdndistinct AS nd
FROM pg_statistic_ext
JOIN pg_statistic_ext_data ON (oid = stxoid)
WHERE stxname = 'addr_ndistinct';
输出示例:
-[ RECORD 1 ]------------------------------------------------------
k | 2 3 4
nd | {"2, 3": 4, "2, 4": 156, "3, 4": 156, "2, 3, 4": 156}
分析过程:
{2, 3}
(city, state):4 个唯一组合{2, 4}
(city, zip_code):156 个唯一组合{3, 4}
(state, zip_code):156 个唯一组合{2, 3, 4}
(city, state, zip_code):156 个唯一组合
5.1.2 GROUP BY 查询优化
sql
-- 没有 N-唯一值统计信息的聚合查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT city, state, COUNT(*)
FROM addresses
GROUP BY city, state;
-- 有 N-唯一值统计信息的聚合查询
-- 规划器能更准确估计分组数量,选择更合适的聚合算法
sql
-- 创建订单统计信息
CREATE STATISTICS order_stats (ndistinct)
ON customer_id, order_date, status FROM orders;
ANALYZE orders;
-- 优化后的分组查询
SELECT customer_id,
DATE_TRUNC('month', order_date) as month,
COUNT(*) as order_count,
SUM(total_amount) as total_sales
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id, DATE_TRUNC('month', order_date);
sql
-- 地区销售统计
SELECT state, city,
COUNT(*) as store_count,
AVG(daily_sales) as avg_daily_sales
FROM store_locations
GROUP BY state, city
ORDER BY state, avg_daily_sales DESC;
5.2 实际业务应用
5.2.1 电商平台商品分析
sql
-- 创建商品表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
category VARCHAR(50),
brand VARCHAR(50),
price_range VARCHAR(20),
rating INTEGER,
created_date DATE
);
-- 创建多维度统计信息
CREATE STATISTICS product_analysis (ndistinct)
ON category, brand, price_range FROM products;
ANALYZE products;
-- 多维度商品分析查询
SELECT category, brand, price_range,
COUNT(*) as product_count,
AVG(rating) as avg_rating
FROM products
GROUP BY category, brand, price_range
HAVING COUNT(*) > 10;
6. 多元 MCV 列表
6.1 最常见值列表的作用
多元 MCV(Most Common Values)列表存储列组合的最常见值,为多列条件查询提供精确的选择性估计。
6.1.1 创建 MCV 统计信息
sql
-- 创建城市和州的 MCV 统计信息
CREATE STATISTICS addr_mcv (mcv) ON city, state FROM addresses;
-- 收集统计信息
ANALYZE addresses;
-- 查看 MCV 内容
SELECT m.*
FROM pg_statistic_ext
JOIN pg_statistic_ext_data ON (oid = stxoid),
pg_mcv_list_items(stxdmcv) m
WHERE stxname = 'addr_mcv';
输出示例:
index | values | nulls | frequency | base_frequency
-------+------------------------+-------+-----------+----------------
0 | {New York, NY} | {f,f} | 0.300000 | 0.090000
1 | {Los Angeles, CA} | {f,f} | 0.200000 | 0.060000
2 | {Chicago, IL} | {f,f} | 0.200000 | 0.060000
3 | {Houston, TX} | {f,f} | 0.200000 | 0.060000
4 | {Miami, FL} | {f,f} | 0.100000 | 0.030000
分析过程:
frequency
:实际频率(在样本中的比例)base_frequency
:基于单列频率计算的理论频率- New York, NY 的实际频率(30%)远高于理论频率(9%)
6.1.2 查询选择性优化
sql
-- 多列条件查询优化
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM addresses
WHERE city = 'New York' AND state = 'NY';
-- 规划器使用 MCV 统计信息提供准确的行数估计
-- 避免了基于独立性假设的错误估计
6.2 复杂业务场景应用
6.2.1 用户行为分析
sql
-- 创建用户行为表
CREATE TABLE user_behavior (
user_id INTEGER,
device_type VARCHAR(20),
os_type VARCHAR(20),
browser VARCHAR(30),
action_type VARCHAR(30),
timestamp TIMESTAMP
);
-- 创建多维度 MCV 统计信息
CREATE STATISTICS user_behavior_mcv (mcv)
ON device_type, os_type, browser FROM user_behavior;
ANALYZE user_behavior;
-- 复杂的用户画像查询
SELECT device_type, os_type, browser,
COUNT(*) as user_count,
COUNT(DISTINCT user_id) as unique_users
FROM user_behavior
WHERE device_type = 'mobile'
AND os_type = 'iOS'
AND browser = 'Safari'
AND timestamp >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY device_type, os_type, browser;
7. 统计信息最佳实践
7.1 统计信息管理策略
7.1.1 自动化统计信息维护
sql
-- 配置自动统计信息收集
ALTER SYSTEM SET track_counts = on;
ALTER SYSTEM SET autovacuum = on;
ALTER SYSTEM SET log_autovacuum_min_duration = 0;
-- 重载配置
SELECT pg_reload_conf();
-- 检查自动统计信息设置
SELECT name, setting, context, short_desc
FROM pg_settings
WHERE name LIKE '%autovacuum%' OR name LIKE '%statistics%';
7.1.2 监控统计信息状态
sql
-- 创建统计信息监控视图
CREATE OR REPLACE VIEW stats_monitoring AS
SELECT
schemaname,
tablename,
attname,
n_distinct,
correlation,
most_common_vals[1:5] as top_5_values,
last_analyze,
last_autoanalyze
FROM pg_stats ps
JOIN pg_stat_user_tables pst ON ps.tablename = pst.relname
WHERE schemaname = 'public';
-- 查看过时的统计信息
SELECT tablename, last_analyze, last_autoanalyze,
CURRENT_TIMESTAMP - last_analyze as analyze_age
FROM pg_stat_user_tables
WHERE last_analyze < CURRENT_TIMESTAMP - INTERVAL '7 days'
OR last_analyze IS NULL;
7.2 性能优化指南
7.2.1 统计信息对象选择策略
7.2.2 统计信息创建原则
TIP
创建统计信息对象的指导原则
- 仅为实际使用的列组合创建:避免不必要的计算开销
- 优先处理性能关键查询:关注慢查询中的列组合
- 考虑数据变化频率:频繁变化的数据需要更频繁的统计信息更新
- 监控统计信息效果:通过查询计划分析验证改善效果
7.2.3 完整的优化示例
sql
-- 电商平台订单查询优化完整示例
-- 1. 创建测试表
CREATE TABLE order_analysis (
order_id SERIAL PRIMARY KEY,
customer_id INTEGER,
product_category VARCHAR(50),
order_date DATE,
shipping_city VARCHAR(50),
shipping_state VARCHAR(20),
order_status VARCHAR(20),
total_amount DECIMAL(10,2)
);
-- 2. 插入模拟数据
INSERT INTO order_analysis (customer_id, product_category, order_date,
shipping_city, shipping_state, order_status, total_amount)
SELECT
(random() * 5000)::INTEGER + 1,
(ARRAY['Electronics', 'Clothing', 'Books', 'Home', 'Sports'])[ceil(random() * 5)],
CURRENT_DATE - (random() * 730)::INTEGER,
(ARRAY['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'])[ceil(random() * 5)],
CASE
WHEN shipping_city = 'New York' THEN 'NY'
WHEN shipping_city = 'Los Angeles' THEN 'CA'
WHEN shipping_city = 'Chicago' THEN 'IL'
WHEN shipping_city = 'Houston' THEN 'TX'
ELSE 'AZ'
END,
(ARRAY['pending', 'processing', 'shipped', 'delivered', 'cancelled'])[ceil(random() * 5)],
(random() * 500 + 50)::DECIMAL(10,2)
FROM generate_series(1, 100000);
-- 3. 创建基础索引
CREATE INDEX idx_order_customer ON order_analysis(customer_id);
CREATE INDEX idx_order_date ON order_analysis(order_date);
CREATE INDEX idx_order_category ON order_analysis(product_category);
-- 4. 分析慢查询模式
-- 常见查询:按客户、类别、地区的订单分析
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT customer_id, product_category, shipping_state,
COUNT(*) as order_count,
SUM(total_amount) as total_sales
FROM order_analysis
WHERE order_date >= CURRENT_DATE - INTERVAL '90 days'
AND order_status IN ('delivered', 'shipped')
GROUP BY customer_id, product_category, shipping_state
HAVING COUNT(*) > 2;
-- 5. 创建针对性的扩展统计信息
-- 地址相关的函数依赖
CREATE STATISTICS order_geo_deps (dependencies)
ON shipping_city, shipping_state FROM order_analysis;
-- 分组分析的 N-唯一值
CREATE STATISTICS order_group_ndist (ndistinct)
ON customer_id, product_category, shipping_state FROM order_analysis;
-- 常见组合的 MCV
CREATE STATISTICS order_combo_mcv (mcv)
ON product_category, order_status FROM order_analysis;
-- 6. 收集统计信息
ANALYZE order_analysis;
-- 7. 验证优化效果
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT customer_id, product_category, shipping_state,
COUNT(*) as order_count,
SUM(total_amount) as total_sales
FROM order_analysis
WHERE order_date >= CURRENT_DATE - INTERVAL '90 days'
AND order_status IN ('delivered', 'shipped')
GROUP BY customer_id, product_category, shipping_state
HAVING COUNT(*) > 2;
-- 8. 监控统计信息使用情况
SELECT
stxname,
stxkind,
array_length(stxkeys, 1) as column_count,
(stxdependencies IS NOT NULL) as has_dependencies,
(stxdndistinct IS NOT NULL) as has_ndistinct,
(stxdmcv IS NOT NULL) as has_mcv
FROM pg_statistic_ext se
JOIN pg_statistic_ext_data sed ON se.oid = sed.stxoid
WHERE stxnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public');
7.3 故障排除和调试
7.3.1 诊断统计信息问题
sql
-- 检查统计信息收集状态
SELECT
schemaname,
tablename,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze,
vacuum_count,
autovacuum_count,
analyze_count,
autoanalyze_count
FROM pg_stat_user_tables
ORDER BY last_analyze DESC NULLS LAST;
-- 查看统计信息目标设置
SELECT
tablename,
attname,
attstattarget,
n_distinct,
correlation
FROM pg_stats
WHERE tablename = 'your_table_name'
ORDER BY abs(correlation) DESC;
7.3.2 性能问题排查流程
Details
统计信息相关性能问题排查步骤
确认统计信息是否最新
sqlSELECT tablename, last_analyze, n_tup_ins + n_tup_upd + n_tup_del as changes FROM pg_stat_user_tables WHERE n_tup_ins + n_tup_upd + n_tup_del > reltuples * 0.1;
检查查询计划的行数估计准确性
sqlEXPLAIN (ANALYZE, BUFFERS, VERBOSE) your_query; -- 比较 planned rows 和 actual rows
分析列间相关性
sql-- 检查是否需要扩展统计信息 SELECT attname, n_distinct, correlation FROM pg_stats WHERE tablename = 'problematic_table';
创建适当的扩展统计信息
sql-- 基于查询模式创建统计信息对象 CREATE STATISTICS stats_name (dependencies, ndistinct, mcv) ON col1, col2, col3 FROM table_name;
8. 总结
PostgreSQL 的统计信息系统是查询优化的基础,理解和正确使用统计信息对于数据库性能至关重要:
8.1 关键要点
- 单列统计信息提供基础的数据分布信息,是所有查询优化的起点
- 扩展统计信息解决多列相关性问题,显著改善复杂查询的性能
- 函数依赖适用于存在确定性关系的列组合
- N-唯一值计数优化分组和聚合查询
- MCV 列表提供精确的多列条件选择性估计
8.2 实施建议
- 定期执行
ANALYZE
保持统计信息最新 - 针对性创建扩展统计信息对象,避免过度创建
- 监控查询性能,及时调整统计信息策略
- 结合实际业务场景选择合适的统计信息类型
通过合理配置和维护统计信息,可以显著提升 PostgreSQL 查询性能,为业务应用提供强有力的数据库支撑。