<KodLexikon/>
Databasschema och tabellrelationer visualiserade på en whiteboard
← Alla artiklar
sql14 min läsning2026-04-15

PostgreSQL och SQL: Från grunder till produktionsdatabas

Queries, index, JOINs, JSONB och prestandaoptimering. Allt du behöver för att bygga och underhålla en PostgreSQL-databas som klarar produktion.

Bild: Jan Antonin Kolar / Unsplash

Varje webbapplikation behöver en databas. Och om du väljer relationsdatabas i dag finns det ett svar som dominerar bland utvecklare: PostgreSQL. Inte för att det är trendigt, utan för att det är det mest kapabla alternativet som dessutom är helt gratis och open source.

Den här guiden tar dig från grundläggande SQL till produktionsredo PostgreSQL. Fokus ligger på det du faktiskt behöver: queries, index, relationer och de misstag som kostar prestanda i produktion.

Varför PostgreSQL?

MySQL fungerar. SQLite räcker för prototyper. Men PostgreSQL ger dig saker som de andra inte har: JSONB-kolumner för flexibla data, fulltext-sökning utan extern tjänst, CTEs för komplexa queries, och en query planner som faktiskt är intelligent.

Supabase, Neon och Railway kör alla PostgreSQL under huven. Om du bygger med moderna verktyg använder du förmodligen redan Postgres utan att tänka på det.

-- Installation (macOS med Homebrew)
brew install postgresql@17
brew services start postgresql@17

-- Skapa databas och användare
createdb mittproject
psql mittproject

-- Eller via Docker (rekommenderat för team)
docker run -d \
  --name postgres \
  -e POSTGRES_PASSWORD=lokalt123 \
  -e POSTGRES_DB=mittproject \
  -p 5432:5432 \
  postgres:17-alpine

SQL-grunder: Tabeller och relationer

SQL handlar om att beskriva vad du vill ha, inte hur databasen ska hämta det. Det är deklarativt, inte imperativt. Det kräver ett annat tankesätt än JavaScript eller Python, men belönar det med kraftfulla frågor.

