水平分表--使用postgresql继承

介绍

自己负责的项目的一张表access_log,条目数达到千万级,sql查询时已经显现出较慢的迹象,于是开始折腾分表,优化查询时间。就从最简单的水平分表入手。access_log表结构:

勘误:季度的单词是quarter,下面拼写quater是错误的

column type modifiers
id integer not null default nextval('access_log_id_seq'::regclass)
userid integer
wxopenid character varying(50) not null
url character varying(200) not null
useragent character varying(500)
accessdate timestamp(0) without time zone
createip character varying(25)
scene character varying(50)

思路

按创建日期accessdate将access_log表分为若干子表,开启约束排除,按日期查询时将直接进入符合条件的子表,提高查询效率。这种做法无法提高插入效率,查询条件与日期无关时也无能为力。考虑到按日期查询是经常发生的操作,该分表方案优化效果明显。

1. 创建主表
create table access_log_main (
id int4 default nextval('access_log_id_seq'::regclass) primary key,
userid int4,
wxopenid varchar(50)  NOT NULL,
url varchar(200)  NOT NULL,
useragent varchar(500) ,
accessdate timestamp(0),
createip varchar(25) ,
scene varchar(50)
);
2. 创建分区表
--继承主表
create table access_log_2017_quater4 (
check (accessdate >= date '2017-10-01' and accessdate < date '2018-01-01')
) inherits (access_log_main);
create table access_log_2018_quater1 (
    check ( accessdate >= date '2018-01-01' and accessdate < date '2018-04-01')
) inherits (access_log_main);
--other表在约束都不匹配时存入,防止丢失数据
create table access_log_other () inherits (access_log_main);

--创建索引
create index access_log_2017_quater4_accessdate on access_log_2017_quater4 (accessdate);
create index access_log_2018_quater1_accessdate on access_log_2018_quater1 (accessdate);
create index access_log_other_accessdate on access_log_other (accessdate);
3. 创建触发器,实现智能insert
create or replace function access_log_insert_trigger()
returns trigger as $$
begin 
    if(new.accessdate < date '2017-10-01') THEN
    insert into access_log values (NEW.*);
    elseif (NEW.accessdate >= date '2017-10-01' and NEW.accessdate < date '2018-01-01') THEN
    insert into access_log_2017_quater4 values (NEW.*);
elseif (NEW.accessdate >= date '2018-01-01' and NEW.accessdate < date '2018-04-01') THEN
    insert into access_log_2018_quater1 values (NEW.*);
    else 
    insert into access_log_other values (NEW.*);
end if;
return null;
END;
$$
language plpgsql;

--创建调用触发器函数的触发器
create trigger insert_on_access_log before insert on access_log_main for each row execute procedure access_log_insert_trigger();
4. 动态添加继承关系:access_log继承主表access_log_main
alter table access_log add constraint access_log_accessdate_check check (accessdate < date '2017-10-01');
create index access_log_accessdate on access_log (accessdate);
alter table access_log inherit access_log_main;

这里简单提一下,创建表时继承使用inherits关键字,而已有的表继承其他表使用inherit关键字。已创建的表继承时,要求子表必须已经包含新父表的所有字段且类型一致。脱离继承关系可以用带有no inderit的alter命令。
另外值得注意的是:继承的索引(包括唯一约束)和外键约束只能用于单个表,而不能包括它们的字表。

查看表结构

到现在准备工作基本完成了,你可查看继承表结构,或者插入记录,验证上面操作是否生效。

---查看accessl_log_main及其所有子表
\d+ access_log_main
---插入记录
insert into access_log_main(userid, wxopenid, url, accessdate, createip, scene) 
values(-1, 'test', '/page/test', '2017-10-08', '127.0.0.1', '1001');
--查询发现上面记录确实插入了表access_log_2017_quater4
select * from access_log_2017_quater4;

更新维护

继承结构非常灵活,你可以随时动态增加/删除子表;添加/解除继承关系,来扩展或者优化分表结构。当然这些操作一般需要修改触发器

--删除子表
drop table access_log_other;
--删除继承关系
alter table access_log no inherit access_log_main;

测试优化效果

1. 首先你需要大量数据,可以写一个如下的函数循环插入数据
create or replace function insertACLOG() returns void as
$$
declare 
i int :=0;
tempTime timestamp;
j int;
begin
while i < 10000000 loop
    j:=round(random()*80);
    tempTime:=date '2017-01-01' + j;
    insert into access_log(userid, wxopenid, url, accessdate, createip, scene) 
    values(round(random() * 1000000), 'test', '/page/test', tempTime, '127.0.0.1', '1001');
    i:= i+1;
raise notice 'holy shit%', i;
end loop;
end;
$$ language plpgsql;
2. 约束排除

开启约束排除后,规划器将查询条件和check约束进行比较,在查询条件与约束冲突的情况下,忽略对表的扫描。分区键和约束设置合理的情况下,将会大大提高效率。
修改{pgsql安装目录}/main/postgresql.conf中的constraint_exclusion参数:


constraint_exclusion值 含义
partition 检查仅用于继承子表和UNION ALL子查询的约束
on 检查所有约束
off 不检查约束

