一、分区表产生的背景
随着使用时间的增加,数据库中的数据量也不断增加,因此数据库查询越来越慢。
加速数据库的方法很多,如添加特定的索引,将日志目录换到单独的磁盘分区,调整数据库引擎的参数等。这些方法都能将数据库的查询性能提高到一定程度。
对于许多应用数据库来说,许多数据是历史数据并且随着时间的推移它们的重要性逐渐降低。如果能找到一个办法将这些可能不太重要的数据隐藏,数据库查询速度将会大幅提高。可以通过DELETE来达到此目的,但同时这些数据就永远不可用了。
因此,需要一个高效的把历史数据从当前查询中隐藏起来并且不造成数据丢失的方法。本文即将介绍的数据库表分区即能达到此效果。
二、分区表结构图
数据库表分区把一个大的物理表分成若干个小的物理表,并使得这些小物理表在逻辑上可以被当成一张表来使用。
主表
/父表
/Master Table
该表是创建子表的模板。它是一个正常的普通表,但正常情况下它并不储存任何数据。子表
/分区表
/Child Table
/Partition Table
这些表继承并属于一个主表。子表中存储所有的数据。主表与分区表属于一对多的关系,也就是说,一个主表包含多个分区表,而一个分区表只从属于一个主表
三、PostgreSQL各个版本 的分区表功能
分区表在不同的文档描述中使用了多个名词:原生分区 = 内置分区表 = 分区表。
PostgreSQL 9.x 之前的版本提供了一种“手动”方式使用分区表的方式,需要使用继承 + 触发器的来实现分区表,使用继承关键字INHERITS,步骤较为繁琐,需要定义附表、子表、子表的约束、创建子表索引,创建分区删除、修改,触发器等。
PostgreSQL 10.x 开始提供了内置分区表(内置是相对于 10.x 之前的手动方式)。内置分区简化了操作,将部分操作内置,最终简单三步就能够创建分区表。但是只支持范围分区(RANGE)和列表分区(LIST),
PostgreSQL 11.x 版本添加了对 HASH 分区。
本文将使用 PostgreSQL 10.x 版本及后续版本中的的内置分区表的使用方式,通过三步来创建分区表
1,创建父表------------指定分区键、分区策略(RANGE | LIST | HASH(11.x 才提供HASH策略))
2,创建分区表----------指定父表,分区键范围(分区键范围重叠之后会直接报错)
3,在分区上创建索引-----通常分区键上的索引是必须的
四、几种分区策略
PostgreSQL内置支持以下3种方式的分区:
1、范围(Range )分区:表被划分为由键列或列集定义的“范围”,分配给不同分区的值的范围之间没有重叠。例如:可以按日期范围或特定业务对象的标识符范围,来进行分区。
2、列表(List)分区:通过显式列出哪些键值出现在每个分区中来对表进行分区。
3、哈希(Hash)分区:(自PG11才提供HASH策略)通过为每个分区指定模数和余数来对表进行分区。每个分区将保存行,分区键的哈希值除以指定的模数将产生指定的余数。
五、建立分区实例
1、创建父表
CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int ) PARTITION BY RANGE (logdate);
2、创建分区表
CREATE TABLE measurement_200711 PARTITION OF measurement FOR VALUES FROM ('2007-11-01') TO ('2007-12-01'); CREATE TABLE measurement_200712 PARTITION OF measurement FOR VALUES FROM ('2007-12-01') TO ('2008-01-01') TABLESPACE fasttablespace; CREATE TABLE measurement_200801 PARTITION OF measurement FOR VALUES FROM ('2008-01-01') TO ('2008-02-01')
3、创建索引
CREATE INDEX index_measurement ON measurement USING btree(logdate);
4、 postgresql.conf配置
(1) enable_partition_pruning=on (分区修剪)启用,否则,查询将不会被优化。
如果不进行分区修剪,上述查询将扫描父表 measurement 的每个分区。启用分区修剪后,计划器将检查每个分区的定义并证明不需要扫描该分区,因为该分区不能包含满足查询的WHERE子句的任何行。当计划器可以证明这一点时,它将从查询计划中排除(修剪)该分区。
(2)constraint_exclusion=partition ,配置项没有被disable 。这一点非常重要,如果该参数项被disable,则基于分区表的查询性能无法得到优化,甚至比不使用分区表直接使用索引性能更低。
5、维护分区
Analyze measurement
六、要建立默认分区
默认分区,用于处理没有分区表的异常插入情况,用于存储无法匹配其他任何分区表的数据。显然,只有 RANGE 分区表和 LIST 分区表需要默认分区。
创建默认分区时,使用 DEFAULT 子句替代 FOR VALUES 子句。
CREATE TABLE measurement_default PARTITION OF measurement DEFAULT;
七、分区表不能自动建,怎么办?
方法1:利用定时器,定时某段时间创建分区表,可以利用操作系统定时器或者程序里面的定时任务,但是太麻烦,弃用。
方法2:编写存储过程脚本,一次性生成未来3年,未来10年的多个分区表,下面给出存储过程的脚本。这不是最优解,后面还提供了方法3。
tablepartitionsadd_day,按天生成分区表,一天一个分区表
CREATE OR REPLACE FUNCTION public.tablepartitionsadd_day( p_tablename text, p_schema text, p_date_start text, p_step integer) RETURNS void LANGUAGE 'plpgsql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$ declare v_cnt int; v_schema_name varchar(500); v_table_name varchar(500); v_curr_limit varchar(50); v_curr_limit1 varchar(50); v_steps int; v_exec_sql text; v_partition_name text; v_date_start text; --select tablePartitionsAdd_day('wry_gasfachourzsdata','public','2018-12-01',365),从2018-12-01的下1天开始 begin v_schema_name = p_schema; v_table_name = p_tablename; v_date_start = p_date_start; v_steps = p_step; v_exec_sql = ' '; for i in 0 .. v_steps loop v_curr_limit = to_char(to_date(v_date_start,'yyyy-mm-dd') + i,'yyyy-mm-dd'); v_curr_limit1 = to_char(to_date(v_curr_limit,'yyyy-mm-dd') + 1,'yyyy-mm-dd'); v_partition_name = to_char(to_date(v_date_start,'yyyy-mm-dd') + i, 'yyyymmdd'); SELECT count(1) into v_cnt from pg_tables where schemaname=v_schema_name and tablename = v_table_name||'_'||v_partition_name; if v_cnt = 0 then v_exec_sql = v_exec_sql||' create table '||v_schema_name||'.'||v_table_name||'_'||v_partition_name||' partition of '||v_table_name||' for values FROM ('''||v_curr_limit||' 00:00:00'||''') TO ('''||v_curr_limit1||' 00:00:00'||''');'; end if; end loop; execute v_exec_sql; end; $BODY$;
tablepartitionsadd_month,按月生成分区表,一月一个分区表
CREATE OR REPLACE FUNCTION public.tablepartitionsadd_month( p_tablename text, p_schema text, p_date_start text, p_step integer) RETURNS void LANGUAGE 'plpgsql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$ declare v_cnt int; v_schema_name varchar(500); v_table_name varchar(500); v_curr_limit varchar(50); v_curr_limit1 varchar(50); v_steps int; v_exec_sql text; v_partition_name text; v_date_start text; --select tablePartitionsAdd_month('wry_gasfachourzsdata','public','2018-12-01',36),从2018-12-01的下个月开始 begin v_schema_name = p_schema; v_table_name = p_tablename; v_date_start = p_date_start; v_steps = p_step; v_exec_sql = ' '; v_curr_limit=v_date_start; for i in 1 .. v_steps loop v_curr_limit = to_char(to_date(v_curr_limit,'yyyy-mm-dd') +interval '1 month','yyyy-mm-dd'); v_curr_limit1 = to_char(to_date(v_curr_limit,'yyyy-mm-dd') +interval '1 month','yyyy-mm-dd'); v_partition_name = to_char(to_date(v_curr_limit,'yyyy-mm-dd'), 'yyyymm'); SELECT count(1) into v_cnt from pg_tables where schemaname=v_schema_name and tablename = v_table_name||'_'||v_partition_name; if v_cnt = 0 then v_exec_sql = v_exec_sql||' create table '||v_schema_name||'.'||v_table_name||'_'||v_partition_name||' partition of '||v_table_name||' for values FROM ('''||v_curr_limit||' 00:00:00'||''') TO ('''||v_curr_limit1||' 00:00:00'||''');'; end if; end loop; execute v_exec_sql; end; $BODY$;
tablepartitionsadd_year,按年生成分区表,一年一个分区表
CREATE OR REPLACE FUNCTION public.tablepartitionsadd_year( p_tablename text, p_schema text, p_date_start text, p_step integer) RETURNS void LANGUAGE 'plpgsql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$ declare v_cnt int; v_schema_name varchar(500); v_table_name varchar(500); v_curr_limit varchar(50); v_curr_limit1 varchar(50); v_steps int; v_exec_sql text; v_partition_name text; v_date_start text; --select tablePartitionsAdd_month('wry_gasfachourzsdata','public','2018-01-01',36),从2018-01-01的下个年开始 begin v_schema_name = p_schema; v_table_name = p_tablename; v_date_start = p_date_start; v_steps = p_step; v_exec_sql = ' '; v_curr_limit=v_date_start; for i in 1 .. v_steps loop v_curr_limit = to_char(to_date(v_curr_limit,'yyyy-mm-dd') +interval '1 year','yyyy-mm-dd'); v_curr_limit1 = to_char(to_date(v_curr_limit,'yyyy-mm-dd') +interval '1 year','yyyy-mm-dd'); v_partition_name = to_char(to_date(v_curr_limit,'yyyy-mm-dd'), 'yyyy'); SELECT count(1) into v_cnt from pg_tables where schemaname=v_schema_name and tablename = v_table_name||'_'||v_partition_name; if v_cnt = 0 then v_exec_sql = v_exec_sql||' create table '||v_schema_name||'.'||v_table_name||'_'||v_partition_name||' partition of '||v_table_name||' for values FROM ('''||v_curr_limit||' 00:00:00'||''') TO ('''||v_curr_limit1||' 00:00:00'||''');'; end if; end loop; execute v_exec_sql; end; $BODY$;
方法3:利用insert触发器脚本,在触发器里面判断时间,然后建分区表和建索引。(网上很多都是老的继承INHERITS方法的触发器)。
这个方法有个问题:插入数据时,因为锁表的原因,无法修改分区表定义。
-
使用应用程序逻辑:在插入数据之前,您的应用程序可以检查需要的分区是否存在,并在必要时创建它。这意味着DDL操作与DML操作是分离的。
-
定期任务:设置一个定期任务(如cron作业),预先为未来的日期创建分区。例如,您可以每月或每周运行此任务,为接下来的月份或周创建分区。
-
通知/监听模式:当插入失败(因为缺少分区)时,您可以捕获这个错误,并触发一个外部进程来创建缺失的分区。这比较复杂,并可能需要一些时间来实现。
-
后插入处理:在插入失败时(例如,由于缺少分区),将数据写入到一个备用表或队列中。然后,定期从那里移动数据到主表,并在此过程中创建任何缺失的分区。
下面是使用通知/监听模式的解决办法:
-
使用
LISTEN
和NOTIFY
:PostgreSQL提供了LISTEN
和NOTIFY
命令,允许客户端监听特定的通知事件并对其做出响应。 -
处理流程:
- 当插入数据到分区表失败(例如,由于缺少分区)时,触发一个函数发送
NOTIFY
事件,其中包含所需分区的相关信息。 - 一个外部的监听进程(或者说worker)会
LISTEN
这个事件。一旦它接收到通知,它会处理通知,比如创建缺失的分区。
- 当插入数据到分区表失败(例如,由于缺少分区)时,触发一个函数发送
-
实施步骤:
a. 定义发送通知的函数:这个函数将在插入失败时被调用。
CREATE OR REPLACE FUNCTION notify_missing_partition(date_needed DATE) RETURNS void LANGUAGE plpgsql AS $$ BEGIN NOTIFY missing_partition, date_needed::TEXT; END; $$;
-
b. 修改应用逻辑:当您尝试插入数据并遇到错误(例如,由于缺少分区)时,调用上面定义的
notify_missing_partition
函数。c. 创建外部监听进程:这个进程会持续监听
missing_partition
事件。当它接收到通知时,它会解析日期,检查分区是否确实不存在,然后创建所需的分区。这可以使用PostgreSQL的驱动和您选择的编程语言来实现。
以下是使用Python和psycopg2
库的一个简化的示例:
import psycopg2 from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT conn = psycopg2.connect("your_connection_string") conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) cursor = conn.cursor() cursor.execute("LISTEN missing_partition;") print("Waiting for notifications on channel 'missing_partition'") while True: conn.poll() while conn.notifies: notify = conn.notifies.pop(0) print("Received NOTIFY:", notify.payload) # Here, you'd parse the date from notify.payload, check if the partition is indeed missing, and then create the required partition.
下面是java的示例:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; import java.sql.ResultSet; import org.postgresql.PGConnection; import org.postgresql.PGNotification; public class PostgreSQLListener { public static void main(String[] args) throws Exception { // JDBC connection parameters String url = "jdbc:postgresql://localhost:5432/your_database"; String user = "your_user"; String password = "your_password"; // Connect to the database Connection conn = DriverManager.getConnection(url, user, password); ((PGConnection) conn).addNotificationListener(new PGNotificationListenerImpl()); // Listen to the `missing_partition` channel try (Statement stmt = conn.createStatement()) { stmt.execute("LISTEN missing_partition;"); while (true) { // Wait a while to allow a notification to be received. Thread.sleep(1000); // Check for notifications. PGNotification[] notifications = ((PGConnection) conn).getNotifications(); if (notifications != null) { for (PGNotification notification : notifications) { System.out.println("Received NOTIFY: " + notification.getParameter()); // Here, you'd parse the date from notification.getParameter(), check if the partition is indeed missing, and then create the required partition. } } } } } } class PGNotificationListenerImpl implements org.postgresql.PGNotificationListener { @Override public void notification(int processId, String channelName, String payload) { System.out.println("Received Notification: " + payload); } }