MODUL 1 - LEKCIJA 2

Dizajniranje i Implementacija Tabela

Ekspertski vodič kroz normalizaciju, storage engine i fizičku implementaciju

⏱️ Trajanje: ~4 časa 📚 Nivo: Srednji do Napredni 🎯 Praktični primjeri: 8

📖 Uvod u Arhitekturu Skladišta

U relacijskom modelu, tabela nije samo set kolona i redova. Ona je fizički prikaz logičkog modela koji direktno utiče na to kako SQL Server Storage Engine zapisuje podatke na disk (8KB pages). Pravilan dizajn je jedini način da osigurate linearnu skalabilnost vaše aplikacije.

📐 Duboko Razumijevanje Normalizacije

Normalizacija je formalni matematički proces (zasnovan na relacijskoj algebri) kojim se podaci dekomponuju u manje tabele kako bi se eliminisale anomalije pri ažuriranju.

1. Prva Normalna Forma (1NF): Atomarnost

Da bi tabela bila u 1NF, moraju biti ispunjena tri uslova:

🚫 Primjer kršenja 1NF

Kolona Vjestine koja sadrži "SQL, C#, Azure" krši 1NF jer vrijednosti nisu atomarne. Rješenje je posebna tabela za vještine.

2. Druga Normalna Forma (2NF): Potpuna Funkcionalna Zavisnost

Tabela je u 2NF ako je u 1NF i ako svaka ne-ključna kolona zavisi od CIJELOG primarnog ključa. Ovo je relevantno samo kod kompozitnih ključeva (kljuć koji se sastoji od više kolona).

🔍 Primjer 2NF

U tabeli OrderProduct(OrderID, ProductID, ProductName), kolona ProductName zavisi samo od ProductID, a ne od OrderID. Ovo krši 2NF. Rješenje je micanje naziva proizvoda u tabelu Products.

3. Treća Normalna Forma (3NF): Tranzitivna Zavisnost

Tabela je u 3NF ako je u 2NF i nema tranzitivnih zavisnosti. To znači da ne-ključna kolona ne smije zavisiti od druge ne-ključne kolone.

💡 Primjer 3NF

U tabeli Employee(ID, FirstName, City, PostalCode), City zavisi od PostalCode, a oboje su ne-ključne kolone. Ovo krši 3NF. Ispravno je imati tabelu Locations(PostalCode, City).

🎯 Praktični Primjeri Normalizacije

Hajde da vidimo normalizaciju u akciji sa stvarnim primjerom koji demonstrira transformaciju od nenormalizovane tabele do pravilno normalizovane baze podataka.

Primjer: Sistem za Evidenciju Projekata i Izvještaja

Zamislite da kreirate sistem za evidenciju projekata i izvještaja u državnoj agenciji. Početna ideja može biti da sve stavite u jednu tabelu, ali to vodi ka problemima sa duplikacijom podataka i integritetom.

❌ Primjer: Nenormalizovana Tabela Projekata i Izvještaja
-- LOŠE: Nenormalizovana tabela (krši 1NF, 2NF, 3NF)
CREATE TABLE Reports_Unnormalized (
    ReportID INT,
    ReportDate DATE,
    EmployeeName NVARCHAR(100),
    EmployeeEmail NVARCHAR(100),
    EmployeeJMBG NVARCHAR(13),
    DepartmentName NVARCHAR(100),
    DepartmentBudget DECIMAL(15,2),
    ProjectName NVARCHAR(200),
    ProjectBudget DECIMAL(15,2),
    DataValue DECIMAL(18,2),
    RegionName NVARCHAR(100)
);
-- Problemi:
-- 1. Više podataka po izvještaju zahtijeva više redova (kršenje 1NF)
-- 2. Informacije o zaposleniku se ponavljaju za svaki podatak (kršenje 2NF)
-- 3. Budžet odjeljenja zavisi od odjeljenja, ne od izvještaja (kršenje 3NF)
-- 4. Ako se budžet projekta promijeni, morate ažurirati sve stare izvještaje
-- 5. Ako zaposlenik promijeni email, morate ažurirati sve njegove izvještaje

