1. 程式人生 > >PostgreSQL 強大的多層表繼承--及其在海量資料分類按月分割槽儲存中的應用

PostgreSQL 強大的多層表繼承--及其在海量資料分類按月分割槽儲存中的應用

http://blog.csdn.net/goldenhawking

    最近發現大家越來越關注 PostgreSQL了。2008年以來,通過對PostgreSQL的實際使用,發現其物件-關係資料庫機制對現實問題非常有幫助。在多重表繼承下,對上億條不同類別的資料條目進行按型號、按月份雙層分割槽管理,既可在總表查閱所有條目的共有欄位,也可在各型別字表查詢附加欄位,非常高效。下面把這種分割槽機制介紹如下!

      實驗平臺:PostgreSQL 9.1

      實驗背景:

      假設有N種資料收集裝置,分別叫做 machine1, machine2...machineN, 各類收集裝置從感測器上採集的資料各不相同。但是他們都包括3個共有屬性:1、採集時刻 2、一個電壓值 3、機器的ID。       這些機器源源不斷的從各個感測器收集資訊,每類機器還有各自不同的附加資料。比如,machine1有當前最大單元數、當前已使用單元數兩個屬性。Machine2有前端感測器的ID和取值。資料量約100萬條/天,要求資料庫容納至少5年的資料。

     設計原則:

      由於採集的頻率高,每天會有上百萬條資料存入,為了考慮縮小索引的規模,提高檢索效率,採用按月分割槽儲存。由於各類機器的欄位各有區別,使得我們必須設計不同的表結構, 分別儲存各類資料。由於要求能夠統一檢索基本資訊、按需檢索額外資訊,我們採用PostgreSQL的表繼承,首先按照機器型別分類,而後各型別機器內按照月份分類。

     資料庫結構:

    全域性ID 序列:

  1. CREATESEQUENCE serial_id_seq  
  2.   INCREMENT 1  
  3.   MINVALUE 1  
  4.   MAXVALUE 9223372036854775807  
  5.   START 1  
  6.   CACHE 1;  
  7. ALTERTABLE serial_id_seq  
  8.   OWNER TO postgres;  

  該序列用於保持全域性ID的唯一性。PostgreSQL各個繼承表中的主鍵約束僅僅限於本表,在不想通過檢查條件確保唯一的情況下,可以通過觸發器手工從序列獲取新的值,以及限制使用者修改ID來保證唯一。基本表(爺爺表),承載了所有機器的共同屬性
  1. CREATETABLE base_table  
  2. (  
  3.   id bigintNOTNULL,  
  4.   dvalue doubleprecision,  
  5.   sample_time timestampwithtime zone,  
  6.   machine_code charactervarying(32),  
  7.   CONSTRAINT pk_base_table_id PRIMARYKEY (id )  
  8. )  
  9. WITH (  
  10.   OIDS=FALSE
  11. );  
  12. ALTERTABLE base_table  
  13.   OWNER TO postgres;  
  14. CREATEINDEX idx_sample_time  
  15.   ON base_table  
  16.   USING btree  
  17.   (sample_time );  

下面為機器型別1建立按型別分割槽子表(爸爸表)
  1. CREATETABLE base_table_machine1  
  2. (  
  3.   max_res integer,  
  4.   curr_res integer,  
  5.   CONSTRAINT pk_base_table_machine1 PRIMARYKEY (id )  
  6. )  
  7. INHERITS (base_table)  
  8. WITH (  
  9.   OIDS=FALSE
  10. );  
  11. ALTERTABLE base_table_machine1  
  12.   OWNER TO postgres;  
  13. CREATEINDEX idx_base_table_machine1_sample_time  
  14.   ON base_table_machine1  
  15.   USING btree  
  16.   (sample_time );  

同樣,為機器2建立按型別分割槽子表
  1. CREATETABLE base_table_machine2  
  2. (  
  3.   manu_id charactervarying(16),  
  4.   manu_value charactervarying(16),  
  5.   CONSTRAINT pk_base_table_machine2 PRIMARYKEY (id )  
  6. )  
  7. INHERITS (base_table)  
  8. WITH (  
  9.   OIDS=FALSE
  10. );  
  11. ALTERTABLE base_table_machine2  
  12.   OWNER TO postgres;  
  13. CREATEINDEX idx_base_table_machine2_sample_time  
  14.   ON base_table_machine2  
  15.   USING btree  
  16.   (sample_time );  

