Przedmowa, czyli jak zrodził się pomysł na partycjonowanie
Początek historii znajduje się tutaj: Po tym, jak większość zasobów do optymalizacji zapytania została wyczerpana, pojawiło się pytanie — co dalej? Tak narodził się pomysł na partycjonowanie.

Liryczne dygresja:
Dokładnie ‘w tamtym momencie’, ponieważ . Dziękuję i Habrze!
A więc, jak jeszcze można sprawić, by klient był jakby szczęśliwy, przy okazji podnosząc własne umiejętności?
Jeśli wszystko maksymalnie uprościć, to dróg do znaczącej poprawy wydajności bazy danych jest zaledwie dwie:
1) Droga ekstensywna — zwiększamy zasoby, zmieniamy konfigurację;
2) Droga intensywna — optymalizacja zapytań
Ponieważ, powtarzam, w tamtym momencie już nie było jasne, co jeszcze zmienić w zapytaniu w celu przyspieszenia, wybrano drogę — zmiany w projekcie tabel.
I tak — pojawia się główne pytanie — co i jak będziemy zmieniać?
Warunki początkowe
Po pierwsze, mamy taki ERD (pokazane warunkowo-upraszczająco):

Kompaktowość i łatwość wbudowania w inne projekty. Kod składa się jedynie z kilku plików w języku C, które nie wymagają do kompilacji zewnętrznych zależności. Skompilowana najprostsza aplikacja zajmuje około 190 KB;
- relacje „wiele do wielu”
- tabela ma już potencjalny klucz partycjonowania
Początkowe zapytanie:
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';
Wyniki wykonania na testowej bazie danych:
Koszt : 502 997.55
Czas wykonania: 505 sekund.
Co widzimy? Zwykłe zapytanie, na podstawie wycinka czasowego.
Przyjmujemy proste logiczne założenie: jeśli mamy próbkę w czasie, to nam pomoże? Oczywiście — sekcjonowanie.
Co sekcjonować?
Na pierwszy rzut oka wybór jest oczywisty — deklaratywne sekcjonowanie tabeli „shipment” według klucza „SHIPMENT_DATE” (wyprzedzając nieco fakty — w rzeczywistości na produkcji wyszło to trochę inaczej).
Jak sekcjonować?
To pytanie również nie jest zbyt trudne. Na szczęście w PostgreSQL 10 pojawił się ludzki mechanizm sekcjonowania.
A zatem:
- Zapisujemy zrzut oryginalnej tabeli — pg_dump source_table
- Usuwamy oryginalną tabelę — drop table source_table
- Tworzymy tabelę nadrzędną z sekcjonowaniem według zakresu — create table source_table
- Tworzymy sekcje — create table source_table, create index
- Importujemy zrzut utworzony w kroku 1 — pg_restore
Skrypty do sekcjonowania
Dla uproszczenia i wygody kroki 2, 3, 4 zostały połączone w jednym skrypcie.
A zatem:
Zapisujemy zrzut oryginalnej tabeli
pg_dump postgres --file=\dump\shipment.dmp --format=c --table=shipment --verbose > \dump\shipment.log 2>&1Usuwamy oryginalną tabelę + Tworzymy tabelę nadrzędną z sekcjonowaniem według zakresu + Tworzymy sekcje
--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 'TWORZENIE TYMICZNEJ TABELI DLA DATY WYSYŁKI';
CREATE TEMP TABLE tmp_shipment_date as select distinct "SHIPMENT_DATE" from shipment order by "SHIPMENT_DATE" ;
RAISE NOTICE 'USUWANIE TABELI 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 'TWORZENIE PARTYCJI DLA TABELI 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
--NOWE granice tylko dla drugiego i kolejnych razy
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 'NAZWA INDEKSU =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_ID") TABLESPACE pg_default ' ) ;
--Usuń flagę pierwszego razu
first_flag := false ;
END LOOP;
end
$$;Importujemy zrzut
pg_restore -d postgres --data-only --format=c --table=shipment --verbose shipment.dmp > /tmp/data_dump/shipment_restore.log 2>&1Sprawdzamy wyniki sekcjonowania
Co z tego mamy? Pełen tekst planu wykonania jest długi i nudny, więc można ograniczyć się do końcowych cyfr.
Było
Koszt: 502 997.55
Czas wykonania: 505 sekund.
Stało się
Koszt: 77 872.36
Czas wykonania: 79 sekund.
To całkiem dobry wynik. Zmniejszyliśmy koszt i czas wykonania. Tak więc wykorzystanie partycjonowania daje oczekiwany efekt i ogólnie — bez niespodzianek.
Zadowolić klienta
Wyniki testowania zostały przedstawione klientowi do rozpatrzenia. Po zapoznaniu się z nimi wydano dość niespodziewany werdykt: „Świetnie, proszę partycjonować tabelę „data””.
Tak, ale badaliśmy zupełnie inną tabelę „shipment”, tabela „data” nie ma pola „SHIPMENT_DATE”.
Nie ma problemu, dodawać, zmieniać. Ważne, aby klient był zadowolony z tego, co wyjdzie w rezultacie, szczegóły realizacji nie są aż tak istotne.
Partycjonujemy główną tabelę „data”
Generalnie nie wystąpiły żadne szczególne trudności. Chociaż algorytm partycjonowania oczywiście się nieco zmienił.
Dodajemy kolumnę „SHIPMENT_DATE” do tabeli „data”
psql -h host -U baza -d użytkownik
=> ALTER TABLE data ADD COLUMN "SHIPMENT_DATE" timestamp without time zone ;Wypełniamy wartości kolumny „SHIPMENT_DATE” w tabeli „data”, wartościami tego samego kolumny z tabeli „shipment”
-----------------------------
--update_data.sql
--aktualizacja zmienionej tabeli "data" do wartości "shipment_data" z tabeli "shipment"
--wersja 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 'Razem %',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 'liczba wierszy = % , z %',row_count,total_rows;
END LOOP;
end
$$;Zapisujemy zrzut tabeli „data”
pg_dump postgres --file=\/dump\/data.dmp --format=c --table=data --verbose >\/dump\/data.log 2>&1Ponownie tworzymy partycjonowaną tabelę „data”
--create_partition_data.sql
--tworzenie partycji dla tabeli "dane waflowe" według kolumny "data wysyłki" z trwaniem jednego miesiąca
--wersja 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 'TWORZENIE TYMACZASOWEJ TABELI DLA DATY WYSYŁKI';
CREATE TEMP TABLE tmp_shipment_date as select distinct "SHIPMENT_DATE" from shipment order by "SHIPMENT_DATE" ;
RAISE NOTICE 'USUNIĘCIE TABELI data';
drop table data cascade ;
RAISE NOTICE 'TWORZENIE TABELI PARTYCJONOWANEJ 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 'TWORZENIE PARTYCJI DLA TABELI 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 'DATA WYSYŁKI=%',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") ;
--Inicjalizacja granic
IF begin_year = '0' THEN
RAISE NOTICE '***Inicjalizacja granic';
first_flag := true ; --flaga pierwszego razu
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;
-- Sprawdzenie bieżącej daty w granicach NIE dla pierwszego razu
RAISE NOTICE 'Bieżące dane = %',to_char( to_date( current_year||'.'||current_month, 'YYYY.MM'), 'YYYY.MM');
RAISE NOTICE 'Dane początkowe = %',to_char( to_date( begin_year||'.'||begin_month, 'YYYY.MM'), 'YYYY.MM');
RAISE NOTICE 'Następne dane = %',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 '***KONTYNUUJ';
CONTINUE ;
ELSE
--NOWE granice tylko dla drugiego i późniejszych razy
RAISE NOTICE '***NOWE GRANICE';
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 'NUMER PARTYCJI % , NAZWA TABELI =%',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 'NAZWA INDEXU =%',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 'NAZWA INDEXU =%',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 'NAZWA INDEXU =%',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 'NAZWA INDEXU =%',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 'NAZWA INDEXU =%',index_name;
EXECUTE format('CREATE INDEX ' || quote_ident(index_name) || ' ON '|| quote_ident(partition_name) ||' USING btree ("SHIPMENT_DATE") TABLESPACE pg_default ' ) ;
--Usuń flagę pierwszego razu
first_flag := false ;
END LOOP;
end
$$;
Wgrywamy zrzut utworzony w kroku 3.
pg_restore -h host -u użytkownik -d baza --data-only --format=c --table=data --verbose data.dmp > data_restore.log 2>&1Tworzymy osobną sekcję dla starych danych
---------------------------------------------------
--create_partition_for_old_dates.sql
--tworzenie partycji dla przechowywania starych dat
--wersja 1.0
do języka 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 'Stara data to %',rec_shipment_date.min_date ;
partition_name := 'data_old_dates' ;
RAISE NOTICE 'NAZWA PARTYCJI TO %',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
$$;Wyniki końcowe:
Było
Koszt: 502 997.55
Czas wykonania: 505 sekund.
Stało się
Koszt: 68 533.70
Czas wykonania: 69 sekundy
Zadowalająco, w pełni zadowalająco. A biorąc pod uwagę, że po drodze udało się w miarę opanować mechanizm partycjonowania w PostgreSQL 10 — świetny wynik.
Liryczne dygresje
Można to zrobić jeszcze lepiej — TAK, MOŻNA!Do tego należy wykorzystać 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 ;
Jeszcze raz przepisujemy zapytanie:
Zapytanie z wykorzystaniem materialized view
WYBIERZ
p."PARAMETER_ID" jako 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
Z danych w INNER JOIN wysyłka s ON s."SHIPMENT_ID" = w."SHIPMENT_ID"
INNER JOIN parametry 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"
GDZIE
s."SHIPMENT_DATE" >= '2018-07-01' AND s."SHIPMENT_DATE" <= '2018-09-30';
I otrzymujemy kolejny wynik:
Było
Koszt: 502 997.55
Czas wykonania: 505 sekund
Stało się
Koszt: 42 481.16
Czas wykonania: 43 sekundy.
Chociaż oczywiście, tak obiecujący wynik jest zwodniczy, ponieważ raporty muszą być odświeżane. Tak więc ostateczny czas uzyskania danych nie pomoże zbytnio. Ale jako eksperyment jest to całkiem interesujące.
W rzeczywistości, jak się okazało, jeszcze raz dziękuję i Habrze! -
Epilog
Zatem, klient jest zadowolony. A trzeba skorzystać z sytuacji.
Nowe zadanie: Co można wymyślić, aby pogłębić i rozszerzyć?
I tutaj przypominam sobie — chłopaki, a nie mamy monitoringu naszych baz danych PostgreSQL.
Szczerze mówiąc, pewien rodzaj monitoringu w postaci Cloud Watch na AWS rzeczywiście istnieje. Ale co z tego monitoringu ma DBA? Tak naprawdę prawie nic.
Jeśli nadarza się okazja do zrobienia czegoś użytecznego i interesującego również dla siebie, nie można z niej nie skorzystać …
BO

Tak oto doszliśmy do najciekawszego:
3 grudnia 2018 roku.
Podjęcie decyzji o rozpoczęciu prac nad badaniem istniejących możliwości monitorowania wydajności zapytań PostgreSQL.
Ale to już zupełnie inna historia.
Ciąg dalszy nastąpi…
Źródło: habr.com
