Optimieren des Datenlayouts

Abgeschlossen

Datenmodellierungsentscheidungen wirken sich erheblich auf die Leistung der Vektorsuche aus. Wie Sie Tabellen strukturieren, Datentypen für Metadaten auswählen und unterstützende Indizes erstellen, bestimmt, ob Abfragen effizient ausgeführt werden, wenn ihr Dataset wächst.

Hinweis

Codebeispiele in dieser Lektion veranschaulichen Schemaentwurfsmuster für Vektordaten mit Metadaten. Passen Sie diese Muster an Ihre spezifischen Datenmodell- und Abfrageanforderungen an.

Überlegungen zur Vektorspeicherung

Vektorspalten verbrauchen erhebliche Speicher- und Verarbeitungsressourcen. Wenn Sie die Speichermerkmale verstehen, können Sie fundierte Entscheidungen zum Schemaentwurf treffen.

Jede Vektordimension benötigt zusätzlich 4 Byte Speicherplatz (bei Gleitkommazahlen einfacher Genauigkeit) zuzüglich eines festen Overheads. Die Beziehung zwischen Dimensionen und Speicher ist linear:

Maße Bytes pro Vektor 1 Millionen Vektoren
384 ~1,5 KB ~1,5 GB
768 ~3 KB ca. 3 GB
1536 ~6 KB ~6 GB
3072 ca. 12 KB ~12 GB

Für einen Produktkatalog mit zwei Millionen Artikeln mit 1536-dimensionalen Einbettungen benötigt die Vektorspalte allein ca. 12 GB Speicherplatz. Das Hinzufügen von HNSW-Indizes erhöht dies um etwa 50%.

Viele Einbettungsmodelle bieten mehrere Dimensionsoptionen. Niedrigere Abmessungen reduzieren Speicher- und Berechnungskosten und halten bei vielen Anwendungsfällen eine angemessene Qualität. Das Angeben von Dimensionen in der Spaltendefinition stellt eine Überprüfung bereit. Versuche, Vektoren mit unterschiedlichen Dimensionen einzufügen, schlagen mit einer Fehlermeldung fehl, wodurch subtile Fehler aufgrund nicht übereinstimmender Einbettungsmodelle verhindert werden. Definieren Sie die Tabelle mit einer expliziten Dimensionseinschränkung, indem Sie embedding vector(768) in der Spaltendefinition verwenden.

Einige Anwendungen benötigen Vektoren aus verschiedenen Modellen. Sie können beispielsweise Produkttiteleinbettungen, Bildeinbettungen und Benutzerverhaltenseinbettungen separat speichern. Jede Vektorspalte benötigt einen eigenen Index, da Sie keinen einzelnen Index erstellen können, der mehrere Vektorspalten abdeckt.

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    title_embedding vector(768),      -- Text embedding model
    image_embedding vector(512),       -- Image embedding model
    category_id INTEGER,
    price NUMERIC(10,2)
);

-- Create separate indexes for each embedding type
CREATE INDEX ON products USING hnsw (title_embedding vector_cosine_ops);
CREATE INDEX ON products USING hnsw (image_embedding vector_cosine_ops);

Metadatendatentypen: strukturierte Spalten im Vergleich zu JSONB

Produktempfehlungen nutzen selten allein die Ähnlichkeit von Vektoren. Abfragen filtern in der Regel nach Kategorie, Preisbereich, Verfügbarkeit oder anderen Attributen vor oder neben der Vektorsuche. Wie Sie diese Metadaten speichern, wirkt sich auf die Abfrageleistung aus.

Strukturierte Spalten verwenden die systemeigenen Datentypen (INTEGER, TIMESTAMP, NUMERIC, TEXT) von PostgreSQL mit expliziten Schemas. Diese Spalten bieten Vorteile bei der Abfrageleistung, weil native Typen effiziente B-Baum-Indizes für Gleichheits- und Bereichsabfragen ermöglichen, Speichereffizienz durch optimierte Speicherformate bieten, Typsicherheit durch Validierung zur Einfügezeit gewährleisten und die Abfrageoptimierung durch genaue Planerstatistiken unterstützen. Verwenden Sie strukturierte Spalten, wenn Attribute zur Entwurfszeit bekannt sind, sie häufig nach bestimmten Attributen filtern oder sortieren, oder die Abfrageleistung ist wichtig.

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    embedding vector(1536),
    category_id INTEGER NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    in_stock BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    brand TEXT,
    rating NUMERIC(2,1)
);

JSONB speichert halbstrukturierte Daten als binäres JSON und bietet Flexibilität für dynamische Attribute. JSONB bietet Schemaflexibilität (unterschiedliche Produkte können unterschiedliche Attribute haben), einfache Weiterentwicklung (Hinzufügen neuer Attribute ohne Schemamigrationen) und geschachtelte Strukturen (Speichern komplexer hierarchischer Daten). JSONB hat jedoch Abfrageaufwand (das Extrahieren von Werten erfordert Analyse), Indexeinschränkungen (GIN-Indizes funktionieren für Einschlussabfragen, aber keine Bereichsabfragen) und Planner-Unsicherheit (Statistiken sind für JSONB-Inhalte weniger präzise).

