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 序列:
- CREATESEQUENCE serial_id_seq
- INCREMENT 1
- MINVALUE 1
- MAXVALUE 9223372036854775807
- START 1
- CACHE 1;
- ALTERTABLE serial_id_seq
- OWNER TO postgres;
該序列用於保持全域性ID的唯一性。PostgreSQL各個繼承表中的主鍵約束僅僅限於本表,在不想通過檢查條件確保唯一的情況下,可以通過觸發器手工從序列獲取新的值,以及限制使用者修改ID來保證唯一。基本表(爺爺表),承載了所有機器的共同屬性
- CREATETABLE base_table
- (
- id bigintNOTNULL,
- dvalue doubleprecision,
- sample_time timestampwithtime zone,
- machine_code charactervarying(32),
- CONSTRAINT pk_base_table_id PRIMARYKEY (id )
- )
- WITH (
- OIDS=FALSE
- );
- ALTERTABLE base_table
- OWNER TO postgres;
- CREATEINDEX idx_sample_time
- ON base_table
- USING btree
- (sample_time );
下面為機器型別1建立按型別分割槽子表(爸爸表)
- CREATETABLE base_table_machine1
- (
- max_res integer,
- curr_res integer,
- CONSTRAINT pk_base_table_machine1 PRIMARYKEY (id )
- )
- INHERITS (base_table)
- WITH (
- OIDS=FALSE
- );
- ALTERTABLE base_table_machine1
- OWNER TO postgres;
- CREATEINDEX idx_base_table_machine1_sample_time
- ON base_table_machine1
- USING btree
- (sample_time );
同樣,為機器2建立按型別分割槽子表
- CREATETABLE base_table_machine2
- (
- manu_id charactervarying(16),
- manu_value charactervarying(16),
- CONSTRAINT pk_base_table_machine2 PRIMARYKEY (id )
- )
- INHERITS (base_table)
- WITH (
- OIDS=FALSE
- );
- ALTERTABLE base_table_machine2
- OWNER TO postgres;
- CREATEINDEX idx_base_table_machine2_sample_time
- ON base_table_machine2
- USING btree
- (sample_time );
其他機器不再贅述。建立完後,我們開始寫建立按月分割槽表的觸發器(兒子表)。按月分割槽會判斷每次插入的資料的時刻,按照月份放到分割槽表中。如果分割槽表不存在,則自動建立。這裡給出機器1、機器2 的觸發器
- -- Function: on_insert_base_table_machine1()
- -- DROP FUNCTION on_insert_base_table_machine1();
- CREATEORREPLACEFUNCTION on_insert_base_table_machine1()
- RETURNStriggerAS
- $BODY$
- DECLARE
- --Variable Hold subtable name
- str_sub_tablename varchar;
- --Variable Hold year\month info with timestamle
- str_sub_sample_time varchar;
- str_sql_cmd varchar;
- str_sub_checkval varchar;
- BEGIN
- --The triggle func will be exectued only when BEFORE INSERT
- IF TG_OP <> 'INSERT'OR TG_TABLE_NAME <>'base_table_machine1'OR TG_WHEN <> 'BEFORE'THEN
- RETURNNULL;
- END IF;
- --Generate Table Name
- str_sub_sample_time = date_part('year',NEW.sample_time)::varchar || '_' ||
- CASEWHEN date_part('month',NEW.sample_time) <10 THEN'0'ELSE''END
- ||date_part('month',NEW.sample_time)::varchar;
- str_sub_tablename = 'machine1_' || str_sub_sample_time;
- --Check if table not created
- select * from pg_tables where schemaname = 'public'and tablename=str_sub_tablename
- into str_sql_cmd;
- IF NOT FOUND THEN
- --Create table Cmd
- str_sql_cmd = '
- CREATETABLE'||str_sub_tablename||'
- (
- CONSTRAINT pk_'|| str_sub_tablename||'PRIMARYKEY (id ),
- CONSTRAINT chk_'|| str_sub_tablename||'
- CHECK(date_part(''year''::text, sample_time) = '||
- date_part('year',NEW.sample_time)::varchar||
- '::doubleprecisionAND
- date_part(''month''::text, sample_time) = '||
- date_part('month',NEW.sample_time)::varchar||'
- )
- )
- INHERITS (base_table_machine1)
- WITH ( OIDS=FALSE );
- ALTERTABLE'||str_sub_tablename||' OWNER TO postgres;
- CREATEINDEX idx_'|| str_sub_tablename||'_sample_time
- ON'|| str_sub_tablename||'
- USING btree (sample_time );
- ';
- EXECUTE str_sql_cmd;
- END IF;
- --insert Data
- str_sql_cmd = 'INSERT INTO '||str_sub_tablename||'
- ( id,dvalue,sample_time,machine_code,max_res,curr_res) VALUES (
- nextval(''serial_id_seq''),$1,$2,$3,$4,$5);
- ';
- EXECUTE str_sql_cmd USING
- NEW.dvalue,
- NEW.sample_time,
- NEW.machine_code,
- NEW.max_res,
- NEW.curr_res;
- --return null because main table does not really contain data
- returnNULL;
- END;
- $BODY$
- LANGUAGE plpgsql VOLATILE
- COST 100;
- ALTERFUNCTION on_insert_base_table_machine1()
- OWNER TO postgres;
- -- Function: on_insert_base_table_machine2()
- -- DROP FUNCTION on_insert_base_table_machine2();
- CREATEORREPLACEFUNCTION on_insert_base_table_machine2()
- RETURNStriggerAS
- $BODY$
- DECLARE
- --Variable Hold subtable name
- str_sub_tablename varchar;
- --Variable Hold year\month info with timestamle
- str_sub_sample_time varchar;
- str_sql_cmd varchar;
- str_sub_checkval varchar;
- BEGIN
- --The triggle func will be exectued only when BEFORE INSERT
- IF TG_OP <> 'INSERT'OR TG_TABLE_NAME <>'base_table_machine2'OR TG_WHEN <> 'BEFORE'THEN
- RETURNNULL;
- END IF;
- --Generate Table Name
- str_sub_sample_time = date_part('year',NEW.sample_time)::varchar || '_' ||
- CASEWHEN date_part('month',NEW.sample_time) <10 THEN'0'ELSE''END
- ||date_part('month',NEW.sample_time)::varchar;
- str_sub_tablename = 'machine2_' || str_sub_sample_time;
- --Check if table not created
- select * from pg_tables where schemaname = 'public'and tablename=str_sub_tablename
- into str_sql_cmd;
- IF NOT FOUND THEN
- --Create table Cmd
- str_sql_cmd = '
- CREATETABLE'||str_sub_tablename||'
- (
- CONSTRAINT pk_'|| str_sub_tablename||'PRIMARYKEY (id ),
- CONSTRAINT chk_'|| str_sub_tablename||'
- CHECK(date_part(''year''::text, sample_time) = '||
- date_part('year',NEW.sample_time)::varchar||
- '::doubleprecisionAND
- date_part(''month''::text, sample_time) = '||
- date_part('month',NEW.sample_time)::varchar||'
- )
- )
- INHERITS (base_table_machine2)
- WITH ( OIDS=FALSE );
- ALTERTABLE'||str_sub_tablename||' OWNER TO postgres;
- CREATEINDEX idx_'|| str_sub_tablename||'_sample_time
- ON'|| str_sub_tablename||'
- USING btree (sample_time );
- ';
- EXECUTE str_sql_cmd;
- END IF;
- --insert Data
- str_sql_cmd = 'INSERT INTO '||str_sub_tablename||'
- ( id,dvalue,sample_time,machine_code,manu_id,manu_value) VALUES (
- nextval(''serial_id_seq''),$1,$2,$3,$4,$5);
- ';
- EXECUTE str_sql_cmd USING
- NEW.dvalue,
- NEW.sample_time,
- NEW.machine_code,
- NEW.manu_id,
- NEW.manu_value;
- --return null because main table does not really contain data
- returnNULL;
- END;
- $BODY$
- LANGUAGE plpgsql VOLATILE
- COST 100;
- ALTERFUNCTION on_insert_base_table_machine2()
- OWNER TO postgres;
最後,為各個爸爸表設定觸發器
- CREATETRIGGER triggle_on_insert_machine1
- BEFORE INSERT
- ON base_table_machine1
- FOR EACH ROW
- EXECUTEPROCEDURE on_insert_base_table_machine1();
- CREATETRIGGER triggle_machine2
- BEFORE INSERT
- ON base_table_machine2
- FOR EACH ROW
- 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條資料
- 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);
- 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);
- 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');
- 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';
自動使用索引關聯到每個子表。