Prefață sau cum a apărut ideea secționării
Povestea începe aici: După ce aproape toate resursele pentru optimizarea interogării, la acel moment, au fost epuizate, s-a pus întrebarea - și ce urmează? Așa a apărut ideea secționării.

O digresiune lirică:
Anume 'la acel moment', deoarece . Mulțumesc și Habr!
Așadar, cum putem face clientul oarecum fericit, în același timp îmbunătățindu-ne propriile abilități?
Dacă simplificăm totul la maxim, atunci, căile de a îmbunătăți radical performanța bazei de date sunt doar două:
1) Calea extensivă - creștem resursele, schimbăm configurația;
2) Calea intensivă - optimizarea interogărilor
Întrucât, repet, la acel moment nu era clar ce să mai schimb în interogare pentru a accelera procesul, s-a ales calea - schimbarea designului tabelelor.
Așadar - apare întrebarea principală - ce și cum vom schimba?
Condițiile inițiale
În primul rând, avem un ERD astfel (prezentat schematic și simplificat):

Caracteristici principale:
- relațiile 'multe la multe'
- tabelul are deja o potențială cheie de secționare
Interogarea inițială:
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';
Rezultatele execuției pe baza de date de test:
Cost : 502 997.55
Timp de execuție: 505 secunde.
Ce vedem? O interogare obișnuită, pe un interval temporal.
Facem o presupunere logică simplă: dacă există un eșantion dintr-o fereastră temporală, ne va ajuta? Corect — secționarea.
Ce să secționăm?
La prima vedere, alegerea este evidentă — secționarea declarativă a tabelului „shipment” după cheia „SHIPMENT_DATE” (avansând puțin, în producție a ieșit puțin diferit).
Cum să secționăm?
Această întrebare nu este nici prea complicată. Din fericire, în PostgreSQL 10, acum avem un mecanism de secționare uman.
Deci:
- Salvăm dump-ul tabelului sursă — pg_dump source_table
- Ștergem tabelul sursă — drop table source_table
- Creăm tabelul părinte cu secționare pe bază de interval — create table source_table
- Creăm secțiuni — create table source_table, create index
- Importăm dump-ul creat la pasul 1 — pg_restore
Scripturi pentru secționare
Pentru simplificare și confort, pașii 2, 3, 4 au fost combinați într-un singur script.
Deci:
Salvăm dump-ul tabelului sursă
pg_dump postgres --file=\/dump\/shipment.dmp --format=c --table=shipment --verbose > \/dump\/shipment.log 2>&1Ștergem tabelul sursă + Creăm tabelul părinte cu secționare pe bază de interval + Creăm secțiuni
--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 'CREARE TABELA TEMPORARĂ PENTRU DATA LIVRĂRII';
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 'CREARE PARTICI pentru tabela 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
--NOI limite doar pentru a doua și după aceea
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 'INDEX NAME =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_ID") TABLESPACE pg_default ' ) ;
--Drop prima dată flag
first_flag := false ;
END LOOP;
end
$$;Importăm dump-ul
pg_restore -d postgres --data-only --format=c --table=shipment --verbose shipment.dmp > \/tmp\/data_dump\/shipment_restore.log 2>&1Verificăm rezultatele secționării
Ce avem ca rezultat? Textul complet al planului de execuție este lung și plictisitor, așa că s-ar putea să ne limităm la cifrele centrale.
A fost
Cost: 502 997.55
Timp de execuție: 505 secunde.
A devenit
Cost: 77 872.36
Timp de execuție: 79 secunde.
Rezultat destul de bun. Am redus costul și timpul de execuție. Astfel, utilizarea secționării oferă efectul scontat și, în general, fără surprize.
Aproape de satisfacția clientului
Rezultatele testării au fost prezentate clientului spre examinare. Iar după ce s-au familiarizat cu ele, au emis un verdict oarecum neașteptat: „Excelent, secționați tabelul «data»”.
Da, dar am studiat un alt tabel «shipment», tabelul «data» nu are câmpul «SHIPMENT_DATE».
Nicio problemă, adăugați, schimbați. Principalul este ca clientul să fie mulțumit de rezultatul final, detaliile implementării nu sunt chiar atât de importante.
Secționăm tabelul principal «data»
În general, nu au apărut dificultăți deosebite. Deși, algoritmul de secționare, bineînțeles, s-a schimbat puțin.
Adăugăm coloana «SHIPMENT_DATE» în tabelul «data»
psql -h gazdă -U bază -d utilizator
=> ALTER TABLE data ADD COLUMN "SHIPMENT_DATE" timestamp without time zone ;Populăm valorile coloanei «SHIPMENT_DATE» în tabelul «data», cu valorile coloanei cu același nume din tabelul «shipment»
-----------------------------
--update_data.sql
--actualizare pentru tabelul modificat "data" cu valorile din "shipment_data" din tabelul "shipment"
--versiune 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 'Total %',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 'row count = % , from %',row_count,total_rows;
END LOOP;
end
$$;Salvăm dump-ul tabelului «data»
pg_dump postgres --file=\/dump\/data.dmp --format=c --table=data --verbose > \/dump\/data.log 2>&1<\/sourceRecreem tabelul secționat «data»
--create_partition_data.sql
--creați partiții pentru tabela "wafer data" pe baza coloanei "shipment_data" cu o durată de o lună
--versiunea 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ȚI TABEL TEMPORAR PENTRU SHIPMENT_DATE';
CREATE TEMP TABLE tmp_shipment_date as select distinct "SHIPMENT_DATE" from shipment order by "SHIPMENT_DATE" ;
RAISE NOTICE 'ȘTERGEȚI TABELA data';
drop table data cascade ;
RAISE NOTICE 'CREAȚI TABELA PARTIȚIONATĂ 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ȚI PARTIȚII PENTRU TABELA 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") ;
--Init borders
IF begin_year = '0' THEN
RAISE NOTICE '***Init borders';
first_flag := true ; --flag pentru prima dată
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;
-- Verificați data curentă în limite, NU pentru prima dată
RAISE NOTICE 'Data curentă = %',to_char( to_date( current_year||'.'||current_month, 'YYYY.MM'), 'YYYY.MM');
RAISE NOTICE 'Data de început = %',to_char( to_date( begin_year||'.'||begin_month, 'YYYY.MM'), 'YYYY.MM');
RAISE NOTICE 'Data următoare = %',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ȚI';
CONTINUE ;
ELSE
--NOI limite doar pentru a doua și după aceea
RAISE NOTICE '***NOI LIMITE';
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 'NUMĂR PARTIȚIE % , NUME TABEL =%',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 'NUME INDEX =%',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 'NUME INDEX =%',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 'NUME INDEX =%',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 'NUME INDEX =%',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 'NUME INDEX =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_DATE") TABLESPACE pg_default ' ) ;
--Ștergeți flag-ul primei vizite
first_flag := false ;
END LOOP;
end
$$;
Încărcăm dump-ul creat la pasul 3.
pg_restore -h host -uuser -d baza --data-only --format=c --table=data --verbose data.dmp > data_restore.log 2>&1Creăm o secțiune separată pentru datele vechi.
---------------------------------------------------
--create_partition_for_old_dates.sql
--creați partiții pentru păstrarea datelor vechi
--versiunea 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 'Data veche este %',rec_shipment_date.min_date ;
partition_name := 'data_old_dates' ;
RAISE NOTICE 'NUMELE PARTIȚIEI ESTE %',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
$$;Rezultatele finale:
A fost
Cost: 502 997.55
Timp de execuție: 505 secunde.
A devenit
Cost: 68 533.70
Timp de execuție: 69 secunde
Onorabil, destul de onorabil. Și având în vedere că pe parcurs am reușit să învăț mai mult sau mai puțin mecanismul de partiționare în PostgreSQL 10 — Un rezultat excelent.
O mică digresiune
Se poate face chiar mai bine — DA, SE POATE!Pentru asta trebuie să folosim MATERIALIZED VIEW.
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 ;
Încă o dată rescriem interogarea:
Interogare folosind materialized view
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';
Și obținem încă un rezultat:
A fost
Cost: 502 997.55
Timp de execuție: 505 secunde
A devenit
Cost: 42 481.16
Timp de execuție: 43 secunde.
Deși, desigur, un rezultat atât de promițător este înșelător; trebuie să facem refresh pe reprezentări. Așa că timpul final pentru obținerea datelor nu va ajuta prea mult. Dar ca experiment, este destul de interesant.
De fapt, după cum s-a dovedit, încă o dată mulțumim și Habr !-
Cuvânt înainte
Deci, clientul este mulțumit. Și nevoie profită de situație.
Sarcină nouă: Ce putem inventa pentru a aprofunda și extinde?
Și aici îmi amintesc — băieți, dar noi nu avem monitorizare pentru bazele noastre de date PostgreSQL.
Punând mâna pe inimă, există un fel de monitorizare sub formă de Cloud Watch pe AWS. Dar ce folos are această monitorizare pentru DBA? Practic, deloc.
Dacă a apărut ocazia de a face ceva util și interesant și pentru mine, nu pot să nu profit de o astfel de ocazie...
CĂCI

Așa am ajuns la cel mai interesant subiect:
3 Decembrie 2018.
Decizia de a începe lucrările pentru a explora oportunitățile de monitorizare a performanței interogărilor PostgreSQL.
Dar aceasta este deja o poveste complet diferită.
Continuarea urmează...
Sursa: habr.com