Bei gefilterten Vektorsuchen wirkt sich die Metadatenfilterleistung direkt auf die Gesamtabfragezeit aus. Strukturierte Spalten mit B-Strukturindizes ermöglichen Es PostgreSQL, Kandidaten vor Vektorabstandsberechnungen schnell einzugrenzen, während JSONB unterschiedliche Abfragemuster und Indextypen erfordert. Ein strukturierter Spaltenfilter wie WHERE category_id = 5 AND price BETWEEN 100 AND 500 kann einen B-Baum-Index auf (category_id, price) verwenden, um Kandidatenzeilen schnell zu identifizieren. Ein JSONB-Filter wie WHERE attributes @> '{"category": "electronics"}' AND (attributes->>'price')::numeric BETWEEN 100 AND 500 erfordert entweder einen GIN-Index (der nicht bei Bereichsabfragen zum Preis hilft) oder eine sequenzielle Überprüfung der JSONB-Spalte.

Viele Anwendungen profitieren von der Kombination strukturierter Spalten und JSONB: Verwenden Sie strukturierte Spalten für häufig gefilterte Attribute, bei denen die Abfrageleistung wichtig ist, und JSONB für dynamische oder selten gefilterte Attribute, bei denen die Schemaflexibilität wertvoller ist. Mit diesem Muster können Sie den häufig vorkommenden Fall optimieren, ohne die Flexibilität für Spezialfälle zu opfern.

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    embedding vector(1536),
    -- Structured columns for common filters
    category_id INTEGER NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    in_stock BOOLEAN DEFAULT true,
    -- JSONB for dynamic attributes
    attributes JSONB DEFAULT '{}'
);

Metadatenindizes für gefilterte Suchvorgänge

Metadatenindizes ergänzen Vektorindizes durch Beschleunigung der Filterphase von Abfragen. Ohne ordnungsgemäße Metadatenindizes muss PostgreSQL möglicherweise alle Zeilen scannen, um Filter vor der Vektorsuche anzuwenden.

Erstellen Sie B-Baum-Indizes für Spalten, die in WHERE-Klauseln verwendet werden. Einzelspaltige Indizes behandeln genaue Übereinstimmungen, während zusammengesetzte Indizes Filterkombinationen behandeln. Zusammengesetzte Indizes sind am effektivsten, wenn Abfragen nach den linkesten Spalten filtern. Ein Index bei (category_id, price) bearbeitet WHERE category_id = 5 und WHERE category_id = 5 AND price < 100 effizient, aber er hilft nicht allein bei WHERE price < 100, da der Preis nicht die linkeste Spalte ist.

-- Single-column index for exact matches
CREATE INDEX idx_products_category ON products (category_id);

-- Composite index for common filter combinations
CREATE INDEX idx_products_category_price ON products (category_id, price);

Wenn die meisten Abfragen nach derselben Bedingung (z. B. Lagerprodukte) filtern, reduziert ein Teilindex die Indexgröße und verbessert die Leistung. Dieser Index ist kleiner als ein vollständiger Index und wird nur für Abfragen verwendet, die enthalten WHERE in_stock = truesind. Bei einem E-Commerce-Empfehlungsalgorithmus, bei dem fast alle Abfragen auf verfügbare Produkte abzielen, kann dies den Indexwartungsaufwand erheblich reduzieren. Erstellen Sie einen Teilindex mit CREATE INDEX idx_products_instock_category ON products (category_id) WHERE in_stock = true;.

Wenn Sie JSONB für Attribute verwenden, unterstützen GIN-Indizes Enthaltenseinsabfragen mit den Operatoren @> (enthält), <@ (enthalten in), ? (Schlüssel vorhanden) und ?|/?& (beliebige/alle Schlüssel existieren). Sie beschleunigen keine Bereichsabfragen oder beliebige JSON-Pfadausdrücke. Erstellen Sie einen GIN-Index mit CREATE INDEX idx_products_attributes ON products USING gin (attributes);. Für häufig abgefragte JSONB-Felder, die Bereichsabfragen benötigen, sollten Sie Ausdrucksindizes in Betracht ziehen. Erstellen Sie einen Ausdrucksindex für ein JSONB-Feld, das mit CREATE INDEX idx_products_json_price ON products (((attributes->>'price')::numeric)); als numerisch extrahiert wurde, um Bereichsabfragen für dieses Feld zu aktivieren.

Kombinieren der Vektorsuche mit Metadatenfiltern

PostgreSQL führt Abfragen aus, indem Indexscans mit Filterung kombiniert werden. Das Verständnis von Ausführungsmustern hilft Ihnen beim Schreiben effizienter Abfragen.

Das effizienteste Muster wendet zuerst Metadatenfilter an, wodurch der Satz von Vektoren reduziert wird, die Ähnlichkeitsberechnungen benötigen. PostgreSQL verwendet Metadatenindizes, um Produkte zu identifizieren, die den Filtern entsprechen, und wendet dann nur Vektorähnlichkeit auf diese Kandidaten an. Wenn 5 % der Produkte die Filterkriterien erfüllen, durchsuchen Sie 100.000 Vektoren anstelle von 2 Millionen.