⚠️ Problemi sa Nenormalizovanom Tabelom

  • Duplikacija podataka: Informacije o zaposleniku se ponavljaju za svaki podatak u izvještaju
  • Anomalije ažuriranja: Promjena email-a zaposlenika zahtijeva ažuriranje više redova
  • Anomalije brisanja: Brisanje izvještaja može slučajno obrisati informacije o zaposleniku
  • Anomalije umetanja: Ne možete dodati podatak bez kreiranja izvještaja
  • Nedosljednost: Budžet projekta može biti različit u različitim izvještajima
✅ Normalizovano Rješenje (3NF)
-- 1. Tabela Zaposlenika (uklanja duplikaciju zaposlenika)
CREATE TABLE Stats.Employees (
    EmployeeID INT PRIMARY KEY IDENTITY(1,1),
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    JMBG NVARCHAR(13) NOT NULL UNIQUE,
    Email NVARCHAR(100) NOT NULL UNIQUE,
    DepartmentID INT NOT NULL,
    Position NVARCHAR(100) NOT NULL,
    Salary DECIMAL(10,2) NOT NULL,
    HireDate DATE NOT NULL,
    CreatedDate DATETIME2 DEFAULT SYSDATETIME()
);
-- Svaki zaposlenik se čuva samo jednom, bez obzira na broj izvještaja

-- 2. Tabela Odjeljenja (uklanja duplikaciju odjeljenja - 3NF)
CREATE TABLE Stats.Departments (
    DepartmentID INT PRIMARY KEY IDENTITY(1,1),
    DepartmentName NVARCHAR(100) NOT NULL UNIQUE,
    DepartmentCode NVARCHAR(20) NOT NULL UNIQUE,
    Budget DECIMAL(15,2) NOT NULL
);
-- Odjeljenja se čuvaju jednom, budžet se ažurira na jednom mjestu

-- 3. Tabela Projekata (uklanja duplikaciju projekata)
CREATE TABLE Stats.Projects (
    ProjectID INT PRIMARY KEY IDENTITY(1,1),
    ProjectName NVARCHAR(200) NOT NULL,
    DepartmentID INT NOT NULL,
    Budget DECIMAL(15,2) NOT NULL,
    StartDate DATE NOT NULL,
    Status NVARCHAR(50) NOT NULL,
    CONSTRAINT FK_Projects_Departments 
        FOREIGN KEY (DepartmentID) REFERENCES Stats.Departments(DepartmentID)
);
-- Projekti se čuvaju jednom, budžet se ažurira na jednom mjestu

-- 4. Tabela Izvještaja (zaglavlje izvještaja)
CREATE TABLE Stats.Reports (
    ReportID INT PRIMARY KEY IDENTITY(1,1),
    ReportName NVARCHAR(200) NOT NULL,
    DepartmentID INT NOT NULL,
    CreatedBy INT NOT NULL,
    CreatedDate DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
    Status NVARCHAR(50) DEFAULT 'Draft',
    ReportType NVARCHAR(50) NOT NULL,
    CONSTRAINT FK_Reports_Departments 
        FOREIGN KEY (DepartmentID) REFERENCES Stats.Departments(DepartmentID),
    CONSTRAINT FK_Reports_CreatedBy 
        FOREIGN KEY (CreatedBy) REFERENCES Stats.Employees(EmployeeID)
);
-- Svaki izvještaj ima jedan red, povezan sa jednim odjeljenjem i zaposlenikom

-- 5. Tabela Podataka Izvještaja (detalji izvještaja - many-to-many relacija)
CREATE TABLE Stats.ReportData (
    ReportDataID INT PRIMARY KEY IDENTITY(1,1),
    ReportID INT NOT NULL,
    DataType NVARCHAR(50) NOT NULL,
    Value DECIMAL(18,2) NULL,
    RegionID INT NULL,
    Year INT NULL,
    CONSTRAINT FK_ReportData_Reports 
        FOREIGN KEY (ReportID) REFERENCES Stats.Reports(ReportID),
    CONSTRAINT FK_ReportData_Regions 
        FOREIGN KEY (RegionID) REFERENCES Stats.Regions(RegionID)
);
-- Svaki podatak u izvještaju je zaseban red
-- Value se čuva jer podaci mogu promijeniti kasnije

