Indekser i SQL Server: Clustered, nonclustered og når du trenger dem

Indekser er det enkeltgrepet som oftest skiller en spørring på 20 millisekunder fra en på 20 sekunder. Samtidig er feil indekser – eller altfor mange – en vanlig årsak til trege oppdateringer og store databaser. Denne guiden forklarer hvordan indekser i SQL Server er bygget opp, forskjellen på clustered og nonclustered, hvordan du lager indekser som faktisk blir brukt, og hvordan du finner indekser som mangler eller aldri brukes.

Kort fortalt
  • En tabell har høyst én clustered indeks, som er tabellen sortert etter nøkkelen.
  • Nonclustered indekser er separate strukturer som peker til radene – de gjør lesing raskere og skriving tregere.
  • Rekkefølgen på kolonnene i en sammensatt indeks er avgjørende. Bruk INCLUDE for å dekke spørringen.
  • Indeksen hjelper bare hvis spørringen er SARGable – ingen funksjoner rundt kolonnen i WHERE.
  • Ikke lag alle indekser SQL Server foreslår. Vurder dem, slå dem sammen og fjern de som ikke brukes.

Hvordan en indeks fungerer

En indeks i SQL Server er et B-tre (balansert tre). Øverst ligger en rotside, i midten mellomnivåer, og nederst bladnivået med selve dataene eller pekere til dem. For å finne en rad leser SQL Server fra roten og ned gjennom treet – typisk 3–4 sider selv i en tabell med hundrevis av millioner rader. Uten en passende indeks må hele tabellen leses (table scan eller clustered index scan).

Tenk på telefonkatalogen: den er sortert på etternavn og fornavn. Å finne «Hansen, Kari» går raskt. Å finne alle med telefonnummer som begynner på 22 krever at du leser hele katalogen – katalogen har ingen «indeks» på telefonnummer.

Heap og clustered indeks

En tabell uten clustered indeks kalles en heap: radene ligger i den rekkefølgen de havnet. En clustered indeks bestemmer den fysiske (logiske) rekkefølgen på radene – bladnivået i indeksen er tabellens data. Derfor kan en tabell bare ha én.

Primærnøkkelen blir som standard clustered indeks, men det er ikke noe krav. En god clustered nøkkel er:

  • Smal: Den kopieres inn i alle nonclustered indekser som peker til raden. En int eller bigint er ideell.
  • Unik: Ellers legger SQL Server til en skjult «uniquifier».
  • Stabil: Endres verdien, må raden flyttes og alle nonclustered indekser oppdateres.
  • Stigende: Nye rader havner sist, og du unngår sidesplitting midt i tabellen. Tilfeldige uniqueidentifier-verdier (NEWID()) gir mye fragmentering; NEWSEQUENTIALID() eller en identitetskolonne er bedre.

Heaper har sin plass for tabeller som bare lastes og leses i sin helhet, men for vanlige OLTP-tabeller bør du ha en clustered indeks.

Nonclustered indekser

En nonclustered indeks er en egen B-trestruktur med kopi av de valgte kolonnene, sortert etter indeksnøkkelen, og en peker til raden (den clustered nøkkelen, eller en radidentifikator i en heap). En tabell kan ha mange, men hver av dem må oppdateres ved hver INSERT, DELETE og UPDATE av indekserte kolonner.

Key lookup og dekkende indekser

Når en indeks finner radene men ikke inneholder alle kolonnene spørringen trenger, må SQL Server slå opp hver rad i den clustered indeksen – en key lookup. For noen få rader er det greit; for tusenvis blir det dyrt, og SQL Server velger da ofte å lese hele tabellen i stedet. Løsningen er en dekkende indeks med INCLUDE:

-- Spørringen
SELECT OrdreId, OrdreDato, Totalbeløp
FROM dbo.Ordre
WHERE KundeId = 4711
ORDER BY OrdreDato DESC;

-- Indeksen: KundeId for søket, OrdreDato for sorteringen, Totalbeløp tas med på bladnivået
CREATE NONCLUSTERED INDEX IX_Ordre_KundeId_OrdreDato
ON dbo.Ordre (KundeId, OrdreDato DESC)
INCLUDE (Totalbeløp);

OrdreId trenger ikke tas med hvis det er den clustered nøkkelen – den finnes allerede i alle nonclustered indekser.

Kolonnerekkefølgen betyr alt

En sammensatt indeks er sortert på første kolonne, deretter andre, og så videre – akkurat som telefonkatalogen. Indeksen over hjelper derfor spørringer som filtrerer på KundeId, eller på KundeId og OrdreDato, men ikke spørringer som bare filtrerer på OrdreDato. Tommelfingerregler:

  • Kolonner som sammenlignes med likhet (=) først, deretter kolonner med intervaller (<, >, BETWEEN).
  • Kolonner som brukes i ORDER BY eller GROUP BY kan legges etter filterkolonnene for å unngå sortering.
  • Kolonner som bare skal hentes ut, legges i INCLUDE, ikke i nøkkelen.

Andre indekstyper

Unik indeks

Håndhever at verdiene er unike, for eksempel e-postadresse eller organisasjonsnummer. Gir også optimalisereren nyttig informasjon.

Filtrert indeks

Indekserer bare et utvalg rader, for eksempel WHERE Status = 'Åpen' når bare noen få prosent av ordrene er åpne. Mindre og billigere å vedlikeholde. Krever at spørringen har et filter som tydelig matcher.

Columnstore-indeks

