Skip to content

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:占用的磁盘页数
  • 索引通常比表占用更少的磁盘页面
  • 这些值由 VACUUMANALYZE 和某些 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 适用条件限制

函数依赖统计信息仅在以下情况下有效:

  1. 简单相等条件column = constant
  2. 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

创建统计信息对象的指导原则

  1. 仅为实际使用的列组合创建:避免不必要的计算开销
  2. 优先处理性能关键查询:关注慢查询中的列组合
  3. 考虑数据变化频率:频繁变化的数据需要更频繁的统计信息更新
  4. 监控统计信息效果:通过查询计划分析验证改善效果

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

统计信息相关性能问题排查步骤

  1. 确认统计信息是否最新

    sql
    SELECT 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;
  2. 检查查询计划的行数估计准确性

    sql
    EXPLAIN (ANALYZE, BUFFERS, VERBOSE) your_query;
    -- 比较 planned rows 和 actual rows
  3. 分析列间相关性

    sql
    -- 检查是否需要扩展统计信息
    SELECT attname, n_distinct, correlation
    FROM pg_stats
    WHERE tablename = 'problematic_table';
  4. 创建适当的扩展统计信息

    sql
    -- 基于查询模式创建统计信息对象
    CREATE STATISTICS stats_name (dependencies, ndistinct, mcv)
    ON col1, col2, col3 FROM table_name;

8. 总结

PostgreSQL 的统计信息系统是查询优化的基础,理解和正确使用统计信息对于数据库性能至关重要:

8.1 关键要点

  1. 单列统计信息提供基础的数据分布信息,是所有查询优化的起点
  2. 扩展统计信息解决多列相关性问题,显著改善复杂查询的性能
  3. 函数依赖适用于存在确定性关系的列组合
  4. N-唯一值计数优化分组和聚合查询
  5. MCV 列表提供精确的多列条件选择性估计

8.2 实施建议

  • 定期执行 ANALYZE 保持统计信息最新
  • 针对性创建扩展统计信息对象,避免过度创建
  • 监控查询性能,及时调整统计信息策略
  • 结合实际业务场景选择合适的统计信息类型

通过合理配置和维护统计信息,可以显著提升 PostgreSQL 查询性能,为业务应用提供强有力的数据库支撑。