| name | postgresql-review |
| description | PostgreSQL query review, optimalisering og beste praksis for Nav-applikasjoner |
| license | MIT |
| compatibility | PostgreSQL database |
| metadata | {"domain":"backend","tags":"postgresql sql optimization review indexing"} |
PostgreSQL Review Skill
Review and optimize PostgreSQL queries, schemas, and patterns for Nav applications. Covers EXPLAIN analysis, index strategies, JSONB patterns, and common anti-patterns.
Query Analysis
Run EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) to analyze queries:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM vedtak
WHERE bruker_id = '12345678901'
AND status = 'aktiv'
ORDER BY opprettet_dato DESC
LIMIT 10;
Red Flags in EXPLAIN Output
| Sign | Problem | Solution |
|---|
Seq Scan on large table | Missing index | CREATE INDEX |
Sort with external merge | Not enough work_mem | Increase work_mem or add index with correct sort order |
Nested Loop with high rows | Cartesian product / missing join index | Add index on join column |
Hash Join with Batches > 1 | work_mem too low | Increase work_mem for the session |
Large difference between estimated and actual rows | Outdated statistics | ANALYZE tablename; |
Index Strategies
CREATE INDEX idx_vedtak_bruker_id ON vedtak(bruker_id);
CREATE INDEX idx_vedtak_bruker_status ON vedtak(bruker_id, status);
CREATE INDEX idx_vedtak_aktive ON vedtak(bruker_id)
WHERE status = 'aktiv';
CREATE INDEX idx_vedtak_covering ON vedtak(bruker_id, status)
INCLUDE (opprettet_dato, belop);
CREATE INDEX CONCURRENTLY idx_vedtak_dato ON vedtak(opprettet_dato);
When to Use What?
| Scenario | Index Type |
|---|
WHERE a = x | B-tree on a |
WHERE a = x AND b = y | Composite (a, b) |
WHERE a = x AND status = 'aktiv' | Partial index WHERE status = 'aktiv' |
WHERE a LIKE 'prefix%' | B-tree (prefix only) |
WHERE a @> '{"key": "val"}' | GIN on JSONB |
| Full-text search | GIN with to_tsvector |
| Geography | GiST |
JSONB Patterns
CREATE INDEX idx_metadata_gin ON hendelser USING GIN (metadata);
SELECT * FROM hendelser
WHERE metadata @> '{"type": "vedtak", "tema": "dagpenger"}';
SELECT
id,
metadata->>'type' AS type,
metadata->'detaljer'->>'belop' AS belop
FROM hendelser;
SELECT * FROM hendelser
WHERE (metadata->>'opprettet')::timestamp > NOW() - INTERVAL '7 days';
CREATE INDEX idx_metadata_opprettet ON hendelser (((metadata->>'opprettet')::timestamp));
Common Table Expressions (CTEs)
WITH aktive_vedtak AS (
SELECT bruker_id, COUNT(*) AS antall
FROM vedtak
WHERE status = 'aktiv'
GROUP BY bruker_id
),
siste_aktivitet AS (
SELECT bruker_id, MAX(opprettet_dato) AS sist_aktiv
FROM aktivitetslogg
GROUP BY bruker_id
)
SELECT
av.bruker_id,
av.antall,
sa.sist_aktiv
FROM aktive_vedtak av
JOIN siste_aktivitet sa USING (bruker_id)
WHERE av.antall > 1;
Window Functions
SELECT
bruker_id,
vedtak_id,
opprettet_dato,
ROW_NUMBER() OVER (PARTITION BY bruker_id ORDER BY opprettet_dato DESC) AS rn
FROM vedtak
WHERE rn = 1;
SELECT
dato,
antall,
SUM(antall) OVER (ORDER BY dato) AS kumulativt
FROM daglig_statistikk;
Anti-patterns
N+1 Queries
val brukere = repository.findAll()
brukere.forEach { bruker ->
val vedtak = vedtakRepository.findByBrukerId(bruker.id)
}
val brukereOgVedtak = repository.findAllWithVedtak()
SELECT *
SELECT * FROM dokument WHERE bruker_id = '12345';
SELECT id, tittel, opprettet_dato FROM dokument WHERE bruker_id = '12345';
Missing LIMIT on Unbounded Data
SELECT * FROM hendelse WHERE type = 'innlogging';
SELECT * FROM hendelse WHERE type = 'innlogging'
ORDER BY opprettet_dato DESC
LIMIT 100;
Connection Pooling
HikariDataSource().apply {
jdbcUrl = System.getenv("DB_JDBC_URL")
?: "jdbc:postgresql://${System.getenv("DB_HOST")}:5432/${System.getenv("DB_DATABASE")}"
username = System.getenv("DB_USERNAME")
password = System.getenv("DB_PASSWORD")
maximumPoolSize = 5
minimumIdle = 1
connectionTimeout = 10_000
idleTimeout = 300_000
maxLifetime = 600_000
validationTimeout = 5_000
}
Migration Strategies for Large Tables
ALTER TABLE stor_tabell ADD COLUMN ny_kolonne BOOLEAN DEFAULT false;
CREATE INDEX CONCURRENTLY idx_ny ON stor_tabell(ny_kolonne);
UPDATE stor_tabell SET ny_kolonne = true WHERE id BETWEEN $1 AND $2;
Checklist