PostgreSQL学习手册(三) 表的继承和分区

1、表的继承:
    这个概念对于不少已经熟悉其余数据库编程的开发人员而言会多少有些陌生,然而它的实现方式和设计原理倒是简单易懂,如今就让咱们从一个简单的例子开始吧。
    1. 第一个继承表:
    CREATE TABLE cities (   --父表
        name        text,
        population float,
        altitude     int
    );
    CREATE TABLE capitals ( --子表
        state      char(2)
    ) INHERITS (cities);
    capitals表继承自cities表的全部属性。在PostgreSQL里,一个表能够从零个或多个其它表中继承属性,并且一个查询既能够引用父表中的全部行,也能够引用父表的全部行加上其全部子表的行,其中后者是缺省行为。
    MyTest=# INSERT INTO cities values('Las Vegas', 1.53, 2174);  --插入父表
    INSERT 0 1
    MyTest=# INSERT INTO cities values('Mariposa',3.30,1953);     --插入父表
    INSERT 0 1
    MyTest=# INSERT INTO capitals values('Madison',4.34,845,'WI');--插入子表
    INSERT 0 1
    MyTest=# SELECT name, altitude FROM cities WHERE altitude > 500; --父表和子表的数据均被取出。
       name     | altitude
    -----------+----------
     Las Vegas |     2174
     Mariposa   |     1953
     Madison    |      845
    (3 rows)
    
    MyTest=# SELECT name, altitude FROM capitals WHERE altitude > 500; --只有子表的数据被取出。
      name   | altitude
    ---------+----------
     Madison |      845
    (1 row)

    若是但愿只从父表中提取数据,则须要在SQL中加入ONLY关键字,如:
    MyTest=# SELECT name,altitude FROM ONLY cities WHERE altitude > 500;
       name     | altitude
    -----------+----------
     Las Vegas |     2174
     Mariposa   |     1953
    (2 rows)
    上例中cities前面的"ONLY"关键字表示该查询应该只对cities进行查找而不包括继承级别低于cities的表。许多咱们已经讨论过的命令--SELECT,UPDATE和DELETE--支持这个"ONLY"符号。
    在执行整表数据删除时,若是直接truncate父表,此时父表和其全部子表的数据均被删除,若是只是truncate子表,那么其父表的数据将不会变化,只是子表中的数据被清空。
    MyTest=# TRUNCATE TABLE cities;  --父表和子表的数据均被删除。
    TRUNCATE TABLE
    MyTest=# SELECT * FROM capitals;
     name | population | altitude | state
    ------+------------+----------+-------
    (0 rows)
    
    2. 肯定数据来源:
    有时候你可能想知道某条记录来自哪一个表。在每一个表里咱们都有一个系统隐含字段tableoid,它能够告诉你表的来源:
    MyTest=# SELECT tableoid, name, altitude FROM cities WHERE altitude > 500;
     tableoid |   name    | altitude
    ----------+-----------+----------
        16532 | Las Vegas |     2174
        16532 | Mariposa  |     1953
        16538 | Madison   |      845
    (3 rows)
    以上的结果只是给出了tableoid,仅仅经过该值,咱们仍是没法看出实际的表名。要完成此操做,咱们就须要和系统表pg_class进行关联,以经过tableoid字段从该表中提取实际的表名,见如下查询:
    MyTest=# SELECT p.relname, c.name, c.altitude FROM cities c,pg_class p WHERE c.altitude > 500 and c.tableoid = p.oid;
     relname  |   name    | altitude
    ----------+-----------+----------
     cities    | Las Vegas |     2174
     cities    | Mariposa   |     1953
     capitals | Madison    |      845
    (3 rows)
    
    3. 数据插入的注意事项:
    继承并不自动从INSERT或者COPY中向继承级别中的其它表填充数据。在咱们的例子里,下面的INSERT语句不会成功:
    INSERT INTO cities (name, population, altitude, state) VALUES ('New York', NULL, NULL, 'NY');
    咱们可能但愿数据被传递到capitals表里面去,可是这是不会发生的:INSERT老是插入明确声明的那个表。
    
    4. 多表继承:
    一个表能够从多个父表继承,这种状况下它拥有父表们的字段的总和。子表中任意定义的字段也会加入其中。若是同一个字段名出如今多个父表中,或者同时出现 在父表和子表的定义里,那么这些字段就会被"融合",这样在子表里面就只有一个这样的字段。要想融合,字段必须是相同的数据类型,不然就会抛出一个错误。 融合的字段将会拥有它所继承的字段的全部约束。
    CREATE TABLE parent1 (FirstCol integer);
    CREATE TABLE parent2 (FirstCol integer, SecondCol varchar(20));
    CREATE TABLE parent3 (FirstCol varchar(200)); 
    --子表child1将同时继承自parent1和parent2表,而这两个父表中均包含integer类型的FirstCol字段,所以child1能够建立成功。
    CREATE TABLE child1 (MyCol timestamp) INHERITS (parent1,parent2);
    --子表child2将不会建立成功,由于其两个父表中均包含FirstCol字段,可是它们的类型不相同。
    CREATE TABLE child2 (MyCol timestamp) INHERITS (parent1,parent3);
    --子表child3一样不会建立成功,由于它和其父表均包含FirstCol字段,可是它们的类型不相同。
    CREATE TABLE child3 (FirstCol varchar(20)) INHERITS(parent1);

    5. 继承和权限:
    表访问权限并不会自动继承。所以,一个试图访问父表的用户还必须具备访问它的全部子表的权限,或者使用ONLY关键字只从父表中提取数据。在向现有的继承层次添加新的子表的时候,请注意给它赋予全部权限。     
    继承特性的一个严重的局限性是索引(包括惟一约束)和外键约束只施用于单个表,而不包括它们的继承的子表。这一点无论对引用表仍是被引用表都是事实,所以在上面的例子里,若是咱们声明cities.name为UNIQUE或者是一个PRIMARY KEY,那么也不会阻止capitals表拥有重复了名字的cities数据行。 而且这些重复的行缺省时在查询cities表的时候会显示出来。实际上,缺省时capitals将彻底没有惟一约束,所以可能包含带有同名的多个行。你应该给capitals增长惟一约束,可是这样作也不会避免与cities的重复。相似,若是咱们声明cities.name REFERENCES某些其它的表,这个约束不会自动广播到capitals。在这种条件下,你能够经过手工给capitals 增长一样的REFERENCES约束来作到这点。
    