-- Skapa en tabell med constraints
CREATE TABLE users (
  id          SERIAL PRIMARY KEY,
  email       VARCHAR(255) UNIQUE NOT NULL,
  name        VARCHAR(100) NOT NULL,
  role        VARCHAR(20) DEFAULT 'user'
              CHECK (role IN ('user', 'admin', 'editor')),
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Skapa en relaterad tabell
CREATE TABLE posts (
  id          SERIAL PRIMARY KEY,
  user_id     INTEGER NOT NULL
              REFERENCES users(id) ON DELETE CASCADE,
  title       VARCHAR(200) NOT NULL,
  content     TEXT,
  published   BOOLEAN DEFAULT false,
  created_at  TIMESTAMPTZ DEFAULT NOW(),
  updated_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Index för vanliga sökningar
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published)
  WHERE published = true;

Notera SERIAL PRIMARY KEY — det skapar en auto-inkrementerande kolumn. I nyare PostgreSQL (10+) kan du använda GENERATED ALWAYS AS IDENTITY istället, men SERIAL fungerar fortfarande överallt.

CRUD: De fyra operationerna

Create, Read, Update, Delete. Varje applikation gör dessa fyra saker med data. SQL gör dem explicita.

-- CREATE: Infoga data
INSERT INTO users (email, name, role)
VALUES ('anna@example.com', 'Anna Lindgren', 'admin')
RETURNING id, email, created_at;

-- RETURNING ger tillbaka den skapade raden — slipp extra SELECT

-- READ: Hämta data
SELECT u.name, u.email, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE u.role = 'admin'
GROUP BY u.id, u.name, u.email
HAVING COUNT(p.id) > 0
ORDER BY post_count DESC;

-- UPDATE: Uppdatera med villkor
UPDATE posts
SET published = true, updated_at = NOW()
WHERE user_id = 1 AND published = false
RETURNING id, title;

-- DELETE: Ta bort med säkerhet
DELETE FROM posts
WHERE id = 42 AND published = false
RETURNING id, title;

JOINs: Koppla ihop tabeller

JOINs är kärnan i relationsdatabaser. De kopplar ihop tabeller via nycklar. Det finns fyra typer du behöver förstå.

-- INNER JOIN: Bara rader som matchar i BÅDA tabeller
SELECT u.name, p.title
FROM users u
INNER JOIN posts p ON p.user_id = u.id;

-- LEFT JOIN: Alla användare, även utan poster
SELECT u.name, p.title
FROM users u
LEFT JOIN posts p ON p.user_id = u.id;

-- Praktiskt exempel: Användare med senaste inlägget
SELECT DISTINCT ON (u.id)
  u.name,
  u.email,
  p.title AS latest_post,
  p.created_at AS posted_at
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
ORDER BY u.id, p.created_at DESC;

-- Subquery: Användare som INTE har publicerat
SELECT name, email
FROM users
WHERE id NOT IN (
  SELECT DISTINCT user_id
  FROM posts
  WHERE published = true
);

Index: Skillnaden mellan 50ms och 5 sekunder

Utan index läser PostgreSQL varje rad i tabellen (sequential scan). Med rätt index hittar den raden direkt via en B-tree-sökning. På en tabell med 10 miljoner rader är det skillnaden mellan en sökning som tar 5 sekunder och en som tar 5 millisekunder.

-- B-tree index (standard, bäst för =, <, >, BETWEEN)
CREATE INDEX idx_users_email ON users(email);

-- Partial index (indexerar bara en delmängd)
CREATE INDEX idx_active_posts ON posts(created_at)
  WHERE published = true;

-- Composite index (flera kolumner)
CREATE INDEX idx_posts_user_published
  ON posts(user_id, published);

-- EXPLAIN ANALYZE: Se hur PostgreSQL kör din query
EXPLAIN ANALYZE
SELECT * FROM posts
WHERE user_id = 1 AND published = true;

-- Resultat visar:
-- Index Scan using idx_posts_user_published
-- rows=12, actual time=0.023..0.031
-- Planning Time: 0.089 ms
-- Execution Time: 0.051 ms

Tumregel: skapa index på kolumner du filtrerar på (WHERE), sorterar på (ORDER BY) och joinar på (JOIN ... ON). Men inte på allt — varje index kostar vid INSERT och UPDATE.

JSONB: Flexibla data i en relationsdatabas

Ibland behöver du flexibla, schemafria data — men vill inte byta till MongoDB. PostgreSQLs JSONB-kolumner ger dig det bästa av båda världar: schemaflexibilitet med SQL-querybarhet.

-- Tabell med JSONB-kolumn
CREATE TABLE events (
  id         SERIAL PRIMARY KEY,
  event_type VARCHAR(50) NOT NULL,
  payload    JSONB NOT NULL DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Infoga JSON-data
INSERT INTO events (event_type, payload)
VALUES ('page_view', '{
  "url": "/produkter/stol-42",
  "user_agent": "Mozilla/5.0",
  "referrer": "https://google.se",
  "duration_ms": 4200
}');

-- Fråga JSON-data
SELECT event_type,
  payload->>'url' AS url,
  (payload->>'duration_ms')::int AS duration
FROM events
WHERE payload->>'referrer' LIKE '%google%'
  AND (payload->>'duration_ms')::int > 3000;

-- GIN-index för snabba JSON-sökningar
CREATE INDEX idx_events_payload ON events
  USING GIN (payload);

CTEs och Window Functions

Common Table Expressions (CTEs) och window functions är det som skiljer "jag kan SQL" från "jag kan SQL på riktigt". De löser problem som annars kräver subqueries eller applikationslogik.

-- CTE: Steg-för-steg-query (läsbar!)
WITH monthly_stats AS (
  SELECT
    DATE_TRUNC('month', created_at) AS month,
    COUNT(*) AS total_posts,
    COUNT(*) FILTER (WHERE published) AS published_posts
  FROM posts
  GROUP BY DATE_TRUNC('month', created_at)
)
SELECT
  month,
  total_posts,
  published_posts,
  ROUND(published_posts::numeric / total_posts * 100) AS publish_rate
FROM monthly_stats
ORDER BY month DESC;

-- Window function: Ranking utan GROUP BY
SELECT
  u.name,
  p.title,
  p.created_at,
  ROW_NUMBER() OVER (
    PARTITION BY u.id
    ORDER BY p.created_at DESC
  ) AS post_rank
FROM users u
JOIN posts p ON p.user_id = u.id;

-- Running total med window function
SELECT
  created_at::date AS day,
  COUNT(*) AS daily_posts,
  SUM(COUNT(*)) OVER (ORDER BY created_at::date) AS running_total
FROM posts
GROUP BY created_at::date
ORDER BY day;

Migrations: Versionera ditt schema

Att köra CREATE TABLE manuellt i produktion är som att deploya genom att FTP:a filer. Det fungerar tills det inte gör det. Migrations versionerar ditt databasschema precis som git versionerar din kod.

-- Med Prisma (Node.js/TypeScript)
npx prisma migrate dev --name add-posts-table

-- Med Drizzle (lättare alternativ)
npx drizzle-kit generate
npx drizzle-kit migrate

-- Med raw SQL-filer (t.ex. golang-migrate)
-- migrations/001_create_users.up.sql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  name VARCHAR(100) NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- migrations/001_create_users.down.sql
DROP TABLE users;

Oavsett verktyg: varje migration ska vara idempotent, ha en rollback, och köras i en transaktion.

Prestandaoptimering i produktion

De flesta prestandaproblem i PostgreSQL beror på tre saker: saknade index, N+1-queries och för breda SELECT. Här är checklistnau.

-- 1. Hitta långsamma queries
-- Aktivera pg_stat_statements (postgresql.conf)
shared_preload_libraries = 'pg_stat_statements'

-- Topp 10 långsammaste queries
SELECT query,
  calls,
  ROUND(mean_exec_time::numeric, 2) AS avg_ms,
  ROUND(total_exec_time::numeric, 2) AS total_ms
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- 2. Hitta saknade index
SELECT schemaname, tablename, seq_scan, idx_scan,
  seq_scan - idx_scan AS too_many_seqscans
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan
  AND seq_scan > 100
ORDER BY too_many_seqscans DESC;

-- 3. Undvik SELECT *
-- DÅLIGT: Hämtar alla kolumner
SELECT * FROM posts WHERE published = true;

-- BRA: Hämta bara det du behöver
SELECT id, title, created_at FROM posts
WHERE published = true;

-- 4. Connection pooling (viktigt i serverless)
-- PgBouncer eller Supabase connection pooler
-- Utan pooling: varje request = ny connection = ~50ms overhead

PostgreSQL med Node.js/TypeScript

I praktiken skriver de flesta utvecklare inte raw SQL i sin applikation. Du väljer en nivå av abstraktion som passar ditt projekt.

// Nivå 1: Raw SQL med 'pg' (mest kontroll)
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

const result = await pool.query(
  'SELECT id, name FROM users WHERE role = $1',
  ['admin']
);
// result.rows = [{ id: 1, name: 'Anna' }]

// VIKTIGT: Använd ALLTID parameteriserade queries ($1, $2)
// ALDRIG string concatenation — det öppnar för SQL injection

// Nivå 2: Query builder med Drizzle
import { drizzle } from 'drizzle-orm/node-postgres';
import { users } from './schema';

const db = drizzle(pool);
const admins = await db
  .select({ id: users.id, name: users.name })
  .from(users)
  .where(eq(users.role, 'admin'));

// Nivå 3: Full ORM med Prisma
const admins = await prisma.user.findMany({
  where: { role: 'admin' },
  select: { id: true, name: true }
});

Backup och säkerhet

En databas utan backup-strategi är en tidsinställd bomb. PostgreSQL har inbyggda verktyg för detta.

# Logisk backup (mest portabel)
pg_dump mittproject > backup_2026-04-15.sql

# Komprimerad backup
pg_dump -Fc mittproject > backup.dump

# Återställ
pg_restore -d mittproject backup.dump

# Automatisera med cron
# crontab -e
0 3 * * * pg_dump -Fc mittproject > /backups/db_$(date +\%Y\%m\%d).dump

# Säkerhet: Begränsa behörigheter
CREATE ROLE app_user WITH LOGIN PASSWORD 'starkt-losenord';
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO app_user;
-- ALDRIG ge DELETE till applikationsanvändaren om det inte behövs
REVOKE DELETE ON ALL TABLES IN SCHEMA public FROM app_user;

Summering

PostgreSQL belönar dig för att förstå dina data. Lär dig skriva effektiva queries, använd index medvetet, och versionera ditt schema med migrations. Börja med raw SQL för att förstå vad som händer, sedan välj en abstraktion (Drizzle eller Prisma) som passar ditt projekts storlek.

Undvik de vanligaste misstagen: SELECT * i produktion, saknade index på foreign keys, och framför allt — string concatenation i queries. Parameteriserade queries är inte valfritt, det är grundläggande säkerhet.

Vill du köra PostgreSQL i en isolerad miljö? Läs vår Docker-guide för utvecklare. Använder du TypeScript? Se hur du väljer mellan query builders och ORMs i TypeScript-guiden. Och för att automatisera migrations i din deploy-pipeline, kolla in CI/CD pipeline-guiden.