随着时间增长,数据量会越来越多,当数据达到一定规模,索引会过大,导致查询/更新/删除的缓慢
如果将这个大表创建为一个分区表,关联有多个分区,插入数据的时候,数据根据一定规则存入不同的分区内,查询的时候既可以到指定分区查询,也可以直接查询分区表,如果使用 PostgreSQL 内置的几种分区规则,如根据时间划分的分区,带有时间条件的检索,则 PostgreSQL 在查询的时默认只关联相关的表,不在时间条件里的表不会参与搜索,这称为 分区剪枝,无疑,这样能够大幅提升查询速度
在 PostgreSQL 中使用分区表有两种方式,<内置声明分区> 与 <个体分区>,前者由系统提供,也就是上一段提及到的方式,它可以根据定义的规则自动将数据存入不同的分区中,并且拥有更好的性能优化,后者的实现基于继承功能,由用户创建触发器的方式来存储数据,拥有更好的灵活性
创建分区表并初始化分区后,向分区表存储数据会根据路由规则保存在不同的分区下,例如,插入到"t_logs"分区表的日志会被自动保存在"t_logs_y2019m02", "t_logs_y2019m03"... 这样的分区内
数据分区后,删除历史数据或者存档变得容易,以往导出数据,删除记录需要针对百万条记录操作,分区后,直接删除相应表或整表导出即可
虽然使用了分区能够很大程度的提升检索速度,但应避免无范围的全表检索,这样的话表分区与大表检索没什么区别,性能得不到改善,应该合理的设置数据检索条件确定检索数据的范围
内置声明分区
范围划分
适合于线性增长的列,如时间,不断增长的数值
-- 创建分区表
CREATE TABLE measurement (
city_id int not null,
logdate date not null,
peaktemp int,
unitsales int
) PARTITION BY RANGE (logdate);
-- 创建分区
CREATE TABLE measurement_y2006m03 PARTITION OF measurement
FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
-- 创建索引
CREATE INDEX ON measurement (logdate);
-- 插入数据到分区
INSERT into measurement (city_id, logdate, peaktemp, unitsales) values (230000, '2006-02-01', '23', 92374);
INSERT into measurement (city_id, logdate, peaktemp, unitsales) values (230000, '2006-03-06', '36', 17281);
列表划分
针对特定字段值的表分区,比如不同系统,可以根据系统ID来分别存入不同的分区内
-- 加载uuid扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 创建表
CREATE TABLE message (
uuid uuid DEFAULT uuid_generate_v4 (),
nodeid TEXT NOT NULL,
systemid TEXT NOT NULL,
time TEXT NOT NULL,
jdoc jsonb NOT NULL
) partition by list (systemid);
CREATE TABLE message_109001 PARTITION OF message FOR VALUES in ('109001');
CREATE TABLE message_109002 PARTITION OF message FOR VALUES in ('109002');
CREATE TABLE message_109003 PARTITION OF message FOR VALUES in ('109003');
CREATE TABLE message_109004 PARTITION OF message FOR VALUES in ('109004');
CREATE TABLE message_109005 PARTITION OF message FOR VALUES in ('109005');
CREATE TABLE message_109006 PARTITION OF message FOR VALUES in ('109006');
CREATE INDEX ON message (nodeid);
CREATE INDEX ON message (systemid);
CREATE INDEX ON message (time);
-- 插入数据
INSERT into message (nodeid, systemid, time, jdoc) values ('111111', '109004', '2019-06-13 19:05:11', '{"hello": "AA系统"}');
INSERT into message (nodeid, systemid, time, jdoc) values ('222222', '109005', '2019-06-13 19:05:12', '{"hello": "BB系统"}');
INSERT into message (nodeid, systemid, time, jdoc) values ('333333', '109006', '2019-06-13 19:05:13', '{"hello": "CC系统"}');
-- 验证查询
EXPLAIN SELECT * FROM message WHERE systemid = '109005';
PostgreSQL 系统内置的分区方案已经可以处理不少的分区场景,当内置的方案不能解决业务的问题时,可以使用集成的方式,用触发器来创建规则
个体分区
使用继承方式实现的个体分区可以有更多的定制化功能
-- SQL 示例
-- 使用继承实现表分区
CREATE TABLE measurement2 (
city_id int not null,
logdate date not null,
peaktemp int,
unitsales int
);
-- 创建子表
CREATE TABLE measurement2_y2006m02 (
CHECK ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
) INHERITS (measurement2);
CREATE TABLE measurement2_y2006m03 (
CHECK ( logdate >= DATE '2006-03-01' AND logdate < DATE '2006-04-01' )
) INHERITS (measurement2);
-- 创建索引
CREATE INDEX measurement2_y2006m02_logdate ON measurement2_y2006m02 (logdate);
CREATE INDEX measurement2_y2006m03_logdate ON measurement2_y2006m03 (logdate);
-- 定义触发器函数
CREATE OR REPLACE FUNCTION measurement_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF ( NEW.logdate >= DATE '2006-02-01' AND
NEW.logdate < DATE '2006-03-01' ) THEN
INSERT INTO measurement2_y2006m02 VALUES (NEW.*);
ELSIF ( NEW.logdate >= DATE '2006-03-01' AND
NEW.logdate < DATE '2006-04-01' ) THEN
INSERT INTO measurement2_y2006m03 VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Date out of range. Fix the measurement_insert_trigger() function!';
END IF;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
-- 创建一个调用该触发器函数的触发器
CREATE TRIGGER insert_measurement_trigger
BEFORE INSERT ON measurement2
FOR EACH ROW EXECUTE FUNCTION measurement_insert_trigger();
-- 插入数据到分区
INSERT into measurement2 (city_id, logdate, peaktemp, unitsales) values (100000, '2006-02-07', '23', 1111);
INSERT into measurement2 (city_id, logdate, peaktemp, unitsales) values (100000, '2006-02-07', '21', 0000);
INSERT into measurement2 (city_id, logdate, peaktemp, unitsales) values (100000, '2006-03-07', '36', 2222);
INSERT into measurement2 (city_id, logdate, peaktemp, unitsales) values (100000, '2018-11-11', '36', 4444);
select * from measurement2;
查看搜索详情
分区剪枝 功能可以针对 内置声明分区 进行性能优化
enable_partition_pruning 设置为是否启用内置声明分区的 分区剪枝 功能
关闭后,搜索会关联所有的分区,开启后,则会智能的剔除无关分区
可以观察SQL查询的解释来进行对比
SET enable_partition_pruning = on; -- off
EXPLAIN SELECT count(*) FROM measurement WHERE logdate > DATE '2006-03-15';
生成分区脚本
文件名:generate_partition_sql_datetime.js
const moment = require("moment")
if (process.argv.length != 5) {
console.log("Usage: node script.js table_name 2019(start) 5(years)")
return;
}
TABLE_NAME = process.argv[2]
START_Y = process.argv[3]
TOTAL_Y = process.argv[4]
LOOP_COUNT = TOTAL_Y * 4
START_STRING = START_Y + "-01-01"
START_YMD = moment(START_STRING)
console.log("-- start --")
from = START_STRING
for (let i=0; i<LOOP_COUNT; i++) {
let tmp_date = START_YMD.add(3, 'months')
let tmp_date_string = tmp_date.format("YYYY-MM-DD")
let to = tmp_date.format("YYYY-MM-DD")
let from_Y = from.split("-")[0]
let from_M = from.split("-")[1]
console.log("CREATE TABLE " + TABLE_NAME + "_y" + from_Y + "m" + from_M + " PARTITION OF " + TABLE_NAME + " FOR VALUES FROM ('" + from + "') TO ('" + to + "');")
from = to
}
console.log("-- end --")
使用及输出:
# 首先安装依赖
npm install moment
执行结果:
生成从2019年开始,一个季度间隔,总时长五年的分区创建SQL(适用于内置声明分区)
参考: