Já falamos um bocado sobre particionamento de tabelas por aqui:
- Particionamento de Tabelas no postgres – Quando?
- Particionamento de Tabelas no postgres – Como
- Particionamento de Tabelas no postgres – Detalhes
Para encerrar a série, vamos juntar tudo numa única função para tornar o dia-a-dia mais simples. Claro, se você tem poucas tabelas particionadas e se criar novas partições é algo raro, então você não deve se preocupar com isso. Assim sendo, sei que estou escrevendo para um público bem pequeno, mas que vem crescendo conforme o postgres sai do cantinho dos fundos dos grandes CPDs para ocupar cada vez mais uma posição de destaque. Bom, uma função genérica para qualquer tipo de particionamento daria muito trabalho para implementar, portanto a função que vou apresentar apresenta algumas características particulares que você pode adaptar para necessidades específicas se quiser:
- Eu utilizo uma tabela para cadastrar o nome das tabelas que serão particionadas e algumas informações específicas sobre o particionamento de cada uma destas tabelas. Assim a nossa função vai sempre varrer esta tabela e criar todas as partições cadastradas de uma vez só;
- A chave será sempre num campo do tipo INTEGER e cada partição utilizará um valor único na chave e não uma série de valores. Então vamos ter coisas como CHAVE = VALOR;
- A função está preparada para trabalhar com versões a partir do 8.3 do postgres. Se você utiliza versões anteriores a 8.3… realmente está na hora de você migrar. E veja se vai logo para o 9.2, pois as vantagens são realmente recompensadoras. Faremos algumas checagens dentro da função para saber com qual versão estamos lidando e fazer os ajustes necessários;
- Não iremos apenas criar as partições. Vamos aproveitar para já fazer algumas manutenções de acordo com as informações cadastradas na nossa tabela de partições.
Ao executar a função vamos executar as seguintes tarefas nesta ordem:
- Verifica se alguma FK de tabela não particionada aponta para uma tabela particionada;
- Carrega dados no cadastro de particionamento;
- Verifica se a tabela mãe cadastrada existe?
- Verifica se o tablespace existe;
- Verifica se a partição a ser criada já existe;
- Cria a partição da tabela;
- Ajusta parâmetros de STORAGE na partição criada;
- Remove restrição NO INHERIT da tabela filha;
- Importa os dados da tabela mãe para a partição;
- Cria CHECK CONSTRAINT para a chave da partição;
- Ajusta o dono da partição criada;
- Ajusta GRANTs para a partição criada;
- Cria ou substitui função do gatilho;
- Ajusta o dono da função;
- Verifica se o gatilho ja existe;
- Cria constraints para tabelas não particionadas;
- Arquiva partição antiga;
- Apaga partição arquivada antiga;
- Apaga partição antiga;
Agora vejamos a nossa tabela para cadastrar os parâmetros de todas tabelas que serão particionadas numa tacada só.
DROP TABLE IF EXISTS parametro_particao; CREATE TABLE parametro_particao ( nome_tabela_esquema varchar(64) NOT NULL, nome_tabela varchar(64) NOT NULL, nome_coluna_chave varchar(64) NOT NULL, nome_tablespace varchar(64) NOT NULL DEFAULT 'pg_default', qt_chave_ativa integer DEFAULT 5, qt_chave_excluir integer, qt_chave_desfilhar integer, ordem_criacao integer, CONSTRAINT parametro_particao_pk PRIMARY KEY (nome_tabela_esquema , nome_tabela ), CONSTRAINT parametro_particao_ck1 CHECK (qt_chave_ativa > 1), CONSTRAINT parametro_particao_ck2 CHECK (qt_chave_excluir > qt_chave_ativa AND qt_chave_excluir < 100), CONSTRAINT parametro_particao_ck3 CHECK (qt_chave_desfilhar > qt_chave_ativa AND qt_chave_desfilhar < 100) ); ALTER TABLE parametro_particao OWNER TO postgres; COMMENT ON TABLE parametro_particao IS 'Cadastro de tabelas particionadas e parâmetros de ajuste de manutenção das mesmas'; COMMENT ON COLUMN parametro_particao.nome_tabela_esquema IS 'Nome do esquema da tabela particionada'; COMMENT ON COLUMN parametro_particao.nome_tabela IS 'Nome da tabela particionada'; COMMENT ON COLUMN parametro_particao.nome_coluna_chave IS 'Nome do campo da tabela que será utilizado como chave para o particionamento'; COMMENT ON COLUMN parametro_particao.nome_tablespace IS 'Nome do tablespace onde a partição será criada'; COMMENT ON COLUMN parametro_particao.qt_chave_ativa IS 'Quantidade de chaves ativas no gatilho de INSERT da tabela mãe'; COMMENT ON COLUMN parametro_particao.qt_chave_excluir IS 'Quantidade de partições a serem mantidas. Serão mantidas "qt_chaves_apagar" com "no_coluna_chave" maiores, as demais serão excluídas. O valor NULL desativa a esclusão de partições'; COMMENT ON COLUMN parametro_particao.qt_chave_desfilhar IS 'Quantidade de partições a serem desfilhadas (deixar de ser uma partição filha da tabela mãe). Serão mantidas "qt_chaves_desfilhar" com "no_coluna_chave" maiores, as demais serão desfilhadas. O valor NULL desativa a desfilhação.'; COMMENT ON COLUMN parametro_particao.ordem_criacao IS 'Órdem de processamento das tabelas. Utilizado para respeitar as FKs das partições.';
Para testar, vamos utilizar as tabelas DETALHE e DETALHE_PEDIDO criadas nos posts anteriores:
INSERT INTO parametro_particao VALUES ('app', 'pedido', 'ano_pedido', 'pedido', 5, 99, 50, 1);
INSERT INTO parametro_particao VALUES ('app', 'pedido_detalhe', 'ano_pedido', 'pedido_detalhe', 5, 99, 50, 2);
E la vai a pedrada… segue o código a função
-- Function: public.cria_particao_tabela(integer, boolean, boolean)
DROP FUNCTION IF EXISTS public.cria_particao_tabela(integer, boolean, boolean);
CREATE OR REPLACE FUNCTION public.cria_particao_tabela(
IN p_next_partition_key integer, --Número da nova partição
IN p_archive boolean, --Colocar como NO INHERIT tabelas marcadas com 'qt_chaves_desfiliar'
IN p_drop boolean, --Apagar partições mais antigas que 'qt_chaves_apagar'
OUT v_cod_erro text, --Mensagem de erro
OUT v_pos_erro integer) --Posição do erro no código
RETURNS record AS
$BODY$
--
-- Descrição: Cria novas partições nas tabelas cadastradas em 'parametro_particao'
-- Autor: Fábio Telles Rodriguez
--
DECLARE
v_sql text;
v_tmp integer;
c_pp record;
c_part record;
-- Pegar FKs de tabelas não particionadas que apontam para tabelas particionadas
c_loop_const_fora CURSOR FOR SELECT tc.conname AS const
FROM
pg_constraint AS tc,
pg_class AS c,
pg_class AS r
WHERE
tc.contype = 'f'
AND tc.conrelid = c.oid
AND tc.confrelid = r.oid
AND c.relname NOT IN (SELECT nome_tabela FROM parametro_particao)
AND r.relname IN (SELECT nome_tabela FROM parametro_particao)
AND NOT EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhrelid = c.oid);
-- Criar FKs de tabelas particionadas que apontam para outras tabelas particionadas
c_loop_const_part CURSOR FOR
SELECT 'ALTER TABLE ' || pp.nome_tabela_esquema || '.' || pp.nome_tabela || '_' || p_next_partition_key ||
' ADD CONSTRAINT ' || conname || '_' || p_next_partition_key || ' ' ||
replace(pg_get_constraintdef(tc.oid),
r.relname, r.relname || '_' || p_next_partition_key) ||
';' AS alter_c
FROM
pg_constraint AS tc,
pg_class AS c,
pg_class AS r,
pg_namespace AS nc,
pg_namespace AS nr,
parametro_particao AS pp
WHERE
tc.contype = 'f'
AND tc.conrelid = c.oid
AND nc.oid = c.relnamespace
AND c.relname = pp.nome_tabela
AND nc.nspname = pp.nome_tabela_esquema
AND tc.confrelid = r.oid
AND nr.oid = r.relnamespace
AND EXISTS (SELECT 1 FROM parametro_particao rpp
WHERE
nr.nspname = rpp.nome_tabela_esquema AND
r.relname = rpp.nome_tabela);
-- Criar FKs de tabelas particionadas que apontam para tabelas não particionadas
c_loop_constraint CURSOR (
p_table_schema varchar,
p_table_name varchar) FOR
SELECT 'ALTER TABLE ' || p_table_schema || '.' || p_table_name || '_' || p_next_partition_key ||
' ADD CONSTRAINT ' || conname || '_' || p_next_partition_key || ' ' || pg_get_constraintdef(tc.oid) || ';' AS alter_c
FROM
pg_constraint AS tc,
pg_class AS c,
pg_class AS r,
pg_namespace AS nc,
pg_namespace AS nr,
parametro_particao AS pp
WHERE
tc.contype = 'f'
AND tc.conrelid = c.oid
AND nc.nspname = p_table_schema
AND nc.nspname = pp.nome_tabela_esquema
AND nc.oid = c.relnamespace
AND c.relname = p_table_name
AND c.relname = pp.nome_tabela
AND tc.confrelid = r.oid
AND nr.oid = r.relnamespace
AND NOT EXISTS (SELECT 1 FROM parametro_particao rpp
WHERE
nr.nspname = rpp.nome_tabela_esquema AND
r.relname = rpp.nome_tabela);
-- Criar condições na função do gatilho para demais partições já existentes
c_loop_partitions CURSOR (
p_part_col varchar,
p_table_schema varchar,
p_table_name varchar,
p_key_active integer ) FOR
SELECT 'ELSIF ( NEW.' || p_part_col || ' = ' || replace(r.relname, c.relname || '_', '') || ') THEN ' || chr(10)
|| ' INSERT INTO ' || p_table_schema || '.' || r.relname || ' VALUES (NEW.*);' || chr(10) AS sql
FROM
pg_inherits i
JOIN pg_class c ON i.inhparent = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_class r ON i.inhrelid = r.oid
WHERE
c.relname = p_table_name AND
n.nspname = p_table_schema AND
r.relname != (p_table_name || '_' || p_next_partition_key)
ORDER BY to_number(replace(r.relname, c.relname || '_', ''),'999999') DESC
LIMIT p_key_active -1;
c_loop_grant CURSOR (
p_table_schema varchar,
p_table_name varchar) FOR
SELECT 'GRANT ' || privilege_type || ' ON ' || table_schema || '.' || table_name || '_' || p_next_partition_key || ' TO ' || grantee ||
CASE is_grantable WHEN 'YES' THEN ' WITH GRANT OPTION' ELSE '' END || ';' sql
FROM information_schema.table_privileges
WHERE
table_schema = p_table_schema AND
table_name = p_table_name;
-- Desfiliar partições com mais de 'qt_chave_desfiliar' além da última
c_loop_arc CURSOR (
p_table_schema varchar,
p_table_name varchar,
p_key_arc integer) FOR
SELECT 'ALTER TABLE ' || p_table_schema || '.' || r.relname || ' NO INHERIT ' || p_table_schema || '.' || p_table_name || ';' AS sql
FROM
pg_inherits i
JOIN pg_class c ON i.inhparent = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_class r ON i.inhrelid = r.oid
WHERE
c.relname = p_table_name AND
n.nspname = p_table_schema
ORDER BY to_number(replace(r.relname, c.relname || '_', ''),'999999') DESC
OFFSET p_key_arc;
-- Apagar partições com mais de 'qt_chave_excluir' além da última
c_loop_drop CURSOR (
p_table_schema varchar,
p_table_name varchar,
p_key_drop integer) FOR
SELECT 'DROP TABLE IF EXISTS ' || p_table_schema || '.' || r.relname || ';' AS sql
FROM
pg_inherits i
JOIN pg_class c ON i.inhparent = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_class r ON i.inhrelid = r.oid
WHERE
c.relname = p_table_name AND
c.relkind = 'r' AND
n.nspname = p_table_schema
ORDER BY to_number(replace(r.relname, c.relname || '_', ''),'999999') DESC
OFFSET p_key_drop;
-- Carregar dados de parametro_particao
c_tabelas CURSOR FOR SELECT
nome_tabela_esquema AS tbl_sch,
nome_tabela AS tbl_nom,
nome_coluna_chave AS par_col,
nome_tablespace AS tbs_def,
qt_chave_ativa AS key_act,
qt_chave_excluir AS key_drp,
qt_chave_desfilhar AS key_arc,
ordem_criacao AS tbl_ord
FROM parametro_particao
ORDER BY ordem_criacao;
-- Chega de declarações, vamos trabalhar!!!
BEGIN
v_cod_erro := 0;
v_pos_erro := 0;
RAISE NOTICE 'cria_particao_tabela|Begin';
-- Verifica se alguma FK de tabela não particionada aponta para uma tabela particionada
FOR c_part IN c_loop_const_fora LOOP
v_pos_erro := 10;
RAISE EXCEPTION 'cria_particao_tabela| A constraint % aponta para uma tabela particionada mas não será particionada também!!!', c_part.const;
END LOOP;
-- Carrega dados no cadastro de particionamento
v_pos_erro := 20;
FOR c_pp IN c_tabelas LOOP
-- A tabela mãe existe?
v_pos_erro := 30;
SELECT 1
INTO v_tmp
FROM information_schema.tables
WHERE
table_catalog = current_database() AND
table_schema = c_pp.tbl_sch AND
table_name = c_pp.tbl_nom;
IF NOT FOUND THEN
v_pos_erro := 40;
RAISE EXCEPTION 'cria_particao_tabela|A Tabela Mãe % configurada em tbom_parametro_particao não existe', c_pp.tbl_nom;
END IF;
-- Verifica se o tablespace existe
v_pos_erro := 50;
SELECT 1
INTO v_tmp
FROM pg_catalog.pg_tablespace
WHERE spcname = c_pp.tbs_def;
IF NOT FOUND THEN
v_pos_erro := 60;
RAISE EXCEPTION 'cria_particao_tabela|A tablespace para a particao da tabela % não existe', c_pp.tbl_nom ;
END IF;
-- Verifica se a particao da tabela ja existe
v_pos_erro := 70;
SELECT 1
INTO v_tmp
FROM information_schema.tables
WHERE
table_name = c_pp.tbl_nom || '_' || p_next_partition_key AND
table_schema = c_pp.tbl_sch;
IF FOUND THEN
v_pos_erro := 80;
RAISE EXCEPTION 'cria_particao_tabela|A partição da tabela % com chave % de valor % já existe',
c_pp.tbl_sch, c_pp.par_col, p_next_partition_key ;
END IF;
-- Cria a partição da tabela
v_pos_erro := 90;
RAISE WARNING 'cria_particao_tabela|Cria particao %', c_pp.tbl_nom || '_' || p_next_partition_key;
v_sql := 'CREATE TABLE ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || '_' || p_next_partition_key
|| '(LIKE ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom
|| ' INCLUDING STORAGE INCLUDING INDEXES INCLUDING DEFAULTS INCLUDING CONSTRAINTS ) INHERITS ('
|| c_pp.tbl_sch || '.' || c_pp.tbl_nom || ') TABLESPACE ' || c_pp.tbs_def;
v_pos_erro := 100;
EXECUTE v_sql;
-- Ajusta parâmetros de STORAGE na partição criada
v_pos_erro := 110;
SELECT 'ALTER TABLE ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || '_' || p_next_partition_key
|| ' SET (' || array_to_string(c.reloptions,',') || ');' AS sql
INTO v_sql
FROM
pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE
c.relname = c_pp.tbl_nom AND
n.nspname = c_pp.tbl_sch;
IF FOUND AND v_sql IS NOT NULL THEN
v_pos_erro := 120;
EXECUTE v_sql;
END IF;
-- Remove restrição NO INHERIT da tabela filha;
-- ### Só funciona a partir da versão 9.2 ###
v_pos_erro := 130;
SELECT 'ALTER TABLE ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || '_' || p_next_partition_key
|| ' DROP CONSTRAINT ' || conname || ';'
INTO v_sql
FROM
pg_constraint cc
JOIN pg_class c ON
cc.conrelid = c.oid AND
cc.connamespace = c.relnamespace
JOIN pg_namespace n ON
c.relnamespace = n.oid
WHERE
connoinherit AND
contype = 'c' AND
c.relname = c_pp.tbl_nom AND
n.nspname = c_pp.tbl_sch;
IF FOUND AND v_sql IS NOT NULL THEN
v_pos_erro := 140;
EXECUTE v_sql;
END IF;
-- Remove restrição NO INHERIT da tabela filha;
v_pos_erro := 150;
v_sql := 'INSERT INTO ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || '_' || p_next_partition_key ||
' SELECT * FROM ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || ' WHERE ' ||
c_pp.par_col || ' = ' || p_next_partition_key || ';';
IF FOUND AND v_sql IS NOT NULL THEN
v_pos_erro := 160;
EXECUTE v_sql;
v_sql := 'DELETE FROM ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || ' WHERE ' || c_pp.par_col ||
' = ' || p_next_partition_key || ';';
v_pos_erro := 170;
EXECUTE v_sql;
END IF;
-- Cria CHECK CONSTRAINT para a chave da partição
v_pos_erro := 180;
v_sql := 'ALTER TABLE ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || '_' || p_next_partition_key
|| ' ADD CONSTRAINT ' || c_pp.tbl_nom || '_' || p_next_partition_key || '_check_' || p_next_partition_key
|| ' CHECK (' || c_pp.par_col || ' = ' || p_next_partition_key || ');';
EXECUTE v_sql;
-- Ajusta o owner da partição criada
v_pos_erro := 190;
SELECT 'ALTER TABLE ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || '_' || p_next_partition_key
|| ' OWNER TO ' || a.rolname || ';'
INTO v_sql
FROM
pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_authid a ON c.relowner = a.oid
WHERE
c.relname = c_pp.tbl_nom AND
n.nspname = c_pp.tbl_sch;
v_pos_erro := 200;
EXECUTE v_sql;
-- Ajusta GRANTs para a partição criada
v_pos_erro := 210;
FOR c_part IN c_loop_grant(c_pp.tbl_sch, c_pp.tbl_nom) LOOP
v_pos_erro := 220;
EXECUTE c_part.sql;
END LOOP;
-- Cria ou substitui função do gatilho
v_pos_erro := 230;
-- Inicio da funcao
v_pos_erro := 240;
v_sql := 'CREATE OR REPLACE FUNCTION ' || c_pp.tbl_sch || '.fc_tg_' || c_pp.tbl_nom || '()' || chr(10)
|| 'RETURNS TRIGGER AS $$' || chr(10)
|| 'BEGIN' || chr(10) || chr(10);
-- Regra para a nova particao
v_pos_erro := 250;
v_sql := v_sql || 'IF ( NEW.' || c_pp.par_col || ' = ' || p_next_partition_key || ') THEN ' || chr(10)
|| ' INSERT INTO ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || '_' || p_next_partition_key
|| ' VALUES (NEW.*);' || chr(10);
-- Regra para as particoes ja existentes
v_pos_erro := 260;
IF c_pp.key_act IS NULL THEN
c_pp.key_act = 100;
END IF;
v_pos_erro := 270;
FOR c_part IN c_loop_partitions (c_pp.par_col, c_pp.tbl_sch, c_pp.tbl_nom, c_pp.key_act) LOOP
v_pos_erro := 280;
v_sql := v_sql || c_part.sql;
END LOOP;
-- Final da funcao
v_pos_erro := 290;
v_sql := v_sql || 'ELSE' || chr(10)
|| ' RAISE EXCEPTION ''Valor de chave inválida para o gatilho de INSERT na tabela %!'',' || c_pp.tbl_nom || ';' || chr(10)
|| 'END IF;' || chr(10) || chr(10)
|| 'RETURN NULL;' || chr(10)
|| 'END;' || chr(10)
|| '$$' || chr(10)
|| 'LANGUAGE plpgsql;';
v_pos_erro := 300;
EXECUTE v_sql;
-- Ajusta o owner função
v_pos_erro := 310;
SELECT 'ALTER FUNCTION ' || c_pp.tbl_sch || '.fc_tg_' || c_pp.tbl_nom || '()'
|| ' OWNER TO ' || a.rolname || ';'
INTO v_sql
FROM
pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_authid a ON c.relowner = a.oid
WHERE
c.relname = c_pp.tbl_nom AND
n.nspname = c_pp.tbl_sch;
v_pos_erro := 320;
EXECUTE v_sql;
-- Verifica se o gatilho ja existe
v_pos_erro := 330;
SELECT 1
INTO v_tmp
FROM information_schema.triggers
WHERE
trigger_name = 'tg_' || c_pp.tbl_nom AND
trigger_schema = c_pp.tbl_sch;
IF NOT FOUND THEN
-- Cria o gatilho na tabela mãe
v_pos_erro := 340;
v_sql := 'CREATE TRIGGER tg_' || c_pp.tbl_nom || chr(10)
|| 'BEFORE INSERT ON ' || c_pp.tbl_sch || '.' || c_pp.tbl_nom || chr(10)
|| ' FOR EACH ROW EXECUTE PROCEDURE ' || c_pp.tbl_sch || '.fc_tg_' || c_pp.tbl_nom || '();';
v_pos_erro := 350;
EXECUTE v_sql;
END IF;
-- Cria constraints para tabelas não particionadas
v_pos_erro := 360;
FOR c_part IN c_loop_constraint(c_pp.tbl_sch, c_pp.tbl_nom) LOOP
v_pos_erro := 360;
EXECUTE c_part.alter_c;
END LOOP;
-- Arquiva partição antiga
v_pos_erro := 370;
IF p_archive AND c_pp.key_arc IS NOT NULL THEN
FOR c_part IN c_loop_arc(c_pp.tbl_sch, c_pp.tbl_nom, c_pp.key_arc) LOOP
v_pos_erro := 380;
EXECUTE c_part.sql;
END LOOP;
END IF;
-- Apaga partição antiga
v_pos_erro := 390;
IF p_drop AND c_pp.key_drp IS NOT NULL THEN
FOR c_part IN c_loop_drop(c_pp.tbl_sch, c_pp.tbl_nom, c_pp.key_drp) LOOP
v_pos_erro := 400;
EXECUTE c_part.sql;
END LOOP;
END IF;
END LOOP;
v_pos_erro := 410;
FOR c_part IN c_loop_const_part LOOP
v_pos_erro := 420;
EXECUTE c_part.alter_c;
END LOOP;
RAISE WARNING 'cria_particao_tabela|Fim';
EXCEPTION
WHEN OTHERS THEN
v_cod_erro := SUBSTR(SQLERRM,1,2000);
RAISE WARNING 'cria_particao_tabela|ERRO!!! POS: %, COD: %, MSG: %', v_pos_erro, SQLSTATE, v_cod_erro;
END;
$BODY$
LANGUAGE plpgsql VOLATILE SECURITY DEFINER
COST 100000;
E por fim o teste no psql:
teste=# set client_min_messages = warning; SET teste=# select cria_particao_tabela(2015, true, true); WARNING: cria_particao_tabela|Cria particao pedido_2015 WARNING: cria_particao_tabela|Cria particao pedido_detalhe_2015 WARNING: cria_particao_tabela|Fim cria_particao_tabela ---------------------- (0,420) (1 row)
Você pode adaptar este script para trabalhar com intervalos ou outros tipos de dados alem de inteiros. Se tiver alguma sugestão para melhorar… é só comentar aqui.
Bom, acho que chega de particionamento por enquanto.

Deixe um comentário