Gegevensindeling optimaliseren

Voltooid

Beslissingen voor gegevensmodellering zijn aanzienlijk van invloed op de prestaties van vectorzoekopdrachten. Hoe u tabellen structureren, gegevenstypen voor metagegevens kiest en ondersteunende indexen maakt, bepaalt of query's efficiënt worden uitgevoerd wanneer uw gegevensset groeit.

Opmerking

Codevoorbeelden in deze eenheid demonstreren schemaontwerppatronen voor vectorgegevens met metagegevens. Pas deze patronen aan uw specifieke gegevensmodel en queryvereisten aan.

Overwegingen voor vectoropslag

Vectorkolommen verbruiken aanzienlijke opslag- en verwerkingsbronnen. Als u de opslagkenmerken begrijpt, kunt u weloverwogen beslissingen nemen over het ontwerp van het schema.

Elke vectordimensie voegt 4 bytes opslagruimte toe (voor float met één precisie) plus vaste overhead. De relatie tussen dimensies en opslag is lineair:

Afmetingen Bytes per vector 1 miljoen vectoren
384 ~1,5 kB ~1,5 GB
768 ~3 kB ongeveer 3 GB
1536 ~6 kB ~6 GB
3072 ~12 kB ~12 GB

Voor een productcatalogus met twee miljoen items die gebruikmaken van 1536-dimensionale insluitingen, vereist de vectorkolom alleen ongeveer 12 GB opslagruimte. Het toevoegen van HNSW-indexen verhoogt dit met ongeveer 50%.

Veel insluitmodellen bieden meerdere dimensieopties. Lagere dimensies verlagen de opslag- en rekenkosten en behouden een redelijke kwaliteit voor veel gebruiksvoorbeelden. Het opgeven van dimensies in de kolomdefinitie biedt validatie. Pogingen om vectoren met verschillende dimensies in te voegen mislukken met een fout, waardoor subtiele bugs door niet-overeenkomende insluitmodellen worden voorkomen. Definieer uw tabel met een expliciete dimensiebeperking met behulp van embedding vector(768) de kolomdefinitie.

Sommige toepassingen hebben vectoren van verschillende modellen nodig. U kunt bijvoorbeeld producttitels, insluitingen van afbeeldingen en gebruikersgedrag afzonderlijk opslaan. Elke vectorkolom heeft een eigen index nodig omdat u geen enkele index kunt maken die meerdere vectorkolommen omvat.

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);

Metagegevensgegevenstypen: gestructureerde kolommen versus JSONB

Productaanbevelingen gebruiken zelden alleen vector-similariteit. Query's filteren doorgaans op categorie, prijsbereik, beschikbaarheid of andere kenmerken voor of naast vectorzoekopdrachten. Hoe u deze metagegevens opslaat, is van invloed op de queryprestaties.

Gestructureerde kolommen maken gebruik van systeemeigen gegevenstypen van PostgreSQL (INTEGER, TIMESTAMP, NUMERIEK, TEKST) met een expliciet schema. Deze kolommen bieden voordelen voor queryprestaties omdat systeemeigen typen efficiënte B-tree-indexen mogelijk maken voor gelijkheids- en bereikquery's, opslagefficiëntie door geoptimaliseerde opslagindelingen, typeveiligheid door invoegtijdvalidatie en queryoptimalisatie via nauwkeurige plannerstatistieken. Gebruik gestructureerde kolommen wanneer kenmerken bekend zijn tijdens het ontwerp, u regelmatig filtert of sorteert op specifieke kenmerken, of queryprestaties zijn essentieel.

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 slaat semi-gestructureerde gegevens op als binaire JSON, wat flexibiliteit biedt voor dynamische kenmerken. JSONB biedt schemaflexubiliteit (verschillende producten kunnen verschillende kenmerken hebben), eenvoudige evolutie (nieuwe kenmerken toevoegen zonder schemamigraties) en geneste structuren (complexe hiërarchische gegevens opslaan). JSONB heeft echter queryoverhead (het extraheren van waarden vereist parseren), indexbeperkingen (GIN-indexen werken voor insluitingsquery's, maar niet voor bereikquery's) en planner-onzekerheid (statistieken zijn minder nauwkeurig voor JSONB-inhoud).

