Próbuję ustalić, które indeksy mają być używane w zapytaniu SQL z WHERE
warunkiem, a GROUP BY
który działa obecnie bardzo wolno.
Moje zapytanie:
SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id
Tabela ma obecnie 32 000 000 wierszy. Czas wykonania zapytania znacznie wzrasta, gdy zwiększam ramy czasowe.
Tabela, o której mowa, wygląda następująco:
CREATE TABLE counter (
id bigserial PRIMARY KEY
, ts timestamp NOT NULL
, group_id bigint NOT NULL
);
Obecnie mam następujące indeksy, ale wydajność jest nadal niska:
CREATE INDEX ts_index
ON counter
USING btree
(ts);
CREATE INDEX group_id_index
ON counter
USING btree
(group_id);
CREATE INDEX comp_1_index
ON counter
USING btree
(ts, group_id);
CREATE INDEX comp_2_index
ON counter
USING btree
(group_id, ts);
Uruchomienie EXPLAIN dla zapytania daje następujący wynik:
"QUERY PLAN"
"HashAggregate (cost=467958.16..467958.17 rows=1 width=4)"
" -> Index Scan using ts_index on counter (cost=0.56..467470.93 rows=194892 width=4)"
" Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"
SQL Fiddle z przykładowymi danymi: http://sqlfiddle.com/#!15/7492b/1
Pytanie
Czy można poprawić wydajność tego zapytania, dodając lepsze indeksy, czy też muszę zwiększyć moc przetwarzania?
Edytuj 1
Używana jest wersja PostgreSQL 9.3.2.
Edytuj 2
Próbowałem @Erwin z EXISTS
:
SELECT group_id
FROM groups g
WHERE EXISTS (
SELECT 1
FROM counter c
WHERE c.group_id = g.group_id
AND ts BETWEEN timestamp '2014-03-02 00:00:00'
AND timestamp '2014-03-05 12:00:00'
);
Niestety nie wydawało się to zwiększać wydajności. Plan zapytań:
"QUERY PLAN"
"Nested Loop Semi Join (cost=1607.18..371680.60 rows=113 width=4)"
" -> Seq Scan on groups g (cost=0.00..2.33 rows=133 width=4)"
" -> Bitmap Heap Scan on counter c (cost=1607.18..158895.53 rows=60641 width=4)"
" Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
" -> Bitmap Index Scan on comp_2_index (cost=0.00..1592.02 rows=60641 width=0)"
" Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
Edytuj 3
Plan zapytania dla zapytania LATERAL z ypercube:
"QUERY PLAN"
"Nested Loop (cost=8.98..1200.42 rows=133 width=20)"
" -> Seq Scan on groups g (cost=0.00..2.33 rows=133 width=4)"
" -> Result (cost=8.98..8.99 rows=1 width=0)"
" One-Time Filter: ($1 IS NOT NULL)"
" InitPlan 1 (returns $1)"
" -> Limit (cost=0.56..4.49 rows=1 width=8)"
" -> Index Only Scan using comp_2_index on counter c (cost=0.56..1098691.21 rows=279808 width=8)"
" Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
" InitPlan 2 (returns $2)"
" -> Limit (cost=0.56..4.49 rows=1 width=8)"
" -> Index Only Scan Backward using comp_2_index on counter c_1 (cost=0.56..1098691.21 rows=279808 width=8)"
" Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
group_id
wartości jest na stole?group_id
a nie pod każdym względem?Odpowiedzi:
Kolejny pomysł, który wykorzystuje również
groups
tabelę i konstrukcję o nazwieLATERAL
join (dla fanów SQL-Server jest to prawie identyczne zOUTER APPLY
). Ma tę zaletę, że agregaty można obliczać w podzapytaniu:Przetestuj w SQL-Fiddle pokazuje, że zapytanie wykonuje skanowanie
(group_id, ts)
indeksu w indeksie.Podobne plany są tworzone przy użyciu 2 łączeń bocznych, jednego dla min i jednego dla maksimum, a także z 2 wbudowanymi skorelowanymi podkwerendami. Można ich również użyć, jeśli chcesz wyświetlić całe
counter
wiersze oprócz dat minimalnych i maksymalnych:źródło
Ponieważ nie masz agregatu na liście wyboru,
group by
jest to prawie to samo, co umieszczeniedistinct
na liście wyboru, prawda?Jeśli tego właśnie chcesz, możesz uzyskać szybkie wyszukiwanie indeksu na comp_2_index, przepisując go, aby użyć zapytania rekurencyjnego, zgodnie z opisem na wiki PostgreSQL .
Zrób widok, aby skutecznie zwracać różne group_ids:
A następnie użyj tego widoku zamiast tabeli odnośników w
exists
częściowym połączeniu Erwina .źródło
Ponieważ są tylko
133 different group_id's
, możesz użyćinteger
(lub nawetsmallint
) dla id_grupy. Jednak niewiele ci to kupi, ponieważ wypełnienie do 8 bajtów zje resztę w tabeli i możliwe indeksy wielokolumnowe. Przetwarzanie zwykłegointeger
powinno być jednak nieco szybsze. Więcej naint
wersetachint2
.@Leo: znaczniki czasu są przechowywane w 8-bajtowych liczbach całkowitych w nowoczesnych instalacjach i mogą być przetwarzane idealnie szybko. Detale.
@ypercube: Indeks włączony
(group_id, ts)
nie może pomóc, ponieważgroup_id
w zapytaniu nie ma żadnego warunku .Twoim głównym problemem jest ogromna ilość danych, które muszą zostać przetworzone:
Widzę, że jesteś zainteresowany jedynie istnieniem
group_id
, a nie faktyczną liczbą. Ponadto istnieją tylko 133 różnegroup_id
s. Dlatego zapytanie może być spełnione przy pierwszym trafieniugorup_id
w danym przedziale czasowym. Stąd ta sugestia dotycząca alternatywnego zapytania z połączeniemEXISTS
częściowym :Zakładając tabelę przeglądową dla grup:
Indeksu
comp_2_index
na(group_id, ts)
teraz staje instrumentalny.SQL Fiddle (w oparciu o skrzypce dostarczone przez @ypercube w komentarzach)
Tutaj zapytanie preferuje indeks
(ts, group_id)
, ale myślę, że dzieje się tak ze względu na konfigurację testu z „klastrowanymi” znacznikami czasu. Jeśli usuniesz indeksy z wiodącymts
( więcej na ten temat ), planista z przyjemnością również użyje indeksu(group_id, ts)
- szczególnie w przypadku skanowania tylko indeksu .Jeśli to zadziała, możesz nie potrzebować tego innego możliwego ulepszenia: Wstępnie agreguj dane w zmaterializowanym widoku, aby drastycznie zmniejszyć liczbę wierszy. Ma to sens szczególnie, jeśli dodatkowo potrzebujesz rzeczywistych obliczeń . Następnie ponosisz koszty przetworzenia wielu wierszy jeden raz podczas aktualizacji mv. Możesz nawet łączyć agregaty dzienne i godzinowe (dwie osobne tabele) i dostosowywać do tego zapytanie.
Czy ramy czasowe w zapytaniach są arbitralne? A może głównie w pełnych minutach / godzinach / dniach?
Utwórz niezbędne indeksy
counter_mv
i dostosuj zapytanie do pracy z nim ...źródło
groups
stołu robi różnicę?ANALYZE
robi różnicę. Ale indeksycounter
nawet się wykorzystują,ANALYZE
gdy tylko przedstawięgroups
tabelę. Chodzi o to, że bez tej tabeli seqscan jest potrzebny do zbudowania zestawu możliwych identyfikatorów grupy. Dodałem więcej do mojej odpowiedzi. I dzięki za skrzypce!group_id
nawet dlaSELECT DISTINCT group_id FROM t;
zapytania?LIMIT 1
może wybrać skanowanie indeksu bitmap, które nie korzysta z wczesnego zatrzymania i zajmuje dużo więcej czasu. (Ale jeśli tabela jest świeżo odkurzana, może preferować skanowanie indeksowe zamiast skanowania mapy bitowej, więc to, co widzisz, zależy od stanu próżni tabeli).