synops/migrations/017_query_performance.sql
vegard b31ee59868 Ytelse: profiler PG-spørringer, optimaliser node_access-oppdatering (oppgave 12.4)
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.
2026-03-18 11:43:19 +00:00

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;