其他機器不再贅述。建立完後,我們開始寫建立按月分割槽表的觸發器(兒子表)。按月分割槽會判斷每次插入的資料的時刻,按照月份放到分割槽表中。如果分割槽表不存在,則自動建立。這裡給出機器1、機器2 的觸發器
  1. -- Function: on_insert_base_table_machine1()
  2. -- DROP FUNCTION on_insert_base_table_machine1();
  3. CREATEORREPLACEFUNCTION on_insert_base_table_machine1()  
  4.   RETURNStriggerAS
  5. $BODY$  
  6. DECLARE
  7. --Variable Hold subtable name
  8. str_sub_tablename varchar;  
  9. --Variable Hold year\month info with timestamle
  10. str_sub_sample_time varchar;  
  11. str_sql_cmd varchar;  
  12. str_sub_checkval varchar;  
  13. BEGIN
  14.     --The triggle func will be exectued only when BEFORE INSERT
  15.     IF TG_OP <> 'INSERT'OR TG_TABLE_NAME <>'base_table_machine1'OR TG_WHEN <> 'BEFORE'THEN
  16.         RETURNNULL;  
  17.     END IF;  
  18.     --Generate Table Name
  19.     str_sub_sample_time = date_part('year',NEW.sample_time)::varchar || '_' ||   
  20.         CASEWHEN date_part('month',NEW.sample_time) <10 THEN'0'ELSE''END
  21.         ||date_part('month',NEW.sample_time)::varchar;  
  22.     str_sub_tablename = 'machine1_' || str_sub_sample_time;  
  23.     --Check if table not created
  24.     select * from pg_tables where schemaname = 'public'and tablename=str_sub_tablename   
  25.         into str_sql_cmd;  
  26.     IF NOT FOUND THEN
  27.         --Create table Cmd
  28.         str_sql_cmd = '  
  29.             CREATETABLE'||str_sub_tablename||'
  30.             (  
  31.                 CONSTRAINT pk_'|| str_sub_tablename||'PRIMARYKEY (id ),  
  32.                 CONSTRAINT chk_'|| str_sub_tablename||'
  33.                  CHECK(date_part(''year''::text, sample_time) = '||  
  34.                  date_part('year',NEW.sample_time)::varchar||  
  35.                  '::doubleprecisionAND
  36.                  date_part(''month''::text, sample_time) = '||  
  37.                  date_part('month',NEW.sample_time)::varchar||'  
  38.                  )  
  39.             )  
  40.             INHERITS (base_table_machine1)  
  41.             WITH ( OIDS=FALSE );  
  42.             ALTERTABLE'||str_sub_tablename||' OWNER TO postgres;  
  43.             CREATEINDEX idx_'|| str_sub_tablename||'_sample_time  
  44.                 ON'|| str_sub_tablename||'
  45.                 USING btree (sample_time );  
  46.             ';  
  47.         EXECUTE str_sql_cmd;  
  48.     END IF;  
  49.     --insert Data
  50.     str_sql_cmd = 'INSERT INTO '||str_sub_tablename||'   
  51.      ( id,dvalue,sample_time,machine_code,max_res,curr_res) VALUES (  
  52.      nextval(''serial_id_seq''),$1,$2,$3,$4,$5);  
  53.     ';  
  54.     EXECUTE str_sql_cmd USING  
  55.         NEW.dvalue,  
  56.         NEW.sample_time,  
  57.         NEW.machine_code,  
  58.         NEW.max_res,  
  59.         NEW.curr_res;  
  60.     --return null because main table does not really contain data
  61.     returnNULL;  
  62. END;  
  63.     $BODY$  
  64.   LANGUAGE plpgsql VOLATILE  
  65.   COST 100;  
  66. ALTERFUNCTION on_insert_base_table_machine1()  
  67.   OWNER TO postgres;  

  1. -- Function: on_insert_base_table_machine2()
  2. -- DROP FUNCTION on_insert_base_table_machine2();
  3. CREATEORREPLACEFUNCTION on_insert_base_table_machine2()  
  4.   RETURNStriggerAS
  5. $BODY$  
  6. DECLARE
  7. --Variable Hold subtable name
  8. str_sub_tablename varchar;  
  9. --Variable Hold year\month info with timestamle
  10. str_sub_sample_time varchar;  
  11. str_sql_cmd varchar;  
  12. str_sub_checkval varchar;  
  13. BEGIN
  14.     --The triggle func will be exectued only when BEFORE INSERT
  15.     IF TG_OP <> 'INSERT'OR TG_TABLE_NAME <>'base_table_machine2'OR TG_WHEN <> 'BEFORE'THEN
  16.         RETURNNULL;  
  17.     END IF;  
  18.     --Generate Table Name
  19.     str_sub_sample_time = date_part('year',NEW.sample_time)::varchar || '_' ||   
  20.         CASEWHEN date_part('month',NEW.sample_time) <10 THEN'0'ELSE''END
  21.         ||date_part('month',NEW.sample_time)::varchar;  
  22.     str_sub_tablename = 'machine2_' || str_sub_sample_time;  
  23.     --Check if table not created
  24.     select * from pg_tables where schemaname = 'public'and tablename=str_sub_tablename   
  25.         into str_sql_cmd;  
  26.     IF NOT FOUND THEN
  27.         --Create table Cmd
  28.         str_sql_cmd = '  
  29.             CREATETABLE'||str_sub_tablename||'
  30.             (  
  31.                 CONSTRAINT pk_'|| str_sub_tablename||'PRIMARYKEY (id ),  
  32.                 CONSTRAINT chk_'|| str_sub_tablename||'
  33.                  CHECK(date_part(''year''::text, sample_time) = '||  
  34.                  date_part('year',NEW.sample_time)::varchar||  
  35.                  '::doubleprecisionAND
  36.                  date_part(''month''::text, sample_time) = '||  
  37.                  date_part('month',NEW.sample_time)::varchar||'  
  38.                  )  
  39.             )  
  40.             INHERITS (base_table_machine2)  
  41.             WITH ( OIDS=FALSE );  
  42.             ALTERTABLE'||str_sub_tablename||' OWNER TO postgres;  
  43.             CREATEINDEX idx_'|| str_sub_tablename||'_sample_time  
  44.                 ON'|| str_sub_tablename||'
  45.                 USING btree (sample_time );  
  46.             ';  
  47.         EXECUTE str_sql_cmd;  
  48.     END IF;  
  49.     --insert Data
  50.     str_sql_cmd = 'INSERT INTO '||str_sub_tablename||'   
  51.      ( id,dvalue,sample_time,machine_code,manu_id,manu_value) VALUES (  
  52.      nextval(''serial_id_seq''),$1,$2,$3,$4,$5);  
  53.     ';  
  54.     EXECUTE str_sql_cmd USING  
  55.         NEW.dvalue,  
  56.         NEW.sample_time,  
  57.         NEW.machine_code,  
  58.         NEW.manu_id,  
  59.         NEW.manu_value;  
  60.     --return null because main table does not really contain data
  61.     returnNULL;  
  62. END;  
  63.     $BODY$  
  64.   LANGUAGE plpgsql VOLATILE  
  65.   COST 100;  
  66. ALTERFUNCTION on_insert_base_table_machine2()  
  67.   OWNER TO postgres;  