Voor gefilterde vectorzoekopdrachten is de prestaties van het metagegevensfilter rechtstreeks van invloed op de totale querytijd. Met gestructureerde kolommen met B-tree-indexen kan PostgreSQL snel kandidaten beperken voor vectorafstandsberekeningen, terwijl JSONB verschillende querypatronen en indextypen vereist. Een gestructureerd kolomfilter zoals WHERE category_id = 5 AND price BETWEEN 100 AND 500 een B-structuurindex (category_id, price) kan gebruiken om snel kandidaatrijen te identificeren. Een JSONB-filter zoals WHERE attributes @> '{"category": "electronics"}' AND (attributes->>'price')::numeric BETWEEN 100 AND 500 vereist een GIN-index (die niet helpt bij bereikquery's op prijs) of een sequentiële scan van de JSONB-kolom.

Veel toepassingen profiteren van het combineren van gestructureerde kolommen en JSONB: gebruik gestructureerde kolommen voor vaak gefilterde kenmerken waarbij queryprestaties belangrijk zijn, en JSONB voor dynamische of zelden gefilterde kenmerken waar schemaflexantie waardevoller is. Met dit patroon kunt u de algemene case optimaliseren zonder dat dit ten koste gaat van flexibiliteit voor edge-zaken.

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 '{}'
);

Metagegevensindexen voor gefilterde zoekopdrachten

Metagegevensindexen vormen een aanvulling op vectorindexen door de filterfase van query's te versnellen. Zonder de juiste metagegevensindexen moet PostgreSQL mogelijk alle rijen scannen om filters toe te passen vóór vectorzoekopdrachten.

B-tree-indexen maken voor kolommen die worden gebruikt in WHERE-clausules. Indexen met één kolom verwerken exacte overeenkomsten, terwijl samengestelde indexen filtercombinaties verwerken. Samengestelde indexen zijn het meest effectief wanneer query's filteren op de meest linkse kolommen. Een index op (category_id, price) verwerkt WHERE category_id = 5 en WHERE category_id = 5 AND price < 100 efficiënt, maar helpt niet alleen met WHERE price < 100 omdat prijs niet de meest linkse kolom is.

-- 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);

Als de meeste query's filteren op dezelfde voorwaarde (zoals producten op voorraad), vermindert een gedeeltelijke index de indexgrootte en verbetert de prestaties. Deze index is kleiner dan een volledige index en wordt alleen gebruikt voor query's die zijn opgenomen WHERE in_stock = true. Voor een e-commerce aanbevelingsengine waarbij bijna alle query's gericht zijn op beschikbare producten, kan dit de overhead voor indexonderhoud aanzienlijk verminderen. Maak een gedeeltelijke index met CREATE INDEX idx_products_instock_category ON products (category_id) WHERE in_stock = true;.

Als u JSONB gebruikt voor kenmerken, ondersteunen GIN-indexen insluitingsquery's met behulp van de operatoren (bevat) @>, (ingesloten door) <@, (sleutel bestaat) ? en (een/enkele/sleutels bestaan) ?|/?&. Ze versnellen geen bereikdoorzoekingen of willekeurige JSON-padexpressies. Maak een GIN-index met CREATE INDEX idx_products_attributes ON products USING gin (attributes);. Voor vaak opgevraagde JSONB-velden waarvoor bereikquery's nodig zijn, kunt u expressie-indexen overwegen. Maak een expressie-index op een JSONB-veld, dat als numeriek met CREATE INDEX idx_products_json_price ON products (((attributes->>'price')::numeric)); is geëxtraheerd, om bereikzoekopdrachten op dat veld mogelijk te maken.

Vectorzoekopdrachten combineren met metagegevensfilters

PostgreSQL voert query's uit door indexscans te combineren met filteren. Inzicht in uitvoeringspatronen helpt u bij het schrijven van efficiënte query's.