💡 Prednosti Normalizacije

  • Integritet Podataka: Informacije o kupcu se čuvaju jednom, ažuriraju na jednom mjestu
  • Efikasnost Skladištenja: Nema duplikata podataka (informacije o kupcu, proizvodu)
  • Lakša Ažuriranja: Promjena cijene proizvoda na jednom mjestu
  • Fleksibilnost Upita: Lako pronaći sve narudžbe za kupca ili sve proizvode u kategoriji
  • Konzistentnost: Nemoguće imati različite email adrese za istog kupca
  • Skalabilnost: Lako dodati nove atribute bez uticaja na postojeće podatke

Upitovanje Normalizovanih Podataka

Sa normalizovanim tabelama, koristite JOIN-ove da kombinujete povezane podatke:

🔗 Primjeri JOIN-ova
-- Dohvatanje detalja izvještaja sa informacijama o zaposleniku, odjeljenju i regiji
SELECT 
    r.ReportID,
    r.CreatedDate,
    e.FirstName + ' ' + e.LastName AS Kreator,
    d.DepartmentName AS Odjeljenje,
    rd.DataType AS TipPodatka,
    reg.RegionName AS Regija,
    rd.Value AS Vrijednost,
    rd.Year AS Godina
FROM Stats.Reports r
INNER JOIN Stats.Employees e ON r.CreatedBy = e.EmployeeID
INNER JOIN Stats.Departments d ON r.DepartmentID = d.DepartmentID
INNER JOIN Stats.ReportData rd ON r.ReportID = rd.ReportID
LEFT JOIN Stats.Regions reg ON rd.RegionID = reg.RegionID
ORDER BY r.CreatedDate DESC;

-- Ukupni broj izvještaja po zaposleniku
SELECT 
    e.FirstName + ' ' + e.LastName AS Zaposlenik,
    d.DepartmentName AS Odjeljenje,
    COUNT(DISTINCT r.ReportID) AS BrojIzvjestaja,
    COUNT(rd.ReportDataID) AS UkupnoPodataka
FROM Stats.Employees e
INNER JOIN Stats.Departments d ON e.DepartmentID = d.DepartmentID
INNER JOIN Stats.Reports r ON e.EmployeeID = r.CreatedBy
INNER JOIN Stats.ReportData rd ON r.ReportID = rd.ReportID
GROUP BY e.EmployeeID, e.FirstName, e.LastName, d.DepartmentName
ORDER BY BrojIzvjestaja DESC;

-- Svi projekti u određenom odjeljenju
SELECT 
    p.ProjectName AS NazivProjekta,
    p.Budget AS Budzet,
    p.Status AS Status,
    d.DepartmentName AS Odjeljenje
FROM Stats.Projects p
INNER JOIN Stats.Departments d ON p.DepartmentID = d.DepartmentID
WHERE d.DepartmentName = 'Agencija za Statistiku'
ORDER BY p.ProjectName;

🔑 Tipovi JOIN-ova

U SQL Server-u postoje različiti tipovi JOIN-ova:

  • INNER JOIN: Vraća samo redove gdje postoji podudaranje u obje tabele
  • LEFT JOIN (LEFT OUTER JOIN): Vraća sve redove iz lijeve tabele i podudarajuće iz desne
  • RIGHT JOIN (RIGHT OUTER JOIN): Vraća sve redove iz desne tabele i podudarajuće iz lijeve
  • FULL OUTER JOIN: Vraća sve redove iz obje tabele, gdje god postoji podudaranje
  • CROSS JOIN: Kartezijanski proizvod - svaki red iz prve tabele sa svakim redom iz druge

📊 Data Types Internals

Kada birate tip podatka, vi zapravo govorite SQL Serveru koliko bajtova da rezerviše na 8KB stranici. Pogrešan odabir može uzrokovati Page Splits i drastično povećati I/O load.

Integer Family - Koliko zapravo troše?

Nervni sistem baze: Unicode vs. Non-Unicode

Odabir između VARCHAR i NVARCHAR je balans između prostora i podrške za jezike.

⚡ Savjet za Performanse: Promjenjiva vs Fiksna Dužina

