SQL Server, de la zero Lecția 24 / 40

Fragmentare, rebuilds, reorgs: mitul celor 30%

Ce a spus de fapt Paul Randal, de ce planul implicit de mentenanță e greșit pe SSD-uri și cum construiești o strategie sensibilă de mentenanță în 2026.

„Rebuild la indexurile cu fragmentare peste 30%. Reorganize între 5% și 30%. Restul, lăsați-le în pace.”

Sfatul ăsta apare într-un milion de tutoriale, în documentații vechi de la Microsoft și în planurile de mentenanță ale celor mai multe firme. Provine dintr-un singur exemplu pe care Paul Randal (omul care literalmente a scris storage engine-ul SQL Server) l-a dat pe un forum în 2008. De atunci a petrecut 17 ani spunând „îmi pare rău, nu mă mai citați pe asta, am inventat numerele ca să ilustrez o idee.”

Astăzi vorbim despre ce contează cu adevărat pentru mentenanța indexurilor în 2026, pe storage modern SSD, cu RCSI activat. Mai puțin decât ți s-a spus și mai simplu decât vechile planuri de mentenanță.

Ce este, de fapt, fragmentarea

Două feluri, denumite confuz și asemănător:

  • Fragmentare externă (logică) — ordinea fizică a paginilor leaf nu se potrivește cu cea logică. Un range scan sare prin disc.
  • Fragmentare internă — paginile leaf nu sunt pline. Pagini pe jumătate goale irosesc RAM și disc.

sys.dm_db_index_physical_stats raportează ambele:

SELECT
    OBJECT_NAME(ips.object_id)                     AS table_name,
    i.name                                          AS index_name,
    ips.index_type_desc,
    ips.avg_fragmentation_in_percent               AS logical_frag_pct,
    ips.avg_page_space_used_in_percent             AS page_fill_pct,
    ips.page_count,
    ips.record_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') AS ips
JOIN sys.indexes AS i
    ON i.object_id = ips.object_id
   AND i.index_id  = ips.index_id
WHERE ips.page_count > 1000
ORDER BY ips.avg_fragmentation_in_percent DESC;

Modul 'SAMPLED' e rapid. 'DETAILED' e amănunțit, dar poate dura ore pe tabele uriașe. Începe cu sampled.

De ce sfatul vechi era pentru spinning rust

„Rebuild peste 30%” avea sens când:

  • Storage-ul era pe discuri rotative. I/O-ul random era de 10× mai lent decât cel secvențial.
  • Range scan-urile erau pattern-ul dominant de acces.
  • Serverele aveau 32GB de RAM și nu puteau ține multe date în cache.
  • SQL Server nu putea face batch-mode sau scan-uri paralele moderne.

În 2026:

  • Cele mai multe SQL Servere de producție rulează pe SSD sau NVMe. I/O-ul random e aproape la fel de rapid ca cel secvențial.
  • Memoria e ieftină. Cele mai multe date hot stau în RAM (buffer pool). Fragmentarea paginilor pe disc e irelevantă când pagina e în cache.
  • Workload-urile sunt diverse — seek-urile pe indexuri înguste domină OLTP-ul, iar pattern-ul de covering index înseamnă mai puține range scan-uri mari.
  • Columnstore și batch mode au pattern-uri de stocare complet separate.

Concluzia: fragmentarea contează mai puțin decât conta în 2008. Nu e gratis — rebuild-ul paginilor defragmentate ajută în continuare la scan-uri mari și umple paginile mai bine — dar nu mai e prioritatea numărul unu.

Ce să faci, concret

