Konfigurowanie PostgreSQL pod kątem wydajności odczytu

39

Nasz system zapisuje wiele danych (rodzaj systemu Big Data). Wydajność zapisu jest wystarczająca dla naszych potrzeb, ale wydajność odczytu jest naprawdę zbyt wolna.

Struktura klucza podstawowego (ograniczenie) jest podobna dla wszystkich naszych tabel:

timestamp(Timestamp) ; index(smallint) ; key(integer).

Tabela może zawierać miliony wierszy, a nawet miliardy wierszy, a żądanie odczytu zwykle dotyczy określonego okresu (znacznik czasu / indeks) i znacznika. Często zdarza się, że zapytanie zwraca około 200 000 wierszy. Obecnie możemy odczytać około 15 000 linii na sekundę, ale musimy być 10 razy szybsi. Czy to możliwe, a jeśli tak, to w jaki sposób?

Uwaga: PostgreSQL jest dostarczany z naszym oprogramowaniem, więc sprzęt różni się w zależności od klienta.

Jest to maszyna wirtualna używana do testowania. Host maszyny wirtualnej to Windows Server 2008 R2 x64 z 24,0 GB pamięci RAM.

Specyfikacja serwera (VMWare maszyny wirtualnej)

Server 2008 R2 x64
2.00 GB of memory
Intel Xeon W3520 @ 2.67GHz (2 cores)

postgresql.conf optymalizacje

shared_buffers = 512MB (default: 32MB)
effective_cache_size = 1024MB (default: 128MB)
checkpoint_segment = 32 (default: 3)
checkpoint_completion_target = 0.9 (default: 0.5)
default_statistics_target = 1000 (default: 100)
work_mem = 100MB (default: 1MB)
maintainance_work_mem = 256MB (default: 16MB)

Definicja tabeli

CREATE TABLE "AnalogTransition"
(
  "KeyTag" integer NOT NULL,
  "Timestamp" timestamp with time zone NOT NULL,
  "TimestampQuality" smallint,
  "TimestampIndex" smallint NOT NULL,
  "Value" numeric,
  "Quality" boolean,
  "QualityFlags" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag" ),
  CONSTRAINT "FK_AnalogTransition_Tag" FOREIGN KEY ("KeyTag")
      REFERENCES "Tag" ("Key") MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS=FALSE,
  autovacuum_enabled=true
);

Pytanie

Wykonanie zapytania zajmuje około 30 sekund w pgAdmin3, ale chcielibyśmy uzyskać ten sam wynik poniżej 5 sekund, jeśli to możliwe.

SELECT 
    "AnalogTransition"."KeyTag", 
    "AnalogTransition"."Timestamp" AT TIME ZONE 'UTC', 
    "AnalogTransition"."TimestampQuality", 
    "AnalogTransition"."TimestampIndex", 
    "AnalogTransition"."Value", 
    "AnalogTransition"."Quality", 
    "AnalogTransition"."QualityFlags", 
    "AnalogTransition"."UpdateTimestamp"
FROM "AnalogTransition"
WHERE "AnalogTransition"."Timestamp" >= '2013-05-16 00:00:00.000' AND "AnalogTransition"."Timestamp" <= '2013-05-17 00:00:00.00' AND ("AnalogTransition"."KeyTag" = 56 OR "AnalogTransition"."KeyTag" = 57 OR "AnalogTransition"."KeyTag" = 58 OR "AnalogTransition"."KeyTag" = 59 OR "AnalogTransition"."KeyTag" = 60)
ORDER BY "AnalogTransition"."Timestamp" DESC, "AnalogTransition"."TimestampIndex" DESC
LIMIT 500000;

Wyjaśnij 1

