2016-05-10 15 views
0

Ich habe große Tischkrümel (ca. 100M + Zeilen, 100 GB). Es ist nur Sammlung von JSON als Text gespeichert. Es hat einen Index für die Spalte run_id mit ungefähr 10K eindeutigen Werten. So ist jeder Lauf klein (1K - 1M Zeilen).Postgresql verwendet keinen Index

Für einfache Abfrage:

explain analyze verbose select * from crumbs c 
where c.run_id='2016-04-26T19_02_01_015Z' limit 10 

-Plan ist gut:

Limit (cost=0.56..36.89 rows=10 width=2262) (actual time=1.978..2.016 rows=10 loops=1) 
    Output: id, robot_id, run_id, content, created_at, updated_at, table_id, fork_id, log, err 
    -> Index Scan using index_crumbs_on_run_id on public.crumbs c (cost=0.56..5533685.73 rows=1523397 width=2262) (actual time=1.975..1.996 rows=10 loops=1) 
     Output: id, robot_id, run_id, content, created_at, updated_at, table_id, fork_id, log, err 
     Index Cond: ((c.run_id)::text = '2016-04-26T19_02_01_015Z'::text) 
Planning time: 0.117 ms 
Execution time: 2.048 ms 

Aber wenn ich versuche, innerhalb json in einer der Spalten gespeichert schauen sie will dann Scan tun:

explain verbose select x from crumbs c, 
lateral json_array_elements(c.content::json) x 
where c.run_id='2016-04-26T19_02_01_015Z' 
limit 10 

-Plan:

Limit (cost=0.01..0.69 rows=10 width=32) 
    Output: x.value 
    -> Nested Loop (cost=0.01..10332878.67 rows=152343800 width=32) 
     Output: x.value 
     -> Seq Scan on public.crumbs c (cost=0.00..7286002.66 rows=1523438 width=895) 
       Output: c.id, c.robot_id, c.run_id, c.content, c.created_at, c.updated_at, c.table_id, c.fork_id, c.log, c.err 
       Filter: ((c.run_id)::text = '2016-04-26T19_02_01_015Z'::text) 
     -> Function Scan on pg_catalog.json_array_elements x (cost=0.01..1.01 rows=100 width=32) 
       Output: x.value 
       Function Call: json_array_elements((c.content)::json) 

Versuchte:

analyze crumbs 

aber machte keinen Unterschied.

Update 1 Deaktivieren der sequenziellen Suche für die gesamte Datenbank funktioniert, aber dies ist keine Option in unserer Anwendung. In vielen anderen Orten sollte seq Scan bleiben:

set enable_seqscan=false; 

-Plan:

Limit (cost=0.57..1.14 rows=10 width=32) (actual time=0.120..0.294 rows=10 loops=1) 
    Output: x.value 
    -> Nested Loop (cost=0.57..8580698.45 rows=152343400 width=32) (actual time=0.118..0.273 rows=10 loops=1) 
     Output: x.value 
     -> Index Scan using index_crumbs_on_run_id on public.crumbs c (cost=0.56..5533830.45 rows=1523434 width=895) (actual time=0.087..0.107 rows=10 loops=1) 
       Output: c.id, c.robot_id, c.run_id, c.content, c.created_at, c.updated_at, c.table_id, c.fork_id, c.log, c.err 
       Index Cond: ((c.run_id)::text = '2016-04-26T19_02_01_015Z'::text) 
     -> Function Scan on pg_catalog.json_array_elements x (cost=0.01..1.01 rows=100 width=32) (actual time=0.011..0.011 rows=1 loops=10) 
       Output: x.value 
       Function Call: json_array_elements((c.content)::json) 
Planning time: 0.124 ms 
Execution time: 0.337 ms 

Update 2:

Schema ist:

CREATE TABLE crumbs 
(
    id serial NOT NULL, 
    run_id character varying(255), 
    content text, 
    created_at timestamp without time zone, 
    updated_at timestamp without time zone, 
    CONSTRAINT crumbs_pkey PRIMARY KEY (id) 
); 