最後,為各個爸爸表設定觸發器
  1. CREATETRIGGER triggle_on_insert_machine1  
  2.   BEFORE INSERT
  3.   ON base_table_machine1  
  4.   FOR EACH ROW  
  5.   EXECUTEPROCEDURE on_insert_base_table_machine1();  

  1. CREATETRIGGER triggle_machine2  
  2.   BEFORE INSERT
  3.   ON base_table_machine2  
  4.   FOR EACH ROW  
  5.   EXECUTEPROCEDURE on_insert_base_table_machine2();  

到此為止,我們可以分別向各個爸爸表(按型別分割槽表)插入資料,而後通過爺爺表(總表)檢索基本資訊,通過爸爸表檢索詳細資訊。對總表的操作會遍歷反饋到所有子表,試圖利用子表的索引進行查詢。由於按月儲存,插入工作只限於本月,所以檢索歷史資料效率很高。

當然了,這只是簡單的實驗,實際欄位要比上述欄位複雜很多。PostgreSQL的物件-關係資料庫對解決上述問題非常有幫助,也全面的運用到我公司的各個環節,達到工業化標準的系統非常穩定,儘管設定了備份,但4年來從未真正用到。我們目前使用 16核心機架伺服器,8GB記憶體,Ubuntu 12.04 LTS,優化配置(Postgresql.conf) 採用設定共享段shared_buffers 512MB, work_mem 32MB,維護maintenance_work_mem  512MB,checkpoint_segments = 16,獲得了穩定而持久的生產力提升。

測試:

插入4條資料

  1. insertinto base_table_machine1 (dvalue,sample_time,machine_code,max_res,curr_res) values (22.17273,'2012-06-01 11:22:11','SC3010-192.168.1.12',1,2);  
  2. insertinto base_table_machine1 (dvalue,sample_time,machine_code,max_res,curr_res) values (12.8273,'2012-07-12 10:23:01','SC3010-192.168.1.14',1,2);  
  3. insertinto base_table_machine2 (dvalue,sample_time,machine_code,manu_id,manu_value) values (4412.1928,'2011-01-21 02:08:34','PK937-192.168.1.113','TP1','E54DF');  
  4. insertinto base_table_machine2 (dvalue,sample_time,machine_code,manu_id,manu_value) values (4412.1928,'2011-12-31 04:21:31','PK937-192.168.1.112','TP2','CB67D');  

看看 select  語句的結果

select * from base_table;

select * from base_table_machine2;

select * from base_table_machine1;

explain select * from base_table where sample_time >='2012-06-21 00:00:00' and sample_time <='2012-07-21 00:00:00';

自動使用索引關聯到每個子表。