"Limit  (cost=0.00..125668.31 rows=500000 width=33) (actual time=2.193..3241.319 rows=500000 loops=1)"
"  Buffers: shared hit=190147"
"  ->  Index Scan Backward using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..389244.53 rows=1548698 width=33) (actual time=2.187..1893.283 rows=500000 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-16 01:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-16 15:00:00-04'::timestamp with time zone))"
"        Filter: (("KeyTag" = 56) OR ("KeyTag" = 57) OR ("KeyTag" = 58) OR ("KeyTag" = 59) OR ("KeyTag" = 60))"
"        Buffers: shared hit=190147"
"Total runtime: 3863.028 ms"

Wyjaśnij 2

W moim ostatnim teście wybranie moich danych zajęło 7 minut! Patrz poniżej:

"Limit  (cost=0.00..313554.08 rows=250001 width=35) (actual time=0.040..410721.033 rows=250001 loops=1)"
"  ->  Index Scan using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..971400.46 rows=774511 width=35) (actual time=0.037..410088.960 rows=250001 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-22 20:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-24 20:00:00-04'::timestamp with time zone) AND ("KeyTag" = 16))"
"Total runtime: 411044.175 ms"
JPelletier
źródło

Odpowiedzi:

52

Wyrównanie danych i wielkość pamięci

W rzeczywistości narzut na krotkę wynosi 24 bajty dla nagłówka krotki plus 4 bajty dla wskaźnika pozycji.
Więcej szczegółów w obliczeniach w tej pokrewnej odpowiedzi:

Podstawy wyrównywania danych i wypełniania w tej pokrewnej odpowiedzi na temat SO:

Mamy trzy kolumny dla klucza podstawowego:

PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag")

"Timestamp"      timestamp (8 bytes)
"TimestampIndex" smallint  (2 bytes)
"KeyTag"         integer   (4 bytes)

Prowadzi do:

 4-bajtowy wskaźnik pozycji w nagłówku strony (nie liczony do wielokrotności 8 bajtów)
---
23 bajty na nagłówek krotki
 1 bajt wypełnienia dla wyrównania danych (lub bitmapy NULL)
 8 bajtów „Znacznik czasu”
 2 bajty „TimestampIndex”
 2 bajty wypełnienia dla wyrównania danych
 4 bajty „KeyTag” 
 0 dopełnienia do najbliższej wielokrotności 8 bajtów
-----
44 bajty na krotkę

Więcej informacji na temat pomiaru wielkości obiektu w tej pokrewnej odpowiedzi:

Kolejność kolumn w indeksie wielokolumnowym

Przeczytaj te dwa pytania i odpowiedzi, aby zrozumieć:

Sposób, w jaki masz swój indeks (klucz podstawowy), umożliwia pobieranie wierszy bez kroku sortowania, co jest atrakcyjne, szczególnie w przypadku LIMIT. Ale wyszukiwanie wierszy wydaje się niezwykle kosztowne.

Zasadniczo w indeksie wielokolumnowym kolumny „równości” powinny być pierwsze, a kolumny „zakresu” ostatnie:

Dlatego wypróbuj dodatkowy indeks z odwróconą kolejnością kolumn :

CREATE INDEX analogransition_mult_idx1
   ON "AnalogTransition" ("KeyTag", "TimestampIndex", "Timestamp");

To zależy od dystrybucji danych. Ale z millions of row, even billion of rowstym może być znacznie szybciej.

Rozmiar krotki jest o 8 bajtów większy ze względu na wyrównanie danych i wypełnianie. Jeśli używasz tego jako zwykłego indeksu, możesz spróbować upuścić trzecią kolumnę "Timestamp". Może być nieco szybszy lub nie (ponieważ może pomóc w sortowaniu).

Możesz zachować oba indeksy. W zależności od wielu czynników preferowany może być twój pierwotny indeks - w szczególności mały LIMIT.

autovacuum i statystyki tabel

Statystyki tabeli muszą być aktualne. Jestem pewien, że masz uruchomioną auto próżnię .

Ponieważ twoja tabela wydaje się być ogromna, a statystyki ważne dla właściwego planu zapytań, znacznie podniosę cel statystyki dla odpowiednich kolumn:

ALTER TABLE "AnalogTransition" ALTER "Timestamp" SET STATISTICS 1000;

