Prefazione o come è nata l'idea della partizionamento
L'inizio della storia qui: Dopo aver esaurito quasi tutte le risorse per ottimizzare la query, a quel punto si è posto il problema: e adesso cosa facciamo? Così è nata l'idea del partizionamento.

Una digressione lirica:
Proprio 'a quel punto', perché . Grazie e a Habr!
Quindi, come possiamo rendere il cliente in qualche modo felice e al contempo migliorare le proprie competenze?
Se semplifichiamo al massimo, ci sono fondamentalmente solo due modi per migliorare radicalmente le prestazioni del database:
1) Approccio estensivo - aumentiamo le risorse, cambiamo la configurazione;
2) Approccio intensivo - ottimizzazione delle query
Poiché, ripeto, a quel punto non era più chiaro cosa modificare nella query per accelerare, si è scelto di cambiare il design delle tabelle.
Dunque, sorge la domanda principale: cosa e come cambieremo?
Condizioni iniziali
Innanzitutto, abbiamo il seguente ERD (mostrato in modo schematico):

Caratteristiche principali:
- relazioni 'molti a molti'
- la tabella ha già una potenziale chiave di partizionamento
Query iniziale:
SELECT
p."PARAMETER_ID" as parameter_id,
pc."PC_NAME" AS pc_name,
pc."CUSTOMER_PARTNUMBER" AS customer_partnumber,
w."LASERMARK" AS lasermark,
w."LOTID" AS lotid,
w."REPORTED_VALUE" AS reported_value,
w."LOWER_SPEC_LIMIT" AS lower_spec_limit,
w."UPPER_SPEC_LIMIT" AS upper_spec_limit,
p."TYPE_CALCUL" AS type_calcul,
s."SHIPMENT_NAME" AS shipment_name,
s."SHIPMENT_DATE" AS shipment_date,
extract(year from s."SHIPMENT_DATE") AS year,
extract(month from s."SHIPMENT_DATE") as month,
s."REPORT_NAME" AS report_name,
p."SPARAM_NAME" AS SPARAM_name,
p."CUSTOMERPARAM_NAME" AS customerparam_name
FROM data w INNER JOIN shipment s ON s."SHIPMENT_ID" = w."SHIPMENT_ID"
INNER JOIN parameters p ON p."PARAMETER_ID" = w."PARAMETER_ID"
INNER JOIN shipment_pc sp ON s."SHIPMENT_ID" = sp."SHIPMENT_ID"
INNER JOIN pc pc ON pc."PC_ID" = sp."PC_ID"
INNER JOIN ( SELECT w2."LASERMARK" , MAX(s2."SHIPMENT_DATE") AS "SHIPMENT_DATE"
FROM shipment s2 INNER JOIN data w2 ON s2."SHIPMENT_ID" = w2."SHIPMENT_ID"
GROUP BY w2."LASERMARK"
) md ON md."SHIPMENT_DATE" = s."SHIPMENT_DATE" AND md."LASERMARK" = w."LASERMARK"
WHERE
s."SHIPMENT_DATE" >= '2018-07-01' AND s."SHIPMENT_DATE" <= '2018-09-30' ;
Risultati dell'esecuzione sul database di test:
Costo : 502 997.55
Tempo di esecuzione: 505 secondi.
Cosa vediamo? È una query normale, su un intervallo temporale.
Facciamo un semplice presupposto logico: se c'è un campione di intervallo temporale, ci aiuterà? Esatto - il partizionamento.
Cosa partizionare?
A prima vista, la scelta è ovvia - partizionamento dichiarativo della tabella 'shipment' per chiave 'SHIPMENT_DATE' (anticipando un po' - alla fine in produzione è andata un po' diversamente).
Come partizionare?
Questo quesito non è neanche troppo complesso. Fortunatamente, in PostgreSQL 10, ora c'è un meccanismo di partizionamento accessibile.
Quindi:
- Salviamo il dump della tabella originale - pg_dump source_table
- Eliminiamo la tabella originale - drop table source_table
- Creiamo la tabella principale con partizionamento per intervallo - create table source_table
- Creiamo le sezioni - create table source_table, create index
- Importiamo il dump creato al passo 1 - pg_restore
Script per il partizionamento
Per semplicità e comodità, i passaggi 2, 3, 4 sono stati uniti in un unico script.
Quindi:
Salviamo il dump della tabella originale
pg_dump postgres --file=/dump/shipment.dmp --format=c --table=shipment --verbose > /dump/shipment.log 2>&1Eliminiamo la tabella originale + Creiamo la tabella principale con partizionamento per intervallo + Creiamo le sezioni
--create_partition_shipment.sql
do language plpgsql $$
declare
rec_shipment_date RECORD ;
partition_name varchar;
index_name varchar;
current_year varchar ;
current_month varchar ;
begin_year varchar ;
begin_month varchar ;
next_year varchar ;
next_month varchar ;
first_flag boolean ;
i integer ;
begin
RAISE NOTICE 'CREA TABELLA TEMPORANEA PER SHIPMENT_DATE';
CREATE TEMP TABLE tmp_shipment_date as select distinct "SHIPMENT_DATE" from shipment order by "SHIPMENT_DATE" ;
RAISE NOTICE 'DROP TABLE shipment';
drop table shipment cascade ;
CREATE TABLE public.shipment
(
"SHIPMENT_ID" integer NOT NULL DEFAULT nextval('shipment_shipment_id_seq'::regclass),
"SHIPMENT_NAME" character varying(30) COLLATE pg_catalog."default",
"SHIPMENT_DATE" timestamp without time zone,
"REPORT_NAME" character varying(40) COLLATE pg_catalog."default"
)
PARTITION BY RANGE ("SHIPMENT_DATE")
WITH (
OIDS = FALSE
)
TABLESPACE pg_default;
RAISE NOTICE 'CREA PARTIZIONI PER LA TABELLA shipment';
current_year:='0';
current_month:='0';
begin_year := '0' ;
begin_month := '0' ;
next_year := '0' ;
next_month := '0' ;
FOR rec_shipment_date IN SELECT * FROM tmp_shipment_date LOOP
RAISE NOTICE 'SHIPMENT_DATE=%',rec_shipment_date."SHIPMENT_DATE";
current_year := date_part('year' ,rec_shipment_date."SHIPMENT_DATE");
current_month := date_part('month' ,rec_shipment_date."SHIPMENT_DATE") ;
IF to_number(current_month,'99') = to_date( begin_year||'.'||begin_month, 'YYYY.MM') AND
to_date( current_year||'.'||current_month, 'YYYY.MM') < to_date( next_year||'.'||next_month, 'YYYY.MM') AND
NOT first_flag
THEN
CONTINUE ;
ELSE
--NUOVI confini solo per la seconda e le volte successive
begin_year := current_year ;
begin_month := current_month ;
IF current_month = '12' THEN
next_year := date_part('year' ,rec_shipment_date."SHIPMENT_DATE" + interval '1 year') ;
ELSE
next_year := current_year ;
END IF;
next_month := date_part('month' ,rec_shipment_date."SHIPMENT_DATE" + interval '1 month') ;
END IF;
partition_name := 'shipment_shipment_date_'||begin_year||'-'||begin_month||'-01-'|| next_year||'-'||next_month||'-01' ;
EXECUTE format('CREATE TABLE ' || quote_ident(partition_name) || ' PARTITION OF shipment FOR VALUES FROM ( %L ) TO ( %L ) ' , current_year||'-'||current_month||'-01' , next_year||'-'||next_month||'-01' ) ;
index_name := partition_name||'_shipment_id_idx';
RAISE NOTICE 'NOME INDICE =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_ID") TABLESPACE pg_default ' ) ;
--Drop first time flag
first_flag := false ;
END LOOP;
end
$$;Importiamo il dump
pg_restore -d postgres --data-only --format=c --table=shipment --verbose shipment.dmp > /tmp/data_dump/shipment_restore.log 2>&1Controlliamo i risultati della partizione
Cosa abbiamo come risultato? Il testo completo del piano di esecuzione è lungo e noioso, quindi è possibile limitarsi ai numeri finali.
C'era
Costo: 502 997.55
Tempo di esecuzione: 505 secondi.
Diventato
Costo: 77 872.36
Tempo di esecuzione: 79 secondi.
Un ottimo risultato. Abbiamo ridotto i costi e il tempo di esecuzione. In questo modo, l'utilizzo della partizione dà l'effetto atteso e, in generale, senza sorprese.
Soddisfare il cliente
I risultati del test sono stati presentati al cliente per la revisione. E dopo averli esaminati, è stata emessa una sentenza piuttosto inaspettata: «Ottimo, partizionate la tabella «data»».
Sì, ma stiamo analizzando un'altra tabella, «shipment», la tabella «data» non ha il campo «SHIPMENT_DATE».
Nessun problema, aggiungete, cambiate. L'importante è che il cliente sia soddisfatto del risultato, i dettagli dell'implementazione non sono particolarmente importanti.
Partizioniamo la tabella principale «data»
In effetti, non ci sono state particolari complessità. Tuttavia, l'algoritmo di partizione è cambiato un po'.
Aggiungiamo la colonna «SHIPMENT_DATE» nella tabella «data»
psql -h host -U database -d user
=> ALTER TABLE data ADD COLUMN "SHIPMENT_DATE" timestamp without time zone ;Compiliamo i valori della colonna «SHIPMENT_DATE» nella tabella «data» con i valori della colonna omonima nella tabella «shipment»
-----------------------------
--update_data.sql
--aggiornamento per la tabella "data" modificata ai valori di "shipment_data" dalla tabella "shipment"
--version 1.0
do language plpgsql $$
declare
rec_shipment_data RECORD ;
shipment_date timestamp without time zone ;
row_count integer ;
total_rows integer ;
begin
select count(*) into total_rows from shipment ;
RAISE NOTICE 'Totale %',total_rows;
row_count:= 0 ;
FOR rec_shipment_data IN SELECT * FROM shipment LOOP
update data set "SHIPMENT_DATE" = rec_shipment_data."SHIPMENT_DATE" where "SHIPMENT_ID" = rec_shipment_data."SHIPMENT_ID";
row_count:= row_count +1 ;
RAISE NOTICE 'conteggio righe = % , da %',row_count,total_rows;
END LOOP;
end
$$;Salviamo il dump della tabella «data»
pg_dump postgres --file=/dump/data.dmp --format=c --table=data --verbose > /dump/data.log 2>&1<sourceRicreiamo la tabella partizionata «data»
--create_partition_data.sql
--crea partizioni per la tabella "wafer data" per intervallo nella colonna "shipment_data" con una durata di un mese
--versione 1.0
do language plpgsql $$
declare
rec_shipment_date RECORD ;
partition_name varchar;
index_name varchar;
current_year varchar ;
current_month varchar ;
begin_year varchar ;
begin_month varchar ;
next_year varchar ;
next_month varchar ;
first_flag boolean ;
i integer ;
begin
RAISE NOTICE 'CREA TABELLA TEMPORANEA PER SHIPMENT_DATE';
CREATE TEMP TABLE tmp_shipment_date as select distinct "SHIPMENT_DATE" from shipment order by "SHIPMENT_DATE" ;
RAISE NOTICE 'CANCELLA TABELLA data';
drop table data cascade ;
RAISE NOTICE 'CREA TABELLA PARTIZIONATA data';
CREATE TABLE public.data
(
"RUN_ID" integer,
"LASERMARK" character varying(20) COLLATE pg_catalog."default" NOT NULL,
"LOTID" character varying(80) COLLATE pg_catalog."default",
"SHIPMENT_ID" integer NOT NULL,
"PARAMETER_ID" integer NOT NULL,
"INTERNAL_VALUE" character varying(75) COLLATE pg_catalog."default",
"REPORTED_VALUE" character varying(75) COLLATE pg_catalog."default",
"LOWER_SPEC_LIMIT" numeric,
"UPPER_SPEC_LIMIT" numeric ,
"SHIPMENT_DATE" timestamp without time zone
)
PARTITION BY RANGE ("SHIPMENT_DATE")
WITH (
OIDS = FALSE
)
TABLESPACE pg_default ;
RAISE NOTICE 'CREA PARTIZIONI PER LA TABELLA data';
current_year:='0';
current_month:='0';
begin_year := '0' ;
begin_month := '0' ;
next_year := '0' ;
next_month := '0' ;
i := 1;
FOR rec_shipment_date IN SELECT * FROM tmp_shipment_date LOOP
RAISE NOTICE 'SHIPMENT_DATE=%',rec_shipment_date."SHIPMENT_DATE";
current_year := date_part('year' ,rec_shipment_date."SHIPMENT_DATE");
current_month := date_part('month' ,rec_shipment_date."SHIPMENT_DATE") ;
--Inizializza i confini
IF begin_year = '0' THEN
RAISE NOTICE '***Inizializza confini';
first_flag := true ; --flag primo giro
begin_year := current_year ;
begin_month := current_month ;
IF current_month = '12' THEN
next_year := date_part('year' ,rec_shipment_date."SHIPMENT_DATE" + interval '1 year') ;
ELSE
next_year := current_year ;
END IF;
next_month := date_part('month' ,rec_shipment_date."SHIPMENT_DATE" + interval '1 month') ;
END IF;
-- RAISE NOTICE 'current_year=% , current_month=% ',current_year,current_month;
-- RAISE NOTICE 'begin_year=% , begin_month=% ',begin_year,begin_month;
-- RAISE NOTICE 'next_year=% , next_month=% ',next_year,next_month;
-- Controlla la data corrente nei confini NON per il primo giro
RAISE NOTICE 'Dati correnti = %',to_char( to_date( current_year||'.'||current_month, 'YYYY.MM'), 'YYYY.MM');
RAISE NOTICE 'Dati iniziali = %',to_char( to_date( begin_year||'.'||begin_month, 'YYYY.MM'), 'YYYY.MM');
RAISE NOTICE 'Prossimi dati = %',to_char( to_date( next_year||'.'||next_month, 'YYYY.MM'), 'YYYY.MM');
IF to_date( current_year||'.'||current_month, 'YYYY.MM') >= to_date( begin_year||'.'||begin_month, 'YYYY.MM') AND
to_date( current_year||'.'||current_month, 'YYYY.MM') < to_date( next_year||'.'||next_month, 'YYYY.MM') AND
NOT first_flag
THEN
RAISE NOTICE '***CONTINUA';
CONTINUE ;
ELSE
--NUOVI confini solo per secondi e successivi giri
RAISE NOTICE '***NUOVI CONFINI';
begin_year := current_year ;
begin_month := current_month ;
IF current_month = '12' THEN
next_year := date_part('year' ,rec_shipment_date."SHIPMENT_DATE" + interval '1 year') ;
ELSE
next_year := current_year ;
END IF;
next_month := date_part('month' ,rec_shipment_date."SHIPMENT_DATE" + interval '1 month') ;
END IF;
IF to_number(current_month,'99') < 10 THEN
current_month := '0'||current_month ;
END IF ;
IF to_number(begin_month,'99') < 10 THEN
begin_month := '0'||begin_month ;
END IF ;
IF to_number(next_month,'99') < 10 THEN
next_month := '0'||next_month ;
END IF ;
RAISE NOTICE 'current_year=% , current_month=% ',current_year,current_month;
RAISE NOTICE 'begin_year=% , begin_month=% ',begin_year,begin_month;
RAISE NOTICE 'next_year=% , next_month=% ',next_year,next_month;
partition_name := 'data_'||begin_year||begin_month||'01_'||next_year||next_month||'01' ;
RAISE NOTICE 'NUMERO PARTIZIONE % , NOME TABELLA =%',i , partition_name;
EXECUTE format('CREATE TABLE ' || quote_ident(partition_name) || ' PARTITION OF data FOR VALUES FROM ( %L ) TO ( %L ) ' , begin_year||'-'||begin_month||'-01' , next_year||'-'||next_month||'-01' ) ;
index_name := partition_name||'_shipment_id_parameter_id_idx';
RAISE NOTICE 'NOME INDICE =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_ID", "PARAMETER_ID") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_lasermark_idx';
RAISE NOTICE 'NOME INDICE =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("LASERMARK" COLLATE pg_catalog."default") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_shipment_id_idx';
RAISE NOTICE 'NOME INDICE =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_ID") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_parameter_id_idx';
RAISE NOTICE 'NOME INDICE =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("PARAMETER_ID") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_shipment_date_idx';
RAISE NOTICE 'NOME INDICE =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_DATE") TABLESPACE pg_default ' ) ;
--Cancella flag primo giro
first_flag := false ;
END LOOP;
end
$$;
Caricando il dump creato al passo 3.
pg_restore -h host -u user -d database --data-only --format=c --table=data --verbose data.dmp > data_restore.log 2>&1Creiamo una sezione separata per i dati vecchi
---------------------------------------------------
--create_partition_for_old_dates.sql
--crea partizioni per mantenere date vecchie
--versione 1.0
do language plpgsql $$
declare
rec_shipment_date RECORD ;
partition_name varchar;
index_name varchar;
begin
SELECT min("SHIPMENT_DATE") AS min_date INTO rec_shipment_date from data ;
RAISE NOTICE 'La data vecchia è %',rec_shipment_date.min_date ;
partition_name := 'data_old_dates' ;
RAISE NOTICE 'IL NOME DELLA PARTIZIONE È %',partition_name;
EXECUTE format('CREATE TABLE ' || quote_ident(partition_name) || ' PARTITION OF data FOR VALUES FROM ( %L ) TO ( %L ) ' , '1900-01-01' ,
to_char( rec_shipment_date.min_date,'YYYY')||'-'||to_char(rec_shipment_date.min_date,'MM')||'-01' ) ;
index_name := partition_name||'_shipment_id_parameter_id_idx';
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_ID", "PARAMETER_ID") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_lasermark_idx';
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("LASERMARK" COLLATE pg_catalog."default") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_shipment_id_idx';
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_ID") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_parameter_id_idx';
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("PARAMETER_ID") TABLESPACE pg_default ' ) ;
index_name := partition_name||'_shipment_date_idx';
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_DATE") TABLESPACE pg_default ' ) ;
end
$$;Risultati finali:
C'era
Costo: 502 997.55
Tempo di esecuzione: 505 secondi.
Diventato
Costo: 68 533.70
Tempo di esecuzione: 69 secondi
Notevole, davvero notevole. E considerando che lungo il percorso ho potuto più o meno apprendere il meccanismo di partizionamento in PostgreSQL 10 — Un ottimo risultato.
Divagazione lirica
E si può fare anche meglio — SÌ, SI PUÒ!Per fare ciò, è necessario utilizzare una VIEW MATERIALIZZATA.
CREATE MATERIALIZED VIEW LASERMARK_VIEW
CREATE MATERIALIZED VIEW LASERMARK_VIEW
AS
SELECT w."LASERMARK" , MAX(s."SHIPMENT_DATE") AS "SHIPMENT_DATE"
FROM shipment s INNER JOIN data w ON s."SHIPMENT_ID" = w."SHIPMENT_ID"
GROUP BY w."LASERMARK" ;
CREATE INDEX lasermark_vw_shipment_date_ind on lasermark_view USING btree ("SHIPMENT_DATE") TABLESPACE pg_default;
analyze lasermark_view ;
Ancora una volta riscriviamo la query:
Query con utilizzo della view materializzata
SELECT
p."PARAMETER_ID" as parameter_id,
pc."PC_NAME" AS pc_name,
pc."CUSTOMER_PARTNUMBER" AS customer_partnumber,
w."LASERMARK" AS lasermark,
w."LOTID" AS lotid,
w."REPORTED_VALUE" AS reported_value,
w."LOWER_SPEC_LIMIT" AS lower_spec_limit,
w."UPPER_SPEC_LIMIT" AS upper_spec_limit,
p."TYPE_CALCUL" AS type_calcul,
s."SHIPMENT_NAME" AS shipment_name,
s."SHIPMENT_DATE" AS shipment_date,
extract(year from s."SHIPMENT_DATE") AS year,
extract(month from s."SHIPMENT_DATE") as month,
s."REPORT_NAME" AS report_name,
p."STC_NAME" AS STC_name,
p."CUSTOMERPARAM_NAME" AS customerparam_name
FROM data w INNER JOIN shipment s ON s."SHIPMENT_ID" = w."SHIPMENT_ID"
INNER JOIN parameters p ON p."PARAMETER_ID" = w."PARAMETER_ID"
INNER JOIN shipment_pc sp ON s."SHIPMENT_ID" = sp."SHIPMENT_ID"
INNER JOIN pc pc ON pc."PC_ID" = sp."PC_ID"
INNER JOIN LASERMARK_VIEW md ON md."SHIPMENT_DATE" = s."SHIPMENT_DATE" AND md."LASERMARK" = w."LASERMARK"
WHERE
s."SHIPMENT_DATE" >= '2018-07-01' AND s."SHIPMENT_DATE" <= '2018-09-30';
E otteniamo un altro risultato:
C'era
Costo: 502 997.55
Tempo di esecuzione: 505 secondi
Diventato
Costo: 42 481.16
Tempo di esecuzione: 43 secondi.
Anche se, naturalmente, un risultato così promettente è ingannevole, le view vanno rinfrescate. Quindi, il tempo finale per ottenere i dati non aiuterà molto. Ma come esperimento è piuttosto interessante.
In realtà, come si è scoperto, un altro grazie e a Habr !-
Epifania
Quindi, il cliente è soddisfatto. E bisogno sfruttare la situazione.
Nuova attività: Cosa si può inventare per approfondire e ampliare?
E qui mi ricordo — ragazzi, ma noi non abbiamo un monitoraggio dei nostri database PostgreSQL.
A dire il vero, c'è un certo tipo di monitoraggio in Cloud Watch su AWS. Ma quale utilità ha questo monitoraggio per il DBA? Fondamentalmente, nessuna.
Se c'è l'opportunità di fare qualcosa di utile e interessante anche per me, non posso lasciarmela sfuggire …
INFATTI

Così siamo arrivati al punto più interessante:
3 Dicembre 2018.
Decisione di iniziare i lavori di ricerca per esplorare le possibilità di monitoraggio delle prestazioni delle query PostgreSQL.
Ma questa è una storia completamente diversa.
Continua…
Fonte: habr.com