Sfat pragmatic pentru un workload de mărimea Runehold în 2026:

  1. Nu rula mentenanță în fiecare noapte. Săptămânal e de obicei suficient; unele firme merg pe lunar.
  2. Folosește scripturile lui Ola Hallengren, nu maintenance plan-ul integrat. Le acoperim în lecția 35.
  3. Sari peste orice are sub ~1.000 de pagini. Mentenanța pe indexuri mici e overhead pur.
  4. Preferă REORGANIZE ca default pentru fragmentare moderată. Online, impact mic, nu ia lock-uri care blochează query-urile.
  5. Folosește REBUILD WITH (ONLINE = ON) (doar Enterprise) pentru fragmentare grea pe tabele hot.
  6. Pe Standard Edition, REBUILD ia un schema lock — fă-l într-o fereastră de mentenanță.
  7. Rulează UPDATE STATISTICS ... WITH FULLSCAN mai des decât crezi. Drift-ul de statistici cauzează mai multe regresii de query decât fragmentarea.

Rebuild vs reorganize

Comparație rapidă:

AspectREBUILDREORGANIZE
Reconstruiește paginile leafDaDa
Reconstruiește paginile intermediareDaNu
Actualizează statisticileDa (full scan)Nu
Online (Enterprise)Da cu ONLINE = ONMereu online
Online (Standard)Nu (schema lock)Mereu online
Memorie necesarăMultăPuțină
Poate fi întreruptNu (face rollback)Da (continuă la rularea următoare)
Folosește-l când…Fragmentare > 30% ȘI îți permiți rebuild-ulFragmentare 5-30%, constrângeri online

REORGANIZE nu actualizează statisticile. Dacă îl rulezi, urmează cu UPDATE STATISTICS pe același index. REBUILD actualizează stats-urile gratis.

Statisticile bat fragmentarea

Paul Randal a spus asta de atâtea ori încât ar merita să fie titlul unei cărți: actualizează statisticile mai întâi, fă-ți griji pentru fragmentare după. Stats-uri învechite fac optimizatorul să aleagă planuri proaste, iar asta încetinește query-urile cu ordine de mărime. Fragmentarea le încetinește poate cu 10-20% pe scan-uri. Priorități.

-- Update de statistici nuclear-from-orbit
UPDATE STATISTICS Sales.Orders WITH FULLSCAN;

-- Tot din baza de date
EXEC sp_updatestats;

Scripturile lui Ola fac asta inteligent — eșantionează tabelele care au nevoie, le sar pe restul. E în continuare alegerea corectă.

Page fill factor

FILLFACTOR îi spune SQL Server-ului cât de pline să facă paginile leaf când construiește sau reconstruiește un index. Default-ul e 0 (tratat ca 100%). Un fill factor mai mic lasă loc pe fiecare pagină pentru insert-uri/update-uri viitoare, reducând page split-urile.

CREATE NONCLUSTERED INDEX IX_Customer_Email
    ON Sales.Customer (Email)
    WITH (FILLFACTOR = 90);

Reguli generale:

  • Tabele append-only (event logs, istoric comenzi): FILLFACTOR = 100. Nu ai insert-uri în mijloc; folosește tot spațiul.
  • Tabele cu update-uri grele în paginile din mijloc (OLTP hot cu scrieri non-secvențiale): 85-95%. Lasă loc pentru update-uri fără split-uri.
  • Orice cu GUID-uri ca cheie clustered: stop, repară schema (lecția 21), apoi gândește-te la fill factor.

Fill factor mai mic înseamnă mai multe pagini, deci mai multă RAM consumată. Trade-off. Nu seta FILLFACTOR = 70 „ca să fii sigur” — irosești memorie degeaba.

Avertismentul SHRINK, fiindcă apare în fiecare lună

Fiecare DBA junior, la un moment dat, rulează DBCC SHRINKDATABASE „ca să recupereze spațiu.” Nu.

SHRINKDATABASE și SHRINKFILE mută paginile de la sfârșitul fișierului la început, amestecându-le arbitrar. Fiecare index pe care-l ating devine maxim fragmentat în proces. Ajungi cu:

  • Un fișier mai mic (bine).
  • Fragmentare oribilă pe fiecare index mare (rău).
  • Evenimente de autogrowth care fac fișierul mare la loc în câteva zile (trist).

