一、分區表產生的背景
隨着使用時間的增加,數據庫中的數據量也不斷增加,因此數據庫查詢越來越慢。
加速數據庫的方法很多,如添加特定的索引,將日志目錄換到單獨的磁盤分區,調整數據庫引擎的參數等。這些方法都能將數據庫的查詢性能提高到一定程度。
對於許多應用數據庫來說,許多數據是歷史數據並且隨着時間的推移它們的重要性逐漸降低。如果能找到一個辦法將這些可能不太重要的數據隱藏,數據庫查詢速度將會大幅提高。可以通過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); } }
