Vorbemerkungen
Sie verwenden ungerade Datentypen. character(24)
? char(n)
ist ein veralteter Typ und fast immer die falsche Wahl. Sie haben Indizes aktiviert person_id
und nehmen wiederholt daran teil. integer
wäre aus mehreren Gründen viel effizienter. (Oder bigint
wenn Sie vorhaben, über die Lebensdauer der Tabelle mehr als 2 Milliarden Zeilen zu brennen.) Verwandte Themen:
LIKE
ist ohne Platzhalter sinnlos. Verwenden Sie =
stattdessen. Schneller.
x.location_host LIKE '2015.testonline.ca'
x.location_host = '2015.testonline.ca'
Verwenden Sie count(e1.*)
oder count(*)
anstatt eine Dummy-Spalte mit dem Wert 1
für jede Unterabfrage hinzuzufügen. (Mit Ausnahme von last ( e3
), wo Sie keine tatsächlichen Daten benötigen.)
Sie sind inkonsistent, wenn Sie das String-Literal timestamp
manchmal in und manchmal nicht ( timestamp '2016-04-30 23:59:59.999'
) umwandeln . Entweder macht es Sinn, dann mach es die ganze Zeit oder nicht, dann mach es nicht.
Das tut es nicht. Im Vergleich zu einer timestamp
Spalte wird timestamp
ohnehin ein String-Literal erzwungen . Sie brauchen also keine explizite Besetzung.
Der Postgres-Datentyp timestamp
hat bis zu 6 Bruchstellen. Ihre BETWEEN
Ausdrücke hinterlassen Eckfälle. Ich habe sie durch weniger fehleranfällige Ausdrücke ersetzt.
Indizes
Wichtig: Um die Leistung zu optimieren, erstellen Sie mehrspaltige Indizes .
Für die erste Unterabfrage hp
:
CREATE INDEX event_pg_location_host_timestamp__idx
ON event_pg (location_host, timestamp_);
Oder fügen Sie person_id
dem Index Folgendes hinzu, wenn Sie nur Index-Scans erhalten können :
CREATE INDEX event_pg_location_host_timestamp__person_id_idx
ON event_pg (location_host, timestamp_, person_id);
Für sehr großehlp
Zeitbereiche, die sich über den größten Teil oder die gesamte Tabelle erstrecken, sollte dieser Index vorzuziehen sein. Er unterstützt auch die Unterabfrage. Erstellen Sie ihn also so oder so:
CREATE INDEX event_pg_location_host_person_id_timestamp__idx
ON event_pg (location_host, person_id, timestamp_);
Für tnk
:
CREATE INDEX event_pg_location_fragment_timestamp__idx
ON event_pg (location_fragment, person_id, timestamp_);
Wenn Ihre Prädikate auf location_host
und location_fragment
Konstanten sind, können wir stattdessen viel billigere Teilindizes verwenden , zumal Ihre location_*
Spalten groß erscheinen:
CREATE INDEX event_pg_hp_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE location_host = '2015.testonline.ca';
CREATE INDEX event_pg_hlp_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE location_host = 'helpcentre.testonline.ca';
CREATE INDEX event_pg_tnk_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE location_fragment = '/file/thank-you';
Erwägen:
Auch hier sind alle diese Indizes mit integer
oder bigint
für wesentlich kleiner und schneller person_id
.
Im Allgemeinen müssen Sie nach ANALYZE
dem Erstellen eines neuen Index die Tabelle aufrufen - oder warten, bis das Autovakuum einsetzt, um dies für Sie zu tun.
Um nur Index-Scans zu erhalten , muss Ihre Tabelle VACUUM
ausreichend bearbeitet werden. Sofort danach VACUUM
als Proof of Concept testen . Lesen Sie die verlinkte Postgres-Wiki-Seite, um weitere Informationen zu erhalten, wenn Sie mit Nur-Index-Scans nicht vertraut sind .
Grundlegende Abfrage
Umsetzung dessen, was ich besprochen habe. Abfrage für kleine Bereiche ( wenige Zeilen pro person_id
):
SELECT count(*)::int AS view_homepage
, count(hlp.hlp_ts)::int AS use_help
, count(tnk.yes)::int AS thank_you
FROM (
SELECT DISTINCT ON (person_id)
person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE timestamp_ >= '2016-04-23'
AND timestamp_ < '2016-05-01'
AND location_host = '2015.testonline.ca'
ORDER BY person_id, timestamp_
) hp
LEFT JOIN LATERAL (
SELECT timestamp_ AS hlp_ts
FROM event_pg y
WHERE y.person_id = hp.person_id
AND timestamp_ >= hp.hp_ts
AND timestamp_ < '2016-05-01'
AND location_host = 'helpcentre.testonline.ca'
ORDER BY timestamp_
LIMIT 1
) hlp ON true
LEFT JOIN LATERAL (
SELECT true AS yes -- we only need existence
FROM event_pg z
WHERE z.person_id = hp.person_id -- we can use hp here
AND location_fragment = '/file/thank-you'
AND timestamp_ >= hlp.hlp_ts -- this introduces dependency on hlp anyways.
AND timestamp_ < '2016-05-01'
ORDER BY timestamp_
LIMIT 1
) tnk ON true;
DISTINCT ON
ist oft billiger für wenige Reihen pro person_id
. Ausführliche Erklärung:
Wenn Sie viele Zeilen pro habenperson_id
(wahrscheinlicher für größere Zeitbereiche), kann der in dieser Antwort in Kapitel 1a beschriebene rekursive CTE(viel) schneller sein:
Siehe es unten integriert.
Optimieren und automatisieren Sie die beste Abfrage
Es ist das alte Rätsel: Eine Abfragetechnik eignet sich am besten für einen kleineren Satz, eine andere für einen größeren Satz. In Ihrem speziellen Fall haben wir von Anfang an einen sehr guten Indikator - die Länge des angegebenen Zeitraums - anhand dessen wir entscheiden können.
Wir verpacken alles in eine PL / pgSQL-Funktion. Meine Implementierung wechselt von DISTINCT ON
rCTE, wenn der angegebene Zeitraum länger als ein festgelegter Schwellenwert ist:
CREATE OR REPLACE FUNCTION f_my_counts(_ts_low_inc timestamp, _ts_hi_excl timestamp)
RETURNS TABLE (view_homepage int, use_help int, thank_you int) AS
$func$
BEGIN
CASE
WHEN _ts_hi_excl <= _ts_low_inc THEN
RAISE EXCEPTION 'Timestamp _ts_hi_excl (1st param) must be later than _ts_low_inc!';
WHEN _ts_hi_excl - _ts_low_inc < interval '10 days' THEN -- example value !!!
-- DISTINCT ON for few rows per person_id
RETURN QUERY
WITH hp AS (
SELECT DISTINCT ON (person_id)
person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE timestamp_ >= _ts_low_inc
AND timestamp_ < _ts_hi_excl
AND location_host = '2015.testonline.ca'
ORDER BY person_id, timestamp_
)
, hlp AS (
SELECT hp.person_id, hlp.hlp_ts
FROM hp
CROSS JOIN LATERAL (
SELECT timestamp_ AS hlp_ts
FROM event_pg
WHERE person_id = hp.person_id
AND timestamp_ >= hp.hp_ts
AND timestamp_ < _ts_hi_excl
AND location_host = 'helpcentre.testonline.ca' -- match partial idx
ORDER BY timestamp_
LIMIT 1
) hlp
)
SELECT (SELECT count(*)::int FROM hp) -- AS view_homepage
, (SELECT count(*)::int FROM hlp) -- AS use_help
, (SELECT count(*)::int -- AS thank_you
FROM hlp
CROSS JOIN LATERAL (
SELECT 1 -- we only care for existence
FROM event_pg
WHERE person_id = hlp.person_id
AND location_fragment = '/file/thank-you'
AND timestamp_ >= hlp.hlp_ts
AND timestamp_ < _ts_hi_excl
ORDER BY timestamp_
LIMIT 1
) tnk
);
ELSE
-- rCTE for many rows per person_id
RETURN QUERY
WITH RECURSIVE hp AS (
( -- parentheses required
SELECT person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE timestamp_ >= _ts_low_inc
AND timestamp_ < _ts_hi_excl
AND location_host = '2015.testonline.ca' -- match partial idx
ORDER BY person_id, timestamp_
LIMIT 1
)
UNION ALL
SELECT x.*
FROM hp, LATERAL (
SELECT person_id, timestamp_ AS hp_ts
FROM event_pg
WHERE person_id > hp.person_id -- lateral reference
AND timestamp_ >= _ts_low_inc -- repeat conditions
AND timestamp_ < _ts_hi_excl
AND location_host = '2015.testonline.ca' -- match partial idx
ORDER BY person_id, timestamp_
LIMIT 1
) x
)
, hlp AS (
SELECT hp.person_id, hlp.hlp_ts
FROM hp
CROSS JOIN LATERAL (
SELECT timestamp_ AS hlp_ts
FROM event_pg y
WHERE y.person_id = hp.person_id
AND location_host = 'helpcentre.testonline.ca' -- match partial idx
AND timestamp_ >= hp.hp_ts
AND timestamp_ < _ts_hi_excl
ORDER BY timestamp_
LIMIT 1
) hlp
)
SELECT (SELECT count(*)::int FROM hp) -- AS view_homepage
, (SELECT count(*)::int FROM hlp) -- AS use_help
, (SELECT count(*)::int -- AS thank_you
FROM hlp
CROSS JOIN LATERAL (
SELECT 1 -- we only care for existence
FROM event_pg
WHERE person_id = hlp.person_id
AND location_fragment = '/file/thank-you'
AND timestamp_ >= hlp.hlp_ts
AND timestamp_ < _ts_hi_excl
ORDER BY timestamp_
LIMIT 1
) tnk
);
END CASE;
END
$func$ LANGUAGE plpgsql STABLE STRICT;
Anruf:
SELECT * FROM f_my_counts('2016-01-23', '2016-05-01');
Der rCTE arbeitet per Definition mit einem CTE. Ich habe auch CTEs für die DISTINCT ON
Abfrage eingegeben (wie ich in den Kommentaren mit @Lennart besprochen habe ), wodurch wir den Satz bei jedem Schritt verwenden können, CROSS JOIN
anstatt ihn LEFT JOIN
zu reduzieren, da wir jeden CTE separat zählen können. Dies hat Auswirkungen in entgegengesetzte Richtungen:
- Zum einen mussten wir die Anzahl der Zeilen reduzieren, was den dritten Join billiger machen sollte.
- Auf der anderen Seite führen wir Overhead für die CTEs ein und benötigen erheblich mehr RAM, was besonders für große Abfragen wie Ihre wichtig sein kann.
Sie müssen testen, welche die anderen überwiegt.