CV laden

Database

PostgreSQL JSONB ist kein Listenindex

Ein GIN auf payload wirkt vollständig. WHERE title->>'ru' macht trotzdem Seq Scan. Containment und Extract sind andere Jobs.

Die Katalogliste in einem Shop, der nach PostgreSQL umgezogen war, machte weiter Seq Scan. In Shops mit MySQL-JSON zog der Katalog manchmal nach PostgreSQL wegen JSONB und GIN. Die Liste filterte weiter publizierte Zeilen und sortierte ein Editorfeld. Das WHERE nutzte title->>'ru'. EXPLAIN zeigte Seq Scan. Der GIN blieb ungenutzt.

Editoren warteten auf das öffentliche Raster. PostgreSQL verbrannte CPU auf Extract. Vue SSR druckte vierzig Kacheln und zahlte einen Full-Table-Read. GIN auf JSONB ist für Containment gebaut: payload @> '{"tag":"sale"}'. Der Operator ->> liefert Text. Diesen Text speichert der GIN nicht, ohne Expression-Index auf den Extract.

JSONB bleibt das Admin-Dokument. Die öffentliche Liste liest es nicht.

GIN trifft Containment, nicht jeden Extract. Eine gespeicherte Spalte kann einen Btree nutzen.
GIN trifft Containment, nicht jeden Extract. Eine gespeicherte Spalte kann einen Btree nutzen.

Generierter Text, Btree fürs Raster

title_ru ist eine stored generated Spalte aus JSONB. Das Listen-SELECT ist id, slug, price, title_ru. Der Index (is_published, sort_order, id) deckt diese Felder. Sortierung bleibt eine kleine Integer vom Editor. Titelsortierung gehört ins Admin, nicht ins öffentliche Raster.

Suche, die JSON braucht, behält einen GIN und nutzt @> oder jsonb_path_exists. Beides in einer Query zu mischen ergibt Seq Scan plus Gin Scan, der trotzdem in work_mem sortiert.

Ich habe einen Expression-Index auf (title->>'ru') versucht. Er half einem Filter auf eine Locale. Die Liste sortierte weiter sort_order und brauchte weiter published. Zwei Indizes, zwei Pläne, und eine dritte Locale bedeutete zwei weitere Expression-Indizes. Generated Columns plus ein Btree waren kleiner im Kopf. Writes wurden etwas schwerer. Das Raster bekam einen Plan, den ich lesen konnte.

Die Liste liest typisierte Spalten. Suche darf GIN nutzen. Eine Query sollte beides nicht schlecht tun.
Die Liste liest typisierte Spalten. Suche darf GIN nutzen. Eine Query sollte beides nicht schlecht tun.

Was ich nicht in JSONB lege

Preis, Published-Flag und Sort-Order bleiben typisierte Spalten. Vue SSR bekommt einen Locale-String. PHP macht kein json_decode dreier Sprachen, um vierzig Kacheln zu drucken. MySQL und PostgreSQL sehen von der Liste gleich aus: Covering-Btree, kompakte Zeilen, JSON vom Hot Path weg.

Diese Site läuft weiter auf MySQL. PostgreSQL ist der andere Katalog. Die Regel ändert sich mit der Engine nicht. JSON ist das Formular. Die Liste ist eine Tabelle.

Nach dem GIN habe ich pg_stat_user_indexes gezogen. idx_scan auf dem GIN blieb für die Listenroute nahe null. idx_scan auf dem Btree bewegte sich. Das ist die Messung, die das Argument beendete, JSONB sei schon indiziert.

Was ich daraus mitnehme

Ich lese EXPLAIN und pg_stat_user_indexes auf der Listenroute, nicht die Existenz eines GIN. Ungenutzter GIN plus Seq Scan ist die übliche Form.

title->>'ru' traue ich nicht, einen Payload-GIN zu nutzen. Containment ist @>. Extract braucht eine Generated Column oder einen Expression-Index.

Die Regel, die ich behalte: typisierte Spalten für Preis, Published, Sort. Ein Locale-String nach Vue. JSONB bleibt das Admin-Dokument. Listen-Sort und JSON-Suche nicht in einer Query mischen.

Zurück zu Notizen