... lub nawet wyżej z miliardami rzędów. Maksymalna wartość to 10000, wartość domyślna to 100.

Zrób to dla wszystkich kolumn WHERElub ORDER BYklauzul. Potem biegnij ANALYZE.

Układ stołu

Będąc przy tym, jeśli zastosujesz zdobytą wiedzę na temat wyrównywania danych i wypełniania, ten zoptymalizowany układ tabeli powinien zaoszczędzić trochę miejsca na dysku i nieco poprawić wydajność (ignorując pk i fk):

CREATE TABLE "AnalogTransition"(
  "Timestamp" timestamp with time zone NOT NULL,
  "KeyTag" integer NOT NULL,
  "TimestampIndex" smallint NOT NULL,
  "TimestampQuality" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  "QualityFlags" smallint,
  "Quality" boolean,
  "Value" numeric
);

CLUSTER / pg_repack

Aby zoptymalizować wydajność odczytu dla zapytań korzystających z określonego indeksu (czy to oryginalnego, czy mojej sugerowanej alternatywy), możesz przepisać tabelę w fizycznej kolejności indeksu. CLUSTERrobi to, ale jest to raczej inwazyjne i wymaga wyłącznej blokady na czas operacji. pg_repackjest bardziej wyrafinowaną alternatywą, która może zrobić to samo bez wyłącznego blokady na stole.
Może to znacznie pomóc w przypadku dużych tabel, ponieważ należy odczytać znacznie mniej bloków tabeli.

Baran

Ogólnie 2 GB fizycznej pamięci RAM to po prostu za mało, aby szybko poradzić sobie z miliardami wierszy. Więcej pamięci RAM może przejść długą drogę - w połączeniu z dostosowanym ustawieniem: oczywiście większy effective_cache_sizena początek.

Erwin Brandstetter
źródło
2
Dodałem prosty indeks tylko do KeyTag i wydaje się teraz dość szybki. Zastosuję również twoje zalecenia dotyczące wyrównywania danych. Wielkie dzięki!
JPelletier
9

Tak więc z planów widzę jedną rzecz: twój indeks jest albo rozdęty (następnie obok podstawowej tabeli), albo po prostu nie jest zbyt dobry dla tego rodzaju zapytań (próbowałem rozwiązać ten problem w moim ostatnim komentarzu powyżej).

Jeden wiersz indeksu zawiera 14 bajtów danych (i trochę dla nagłówka). Teraz, obliczając na podstawie liczb podanych w planie: masz 500 000 wierszy ze 190147 stron - co oznacza średnio mniej niż 3 użyteczne wiersze na stronę, czyli około 37 bajtów na stronę o wielkości 8 kb. To bardzo zły stosunek, prawda? Ponieważ pierwszą kolumną indeksu jest Timestamppole i jest ono używane w zapytaniu jako zakres, planista może - i robi - wybrać indeks, aby znaleźć pasujące wiersze. Ale nie TimestampIndexwspomniano o tym w WHEREwarunkach, więc filtrowanie KeyTagnie jest zbyt skuteczne, ponieważ te wartości prawdopodobnie pojawiają się losowo na stronach indeksu.

Tak więc jedną z możliwości jest zmiana definicji indeksu na

CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp", "KeyTag", "TimestampIndex")

(lub, biorąc pod uwagę obciążenie systemu, utwórz ten indeks jako nowy:

CREATE INDEX CONCURRENTLY "idx_AnalogTransition" 
    ON "AnalogTransition" ("Timestamp", "KeyTag", "TimestampIndex");
  • to na pewno trochę potrwa, ale tymczasem nadal możesz pracować.)

Inna możliwość, że duża część stron indeksu jest zajęta przez martwe wiersze, które można usunąć przez odkurzanie. Utworzyłeś tabelę z ustawieniami autovacuum_enabled=true- ale czy kiedykolwiek zacząłeś automatyczne odkurzanie? Lub uruchomić VACUUMręcznie?

dezso
źródło