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語句時,要爲每個分區都執行該命令,而不是僅僅對主表執行該命令。 數據庫

相關文章
相關標籤/搜索