Regulă: niciodată SHRINK pe baze de date de producție. Dacă ai nevoie de spațiu, mișcarea corectă e de obicei:

  • Eliberează disc altundeva.
  • Arhivează / fă partition-switch-out pe date vechi întâi.
  • Șterge indexurile nefolosite (lecția 22 are query-ul).
  • Ca ultimă soluție, SHRINKFILE (file, truncateonly) taie doar spațiul liber de la sfârșitul fișierului fără să mute paginile.

Când contează fragmentarea cu adevărat

Listă din lumea reală:

  • Range scan-uri mari pe un index hot care stă pe disc. Fragmentarea logică costă I/O real.
  • Read replica grele unde query-urile rulează de pe disc (niciodată complet în cache).
  • Tabele clustered pe GUID unde fiecare insert face page split. Rebuild-urile ajută o zi; repară cheia clustered în loc.
  • După un delete mare unic care lasă pagini goale împrăștiate prin index.

Pentru orice altceva — OLTP tipic de mărimea Runehold, storage SSD, RCSI on, covering indexes pentru query-urile hot — fragmentarea e o preocupare minoră. Concentrează-te pe planurile de execuție și pe statistici, nu pe planul de mentenanță.

Un job de mentenanță sensibil

Pe serverul de prod al Runehold, job-ul de Agent arată cam așa:

  • Zilnic, 03:00: UPDATE STATISTICS pe tabelele modificate de ieri (folosind IndexOptimize de la Ola).
  • Săptămânal, duminică 01:00: IndexOptimize de la Ola pe toate bazele de date user cu praguri sensibile:
    • Indexuri < 1.000 pagini: skip.
    • 5-30% fragmentate: reorganize.
    • 30%+ fragmentate: rebuild.
    • Toate astea sunt default-urile lui Ola, deci e un one-liner.
  • Lunar: DBCC CHECKDB complet pe fiecare bază de date (verificare integritate, nu fragmentare).
  • Niciodată: SHRINKDATABASE.

Plictisitor, eficient, rulează ani buni fără atenție.

Rulează asta pe propria mașină

USE Runehold;
GO

-- 1. Inventariază-ți indexurile
SELECT
    OBJECT_NAME(ips.object_id)                     AS table_name,
    i.name                                          AS index_name,
    ips.index_type_desc,
    ips.avg_fragmentation_in_percent               AS frag_pct,
    ips.avg_page_space_used_in_percent             AS fill_pct,
    ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') AS ips
JOIN sys.indexes AS i
    ON i.object_id = ips.object_id AND i.index_id = ips.index_id
WHERE ips.page_count > 100
ORDER BY frag_pct DESC;

-- 2. Reorganize pe un index specific (online, impact mic)
ALTER INDEX IX_Customer_Email ON Sales.Customer REORGANIZE;

-- 3. Rebuild (doar Enterprise îl face online)
ALTER INDEX IX_Customer_Email ON Sales.Customer
    REBUILD WITH (ONLINE = ON, FILLFACTOR = 90);

-- 4. Update de statistici cu full scan
UPDATE STATISTICS Sales.Customer WITH FULLSCAN;

-- 5. Le faci pe toate deodată pentru un tabel (Ola-lite)
ALTER INDEX ALL ON Sales.Orders REBUILD WITH (ONLINE = ON, FILLFACTOR = 90);
UPDATE STATISTICS Sales.Orders WITH FULLSCAN;

Rulează query-ul de inventar pe un server care merge de ceva timp. Observă cât de puține dintre indexurile tale au fragmentare cu adevărat semnificativă. Cele mai multe firme la care am fost au poate cinci indexuri care contează și cincizeci care nu.

Modulul 4 e gata. Indexuri, covering, filtered, columnstore, fragmentare — toate acoperite. Acum ai opinii despre când să indexezi și când să lași lucrurile în pace.

Urmează: modulul 5, planuri de execuție și performanță. Lecția 25 (deja scrisă) e citirea principală; lecțiile 26-28 intră mai în profunzime în statistici, parameter sniffing și Query Store.

Caută