Lagrer data kolonnevis og komprimert. Ekstremt effektiv for analyse og aggregering over millioner av rader, og grunnlaget for batch mode-kjøring. Kan være clustered (hele tabellen) eller nonclustered (i tillegg til en vanlig radbasert tabell).

SARGable spørringer: når indeksen ikke blir brukt

En betingelse er SARGable (Search ARGument able) når SQL Server kan bruke den til å søke direkte i indeksen. Det typiske problemet er funksjoner eller beregninger rundt kolonnen:

Ikke SARGableSARGable
WHERE YEAR(OrdreDato) = 2026WHERE OrdreDato >= '2026-01-01' AND OrdreDato < '2027-01-01'
WHERE LEFT(Navn, 3) = 'Han'WHERE Navn LIKE 'Han%'
WHERE Navn LIKE '%sen'Ingen enkel løsning – vurder fulltekstindeks
WHERE Pris * 1.25 > 100WHERE Pris > 100 / 1.25
WHERE ISNULL(Status, 'Ny') = 'Ny'WHERE Status = 'Ny' OR Status IS NULL

Implisitt konvertering

En snikende variant er ulike datatyper: Er kolonnen varchar og parameteren nvarchar, må SQL Server konvertere hver verdi i kolonnen før den kan sammenligne, og indeksen kan ikke brukes til å søke. Dette skjer ofte med ORM-er og ADO.NET når tekstparametere sendes som nvarchar uten at typen er angitt. Se etter advarselen CONVERT_IMPLICIT i kjøreplanen, og la datatypene i koden matche databasen.

Prisen for indekser

Hver nonclustered indeks gjør lesing raskere, men:

  • Hver INSERT og DELETE må oppdatere alle indeksene på tabellen.
  • Indeksene tar plass på disk, i minnet og i backupen.
  • Vedlikehold (statistikk og eventuelt ombygging) tar lengre tid.

For en tabell med mye skriving er 5–10 nonclustered indekser ofte et fornuftig tak. Har du 30, er det sannsynligvis overlappende og ubrukte indekser i blant.

Finn manglende indekser

SQL Server registrerer indekser optimalisereren skulle ønske fantes. Listen er et godt utgangspunkt, men ikke en fasit: forslagene overlapper hverandre, tar ikke hensyn til kolonnerekkefølge for sortering, og vet ingenting om kostnaden ved skriving.

SELECT TOP (20)
       DB_NAME(d.database_id)        AS DatabaseNavn,
       d.statement                   AS Tabell,
       d.equality_columns,
       d.inequality_columns,
       d.included_columns,
       s.user_seeks,
       s.avg_user_impact             AS EstimertForbedringProsent
FROM sys.dm_db_missing_index_details AS d
JOIN sys.dm_db_missing_index_groups AS g ON g.index_handle = d.index_handle
JOIN sys.dm_db_missing_index_group_stats AS s ON s.group_handle = g.index_group_handle
ORDER BY s.user_seeks * s.avg_user_impact DESC;

Statistikken nullstilles når SQL Server startes på nytt, så se på den etter en periode med normal drift.

Finn indekser som ikke brukes

SELECT OBJECT_NAME(i.object_id) AS Tabell,
       i.name                   AS Indeks,
       ISNULL(u.user_seeks + u.user_scans + u.user_lookups, 0) AS Lesinger,
       ISNULL(u.user_updates, 0)                               AS Oppdateringer
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS u
       ON u.object_id = i.object_id AND u.index_id = i.index_id AND u.database_id = DB_ID()
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
  AND i.type_desc = 'NONCLUSTERED'
  AND i.is_primary_key = 0
  AND i.is_unique_constraint = 0
ORDER BY Lesinger, Oppdateringer DESC;

En indeks med null lesinger og mange oppdateringer koster bare. Men vær forsiktig: statistikken nullstilles ved omstart, og en indeks kan være kritisk for en månedlig rapport. Deaktiver gjerne indeksen (ALTER INDEX ... DISABLE) en periode før du sletter den.

Fragmentering

Når rader settes inn midt i en full side, splittes siden, og indeksen blir fragmentert: sidene ligger ikke lenger i rekkefølge, og de er bare delvis fylt. På spinnende disker var dette et stort ytelsesproblem. På SSD og i SAN-lagring betyr rekkefølgen mye mindre, men lav sidetetthet (halvfulle sider) betyr fortsatt at mer må leses og mer minne brukes. Vedlikehold av indekser og statistikk er beskrevet i guiden om vedlikehold.

Vanlige spørsmål

Bør jeg indeksere alle fremmednøkler?

Som hovedregel ja. SQL Server lager ikke indeks på fremmednøkler automatisk. Uten indeks må hele den refererende tabellen leses når en rad i foreldretabellen slettes eller oppdateres, og join-er blir dyrere.

Hva er forskjellen på en primærnøkkel og en clustered indeks?

Primærnøkkelen er en logisk regel om at verdien er unik og ikke NULL. Den clustered indeksen er en fysisk lagringsstruktur. De er ofte den samme kolonnen, men trenger ikke være det.

Hvorfor bruker ikke SQL Server indeksen min?

De vanligste grunnene er at spørringen ikke er SARGable, at indeksen ikke dekker spørringen og at oppslagene blir for mange, at kolonnerekkefølgen ikke passer, eller at statistikken er utdatert slik at optimalisereren tror det er færre eller flere rader enn det faktisk er. Se guiden om trege spørringer.

Les også