2、分区表:
    1. 概述分区表:
    分区的意思是把逻辑上的一个大表分割成物理上的几块儿,分区能够提供若干好处:
    1). 某些类型的查询性能能够获得极大提高。
    2). 更新的性能也能够获得提高,由于表的每块的索引要比在整个数据集上的索引要小。若是索引不能所有放在内存里,那么在索引上的读和写都会产生更多的磁盘访问。
    3). 批量删除能够用简单地删除某个分区来实现。
    4). 将不多用的数据能够移动到便宜的、慢一些地存储介质上。 
    假设当前的数据库并不支持分区表,而咱们的应用所需处理的数据量也很是大,对于这种应用场景,咱们不得不人为的将该大表按照必定的规则,手工拆分红多个小表,让每一个小表包含不一样区间的数据。这样一来,咱们就必须在数据插入、更新、删除和查询以前,先计算本次的指令须要操做的小表。对于有些查询而言,因为查询区间可能会跨越多个小表,这样咱们又不得不将多个小表的查询结果进行union操做,以合并来自多个表的数据,并最终造成一个结果集返回给客户端。可见,若是咱们正在使用的数据库不支持分区表,那么在适合其应用的场景下,咱们就须要作不少额外的编程工做以弥补这一缺失。然而须要说明的是,尽管功能能够勉强应付,可是性能却和分区表没法相提并论。
    目前PostgreSQL支持的分区形式主要为如下两种:
    1). 范围分区: 表被一个或者多个键字字段分区成"范围",在这些范围之间没有重叠的数值分布到不一样的分区里。好比,咱们能够为特定的商业对象根据数据范围分区,或者根据标识符范围分区。
    2). 列表分区: 表是经过明确地列出每一个分区里应该出现那些键字值实现的。 

    2. 实现分区:
    1). 建立"主表",全部分区都从它继承。
    CREATE TABLE measurement (            --主表
        city_id      int    NOT NULL,
        logdate     date  NOT NULL,
        peaktemp int,
    );    
    2). 建立几个"子"表,每一个都从主表上继承。一般,这些"子"表将不会再增长任何字段。咱们将把子表称做分区,尽管它们就是普通的PostgreSQL表。
    CREATE TABLE measurement_yy04mm02 ( ) INHERITS (measurement);
    CREATE TABLE measurement_yy04mm03 ( ) INHERITS (measurement);
    ...
    CREATE TABLE measurement_yy05mm11 ( ) INHERITS (measurement);
    CREATE TABLE measurement_yy05mm12 ( ) INHERITS (measurement);
    CREATE TABLE measurement_yy06mm01 ( ) INHERITS (measurement);
    上面建立的子表,均以年、月的形式进行范围划分,不一样年月的数据将归属到不一样的子表内。这样的实现方式对于清空分区数据而言将极为方便和高效,即直接执行DROP TABLE语句删除相应的子表,以后在根据实际的应用考虑是否重建该子表(分区)。相比于直接DROP子表,PostgreSQL还提供了另一种更为方便的方式来管理子表:
    ALTER TABLE measurement_yy06mm01 NO INHERIT measurement;
    和直接DROP相比,该方式仅仅是使子表脱离了原有的主表,而存储在子表中的数据仍然能够获得访问,由于此时该表已经被还原成一个普通的数据表了。这样对于数据库的DBA来讲,就能够在此时对该表进行必要的维护操做,如数据清理、归档等,在完成诸多例行性的操做以后,就能够考虑是直接删除该表(DROP TABLE),仍是先清空该表的数据(TRUNCATE TABLE),以后再让该表从新继承主表,如:
    ALTER TABLE measurement_yy06mm01 INHERIT measurement;
    3). 给分区表增长约束,定义每一个分区容许的健值。同时须要注意的是,定义的约束要确保在不一样的分区里不会有相同的键值。所以,咱们须要将上面"子"表的定义修改成如下形式:
    CREATE TABLE measurement_yy04mm02 (
        CHECK ( logdate >= DATE '2004-02-01' AND logdate < DATE '2004-03-01')
    ) INHERITS (measurement);
    CREATE TABLE measurement_yy04mm03 (
        CHECK (logdate >= DATE '2004-03-01' AND logdate < DATE '2004-04-01')
    ) INHERITS (measurement);
    ...
    CREATE TABLE measurement_yy05mm11 (
        CHECK (logdate >= DATE '2005-11-01' AND logdate < DATE '2005-12-01')
    ) INHERITS (measurement);
    CREATE TABLE measurement_yy05mm12 (
        CHECK (logdate >= DATE '2005-12-01' AND logdate < DATE '2006-01-01')
    ) INHERITS (measurement);
    CREATE TABLE measurement_yy06mm01 (
        CHECK (logdate >= DATE '2006-01-01' AND logdate < DATE '2006-02-01')
    ) INHERITS (measurement);    
    4). 尽量基于键值建立索引。若是须要,咱们也一样能够为子表中的其它字段建立索引。
    CREATE INDEX measurement_yy04mm02_logdate ON measurement_yy04mm02 (logdate);
    CREATE INDEX measurement_yy04mm03_logdate ON measurement_yy04mm03 (logdate);
    ...
    CREATE INDEX measurement_yy05mm11_logdate ON measurement_yy05mm11 (logdate);
    CREATE INDEX measurement_yy05mm12_logdate ON measurement_yy05mm12 (logdate);
    CREATE INDEX measurement_yy06mm01_logdate ON measurement_yy06mm01 (logdate);    
    5). 定义一个规则或者触发器,把对主表的修改重定向到适当的分区表。
    若是数据只进入最新的分区,咱们能够设置一个很是简单的规则来插入数据。咱们必须每月都从新定义这个规则,即修改重定向插入的子表名,这样它老是指向当前分区。
    CREATE OR REPLACE RULE measurement_current_partition AS
    ON INSERT TO measurement
    DO INSTEAD
    INSERT INTO measurement_yy06mm01 VALUES (NEW.city_id, NEW.logdate, NEW.peaktemp);
    其中NEW是关键字,表示新数据字段的集合。这里能够经过点(.)操做符来获取集合中的每个字段。
    咱们可能想插入数据而且想让服务器自动定位应该向哪一个分区插入数据。咱们能够用像下面这样的更复杂的规则集来实现这个目标。
    CREATE RULE measurement_insert_yy04mm02 AS
    ON INSERT TO measurement WHERE (logdate >= DATE '2004-02-01' AND logdate < DATE '2004-03-01')
    DO INSTEAD
    INSERT INTO measurement_yy04mm02 VALUES (NEW.city_id, NEW.logdate, NEW.peaktemp);
    ...
    CREATE RULE measurement_insert_yy05mm12 AS
    ON INSERT TO measurement WHERE (logdate >= DATE '2005-12-01' AND logdate < DATE '2006-01-01')
    DO INSTEAD
    INSERT INTO measurement_yy05mm12 VALUES (NEW.city_id, NEW.logdate, NEW.peaktemp);
    CREATE RULE measurement_insert_yy06mm01 AS
    ON INSERT TO measurement WHERE (logdate >= DATE '2006-01-01' AND logdate < DATE '2006-02-01')
    DO INSTEAD
    INSERT INTO measurement_yy06mm01 VALUES (NEW.city_id, NEW.logdate, NEW.peaktemp);    
    请注意每一个规则里面的WHERE子句正好匹配其分区的CHECK约束。
    能够看出,一个复杂的分区方案可能要求至关多的DDL。在上面的例子里咱们须要每月建立一次新分区,所以写一个脚本自动生成须要的DDL是明智的。除此以外,咱们还不难推断出,分区表对于新数据的批量插入操做有必定的抑制,这一点在Oracle中也一样如此。  
    除了上面介绍的经过Rule的方式重定向主表的数据到各个子表,咱们还能够经过触发器的方式来完成此操做,相比于基于Rule的重定向方法,基于触发器的方式可能会带来更好的插入效率,特别是针对非批量插入的状况。然而对于批量插入而言,因为Rule的额外开销是基于表的,而不是基于行的,所以效果会好于触发器方式。另外一个须要注意的是,copy操做将会忽略Rules,若是咱们想要经过COPY方法来插入数据,你只能将数据直接copy到正确的子表,而不是主表。这种限制对于触发器来讲是不会形成任何问题的。基于Rule的重定向方式还存在另一个问题,就是当插入的数据不在任何子表的约束中时,PostgreSQL也不会报错,而是将数据直接保留在主表中。
    6). 添加新分区:
    这里将介绍两种添加新分区的方式,第一种方法简单且直观,咱们只是建立新的子表,同时为其定义新的检查约束,如:
    CREATE TABLE measurement_y2008m02 (
        CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' )
    ) INHERITS (measurement);
    第二种方法的建立步骤相对繁琐,但更为灵活和实用。见如下四步:
    /* 建立一个独立的数据表(measurement_y2008m02),该表在建立时以未来的主表(measurement)为模板,包含模板表的缺省值(DEFAULTS)和一致性约束(CONSTRAINTS)。*/
    CREATE TABLE measurement_y2008m02
        (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
    /* 为该表建立将来做为子表时须要使用的检查约束。*/
    ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
        CHECK (logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01');
    /* 导入数据到该表。下面只是给出一种导入数据的方式做为例子。在导入数据以后,若有可能,还能够作进一步的数据处理,如数据转换、过滤等。*/
    \copy measurement_y2008m02 from 'measurement_y2008m02'
    /* 在适当的时候,或者说在须要的时候,让该表继承主表。*/
    ALTER TABLE measurement_y2008m02 INHERIT measurement;
    7). 确保postgresql.conf里的配置参数constraint_exclusion是打开的。没有这个参数,查询不会按照须要进行优化。这里咱们须要作的是确保该选项在配置文件中没有被注释掉。
    /> pwd
    /opt/PostgreSQL/9.1/data
    /> cat postgresql.conf | grep "constraint_exclusion"
    constraint_exclusion = partition        # on, off, or partition

    3. 分区和约束排除:
    约束排除(Constraint exclusion)是一种查询优化技巧,它改进了用上面方法定义的表分区的性能。好比:
    SET constraint_exclusion = on;
    SELECT count(*) FROM measurement WHERE logdate >= DATE '2006-01-01';
    若是没有约束排除,上面的查询会扫描measurement表中的每个分区。打开了约束排除以后,规划器将检查每一个分区的约束而后再试图证实该分区不须要被扫描,由于它不能包含任何符合WHERE子句条件的数据行。若是规划器能够证实这个,它就把该分区从查询规划里排除出去。
    你可使用EXPLAIN命令显示一个规划在constraint_exclusion打开和关闭状况下的不一样。用上面方法设置的表的典型的缺省规划是:    
    SET constraint_exclusion = off;
    EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2006-01-01';    
                                              QUERY PLAN
    -----------------------------------------------------------------------------------------------
     Aggregate  (cost=158.66..158.68 rows=1 width=0)
       ->  Append  (cost=0.00..151.88 rows=2715 width=0)
             ->  Seq Scan on measurement  (cost=0.00..30.38 rows=543 width=0)
                   Filter: (logdate >= '2006-01-01'::date)
             ->  Seq Scan on measurement_yy04mm02 measurement  (cost=0.00..30.38 rows=543 width=0)
                   Filter: (logdate >= '2006-01-01'::date)
             ->  Seq Scan on measurement_yy04mm03 measurement  (cost=0.00..30.38 rows=543 width=0)
                   Filter: (logdate >= '2006-01-01'::date)
    ...
             ->  Seq Scan on measurement_yy05mm12 measurement  (cost=0.00..30.38 rows=543 width=0)
                   Filter: (logdate >= '2006-01-01'::date)
             ->  Seq Scan on measurement_yy06mm01 measurement  (cost=0.00..30.38 rows=543 width=0)
                   Filter: (logdate >= '2006-01-01'::date) sql


    从上面的查询计划中能够看出,PostgreSQL扫描了全部分区。下面咱们再看一下打开约束排除以后的查询计划:
    SET constraint_exclusion = on;
    EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2006-01-01';    
                                              QUERY PLAN
    -----------------------------------------------------------------------------------------------
     Aggregate  (cost=63.47..63.48 rows=1 width=0)
       ->  Append  (cost=0.00..60.75 rows=1086 width=0)
             ->  Seq Scan on measurement  (cost=0.00..30.38 rows=543 width=0)
                   Filter: (logdate >= '2006-01-01'::date)
             ->  Seq Scan on measurement_yy06mm01 measurement  (cost=0.00..30.38 rows=543 width=0)
                   Filter: (logdate >= '2006-01-01'::date)
    请注意,约束排除只由CHECK约束驱动,而不会由索引驱动。
    目前版本的PostgreSQL中该配置的缺省值是partition,该值是介于on和off之间的一种行为方式,即规划器只会将约束排除应用于基于分区表的查询,而on设置则会为全部查询都进行约束排除,那么对于普通数据表而言,也将不得不承担由该机制而产生的额外开销。
    
    约束排除在使用时有如下几点注意事项:
    1). 约束排除只是在查询的WHERE子句包含约束的时候才生效。一个参数化的查询不会被优化,由于在运行时规划器不知道该参数会选择哪一个分区。所以像CURRENT_DATE这样的函数必须避免。把分区键值和另一个表的字段链接起来也不会获得优化。
    2). 在CHECK约束里面要避免跨数据类型的比较,由于目前规划器会没法证实这样的条件为假。好比,下面的约束会在x是整数字段的时候可用,可是在x是一个bigint的时候不能用:
    CHECK (x = 1)
    对于bigint字段,咱们必须使用相似下面这样的约束:
    CHECK (x = 1::bigint)
    这个问题并不只仅局限于bigint数据类型,它可能会发生在任何约束的缺省数据类型与其比较的字段的数据类型不匹配的场合。在提交的查询里的跨数据类型的比较一般是OK的,只是不能在CHECK条件里。
    3). 在主表上的UPDATE和DELETE命令并不执行约束排除。
    4). 在规划器进行约束排除时,主表上的全部分区的全部约束都将会被检查,所以,大量的分区会显著增长查询规划的时间。
    5). 在执行ANALYZE语句时,要为每个分区都执行该命令,而不是仅仅对主表执行该命令。 数据库

相关文章
相关标签/搜索