-- Efficient: filter narrows candidates before vector search
SELECT id, name, embedding <=> $1 AS distance
FROM products
WHERE category_id = 5
  AND in_stock = true
  AND price BETWEEN 100 AND 500
ORDER BY embedding <=> $1
LIMIT 10;

Verwenden Sie EXPLAIN ANALYZE, um zu überprüfen, ob Abfragen die erwarteten Indizes verwenden und Leistungsengpässe zu identifizieren. Der Abfrageplan zeigt an, ob PostgreSQL Ihre Metadatenindizes verwendet, um Kandidaten vor der Vektorsuche zu filtern, oder ob es sich um sequenzielle Scans handelt, die jede Zeile untersuchen. Suchen Sie nach Index Scan oder Bitmap Index Scan in Metadatenspalten (effizient), Index Scan mit dem Vektor-Index (effizient) und Seq Scan bei großen Tabellen (potenziell ineffizient). Wenn unerwartete sequenzielle Scans angezeigt werden, überprüfen Sie, ob entsprechende Indizes vorhanden sind und die Statistiken aktuell sind mit ANALYZE products;.

Einige Abfragen eignen sich nicht für eine effiziente Filterung. Wenn Filter nicht viele Zeilen beseitigen (wie WHERE price > 0, das fast alle Produkte übereinstimmen), überspringt PostgreSQL Metadatenindizes möglicherweise vollständig und basiert nur auf dem Vektorindex. Dies wird erwartet, da der Optimierer kostenbasierte Entscheidungen trifft.

Manchmal benötigen Sie Ergebnisse aus dem Vektorindex, die auch Einschränkungen erfüllen, die vorher nicht effizient gefiltert werden können. Das Muster nach der Filterung ruft mehr Vektor-ähnliche Kandidaten ab als erforderlich, und wendet dann Filter an. Passen Sie den inneren GRENZWERT basierend auf der erwarteten Filterauswahl an.

-- Get more candidates than needed, then filter
WITH candidates AS (
    SELECT id, name, price, in_stock, embedding <=> $1 AS distance
    FROM products
    ORDER BY embedding <=> $1
    LIMIT 100
)
SELECT id, name, distance
FROM candidates
WHERE in_stock = true AND price BETWEEN 100 AND 500
ORDER BY distance
LIMIT 10;

Tabellenpartitionierung für große Datasets

Die Partitionierung unterteilt eine große Tabelle in kleinere, verwaltbare Teile. Bei Vektorworkloads kann die Partitionierung die Abfrageleistung verbessern und die Wartung vereinfachen.

Erwägen Sie die Partitionierung, wenn Tabellen Zehnmillionen Zeilen überschreiten, Abfragen sich natürlich nach Partitionsschlüsseln wie Datum, Kategorie oder Mandant filtern, Sie alte Daten effizient entfernen müssen (Partition-Pruning), oder wenn die Indexaufbauzeiten für die vollständige Tabelle unerträglich werden.

Für Anwendungen, die Zeitreiheneinbettungen verarbeiten (z. B. Benutzeraktivitätsvektoren oder inhalte, die im Laufe der Zeit veröffentlicht wurden), ist die Bereichspartitionierung nach Datum wirksam. Abfragen, die nach Datum filtern, durchsuchen nur relevante Partitionen. Jede Partition verfügt über eigene Indizes, wodurch die Wartung besser verwaltbar wird.

-- Create partitioned table
CREATE TABLE user_interactions (
    id BIGSERIAL,
    user_id BIGINT NOT NULL,
    embedding vector(768),
    created_at TIMESTAMP NOT NULL,
    interaction_type TEXT
) PARTITION BY RANGE (created_at);

-- Create partitions for each month
CREATE TABLE user_interactions_2025_01
    PARTITION OF user_interactions
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

Für mehrinstanzenfähige Anwendungen oder Produktkataloge mit natürlichen Kategorien kann die Listen- oder Hashpartitionierung hilfreich sein. Nach Kategorie gefilterte Abfragen scannen nur die relevante Partition, wodurch sowohl die gescannten Daten als auch die Indexgröße reduziert werden.

Erstellen Sie Indizes auf der übergeordneten Tabelle, um mit CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops); automatisch passende Indizes auf allen Partitionen zu erstellen. Jede Partition verfügt über einen eigenen Index, der unabhängig erstellt oder neu erstellt werden kann. Dies ist für große Datasets nützlich, bei denen die Neuerstellung eines einzelnen globalen Indexes Stunden dauern würde.

Die Partitionierung fügt Komplexität hinzu. Abfragen, die viele Partitionen umfassen, sind möglicherweise langsamer als in einer einzelnen Tabelle. Für partitionsübergreifende eindeutige Einschränkungen ist der Partitionsschlüssel in der Einschränkung erforderlich. Anwendungslogik benötigt möglicherweise das Bewusstsein für Partitionsgrenzen. Bewerten Sie, ob Ihre Abfragemuster vor der Implementierung der Partitionierung mit potenziellen Partitionsschlüsseln übereinstimmen.

Weitere Ressourcen