Koristite CHAR(N) samo ako je dužina podataka UVIJEK ista (npr. ISO kod države 'BA'). Za sve ostalo koristite VARCHAR/NVARCHAR jer SQL Server neće trošiti prostor na prazne karaktere.

📁 Napredni rad sa Shemama

Sheme (Schemas) su u SQL Serveru više od pukih foldera. One su sigurnosni kontejneri.

🔐 Sigurnost kroz Sheme
-- Kreiranje korisnika koji ima pristup samo prodaji
CREATE USER SalesManager WITHOUT LOGIN;
GO

-- Dozvoli korisniku sve operacije ali samo unutar Sales sheme
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::Sales TO SalesManager;
GO

-- Ovim izolujemo HR podatke od Sales manager-a bez komplikovanih dozvola na nivou tabela.
-- SalesManager može pristupiti samo tabelama u Sales shemi, ne može vidjeti HR shemu.

-- Primjer: Kreiranje više shema za organizaciju
CREATE SCHEMA HR;  -- Za ljudske resurse
CREATE SCHEMA Sales;  -- Za prodaju
CREATE SCHEMA Finance;  -- Za finansije
CREATE SCHEMA IT;  -- Za IT resurse
GO

-- Svaka shema može imati svoje tabele, views, procedures
-- Ovo omogućava bolju organizaciju i sigurnost

🔑 Prednosti Korištenja Shema

  • Organizacija: Grupišete povezane objekte zajedno
  • Sigurnost: Možete dati permisije na nivou sheme umjesto pojedinačnih tabela
  • Namespace: Možete imati tabele sa istim imenom u različitim shemama
  • Održivost: Lakše pronaći i upravljati objektima

🎯 Praktična Vježba: Od Heapa do 3NF

Zadatak: Ispravljanje katastrofalnog dizajna

Dobili ste "Excel-oliku" tabelu koja krši sve norme. Trebate je dekomponovati.

Izvorna Tabela: RawData(StudentID, FirstName, Subject, Grade, BirthDate, ProfessorName, ProfessorOffice)

Zadatak 1: Identifikujte kršenje 2NF (Profesor zavisnosti).

Zadatak 2: Kreirajte T-SQL skriptu koja pravi 3 odvojene tabele (Students, Professors, Grades).

💡 Profesionalno Rješenje
-- 1. Tabela Employees (Čist 3NF)
CREATE TABLE Stats.Employees (
    EmployeeID INT PRIMARY KEY IDENTITY(1,1),
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    JMBG NVARCHAR(13) NOT NULL UNIQUE,
    HireDate DATE NOT NULL
);

-- 2. Tabela Departments
CREATE TABLE Stats.Departments (
    DepartmentID INT IDENTITY(1,1) PRIMARY KEY,
    DepartmentName NVARCHAR(100) NOT NULL,
    DepartmentCode NVARCHAR(20) NOT NULL UNIQUE
);

-- 3. Tabela Projects (Poveznica - Many to Many preko ove tabele)
CREATE TABLE Stats.Projects (
    ProjectID INT PRIMARY KEY IDENTITY(1,1),
    EmployeeID INT CONSTRAINT FK_Projects_Employees REFERENCES Stats.Employees(EmployeeID),
    DepartmentID INT CONSTRAINT FK_Projects_Departments REFERENCES Stats.Departments(DepartmentID),
    ProjectName NVARCHAR(200) NOT NULL,
    Budget DECIMAL(15,2) NOT NULL CHECK (Budget >= 0),
    StartDate DATE DEFAULT GETDATE()
);
GO

⚙️ Ispod haube: Šta se dešava pri INSERT-u?

Kada izvršite INSERT, SQL Server ne piše odmah na disk.

  1. Podatak se piše u Buffer Pool (RAM).
  2. Transakcija se bilježi u Transaction Log (LDF fajl) - ovo je sinhrono i garantuje ACID.
  3. Proces zvani CHECKPOINT periodično prepisuje prljave stranice (dirty pages) iz RAM-a u Data file (MDF).

✅ Zaključak

U ovoj lekciji smo postavili temelje za eksperta:

📚 Sljedeća Lekcija

U Lekciji 1.3 istražujemo Temporal Tables - kako SQL Server 2022 automatski pamti prošlost bez vašeg truda.