CREATE INDEX index_crumbs_on_run_id 
    ON crumbs 
    USING btree 
    (run_id COLLATE pg_catalog."default"); 

Update 3

Umschreiben Abfrage wie folgt:

select json_array_elements(c.content::json) x 
from crumbs c 
where c.run_id='2016-04-26T19_02_01_015Z' 
limit 10 

Ruft richtigen Plan. Noch unklar, warum ein falscher Plan für die zweite Abfrage gewählt wurde.

+1

'((run_id) :: text = '2016-04-26T19_02_01_015Z' :: Text)' run_id sieht für mich wie ein Zeitstempel aus. Warum speichern Sie es als Textfeld? Außerdem: bitte fügen Sie die Tabellendefinition (en) einschließlich der Indizes hinzu. – joop

+0

Ja, run_id ist ein Zeitstempel mit Textpräfix. Ich verpass das Präfix in Frage, um zu vermeiden, dass eine nicht verwandte Komplexität eingeführt wird. Aktualisieren von Ausgaben mit EXPLAIN analysieren jetzt ausführlich. –

+1

Klingt wie eine Situation taylor für jsonb – e4c5

Antwort

0

Umschreiben der Abfrage, so dass die Grenze ersten und dann aufgebracht wird das Kreuz gegen die Funktion anschließen sollte Postgres den Index machen verwenden:

eine abgeleitete Tabelle:

select x 
from (
    select * 
    from crumbs 
    where run_id='2016-04-26T19_02_01_015Z' 
    limit 10 
) c 
    cross join lateral json_array_elements(c.content::json) x 

Alternativ mit einem CTE:

with c as (
    select * 
    from crumbs 
    where run_id='2016-04-26T19_02_01_015Z' 
    limit 10 
) 
select x 
from c 
    cross join lateral json_array_elements(c.content::json) x 

Oder nutzen.210 direkt in der Auswahlliste:

select json_array_elements(c.content::json) 
from crumbs c 
where c.run_id='2016-04-26T19_02_01_015Z' 
limit 10 

Dies ist jedoch etwas anders als die anderen zwei Abfragen, weil sie die Grenze nach „Auseinanderschieben“ der JSON-Array gelten, nicht auf der Anzahl der zurückgegebenen Zeilen aus der crumbs Tabelle (was Ihre erste Abfrage tut).

+0

Haben Sie einen Hinweis darauf, dass SRFs generell nicht in die select-Klausel aufgenommen werden sollten? – yieldsfalsehood

+0

@yieldsfalseighood: Dies wurde mehrmals auf den Mailinglisten erwähnt.Ein Grund dafür, es zu entfremden - soweit ich mich erinnere - war, dass dieses Verhalten nicht klar definiert ist, wenn Sie andere Spalten zusammen mit der Funktion auswählen. Aber ich kann das jetzt nicht finden. –

+0

Danke für die Umschreibungen. Sie arbeiten, aber ich bleibe im magischen Land - scheint wie identische Fragen, aber die Pläne sind anders und deshalb können wir nie sicher sein, ob der falsche Plan irgendwann ausgewählt wird. –

0

Sie haben drei verschiedene Probleme. Zuerst tippt der limit 10 in der ersten Abfrage den Planer zugunsten des Index-Scans, der ansonsten ziemlich teuer wäre, alle Zeilen zu erhalten, die dem run_id entsprechen. Zu Vergleichszwecken möchten Sie möglicherweise sehen, wie der erste (nicht verbundene) Abfrageplan aussieht, wenn Sie das Limit entfernen. Ich schätze, der Planer wechselt zu einem Tabellenscan.

Zweitens ist diese seitliche Verbindung unnötig und wirft den Planer weg. Sie können die Elemente des Content-Array in Ihrer select-Klausel wie so erweitern:

select json_array_elements(content::json) 
from crumbs 
where run_id = '2016-04-26T19_02_01_015Z' 
; 

Dieses wahrscheinlicher ist es, den Index-Scan zu verwenden, um Zeilen zu pflücken für die run_id, dann „UNNEST“ die Array-Elemente für Sie.

