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