PostgreSQL教程FG282-PG分区表实战:海量数据存储优化
本文档风哥主要介绍PostgreSQL分区表的实战应用,包括分区表的创建、管理、性能优化等内容。风哥教程参考PostgreSQL官方文档Partitioning内容,适合处理海量数据的企业级应用场景。更多视频教程www.fgedu.net.cn
Part01-基础概念与理论知识
1.1 PostgreSQL分区表的概念
PostgreSQL分区表是将一个大表分割成多个较小的子表(称为分区)的技术,每个分区都是一个独立的表,但从外部看仍然是一个单一的表。分区表的主要特点:
- 将大表逻辑上分割成多个小表
- 每个分区存储一部分数据
- 查询时只访问相关分区,提高查询性能
- 维护操作(如VACUUM)可以针对单个分区进行
- 支持数据的快速删除和归档
分区表适用于存储大量数据的表,特别是那些数据可以按照某种规则(如时间、范围、列表等)进行逻辑分割的场景。
1.2 分区表类型
PostgreSQL支持多种分区表类型:
1. 范围分区(Range Partitioning):根据指定的范围值进行分区
– 适用于时间序列数据
– 适用于连续范围的数据
2. 列表分区(List Partitioning):根据指定的离散值进行分区
– 适用于类别数据
– 适用于有限的离散值
3. 哈希分区(Hash Partitioning):根据哈希函数的结果进行分区
– 适用于均匀分布数据
– 适用于随机数据
4. 复合分区:结合多种分区类型
– 例如:先按范围分区,再按列表分区
– 适用于复杂的数据分布
# 分区表结构
– 分区表:主表,定义分区策略
– 分区:子表,存储实际数据
– 分区键:用于确定数据应该存储在哪个分区的列
– 分区边界:定义每个分区的范围或值
1.3 分区表的优势
PostgreSQL分区表的优势:
1. 查询性能:只扫描相关分区,减少I/O
2. 维护性能:维护操作可以针对单个分区
3. 索引性能:每个分区的索引更小,查询更快
4. 写入性能:可以并行写入不同分区
# 管理优势
1. 数据归档:可以轻松删除或归档旧数据
2. 数据加载:可以快速加载大量数据到新分区
3. 备份恢复:可以针对单个分区进行备份和恢复
4. 存储管理:可以将不同分区存储在不同表空间
# 可扩展性优势
1. 水平扩展:可以通过添加新分区来扩展存储
2. 负载均衡:可以将分区分布在不同存储设备上
3. 并行处理:支持分区级并行操作
Part02-生产环境规划与建议
2.1 分区表规划
在实施分区表前,需要进行详细的规划:
1. 数据量评估:估算表的大小和增长趋势
2. 数据分布:分析数据的分布特点
3. 查询模式:分析常见的查询类型和条件
4. 分区策略:选择合适的分区类型和键
5. 分区数量:确定合适的分区数量
6. 存储规划:规划分区的存储位置
# 数据量估算
– 当前数据量:表的大小和行数
– 数据增长:每年/每月的数据增长速率
– 预期数据量:未来3-5年的预期数据量
# 查询模式分析
– 时间范围查询:如按日期查询
– 范围查询:如按数值范围查询
– 等值查询:如按类别查询
– 连接查询:与其他表的连接操作
– 聚合查询:如统计分析
# 维护需求分析
– 数据归档:旧数据的归档策略
– 数据加载:新数据的加载频率和方式
– 索引维护:索引的重建和优化
– 备份恢复:备份和恢复的策略
2.2 分区策略选择
分区策略选择建议:
– 时间序列数据:范围分区(按日期)
– 类别数据:列表分区(按类别)
– 随机分布数据:哈希分区
– 复杂数据分布:复合分区
# 分区键选择
– 选择查询中最常用的条件列
– 选择数据分布均匀的列
– 选择基数适当的列
– 避免使用频繁更新的列
# 分区粒度选择
– 时间分区:按天、周、月、季度、年
– 范围分区:根据数据分布确定范围大小
– 列表分区:根据类别数量确定分区数量
– 哈希分区:根据数据量和性能需求确定分区数量
# 分区数量建议
– 不要创建过多分区(建议不超过1000个)
– 每个分区的大小适中(建议1-10GB)
– 考虑管理和维护的复杂性
2.3 性能考虑因素
分区表性能考虑因素:
- 存储设备:使用高性能存储,如SSD,特别是对于频繁访问的分区
- 内存配置:增加shared_buffers以提高分区表的缓存效率
- 索引策略:为每个分区创建适当的索引
- 查询优化:确保查询使用分区剪枝(partition pruning)
- 维护计划:定期对分区进行VACUUM和ANALYZE操作
Part03-生产环境项目实施方案
3.1 范围分区
3.1.1 创建范围分区表
CREATE TABLE fgedu_fgfgfgsales (
id SERIAL,
sale_date DATE NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id, sale_date)
) PARTITION BY RANGE (sale_date);
# 创建分区
CREATE TABLE fgedu_fgfgfgsales_2026_q1 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-01-01’) TO (‘2026-04-01’);
CREATE TABLE fgedu_fgfgfgsales_2026_q2 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-04-01’) TO (‘2026-07-01’);
CREATE TABLE fgedu_fgfgfgsales_2026_q3 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-07-01’) TO (‘2026-10-01’);
CREATE TABLE fgedu_fgfgfgsales_2026_q4 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-10-01’) TO (‘2027-01-01’);
# 为每个分区创建索引
CREATE INDEX idx_fgedu_fgfgfgsales_2026_q1_product_id ON fgedu_fgfgfgsales_2026_q1 (product_id);
CREATE INDEX idx_fgedu_fgfgfgsales_2026_q2_product_id ON fgedu_fgfgfgsales_2026_q2 (product_id);
CREATE INDEX idx_fgedu_fgfgfgsales_2026_q3_product_id ON fgedu_fgfgfgsales_2026_q3 (product_id);
CREATE INDEX idx_fgedu_fgfgfgsales_2026_q4_product_id ON fgedu_fgfgfgsales_2026_q4 (product_id);
# 插入测试数据
INSERT INTO fgedu_fgfgfgsales (sale_date, product_id, quantity, amount)
VALUES
(‘2026-01-15’, 1, 10, 100.00),
(‘2026-02-20’, 2, 5, 50.00),
(‘2026-04-10’, 1, 15, 150.00),
(‘2026-07-05’, 3, 8, 80.00),
(‘2026-10-20’, 2, 12, 120.00);
# 查询数据
SELECT * FROM fgedu_fgfgfgsales;
# 查看分区情况
SELECT tableoid::regclass AS partition, *
FROM fgedu_fgfgfgsales;
3.1.2 管理范围分区
CREATE TABLE fgedu_fgfgfgsales_2027_q1 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2027-01-01’) TO (‘2027-04-01’);
# 为新分区创建索引
CREATE INDEX idx_fgedu_fgfgfgsales_2027_q1_product_id ON fgedu_fgfgfgsales_2027_q1 (product_id);
# 删除旧分区
DROP TABLE fgedu_fgfgfgsales_2026_q1;
# 合并分区
— 先删除现有分区
DROP TABLE fgedu_fgfgfgsales_2026_q2, fgedu_fgfgfgsales_2026_q3;
— 创建合并后的分区
CREATE TABLE fgedu_fgfgfgsales_2026_h2 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-04-01’) TO (‘2026-10-01’);
# 拆分分区
— 先删除现有分区
DROP TABLE fgedu_fgfgfgsales_2026_q4;
— 创建拆分后的分区
CREATE TABLE fgedu_fgfgfgsales_2026_oct PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-10-01’) TO (‘2026-11-01’);
CREATE TABLE fgedu_fgfgfgsales_2026_nov PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-11-01’) TO (‘2026-12-01’);
CREATE TABLE fgedu_fgfgfgsales_2026_dec PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-12-01’) TO (‘2027-01-01’);
3.2 列表分区
3.2.1 创建列表分区表
CREATE TABLE fgedu_orders (
id SERIAL,
order_date DATE NOT NULL,
customer_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id, status)
) PARTITION BY LIST (status);
# 创建分区
CREATE TABLE fgedu_orders_pending PARTITION OF fgedu_orders
FOR VALUES IN (‘pending’);
CREATE TABLE fgedu_orders_processing PARTITION OF fgedu_orders
FOR VALUES IN (‘processing’);
CREATE TABLE fgedu_orders_shipped PARTITION OF fgedu_orders
FOR VALUES IN (‘shipped’);
CREATE TABLE fgedu_orders_delivered PARTITION OF fgedu_orders
FOR VALUES IN (‘delivered’);
CREATE TABLE fgedu_orders_cancelled PARTITION OF fgedu_orders
FOR VALUES IN (‘cancelled’);
# 为每个分区创建索引
CREATE INDEX idx_fgedu_orders_pending_customer_id ON fgedu_orders_pending (customer_id);
CREATE INDEX idx_fgedu_orders_processing_customer_id ON fgedu_orders_processing (customer_id);
CREATE INDEX idx_fgedu_orders_shipped_customer_id ON fgedu_orders_shipped (customer_id);
CREATE INDEX idx_fgedu_orders_delivered_customer_id ON fgedu_orders_delivered (customer_id);
CREATE INDEX idx_fgedu_orders_cancelled_customer_id ON fgedu_orders_cancelled (customer_id);
# 插入测试数据
INSERT INTO fgedu_orders (order_date, customer_id, status, total_amount)
VALUES
(‘2026-04-01’, 1, ‘pending’, 100.00),
(‘2026-04-02’, 2, ‘processing’, 150.00),
(‘2026-04-03’, 3, ‘shipped’, 200.00),
(‘2026-04-04’, 1, ‘delivered’, 100.00),
(‘2026-04-05’, 2, ‘cancelled’, 150.00);
# 查询数据
SELECT * FROM fgedu_orders;
# 查看分区情况
SELECT tableoid::regclass AS partition, *
FROM fgedu_orders;
3.2.2 管理列表分区
CREATE TABLE fgedu_orders_refunded PARTITION OF fgedu_orders
FOR VALUES IN (‘refunded’);
# 为新分区创建索引
CREATE INDEX idx_fgedu_orders_refunded_customer_id ON fgedu_orders_refunded (customer_id);
# 修改分区
— 先删除现有分区
DROP TABLE fgedu_orders_cancelled;
— 创建新的分区,包含多个值
CREATE TABLE fgedu_orders_cancelled_refunded PARTITION OF fgedu_orders
FOR VALUES IN (‘cancelled’, ‘refunded’);
# 处理未分区的数据
— 创建默认分区
CREATE TABLE fgedu_orders_default PARTITION OF fgedu_orders
DEFAULT;
# 为默认分区创建索引
CREATE INDEX idx_fgedu_orders_default_customer_id ON fgedu_orders_default (customer_id);
# 插入未分区的数据
INSERT INTO fgedu_orders (order_date, customer_id, status, total_amount)
VALUES (‘2026-04-06’, 3, ‘returned’, 200.00);
# 查询默认分区的数据
SELECT * FROM fgedu_orders_default;
3.3 哈希分区
3.3.1 创建哈希分区表
CREATE TABLE fgedu_fgedus (
id SERIAL,
fgeduname VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
registration_date DATE NOT NULL,
PRIMARY KEY (id)
) PARTITION BY HASH (id);
# 创建分区
CREATE TABLE fgedu_fgedus_part1 PARTITION OF fgedu_fgedus
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE fgedu_fgedus_part2 PARTITION OF fgedu_fgedus
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE fgedu_fgedus_part3 PARTITION OF fgedu_fgedus
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE fgedu_fgedus_part4 PARTITION OF fgedu_fgedus
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
# 为每个分区创建索引
CREATE INDEX idx_fgedu_fgedus_part1_email ON fgedu_fgedus_part1 (email);
CREATE INDEX idx_fgedu_fgedus_part2_email ON fgedu_fgedus_part2 (email);
CREATE INDEX idx_fgedu_fgedus_part3_email ON fgedu_fgedus_part3 (email);
CREATE INDEX idx_fgedu_fgedus_part4_email ON fgedu_fgedus_part4 (email);
# 插入测试数据
INSERT INTO fgedu_fgedus (fgeduname, email, registration_date)
VALUES
(‘fgedu1’, ‘fgedu1@fgedu.net.cn’, ‘2026-04-01’),
(‘fgedu2’, ‘fgedu2@fgedu.net.cn’, ‘2026-04-02’),
(‘fgedu3’, ‘fgedu3@fgedu.net.cn’, ‘2026-04-03’),
(‘fgedu4’, ‘fgedu4@fgedu.net.cn’, ‘2026-04-04’),
(‘fgedu5’, ‘fgedu5@fgedu.net.cn’, ‘2026-04-05’);
# 查询数据
SELECT * FROM fgedu_fgedus;
# 查看分区情况
SELECT tableoid::regclass AS partition, *
FROM fgedu_fgedus;
3.3.2 管理哈希分区
— 注意:哈希分区的数量通常在创建时确定,增加分区需要重新创建表
— 以下是一个重新分区的示例
— 1. 创建新的分区表
CREATE TABLE fgedu_fgedus_new (
id SERIAL,
fgeduname VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
registration_date DATE NOT NULL,
PRIMARY KEY (id)
) PARTITION BY HASH (id);
— 2. 创建更多分区
CREATE TABLE fgedu_fgedus_new_part1 PARTITION OF fgedu_fgedus_new
FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE fgedu_fgedus_new_part2 PARTITION OF fgedu_fgedus_new
FOR VALUES WITH (MODULUS 8, REMAINDER 1);
— 创建其他分区…
— 3. 迁移数据
INSERT INTO fgedu_fgedus_new SELECT * FROM fgedu_fgedus;
— 4. 重命名表
ALTER TABLE fgedu_fgedus RENAME TO fgedu_fgedus_old;
ALTER TABLE fgedu_fgedus_new RENAME TO fgedu_fgedus;
— 5. 为新分区创建索引
CREATE INDEX idx_fgedu_fgedus_new_part1_email ON fgedu_fgedus_new_part1 (email);
— 创建其他索引…
— 6. 删除旧表
DROP TABLE fgedu_fgedus_old;
Part04-生产案例与实战讲解
4.1 分区表实战案例
4.1.1 时间序列数据分区
# 1. 数据库设计
CREATE TABLE fgedu_fgfgfgsales (
id SERIAL,
sale_date TIMESTAMP NOT NULL,
product_id INTEGER NOT NULL,
customer_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id, sale_date)
) PARTITION BY RANGE (sale_date);
# 2. 创建分区函数(自动创建分区)
CREATE OR REPLACE FUNCTION create_fgfgfgsales_partition(p_year INTEGER, p_month INTEGER)
RETURNS VOID AS $$
DECLARE
v_partition_name TEXT;
v_start_date TIMESTAMP;
v_end_date TIMESTAMP;
BEGIN
v_partition_name := ‘fgedu_fgfgfgsales_’ || p_year || ‘_’ || LPAD(p_month::TEXT, 2, ‘0’);
v_start_date := make_timestamp(p_year, p_month, 1, 0, 0, 0);
v_end_date := v_start_date + INTERVAL ‘1 month’;
EXECUTE format(
‘CREATE TABLE fgedu_IF NOT EXISTS %I PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (%L) TO (%L)’,
v_partition_name, v_start_date, v_end_date
);
EXECUTE format(
‘CREATE INDEX IF NOT EXISTS %I ON %I (product_id)’,
v_partition_name || ‘_product_id_idx’, v_partition_name
);
EXECUTE format(
‘CREATE INDEX IF NOT EXISTS %I ON %I (customer_id)’,
v_partition_name || ‘_customer_id_idx’, v_partition_name
);
END;
$$ LANGUAGE plpgsql;
# 3. 创建未来6个月的分区
DO $$
DECLARE
i INTEGER;
BEGIN
FOR i IN 0..5 LOOP
PERFORM create_fgfgfgsales_partition(EXTRACT(YEAR FROM current_date + INTERVAL ‘1 month’ * i)::INTEGER,
EXTRACT(MONTH FROM current_date + INTERVAL ‘1 month’ * i)::INTEGER);
END LOOP;
END;
$$;
# 4. 查看分区
\dt fgedu_fgfgfgsales*
# 5. 插入测试数据
INSERT INTO fgedu_fgfgfgsales (sale_date, product_id, customer_id, quantity, unit_price, total_amount)
VALUES
(‘2026-04-01 10:00:00’, 1, 1, 2, 50.00, 100.00),
(‘2026-04-15 14:30:00’, 2, 2, 1, 100.00, 100.00),
(‘2026-05-01 09:00:00’, 1, 3, 3, 50.00, 150.00),
(‘2026-05-10 11:00:00’, 3, 1, 1, 200.00, 200.00);
# 6. 查询示例
— 查询特定月份的数据
SELECT * FROM fgedu_fgfgfgsales WHERE sale_date BETWEEN ‘2026-04-01’ AND ‘2026-04-30’;
— 查看查询执行计划(验证分区剪枝)
EXPLAIN ANALYZE SELECT * FROM fgedu_fgfgfgsales WHERE sale_date BETWEEN ‘2026-04-01’ AND ‘2026-04-30’;
— 聚合查询
SELECT product_id, SUM(total_amount) AS total_fgfgfgsales
FROM fgedu_fgfgfgsales
WHERE sale_date BETWEEN ‘2026-04-01’ AND ‘2026-05-31’
GROUP BY product_id
ORDER BY total_fgfgfgsales DESC;
# 7. 自动分区维护
— 创建定时任务,每月创建下个月的分区
CREATE OR REPLACE FUNCTION schedule_partition_creation()
RETURNS VOID AS $$
BEGIN
PERFORM create_fgfgfgsales_partition(EXTRACT(YEAR FROM current_date + INTERVAL ‘1 month’)::INTEGER,
EXTRACT(MONTH FROM current_date + INTERVAL ‘1 month’)::INTEGER);
END;
$$ LANGUAGE plpgsql;
— 使用pg_cron扩展设置定时任务
— 每月1日执行
— SELECT cron.schedule(‘0 0 1 * *’, ‘SELECT schedule_partition_creation();’);
4.1.2 日志数据分区
# 1. 数据库设计
CREATE TABLE fgedu_fgapplication_logs (
id SERIAL,
log_time TIMESTAMP NOT NULL,
level VARCHAR(20) NOT NULL,
message TEXT NOT NULL,
source VARCHAR(100) NOT NULL,
fgedu_id INTEGER,
PRIMARY KEY (id, log_time)
) PARTITION BY RANGE (log_time);
# 2. 创建分区函数
CREATE OR REPLACE FUNCTION create_log_partition(p_date DATE)
RETURNS VOID AS $$
DECLARE
v_partition_name TEXT;
v_start_date TIMESTAMP;
v_end_date TIMESTAMP;
BEGIN
v_partition_name := ‘fgedu_fgapplication_logs_’ || to_char(p_date, ‘YYYYMMDD’);
v_start_date := p_date::TIMESTAMP;
v_end_date := v_start_date + INTERVAL ‘1 day’;
EXECUTE format(
‘CREATE TABLE fgedu_IF NOT EXISTS %I PARTITION OF fgedu_fgapplication_logs
FOR VALUES FROM (%L) TO (%L)’,
v_partition_name, v_start_date, v_end_date
);
EXECUTE format(
‘CREATE INDEX IF NOT EXISTS %I ON %I (level)’,
v_partition_name || ‘_level_idx’, v_partition_name
);
EXECUTE format(
‘CREATE INDEX IF NOT EXISTS %I ON %I (source)’,
v_partition_name || ‘_source_idx’, v_partition_name
);
END;
$$ LANGUAGE plpgsql;
# 3. 创建未来7天的分区
DO $$
DECLARE
i INTEGER;
BEGIN
FOR i IN 0..6 LOOP
PERFORM create_log_partition(current_date + i);
END LOOP;
END;
$$;
# 4. 插入测试数据
INSERT INTO fgedu_fgapplication_logs (log_time, level, message, source, fgedu_id)
VALUES
(‘2026-04-02 10:00:00’, ‘INFO’, ‘User logged in’, ‘auth’, 1),
(‘2026-04-02 10:30:00’, ‘ERROR’, ‘Database connection failed’, ‘db’, NULL),
(‘2026-04-03 09:00:00’, ‘INFO’, ‘Application started’, ‘fgapp’, NULL),
(‘2026-04-03 11:00:00’, ‘WARNING’, ‘Disk space low’, ‘system’, NULL);
# 5. 查询示例
— 查询特定日期的日志
SELECT * FROM fgedu_fgapplication_logs WHERE log_time BETWEEN ‘2026-04-02’ AND ‘2026-04-02 23:59:59’;
— 查询错误级别日志
SELECT * FROM fgedu_fgapplication_logs WHERE level = ‘ERROR’;
— 按来源统计日志数量
SELECT source, COUNT(*) AS log_count
FROM fgedu_fgapplication_logs
WHERE log_time BETWEEN ‘2026-04-01’ AND ‘2026-04-03’
GROUP BY source
ORDER BY log_count DESC;
# 6. 清理旧日志
— 删除7天前的分区
DO $$
DECLARE
v_partition_name TEXT;
v_date DATE;
BEGIN
v_date := current_date – INTERVAL ‘7 days’;
v_partition_name := ‘fgedu_fgapplication_logs_’ || to_char(v_date, ‘YYYYMMDD’);
EXECUTE format(‘DROP TABLE IF EXISTS %I’, v_partition_name);
END;
$$;
4.2 分区表性能优化
4.2.1 查询优化
# 1. 使用分区剪枝
— 确保查询条件包含分区键
— 好的查询(使用分区剪枝)
EXPLAIN ANALYZE SELECT * FROM fgedu_fgfgfgsales WHERE sale_date BETWEEN ‘2026-04-01’ AND ‘2026-04-30’;
— 不好的查询(不使用分区剪枝)
EXPLAIN ANALYZE SELECT * FROM fgedu_fgfgfgsales WHERE product_id = 1;
# 2. 索引优化
— 为每个分区创建适当的索引
— 为分区键创建索引(如果频繁用于查询)
CREATE INDEX idx_fgedu_fgfgfgsales_sale_date ON fgedu_fgfgfgsales (sale_date);
— 为常用查询列创建索引
CREATE INDEX idx_fgedu_fgfgfgsales_product_id ON fgedu_fgfgfgsales (product_id);
# 3. 聚合查询优化
— 使用并行查询
SET max_parallel_workers_per_gather = 4;
— 优化聚合查询
EXPLAIN ANALYZE SELECT product_id, SUM(total_amount) AS total_fgfgfgsales
FROM fgedu_fgfgfgsales
WHERE sale_date BETWEEN ‘2026-04-01’ AND ‘2026-06-30’
GROUP BY product_id
ORDER BY total_fgfgfgsales DESC;
# 4. 连接查询优化
— 确保连接条件包含分区键
EXPLAIN ANALYZE SELECT s.*, p.name
FROM fgedu_fgfgfgsales s
JOIN fgedu_products p ON s.product_id = p.id
WHERE s.sale_date BETWEEN ‘2026-04-01’ AND ‘2026-04-30’;
# 5. 限制结果集
— 使用LIMIT限制返回结果
SELECT * FROM fgedu_fgfgfgsales WHERE sale_date BETWEEN ‘2026-04-01’ AND ‘2026-04-30’ LIMIT 100;
4.2.2 存储优化
# 1. 表空间管理
— 为不同分区指定不同表空间
— 创建表空间
CREATE TABLESPACE fast_ssd LOCATION ‘/path/to/ssd’;
CREATE TABLESPACE archive_hdd LOCATION ‘/path/to/hdd’;
— 创建分区时指定表空间
CREATE TABLE fgedu_fgfgfgsales_2026_q1 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-01-01’) TO (‘2026-04-01’)
TABLESPACE archive_hdd;
CREATE TABLE fgedu_fgfgfgsales_2026_q2 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2026-04-01’) TO (‘2026-07-01’)
TABLESPACE fast_ssd;
# 2. 压缩存储
— 为分区启用压缩
ALTER TABLE fgedu_fgfgfgsales_2026_q1 SET (autovacuum_enabled = true, toast.compress = ‘pglz’);
# 3. 分区维护
— 定期清理旧分区
DO $$
DECLARE
v_partition_name TEXT;
v_date DATE;
BEGIN
FOR i IN 12..24 LOOP
v_date := current_date – INTERVAL ‘1 month’ * i;
v_partition_name := ‘fgedu_fgfgfgsales_’ || to_char(v_date, ‘YYYY_MM’);
EXECUTE format(‘DROP TABLE IF EXISTS %I’, v_partition_name);
END LOOP;
END;
$$;
# 4. 批量操作
— 批量插入数据
COPY fgedu_fgfgfgsales FROM ‘/path/to/fgfgfgsales_data.csv’ DELIMITER ‘,’ CSV HEADER;
— 批量更新数据
UPDATE fgedu_fgfgfgsales SET total_amount = quantity * unit_price
WHERE sale_date BETWEEN ‘2026-04-01’ AND ‘2026-04-30’;
# 5. 分区合并
— 合并多个小分区
CREATE TABLE fgedu_fgfgfgsales_2025 PARTITION OF fgedu_fgfgfgsales
FOR VALUES FROM (‘2025-01-01’) TO (‘2026-01-01’);
— 迁移数据
INSERT INTO fgedu_fgfgfgsales_2025 SELECT * FROM fgedu_fgfgfgsales WHERE sale_date BETWEEN ‘2025-01-01’ AND ‘2025-12-31’;
— 删除旧分区
DROP TABLE fgedu_fgfgfgsales_2025_q1, fgedu_fgfgfgsales_2025_q2, fgedu_fgfgfgsales_2025_q3, fgedu_fgfgfgsales_2025_q4;
4.2.3 维护优化
# 1. 定期VACUUM和ANALYZE
— 对所有分区执行VACUUM和ANALYZE
DO $$
DECLARE
v_partition RECORD;
BEGIN
FOR v_partition IN
SELECT inhrelid::regclass AS partition
FROM pg_inherits
WHERE inhparent = ‘fgedu_fgfgfgsales’::regclass
LOOP
EXECUTE format(‘VACUUM ANALYZE %I’, v_partition.partition);
END LOOP;
END;
$$;
# 2. 索引重建
— 定期重建索引
DO $$
DECLARE
v_partition RECORD;
BEGIN
FOR v_partition IN
SELECT inhrelid::regclass AS partition
FROM pg_inherits
WHERE inhparent = ‘fgedu_fgfgfgsales’::regclass
LOOP
EXECUTE format(‘REINDEX TABLE %I’, v_partition.partition);
END LOOP;
END;
$$;
# 3. 统计信息更新
— 手动更新统计信息
ANALYZE fgedu_fgfgfgsales;
# 4. 监控分区大小
— 查看各分区大小
SELECT
inhrelid::regclass AS partition,
pg_size_pretty(pg_relation_size(inhrelid)) AS size
FROM
pg_inherits
WHERE
inhparent = ‘fgedu_fgfgfgsales’::regclass
ORDER BY
size DESC;
# 5. 监控分区使用情况
— 查看各分区的记录数
SELECT
inhrelid::regclass AS partition,
count(*) AS row_count
FROM
pg_inherits
JOIN
pg_class ON inhrelid = oid
JOIN
pg_namespace ON pg_class.relnamespace = pg_namespace.oid
WHERE
inhparent = ‘fgedu_fgfgfgsales’::regclass
GROUP BY
inhrelid
ORDER BY
row_count DESC;
4.3 分区表常见问题处理
4.3.1 常见错误及解决方法
# 错误信息:ERROR: cannot update partition key column “sale_date”
# 解决方法:
– 避免更新分区键
– 如果需要更新分区键,使用DELETE + INSERT替代
# 2. 分区不存在
# 错误信息:ERROR: no partition of relation “fgedu_fgfgfgsales” found for row
# 解决方法:
– 创建默认分区
– 自动创建分区的机制
– 在插入前检查分区是否存在
# 3. 分区剪枝不生效
# 问题:查询不使用分区剪枝,扫描所有分区
# 解决方法:
– 确保查询条件包含分区键
– 确保分区键的数据类型正确
– 运行ANALYZE更新统计信息
# 4. 分区过多
# 问题:创建了过多分区,导致管理困难
# 解决方法:
– 合并小分区
– 调整分区粒度
– 定期清理旧分区
# 5. 索引膨胀
# 问题:分区表索引占用空间过大
# 解决方法:
– 定期重建索引
– 优化索引策略
– 使用部分索引
# 6. 性能下降
# 问题:分区表查询性能下降
# 解决方法:
– 检查分区剪枝是否生效
– 优化查询语句
– 维护索引和统计信息
– 调整内存配置
4.3.2 分区表监控
# 1. 监控分区数量
SELECT
inhparent::regclass AS parent_table,
count(*) AS partition_count
FROM
pg_inherits
GROUP BY
inhparent
ORDER BY
partition_count DESC;
# 2. 监控分区大小
SELECT
inhparent::regclass AS parent_table,
inhrelid::regclass AS partition,
pg_size_pretty(pg_relation_size(inhrelid)) AS size
FROM
pg_inherits
ORDER BY
parent_table, size DESC;
# 3. 监控分区使用情况
SELECT
inhparent::regclass AS parent_table,
inhrelid::regclass AS partition,
count(*) AS row_count
FROM
pg_inherits
JOIN
pg_class ON inhrelid = oid
JOIN
pg_namespace ON pg_class.relnamespace = pg_namespace.oid
GROUP BY
inhparent, inhrelid
ORDER BY
parent_table, row_count DESC;
# 4. 监控分区查询性能
SELECT
query,
calls,
total_exec_time,
mean_exec_time
FROM
pg_stat_statements
WHERE
query LIKE ‘%fgedu_fgfgfgsales%’
ORDER BY
total_exec_time DESC
LIMIT 10;
# 5. 监控分区维护操作
SELECT
usename,
fgapplication_name,
query
FROM
pg_stat_activity
WHERE
query LIKE ‘%VACUUM%’ OR
query LIKE ‘%ANALYZE%’ OR
query LIKE ‘%REINDEX%’;
Part05-风哥经验总结与分享
5.1 分区表最佳实践
PostgreSQL分区表最佳实践:
- 选择合适的分区策略:根据数据特点和查询模式选择合适的分区类型
- 合理设计分区键:选择查询频繁、分布均匀的列作为分区键
- 适当的分区粒度:避免创建过多或过少的分区
- 自动分区管理:实现自动化的分区创建和清理机制
- 为每个分区创建索引:根据查询需求为每个分区创建适当的索引
- 定期维护:定期执行VACUUM、ANALYZE和REINDEX操作
- 监控分区表:定期监控分区表的使用情况和性能
- 存储管理:将不同分区存储在不同表空间,优化存储资源
5.2 分区表实施检查清单
## 规划阶段
– [ ] 评估数据量和增长趋势
– [ ] 分析查询模式和数据分布
– [ ] 选择合适的分区类型和键
– [ ] 确定分区粒度和数量
– [ ] 规划存储策略
## 实施阶段
– [ ] 创建分区表和分区
– [ ] 为每个分区创建适当的索引
– [ ] 实现分区创建和管理的自动化
– [ ] 测试分区表的功能和性能
– [ ] 迁移现有数据到分区表
## 优化阶段
– [ ] 优化查询语句,确保使用分区剪枝
– [ ] 调整内存和存储配置
– [ ] 优化索引策略
– [ ] 监控和分析性能指标
## 维护阶段
– [ ] 定期创建新分区
– [ ] 定期清理旧分区
– [ ] 定期执行VACUUM和ANALYZE
– [ ] 定期重建索引
– [ ] 监控分区表的使用情况
## 扩展阶段
– [ ] 考虑分区表的水平扩展
– [ ] 评估分区策略的有效性
– [ ] 调整分区策略以适应业务变化
– [ ] 考虑使用分区表的高级特性
5.3 分区表工具推荐
PostgreSQL分区表相关工具推荐:
- pg_partman:PostgreSQL分区管理工具,支持自动分区创建和维护
- pg_cron:PostgreSQL定时任务扩展,用于自动执行分区维护任务
- pgAdmin:图形化工具,支持分区表的管理和监控
- PostgreSQL partitioning:内置的分区表功能
- TimescaleDB:基于PostgreSQL的时序数据库扩展,优化时间序列数据的分区
- pg_stat_statements:统计查询性能的扩展,用于监控分区表查询性能
本文由风哥教程整理发布,仅用于学习测试使用,转载注明出处:http://www.fgedu.net.cn/10327.html