partition是默认设置。它往往使用继承和分表提高性能。
去掉注释,值改为partition或on,重启postgresql服务:service postgresql restart

--查看constraint_exclusion状态
show constraint_exclusion;
--开启约束排除后,你仍然可以在事务里临时关闭,观察其关闭与开启的效率改变
set constraint_exclusion = off;
explain select count(1) from access_log_main  where accessdate > '2017-10-01';
3. 优化效果

数据总量:2000w;目标分表数据量300w;查询在不同的数据库以作对比,防止干扰。

select * from access_log/access_log_main where userid = xxxx and accessdate > '2018-01-01';
分表方案 1 2 3 4 5 6 average
access_log 2.67s 2.68s 2.59s 2.70s 2.68s 2.63s 约2.6s
access_log_main 0.53s 0.51s 0.48s 0.49s 0.43s 0.47s 约0.48s

扩展

随着时间增加,分表也要不断更新。写一个脚本自动生成需要的DDL是明智的。

--access_log_parttable.sql
do $$
declare 
nexttime timestamp;
yearnum int4;
quarter int4;
starttime timestamp;
endtime timestamp;
tablename varchar;
indexname varchar;
triggerfuncname varchar;
tempsql varchar;
begin
nexttime := now() + interval '3 months';
yearnum := date_part('year', nexttime);
quarter := date_part('quarter', nexttime);
starttime := date_trunc('quarter', nexttime);
endtime := nexttime + interval '3 months';
endtime := date_trunc('quarter', endtime);
tablename := 'access_log_' || yearnum || '_quarter' || quarter ;
indexname := tablename || '_accessdate' ; 
triggerfuncname := 'access_log_other_' || yearnum || '_quarter' || quarter ;

raise notice 'nexttime: %', nexttime;
raise notice 'year: %', yearnum;
raise notice 'quarter:  %', quarter;
raise notice 'starttime:    %', starttime;
raise notice 'endtime:  %', endtime;
raise notice 'tablename:    %', tablename;
raise notice 'indexname:    %', indexname;
raise notice 'triggerfuncname:  %', triggerfuncname;

tempsql := 'create table ' || tablename || '(
check (accessdate >= date ''' || starttime || ''' and accessdate < date ''' || endtime || ''' )
) inherits (access_log_main);';
execute(tempsql);
 
tempsql := 'create index ' || indexname || ' on ' || tablename || ' (accessdate)';
execute(tempsql);

tempsql := 'create or replace function ' || triggerfuncname || '()
returns trigger as
$body$ 
begin
    if(new.accessdate >= date ''' || starttime || ''' and new.accessdate < date ''' || endtime || ''' ) then
    insert into ' || tablename || ' values(new.*);
return null;
    else
    return new;
    end if;
end;
$body$ 
language plpgsql';
execute(tempsql);

tempsql := 'drop trigger if exists inseret_on_' || triggerfuncname || ' on access_log_other';
execute(tempsql);

tempsql := 'create trigger inseret_on_' || triggerfuncname || ' before insert on access_log_other
 for each row execute procedure ' || triggerfuncname || '()';
execute(tempsql);

end 
$$ LANGUAGE plpgsql;

简单罗列一下上面脚本涉及的知识点,学到蛮多:

  • DO [ LANGUAGE lang_name ] code $$ 执行匿名代码块
  • 各种时间/日期的处理函数,尤其是date_tranc,非常好用
  • 拼接sql,并用execute执行
  • 多触发器时,按字母顺序执行。返回null后续皆不执行,返回new后续继续执行。详细

坑:

  • 上面代码43行,必须用body,而不是$$,否则一直报错:error at or near 'begin'(line 44)
接下来便是在linux上定时执行下面命令:
psql -f 'access_log_parttable.sql' "user=xxxx password=xxxx dbname=xxxx";

至于linux设置自动执行脚本,参考这里

参考
  1. 官方文档
    http://www.postgres.cn/docs/9.4/ddl-partitioning.html

  2. 继承详细步骤
    http://www.cnblogs.com/mchina/archive/2013/04/09/2973427.html

  3. 数据库分表概述
    http://www.infoq.com/cn/articles/key-steps-and-likely-problems-of-split-table
    http://www.infoq.com/cn/articles/key-steps-and-likely-problems-of-horizontal-split-table?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_articles_clk

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,894评论 2 89
  • --- layout: post title: "如果有人问你关系型数据库的原理,叫他看这篇文章(转)" date...
    蓝坠星阅读 775评论 0 3
  • 读书营精读活动要求十天精读一本书。除了打卡交作业的《说话的魅力》,我同时选择了另外一本《书都不会读,你还想成...
    猫悟空阅读 292评论 0 4
  • 这是真正的零基础吧,从这个水平开始坚持画21天试试!
    蝶舞归来阅读 205评论 0 2
  • A 并不是每一件事都可以想到之后会立刻行动。比如给儿子读书。为了自己,也为了儿子,归根结底是为了儿子。 B 每年都...
    褚庆洋阅读 129评论 0 0