Aber das dritte versteckte Problem ist, was Sie eigentlich versuchen zu bekommen.Wenn Sie diese letzte Abfrage ausführen, befinden Sie sich im selben Boot wie die erste (nicht verbundene) Abfrage ohne Begrenzung, was bedeutet, dass Sie wahrscheinlich keinen Index-Scan erhalten werden (nicht, dass das von Natur aus schlecht ist, wenn Sie es sind Lesen eines so großen Stücks des Tisches).

Möchten Sie nur die ersten willkürlichen Array-Elemente aus allen Content-Arrays in diesem Lauf? Wenn dies der Fall ist, dann sollte das Ende einer Geschichte mit einer Limitklausel enden. Wenn Sie alle Array-Elemente für diesen bestimmten Lauf möchten, müssen Sie möglicherweise nur einen Tabellenscan akzeptieren, obwohl Sie ohne den lateralen Join möglicherweise in einer viel besseren Situation als die ursprüngliche Abfrage sind.

+0

Das Entfernen des Limits erhält in allen Fällen den korrekten Plan (verwendet den Index, nicht den vollständigen Scan). Der Zweck des Limits ist, wie Sie erraten haben - um beliebige Elemente für die Vorschau des vollständigen Jobs zu erhalten. Ihre Neufassung löst das Problem. Aber das Geheimnis bleibt, warum PostgreSQL einen sehr teuren Plan (100 GB) wählt, wenn ein günstiger Plan verfügbar ist. –

0

Datenmodellierung Vorschläge:

 -- Suggest replacing the column run_id (low cardinality, and rather fat) 
     -- by a reference to a domain table, like: 
     -- ------------------------------------------------------------------ 
CREATE TABLE runs 
     (run_seq serial NOT NULL PRIMARY KEY 
     , run_id character varying UNIQUE 
     ); 

     -- Grab all the distinct values occuring in crumbs.run_id 
     -- ------------------------------------------------------- 
INSERT INTO runs (run_id) 
SELECT DISTINCT run_id FROM crumbs; 

     -- Add an FK column 
     -- ----------------- 
ALTER TABLE crumbs 
     ADD COLUMN run_seq integer REFERENCES runs(run_seq) 
     ; 

UPDATE crumbs c 
SET run_seq = r.run_seq 
FROM runs r 
WHERE r.run_id = c.run_id 
     ; 
VACUUM ANALYZE runs; 

     -- Drop old column and set new column to not nullable 
     -- --------------------------------------------------- 
ALTER TABLE crumbs 
     DROP COLUMN run_id 
     ; 
ALTER TABLE crumbs 
     ALTER COLUMN run_seq SET NOT NULL 
     ; 

     -- Recreate the supporting index for the FK 
     -- adding id to support index-only lookups 
     -- (and enforce uniqueness) 
     -- ------------------------------------- 
CREATE UNIQUE INDEX index_crumbs_run_seq_id ON crumbs (run_seq,id) 
     ; 

     -- Refresh statistics 
     -- ------------------ 
VACUUM ANALYZE crumbs; -- this may take some time ... 

-- and then: join the runs table to your original crumbs table 
-- ----------------------------------------------------------- 
-- explain analyze 
SELECT x FROM crumbs c 
JOIN runs r ON r.run_seq = c.run_seq 
     , lateral json_array_elements(c.content::json) x 
WHERE r.run_id='2016-04-26T19_02_01_015Z' 
LIMIT 10 
     ; 

Oder: Verwenden Sie die anderen Vorschlag der Beantworter mit einem ähnlichen verbinden.


Aber vielleicht noch besser: ersetzt die hässliche run_id Textzeichenfolge durch einen tatsächlichen Zeitstempel.

+0

Danke! Wir haben Tisch läuft. Es ist nur eine Vorliebe, lesbare Schlüssel auf Kosten von zusätzlichem Speicher anstelle von ganzen Zahlen zu haben. –

+0

Sie beschweren sich über einen "teuren Plan", aber Sie möchten Ihre teuren Datenmodellierungsfehler nicht beheben? – joop

Verwandte Themen