Het meest efficiënte patroon past eerst metagegevensfilters toe, waardoor de set vectoren waarvoor gelijkenisberekeningen nodig zijn, wordt verminderd. PostgreSQL maakt gebruik van metagegevensindexen om producten te identificeren die overeenkomen met de filters en past vervolgens vector-overeenkomsten alleen toe op die kandidaten. Als 5% producten overeenkomen met de filters, zoekt u 100.000 vectoren in plaats van 2 miljoen.

-- 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;

Gebruik EXPLAIN ANALYZE dit om te controleren of query's verwachte indexen gebruiken en prestatieknelpunten identificeren. In het queryplan ziet u of PostgreSQL uw metagegevensindexen gebruikt om kandidaten te filteren vóór vectorzoekopdrachten, of dat het gaat om sequentiële scans die elke rij onderzoeken. Zoek naar indexscan of bitmapindexscan op metagegevenskolommen (efficiënt), Indexscan met behulp van de vectorindex (efficiënt) en Seq Scan op grote tabellen (mogelijk inefficiënt). Als u onverwachte opeenvolgende scans ziet, controleert u of de juiste indexen bestaan en of de statistieken actueel zijn met behulp van ANALYZE products;.

Sommige query's lenen zich niet voor efficiënte filtering. Wanneer filters niet veel rijen elimineren (zoals WHERE price > 0 waar bijna alle producten overeenkomen), kan PostgreSQL metagegevensindexen volledig overslaan en alleen afhankelijk zijn van de vectorindex. Dit is verwacht gedrag omdat de optimizer kostengebaseerde beslissingen neemt.

Soms hebt u resultaten van de vectorindex nodig die ook voldoen aan beperkingen die niet efficiënt vooraf kunnen worden gefilterd. Het patroon na filteren haalt meer vector-vergelijkbare kandidaten op dan nodig is en past vervolgens filters toe. Pas de binnenste LIMIET aan op basis van de verwachte filterselectiviteit.

-- 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;

Tabelpartitionering voor grote gegevenssets

Partitionering verdeelt een grote tabel in kleinere, beter beheerbare stukken. Voor vectorworkloads kan partitioneren de prestaties van query's verbeteren en onderhoud vereenvoudigen.

Overweeg partitionering wanneer tabellen meer dan tientallen miljoenen rijen bevatten, query's natuurlijk worden gefilterd op partitiesleutel (datum, categorie, tenant), moet u efficiënt oude gegevens verwijderen (partition pruning) of index build tijden onhoudbaar worden voor de hele tabel.

Voor toepassingen die tijdreeks-embeddings verwerken (zoals gebruikersactiviteitsvectoren of inhoud die over tijd is gepubliceerd), is datumgebaseerde partitiebereiken effectief. Query's die filteren op datum, scannen alleen relevante partities. Elke partitie heeft zijn eigen indexen, waardoor onderhoud beter beheerbaar wordt.

-- 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');

Voor multitenant-toepassingen of productcatalogussen met natuurlijke categorieën, kan lijst- of hashpartitionering helpen. Query's gefilterd op categorie scannen alleen de relevante partitie, waardoor zowel de gescande gegevens als de indexgrootte worden verminderd.

Maak indexen in de bovenliggende tabel om automatisch overeenkomende indexen op alle partities te maken met behulp van CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops);. Elke partitie heeft een eigen index, die onafhankelijk kan worden gebouwd of herbouwd. Dit is waardevol voor grote gegevenssets waarbij het herbouwen van één globale index uren zou duren.

Partitionering voegt complexiteit toe. Query's die veel partities omvatten, kunnen langzamer zijn dan in één tabel. Unieke beperkingen voor meerdere partities vereisen de partitiesleutel in de beperking. Toepassingslogica heeft mogelijk kennis nodig van partitiegrenzen. Evalueer of uw querypatronen overeenkomen met mogelijke partitiesleutels voordat u partitionering implementeert.

Aanvullende bronnen