/* part_functions.sql - A set of functions aiding in creating partitioned tables with PostgreSQL 8.1+ @author Mikael Carneholm This file is public domain, feel free to redistribute! DISCLAIMER: UNDERSTAND WHAT YOU ARE DOING AND USE THE FUNCTIONS WITH CARE, THE AUTHOR DOES NOT TAKE ANY RESPONSIBLITY FOR DATA LOSS DUE TO MISUSE Contents: --------- partition_add(tblname text, part_suffix text, condition text, tbspc text) partition_delete(tblname text, part_suffix text) range_partition_year(tblname text, part_col text, the_year int, tbspc text) range_partition_years_between(tblname text, part_col text, start_year int, end_year int, tbspc text) range_partition_years_between_ba(tblname text, part_col text, start_year int, end_year int, tbspc text) partition_index(tblname text, part_col text, tbspc text) range_partition_index_year(tblname text, part_col text, the_year int, tbspc text) range_partition_index_years_between(tblname text, part_col text, start_year int, end_year int, tbspc text) range_partition_index_years_between_ba(tblname text, part_col text, start_year int, end_year int, tbspc text) partition_truncate_all(parent text) partition_drop_all(parent text) Steps needed to create 12 indexed subpartitions per year for table 'orders', for years 2001 - 2005 + 2 indexed partitions holding data before 2001-01-01 00:00:00 and after 2005-12-31 23:59:59 1. CREATE TABLE orders ( order_id BIGSERIAL PRIMARY KEY, customer_id INTEGER NOT NULL, product_id INTEGER NOT NULL, order_date DATE NOT NULL ); 2. select range_partition_years_between_ba('orders', 'order_date', 2001, 2005, 'tbspc1'); 3. select range_partition_index_years_between_ba('orders', 'order_date', 2001, 2005, 'tbspc2'); */ /*----------------------------------------------------------------------------------------------------------------- Utility function that creates a range/list partition with accompanying insert & update rules (+supporting update function) for a table Example usage: select partition_add( 'orders', '0501', 'order_date >= ''20050101'' and order_date < ''20050201'' ', 'tbspc1'); => created table: orders_0501 (on tablespace tbspc1) => created or updated function: orders_update_func => created or updated rules: orders_update_rule, orders_0501_insert_rule select partition_add( 'customers', 'uscanada', 'country in (''us'',''canada'') ', null); => created table: customers_uscanada (on default tablespace) => created or updated function: customers_update_func => created or updated rules: customers_update_rule, customers_uscanada_insert_rule Note: strings passed as the condition param need to be escaped (see above example) ------------------------------------------------------------------------------------------------------------------*/ create or replace function partition_add(tblname text, part_suffix text, condition text, tbspc text) returns text as $$ declare table_to_partition pg_class.relname%TYPE := null; name_base text := ''; table_columns text := ''; table_columns_new text := ''; oldrow_string text := ''; column_name record; pk_name pg_attribute.attname%TYPE; create_table_cmd text := ''; create_insert_rule_cmd text := ''; create_update_func_cmd text := ''; create_update_rule_cmd text := ''; begin -- check that table exists select pgc.relname into table_to_partition from pg_class pgc where pgc.relname = tblname; if table_to_partition is null then raise exception 'table % does not exist', tblname; end if; -- check presence of PK select a.attname::text into pk_name from pg_class c join pg_constraint o on ( c.relname = tblname and c.oid = o.conrelid and o.contype='p') join pg_attribute a on (a.attrelid = c.oid and a.attnum = any (o.conkey)); if pk_name is null then raise exception 'table % has no primary key', tblname; end if; -- ok, proceed name_base := tblname || '_' || part_suffix; -- create column lists for rules & functions for column_name in execute 'select pga.attname from pg_attribute pga, pg_class pgc where pga.attrelid = pgc.relfilenode and pgc.relname = '''|| tblname || ''' and pga.attnum > 0' loop table_columns := table_columns || column_name.attname || ','; table_columns_new := table_columns_new ||'NEW.'|| column_name.attname || ','; oldrow_string := oldrow_string ||'oldrow.'|| column_name.attname || ','; end loop; -- chop off last ',' from column lists table_columns := substr(table_columns,1,length(table_columns)-1); table_columns_new := substr(table_columns_new,1,length(table_columns_new)-1); oldrow_string := substr(oldrow_string,1,length(oldrow_string)-1); -- create table command string create_table_cmd := 'create table ' || name_base || ' ( ' || 'constraint ' || name_base || '_insert_check check ('|| condition ||')' || ' ) ' || 'inherits (' || tblname || ')'; if tbspc is not null then create_table_cmd := create_table_cmd||' tablespace '||tbspc; end if; create_table_cmd := create_table_cmd||';'; -- insert rule command string create_insert_rule_cmd := 'create or replace rule '|| name_base ||'_insert_rule as '|| 'on insert to '|| tblname ||' where ('|| condition ||') '|| 'do instead insert into '|| name_base ||'('||table_columns||') values ('|| table_columns_new ||');' ; -- update function command string create_update_func_cmd := 'create or replace function '||tblname||'_update_func(oldrow '||tblname||') returns void as '''|| 'begin '|| ' delete from '||tblname||' where '||pk_name||'=oldrow.'||pk_name||'; '|| ' insert into '||tblname||' ('||table_columns||') values ('||oldrow_string||'); '|| 'end; '''|| 'language plpgsql;'; -- update rule command string create_update_rule_cmd := 'create or replace rule '||tblname||'_update_rule as '|| 'on update to '||tblname||' '|| 'do instead select '||tblname||'_update_func(NEW);'; raise notice 'Creating table %', name_base; execute create_table_cmd; raise notice 'Creating rule %', name_base ||'_insert_rule'; execute create_insert_rule_cmd; raise notice 'Creating/replacing function %', tblname ||'_update_func'; execute create_update_func_cmd; raise notice 'Creating/replacing rule %', tblname ||'_update_rule'; execute create_update_rule_cmd; return 'PARTITION'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that drops a partition with along its accompanying insert rule Example usage: select partition_delete('mytable', '0501'); => dropped table: mytable_0501 => dropped rules: mytable_0501_insert_rule ------------------------------------------------------------------------------------------------------------------*/ create or replace function partition_delete(tblname text, part_suffix text) returns text as $$ begin raise notice 'Dropping rule %', tblname ||'_'|| part_suffix ||'_insert_rule'; execute 'drop rule '|| tblname ||'_'|| part_suffix ||'_insert_rule on '|| tblname; raise notice 'Dropping table %', tblname ||'_'|| part_suffix; execute 'drop table '|| tblname ||'_'|| part_suffix; return 'DROP'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that creates 12 month partitions on table tblname on column part_col, for year the_year Example usage: select range_partition_year('mytable', 'datecol', 2005, 'tbspc1'); => created tables: mytable_200501, mytable_200502, mytable_200503 etc (on tablespace tbspc1) => created or updated function: mytable_update_func => created or updates rules: mytable_update_rule, mytable_200501_insert_rule, mytable_200502_insert_rule, etc select range_partition_year('mytable', 'datecol', 2005, null); => same as above, but the tables will be created on default tablespace ------------------------------------------------------------------------------------------------------------------*/ create or replace function range_partition_year(tblname text, part_col text, the_year int, tbspc text) returns text as $$ declare c_year text := trim(to_char(the_year, '9999')); begin perform partition_add(tblname, c_year||'01', part_col||' >= '''||c_year||'0101'' and '|| part_col ||' < '''||c_year||'0201''', tbspc); perform partition_add(tblname, c_year||'02', part_col||' >= '''||c_year||'0201'' and '|| part_col ||' < '''||c_year||'0301''', tbspc); perform partition_add(tblname, c_year||'03', part_col||' >= '''||c_year||'0301'' and '|| part_col ||' < '''||c_year||'0401''', tbspc); perform partition_add(tblname, c_year||'04', part_col||' >= '''||c_year||'0401'' and '|| part_col ||' < '''||c_year||'0501''', tbspc); perform partition_add(tblname, c_year||'05', part_col||' >= '''||c_year||'0501'' and '|| part_col ||' < '''||c_year||'0601''', tbspc); perform partition_add(tblname, c_year||'06', part_col||' >= '''||c_year||'0601'' and '|| part_col ||' < '''||c_year||'0701''', tbspc); perform partition_add(tblname, c_year||'07', part_col||' >= '''||c_year||'0701'' and '|| part_col ||' < '''||c_year||'0801''', tbspc); perform partition_add(tblname, c_year||'08', part_col||' >= '''||c_year||'0801'' and '|| part_col ||' < '''||c_year||'0901''', tbspc); perform partition_add(tblname, c_year||'09', part_col||' >= '''||c_year||'0901'' and '|| part_col ||' < '''||c_year||'1001''', tbspc); perform partition_add(tblname, c_year||'10', part_col||' >= '''||c_year||'1001'' and '|| part_col ||' < '''||c_year||'1101''', tbspc); perform partition_add(tblname, c_year||'11', part_col||' >= '''||c_year||'1101'' and '|| part_col ||' < '''||c_year||'1201''', tbspc); perform partition_add(tblname, c_year||'12', part_col||' >= '''||c_year||'1201'' and '|| part_col ||' < '''||to_char(the_year+1, '9999')||'0101''', tbspc); return 'PARTITION'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that range partitions a table on column part_col between start_year and end_year, creating ((end_year-start_year)+1)*12 partitions Example usage: select range_partition_years_between('mytable', 'datecol', 2001, 2003, 'tbspc1'); => created tables: mytable_2001*, mytable_2002*, mytable_2003* (on tablespace tbspc1) => created or updated function: mytable_update_func => created or updated rules: mytable_update, mytable_2001??_insert_rule, mytable_2002??_insert_rule, etc select range_partition_years_between('mytable', 'datecol', 2001, 2003, null); => same as above, but the tables will be created on default tablespace ------------------------------------------------------------------------------------------------------------------*/ create or replace function range_partition_years_between(tblname text, part_col text, start_year int, end_year int, tbspc text) returns text as $$ begin for i in start_year..end_year loop perform range_partition_year(tblname, part_col, i, tbspc); end loop; return 'PARTITION'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that range partitions a table on column part_col between start_year and end_year, creating ((end_year-start_year)+3)*12 partitions (ba = "before" and "after" partitions) Example usage: select range_partition_years_between_ba('mytable', 'datecol', 2001, 2003, 'tbspc1'); => created tables: mytable_before2001, mytable_2001*, mytable_2002*, mytable_2003*, mytable_after2003 (on tablespace tbspc1) => created or updated function: mytable_update_func => created or updated rules: mytable_update, mytable_before2001, mytable_2001??_insert_rule, mytable_2002??_insert_rule, mytable_2003??_insert_rule, mytable_after2003_insert_rule select range_partition_years_between_ba('mytable', 'datecol', 2001, 2003, null); => same as above, but the tables will be created on default tablespace ------------------------------------------------------------------------------------------------------------------*/ create or replace function range_partition_years_between_ba(tblname text, part_col text, start_year int, end_year int, tbspc text) returns text as $$ declare c_start_year text := trim(to_char(start_year, '9999')); c_end_year text := trim(to_char(end_year, '9999')); begin perform partition_add(tblname, 'before'||c_start_year, part_col||' < '''||start_year||'0101''', tbspc); for i in start_year..end_year loop perform range_partition_year(tblname, part_col, i, tbspc); end loop; perform partition_add(tblname, 'after'||c_end_year, part_col||' >= '''||to_char(end_year+1, '9999')||'0101''', tbspc); return 'PARTITION'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that creates an index on column part_col for table tblname Example usage: select partition_index('orders_200501', 'order_date', 'tbspc1'); => created index: idx_orders_200501_order_date select partition_index('orders_200501', 'order_date', null); => same as above, but the index will be created on default tablespace ------------------------------------------------------------------------------------------------------------------*/ create or replace function partition_index(tblname text, part_col text, tbspc text) returns text as $$ declare create_index_cmd text := ''; begin create_index_cmd := 'create index idx_'||tblname||'_'||part_col||' on '||tblname||'('||part_col||')'; if tbspc is not null then create_index_cmd := create_index_cmd||' tablespace '||tbspc; end if; create_index_cmd := create_index_cmd||';'; raise notice 'Creating index %', tblname||'_'||part_col; execute create_index_cmd; return 'CREATE'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that creates an index on column part_col for the 12 month partitions on table tblname for one year Example usage: select range_partition_index_year('orders', 'order_date', 2005, 'tbspc1'); => created indexes: idx_orders_200501_order_date .. idx_orders_200512_order_date select range_partition_index_year('orders', 'order_date', 2005, null); => same as above, but the indexes will be created on default tablespace ------------------------------------------------------------------------------------------------------------------*/ create or replace function range_partition_index_year(tblname text, part_col text, the_year int, tbspc text) returns text as $$ begin for i in 1..12 loop perform partition_index(tblname||'_'||the_year::text||lpad(i::text, 2, '0'), part_col, tbspc); end loop; return 'CREATE'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that creates an index on column part_col for the 12 month partitions on table tblname for years between start_year and end_year Example usage: select range_partition_index_years_between('orders', 'order_date', 2003, 2005, 'tbspc1'); => created indexes: idx_orders_2003??_order_date .. idx_orders_2005??_order_date select range_partition_index_years_between('orders', 'order_date', 2003, 2005, null); => same as above, but the indexes will be created on default tablespace ------------------------------------------------------------------------------------------------------------------*/ create or replace function range_partition_index_years_between(tblname text, part_col text, start_year int, end_year int, tbspc text) returns text as $$ begin for i in start_year..end_year loop perform range_partition_index_year(tblname, part_col, i, tbspc); end loop; return 'CREATE'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that creates an index on column part_col for the 12 month partitions on table tblname for years between start_year and end_year, creating ((end_year-start_year)+3)*12 indexes (ba = "before" and "after" partitions) Example usage: select range_partition_index_years_between_ba('orders', 'order_date', 2003, 2005, 'tbspc1'); => created indexes: idx_orders_before2003_order_date, idx_orders_2003??_order_date, idx_orders_2004??_order_date, idx_orders_2005??_order_date, idx_orders_after2005_order_date select range_partition_index_years_between_ba('orders', 'order_date', 2003, 2005, null); => same as above, but the indexes will be created on default tablespace ------------------------------------------------------------------------------------------------------------------*/ create or replace function range_partition_index_years_between_ba(tblname text, part_col text, start_year int, end_year int, tbspc text) returns text as $$ begin perform partition_index(tblname||'_before'||start_year::text, part_col, tbspc); for i in start_year..end_year loop perform range_partition_index_year(tblname, part_col, i, tbspc); end loop; perform partition_index(tblname||'_after'||end_year::text, part_col, tbspc); return 'CREATE'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that truncates a table along with all it's children (partitions) Example usage: select partition_truncate_all('mytable') => truncated tables: mytable* ------------------------------------------------------------------------------------------------------------------*/ create or replace function partition_truncate_all(parent text) returns text as $$ declare tblname record; begin for tblname in execute 'select tablename from pg_tables where tableowner = '''|| user ||''' and tablename like '''||parent||'%''' loop execute 'truncate '||tblname.tablename::text; end loop; return 'TRUNCATE'; end; $$ language plpgsql; /*----------------------------------------------------------------------------------------------------------------- Utility function that drops a table along with all it's children (partitions) Example usage: select partition_drop_all('mytable') => dropped tables: mytable* ------------------------------------------------------------------------------------------------------------------*/ create or replace function partition_drop_all(parent text) returns text as $$ declare tblname record; begin for tblname in execute 'select distinct tablename from pg_tables where tableowner = '''|| user ||''' and tablename like '''||parent||'_%'' order by tablename desc' loop execute 'drop table '||tblname.tablename::text||' cascade'; end loop; return 'DROP'; end; $$ language plpgsql; -- TODO: range_partition_analyze*