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.