Profilert alle kritiske PG-spørringer med EXPLAIN ANALYZE. Identifiserte at recompute_access brukte single-column index (idx_edges_type) med lav selektivitet, og RLS-policyer manglet composite indexes for effektive oppslag. Endringer: Migrasjon 017_query_performance.sql: - 6 nye composite indexes: - idx_edges_target_type (target_id, edge_type) — recompute_access + belongs_to - idx_edges_source_type (source_id, edge_type) — alias-oppslag - idx_edges_target_memberof (partial, member_of) — team-propagering - idx_nodes_created_at_desc — ORDER BY created_at DESC - idx_nodes_kind_created — filtrer på kind + sorter - idx_na_subject_covering INCLUDE (object_id) — RLS without heap lookup - Optimalisert recompute_access(): steg 3 og 4 kjøres nå bare når det er relevant (EXISTS-sjekk først). For vanlige brukere (ikke team) unngår dette to fulle INSERT-SELECT-operasjoner. - via_edge oppdateres nå korrekt ved access-nivå-endring. Slow query logging (maskinrommet): - Forespørsler >200ms logges som WARN med tag slow_request - PG-spørringer >100ms logges som WARN med tag slow_query - recompute_access-kall logges med varighet for overvåking - Nytt pg_stats-felt i /metrics med tabell- og index-statistikk, cache hit ratio, og node_access-telling Dokumentasjon oppdatert i docs/infra/observerbarhet.md.
170 lines
7.4 KiB
PL/PgSQL
170 lines
7.4 KiB
PL/PgSQL
-- 017_query_performance.sql
|
|
-- Ytelsesoptimalisering: composite indexes og forbedret recompute_access.
|
|
--
|
|
-- Profileringsresultater viste at:
|
|
-- 1. recompute_access step 2 bruker idx_edges_type (lav selektivitet) og
|
|
-- filtrerer på target_id etterpå — trenger composite index.
|
|
-- 2. RLS edge_select policy gjør IN-sjekk mot node_access for source_id
|
|
-- og target_id — composite index på edges (target_id, edge_type) hjelper
|
|
-- joinmønsteret i recompute_access og belongs_to-oppslag.
|
|
-- 3. Alias-oppslag (source_id + edge_type + system) mangler composite index.
|
|
-- 4. nodes-oppslag sortert på created_at DESC mangler index.
|
|
-- 5. recompute_access kjører steg 3 og 4 uansett, selv om de sjelden
|
|
-- treffer — optimaliser med betingede sjekker.
|
|
--
|
|
-- Ref: oppgave 12.4, docs/retninger/datalaget.md
|
|
|
|
BEGIN;
|
|
|
|
-- =============================================================================
|
|
-- Composite indexes på edges
|
|
-- =============================================================================
|
|
|
|
-- Dekker recompute_access steg 2 (belongs_to-oppslag mot target)
|
|
-- og generelle "hent barn av node"-spørringer (query_board, etc.)
|
|
CREATE INDEX IF NOT EXISTS idx_edges_target_type
|
|
ON edges (target_id, edge_type);
|
|
|
|
-- Dekker alias-oppslag og "finn utgående edges av type"-spørringer
|
|
CREATE INDEX IF NOT EXISTS idx_edges_source_type
|
|
ON edges (source_id, edge_type);
|
|
|
|
-- Dekker member_of-oppslag i recompute_access steg 3 (covering index)
|
|
-- Inkluderer source_id for å unngå heap-oppslag
|
|
CREATE INDEX IF NOT EXISTS idx_edges_target_memberof
|
|
ON edges (target_id)
|
|
WHERE edge_type = 'member_of';
|
|
|
|
-- =============================================================================
|
|
-- Composite index på nodes for sortering
|
|
-- =============================================================================
|
|
|
|
-- query_nodes bruker ORDER BY created_at DESC med LIMIT/OFFSET.
|
|
-- Med RLS kreves sekvensielt scan, men for ikke-RLS-spørringer
|
|
-- (superuser context) gir dette stor gevinst.
|
|
CREATE INDEX IF NOT EXISTS idx_nodes_created_at_desc
|
|
ON nodes (created_at DESC);
|
|
|
|
-- Dekker vanlig mønster: filtrer på node_kind, sorter på created_at
|
|
CREATE INDEX IF NOT EXISTS idx_nodes_kind_created
|
|
ON nodes (node_kind, created_at DESC);
|
|
|
|
-- =============================================================================
|
|
-- Covering index på node_access for RLS
|
|
-- =============================================================================
|
|
|
|
-- RLS-policyen gjør: WHERE subject_id = current_node_id()
|
|
-- og returnerer object_id. Covering index unngår heap-oppslag.
|
|
-- Erstatter ikke idx_na_subject som brukes av PK, men PostgreSQL
|
|
-- velger denne fordi den dekker hele spørringen.
|
|
CREATE INDEX IF NOT EXISTS idx_na_subject_covering
|
|
ON node_access (subject_id) INCLUDE (object_id);
|
|
|
|
-- =============================================================================
|
|
-- Optimalisert recompute_access
|
|
-- =============================================================================
|
|
|
|
-- Hovedforbedring: steg 3 (team→medlemmer) og steg 4 (arv fra team)
|
|
-- kjøres nå bare når det er relevant. Steg 3 sjekker om subject faktisk
|
|
-- er et team (har member_of-edges mot seg). Steg 4 sjekker om root_node
|
|
-- faktisk har egne node_access-rader (= er et team/entitet med tilgang).
|
|
-- For den vanlige casen (bruker→samling owner/admin) gjør dette at
|
|
-- steg 3 og 4 er billige EXISTS-sjekker i stedet for fulle INSERT-selects.
|
|
|
|
CREATE OR REPLACE FUNCTION recompute_access(
|
|
p_subject_id UUID,
|
|
p_root_node_id UUID,
|
|
p_access access_level,
|
|
p_via_edge UUID
|
|
) RETURNS void AS $$
|
|
BEGIN
|
|
-- Steg 1: Direkte tilgang til roten
|
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
|
VALUES (p_subject_id, p_root_node_id, p_access, p_via_edge)
|
|
ON CONFLICT (subject_id, object_id)
|
|
DO UPDATE SET access = GREATEST(node_access.access, p_access),
|
|
via_edge = CASE
|
|
WHEN p_access > node_access.access THEN p_via_edge
|
|
ELSE node_access.via_edge
|
|
END;
|
|
|
|
-- Steg 2: Transitiv: noder som tilhører roten (belongs_to)
|
|
-- Bruker idx_edges_target_type (target_id, edge_type) for rask oppslag
|
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
|
SELECT p_subject_id, e.source_id, p_access, p_via_edge
|
|
FROM edges e
|
|
WHERE e.target_id = p_root_node_id
|
|
AND e.edge_type = 'belongs_to'
|
|
ON CONFLICT (subject_id, object_id)
|
|
DO UPDATE SET access = GREATEST(node_access.access, p_access),
|
|
via_edge = CASE
|
|
WHEN p_access > node_access.access THEN p_via_edge
|
|
ELSE node_access.via_edge
|
|
END;
|
|
|
|
-- Steg 3: Hvis subject er et team: propager til alle teammedlemmer.
|
|
-- Sjekk først om det finnes member_of-edges (= subject er et team).
|
|
-- For vanlige brukere er dette en billig EXISTS som returnerer false.
|
|
IF EXISTS (
|
|
SELECT 1 FROM edges
|
|
WHERE target_id = p_subject_id AND edge_type = 'member_of'
|
|
LIMIT 1
|
|
) THEN
|
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
|
SELECT e.source_id, na.object_id, na.access, na.via_edge
|
|
FROM node_access na
|
|
JOIN edges e ON e.target_id = p_subject_id
|
|
AND e.edge_type = 'member_of'
|
|
WHERE na.subject_id = p_subject_id
|
|
ON CONFLICT (subject_id, object_id)
|
|
DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access),
|
|
via_edge = CASE
|
|
WHEN EXCLUDED.access > node_access.access THEN EXCLUDED.via_edge
|
|
ELSE node_access.via_edge
|
|
END;
|
|
END IF;
|
|
|
|
-- Steg 4: Team-transitivitet — arv tilgang fra teamet.
|
|
-- Bare relevant når subject meldes inn i et team (root_node er team).
|
|
-- Sjekk om root_node har egne node_access-rader som subject.
|
|
IF EXISTS (
|
|
SELECT 1 FROM node_access
|
|
WHERE subject_id = p_root_node_id
|
|
LIMIT 1
|
|
) THEN
|
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
|
SELECT p_subject_id, na.object_id, na.access, na.via_edge
|
|
FROM node_access na
|
|
WHERE na.subject_id = p_root_node_id
|
|
ON CONFLICT (subject_id, object_id)
|
|
DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access),
|
|
via_edge = CASE
|
|
WHEN EXCLUDED.access > node_access.access THEN EXCLUDED.via_edge
|
|
ELSE node_access.via_edge
|
|
END;
|
|
END IF;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- =============================================================================
|
|
-- pg_stat_statements (krever manuell konfigurasjon)
|
|
-- =============================================================================
|
|
|
|
-- pg_stat_statements krever shared_preload_libraries i postgresql.conf.
|
|
-- For å aktivere:
|
|
-- 1. Legg til i docker-compose.yml for postgres:
|
|
-- command: postgres -c shared_preload_libraries=pg_stat_statements
|
|
-- -c pg_stat_statements.track=all
|
|
-- 2. Restart postgres-containeren
|
|
-- 3. Kjør: CREATE EXTENSION pg_stat_statements;
|
|
-- 4. Se topp-spørringer: SELECT * FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
|
|
|
|
-- =============================================================================
|
|
-- Oppdater statistikk for query planner
|
|
-- =============================================================================
|
|
|
|
ANALYZE nodes;
|
|
ANALYZE edges;
|
|
ANALYZE node_access;
|
|
|
|
COMMIT;
|