Wprowadzenie: Czym jest SQL?

SQL (Structured Query Language) to język używany do zarządzania danymi w relacyjnych bazach danych. W kontekście agencji nieruchomości, baza danych pozwala na efektywne przechowywanie informacji o klientach, nieruchomościach, ofertach i transakcjach, a także automatyzację procesów biznesowych.

Ten przewodnik przedstawia najważniejsze możliwości SQL na przykładzie rzeczywistego systemu. Dowiesz się, jak zbudować solidną strukturę bazy danych, uprościć dostęp do danych za pomocą widoków, zwiększyć wydajność dzięki funkcjom, zautomatyzować procesy za pomocą triggerów i stworzyć zaawansowaną logikę biznesową w procedurach składowanych.

1. Struktura bazy danych

Fundament każdej aplikacji bazodanowej – poprawnie zaprojektowane tabele to podstawa działającego systemu. Nasza struktura wykorzystuje klucze główne (PRIMARY KEY) do unikalnej identyfikacji rekordów, klucze obce (FOREIGN KEY) do powiązań między tabelami oraz ograniczenia (CHECK constraints) do walidacji danych.

Dlaczego to ważne?

Ograniczenia na poziomie bazy danych gwarantują integralność danych niezależnie od tego, która aplikacja lub użytkownik wprowadza zmiany. Na przykład, nikt nie może dodać klienta poniżej 18 roku życia ani nieruchomości z ceną ujemną.

Definicja tabel
CREATE TABLE Klienci (
    KlientID INT PRIMARY KEY IDENTITY(1,1),
    Imie VARCHAR(50) NOT NULL,
    Nazwisko VARCHAR(50) NOT NULL,
    Email VARCHAR(100) UNIQUE NOT NULL,
    Wiek INT CHECK (Wiek >= 18 AND Wiek <= 100)
);

CREATE TABLE Nieruchomosci (
    NieruchomoscID INT PRIMARY KEY IDENTITY(1,1),
    Adres VARCHAR(200) NOT NULL,
    Cena DECIMAL(15,2) CHECK (Cena > 0),
    Typ VARCHAR(50),
    Powierzchnia DECIMAL(10,2) CHECK (Powierzchnia > 0)
);

CREATE TABLE Oferty (
    OfertaID INT PRIMARY KEY IDENTITY(1,1),
    KlientID INT FOREIGN KEY REFERENCES Klienci(KlientID),
    NieruchomoscID INT FOREIGN KEY REFERENCES Nieruchomosci(NieruchomoscID),
    DataOferta DATE NOT NULL,
    Status VARCHAR(20) CHECK (Status IN ('Aktywna', 'Zakonczona', 'Wycofana'))
);

CREATE TABLE Transakcje (
    TransakcjaID INT PRIMARY KEY IDENTITY(1,1),
    OfertaID INT FOREIGN KEY REFERENCES Oferty(OfertaID),
    DataTransakcji DATE NOT NULL,
    Kwota DECIMAL(15,2) CHECK (Kwota > 0)
);

2. Widoki (VIEW)

Widoki to „wirtualne tabele” – nie przechowują danych, ale prezentują je w sposób uproszczony. Zamiast za każdym razem pisać skomplikowane zapytania z wieloma JOIN-ami, tworzymy widok i odwołujemy się do niego jak do zwykłej tabeli. To szczególnie przydatne, gdy dane z wielu tabel są często używane razem.

Zastosowanie biznesowe

Nasz widok vw_OffertyPodsumowanie łączy dane z czterech tabel, pokazując pełny obraz każdej oferty: kto jest klientem, jaka to nieruchomość, ile było transakcji i na jaką sumę. To idealna baza do raportów i dashboardów.

Widok Oferty – Podsumowanie
CREATE VIEW vw_OffertyPodsumowanie AS
SELECT
    O.OfertaID,
    K.Imie AS ImieKlienta,
    K.Nazwisko AS NazwiskoKlienta,
    N.Adres AS AdresNieruchomosci,
    N.Cena AS CenaNieruchomosci,
    O.Status,
    COUNT(T.TransakcjaID) AS LiczbaTransakcji,
    ISNULL(SUM(T.Kwota), 0) AS SumaKwotTransakcji
FROM
    Oferty O
    INNER JOIN Klienci K ON O.KlientID = K.KlientID
    INNER JOIN Nieruchomosci N ON O.NieruchomoscID = N.NieruchomoscID
    LEFT JOIN Transakcje T ON O.OfertaID = T.OfertaID
GROUP BY
    O.OfertaID, K.Imie, K.Nazwisko, N.Adres, N.Cena, O.Status;

-- Użycie widoku
SELECT * FROM vw_OffertyPodsumowanie WHERE Status = 'Aktywna';
Wynik zapytania do widoku vw_OffertyPodsumowanie
Wynik zapytania do widoku vw_OffertyPodsumowanie.

3. Funkcje SQL

Funkcje to wielokrotnego użytku komponenty logiki – możesz je wywoływać z parametrami i otrzymywać zwracane wartości. Rozróżniamy funkcje tabelaryczne (zwracające zestawy rekordów – jak mini-zapytania) oraz skalarne (zwracające pojedyncze wartości – na przykład obliczoną prowizję).

Kiedy używać funkcji?

Gdy ta sama logika jest potrzebna w wielu miejscach. Na przykład funkcja fn_getClientOffers może być używana w raportach, na stronie klienta czy w API – zawsze zwróci aktualne oferty dla danego e-maila. Jedna implementacja, wiele zastosowań.

Przykładowe funkcje
CREATE FUNCTION dbo.fn_getClientOffers
(
    @Email VARCHAR(100)
)
RETURNS TABLE
AS
RETURN
(
    SELECT
        O.OfertaID,
        O.DataOferta,
        O.Status,
        N.Adres,
        N.Cena
    FROM
        Oferty O
        INNER JOIN Klienci K ON O.KlientID = K.KlientID
        INNER JOIN Nieruchomosci N ON O.NieruchomoscID = N.NieruchomoscID
    WHERE
        K.Email = @Email
);

-- Test funkcji
SELECT * FROM dbo.fn_getClientOffers('anna.nowak@example.com');


CREATE FUNCTION dbo.fn_calculateCommission
(
    @Price DECIMAL(15,2),
    @CommissionRate DECIMAL(5,2)
)
RETURNS DECIMAL(15,2)
AS
BEGIN
    RETURN @Price * (@CommissionRate / 100);
END;

-- Test funkcji
SELECT dbo.fn_calculateCommission(500000, 3.5) AS Prowizja;
Wynik funkcji fn_getClientOffers
Przykładowy wynik funkcji fn_getClientOffers dla wybranego klienta.

4. Procedury składowane (PROCEDURE)

Procedury składowane to „programy w bazie danych” – mogą zawierać złożoną logikę biznesową, instrukcje warunkowe (IF), pętle, obsługę błędów i wiele operacji na danych. W przeciwieństwie do funkcji procedury mogą modyfikować dane, wywoływać inne procedury i zwracać wiele zestawów wyników.

Dlaczego procedury?

Centralizacja logiki biznesowej w bazie danych oznacza, że wszystkie aplikacje (web, mobile, desktop) używają tych samych reguł. Zmiana procesu biznesowego? Aktualizujesz jedną procedurę, a nie kod w kilku aplikacjach. Dodatkowo procedury często działają szybciej, bo kod SQL jest już skompilowany.

Przykładowe procedury składowane
CREATE PROCEDURE usp_InsertOrUpdateKlient
    @KlientID INT,
    @Imie VARCHAR(50),
    @Nazwisko VARCHAR(50),
    @Email VARCHAR(100),
    @Wiek INT
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Logika biznesowa: poprawność wieku
    IF @Wiek < 18 OR @Wiek > 100
    BEGIN
        PRINT 'Błąd: Wiek musi być w przedziale 18-100 lat.';
        RETURN;
    END
    
    -- Jeśli dany KlientID istnieje, zaktualizuj
    IF EXISTS (SELECT 1 FROM Klienci WHERE KlientID = @KlientID)
    BEGIN
        UPDATE Klienci
        SET Imie = @Imie, Nazwisko = @Nazwisko, Email = @Email, Wiek = @Wiek
        WHERE KlientID = @KlientID;
        
        PRINT 'Rekord został zaktualizowany.';
    END
    ELSE
    BEGIN
        -- W przeciwnym wypadku dodaj nowy
        INSERT INTO Klienci (KlientID, Imie, Nazwisko, Email, Wiek)
        VALUES (@KlientID, @Imie, @Nazwisko, @Email, @Wiek);
        
        PRINT 'Nowy rekord został dodany.';
    END
END;


CREATE PROCEDURE sp_AddNewOffer
    @KlientEmail VARCHAR(100),
    @AdresNieruchomosci VARCHAR(200),
    @CenaNieruchomosci DECIMAL(15,2),
    @TypNieruchomosci VARCHAR(50),
    @PowierzchniaNieruchomosci DECIMAL(10,2)
AS
BEGIN
    DECLARE @KlientID INT;
    DECLARE @NieruchomoscID INT;
    
    -- Sprawdź czy klient istnieje
    SELECT @KlientID = KlientID FROM Klienci WHERE Email = @KlientEmail;
    
    IF @KlientID IS NULL
    BEGIN
        RAISERROR('Klient nie istnieje!', 16, 1);
        RETURN;
    END;
    
    -- Dodaj nieruchomość
    INSERT INTO Nieruchomosci (Adres, Cena, Typ, Powierzchnia)
    VALUES (@AdresNieruchomosci, @CenaNieruchomosci, 
            @TypNieruchomosci, @PowierzchniaNieruchomosci);
    
    SET @NieruchomoscID = SCOPE_IDENTITY();
    
    -- Dodaj ofertę
    INSERT INTO Oferty (KlientID, NieruchomoscID, DataOferta, Status)
    VALUES (@KlientID, @NieruchomoscID, GETDATE(), 'Aktywna');
    
    PRINT 'Oferta została dodana pomyślnie!';
END;


CREATE PROCEDURE sp_GetClientStatistics
    @KlientID INT,
    @LiczbaOfert INT OUTPUT,
    @LacznaWartoscTransakcji DECIMAL(15,2) OUTPUT
AS
BEGIN
    SELECT 
        @LiczbaOfert = COUNT(DISTINCT O.OfertaID),
        @LacznaWartoscTransakcji = ISNULL(SUM(T.Kwota), 0)
    FROM 
        Oferty O
        LEFT JOIN Transakcje T ON O.OfertaID = T.OfertaID
    WHERE 
        O.KlientID = @KlientID;
END;
Test dodania nowego rekordu klienta
Test 1: dodanie nowego klienta (nowy rekord).
Test aktualizacji istniejącego rekordu klienta
Test 2: aktualizacja istniejącego klienta.
Test walidacji wieku klienta
Test 3: walidacja wieku – próba dodania osoby poniżej 18 lat.

Testy pokazują

Test 1 dodaje nowego klienta (np. ID = 11), Test 2 aktualizuje istniejącego (ID = 1), a Test 3 demonstruje walidację – próba dodania osoby w wieku 16 lat zostaje odrzucona z odpowiednim komunikatem błędu.

5. Triggery (TRIGGER)

Triggery to „strażnicy bazy danych” – automatycznie reagują na zmiany w tabelach. Gdy ktoś próbuje dodać, zaktualizować lub usunąć rekord, trigger może sprawdzić poprawność operacji, zalogować zmianę, zaktualizować powiązane dane lub nawet anulować całą operację. Działają niezależnie od aplikacji – nawet jeśli ktoś zmieni dane bezpośrednio w bazie, trigger zadziała.

Audyt i compliance

Triggery są nieocenione w systemach wymagających śledzenia zmian. Każda modyfikacja statusu oferty może być automatycznie logowana z informacją kto, kiedy i co zmienił. To kluczowe dla branży nieruchomości, gdzie historia transakcji musi być przejrzysta.

Przykładowe triggery
-- Tabela audytu
CREATE TABLE AuditOferty (
    AuditID INT IDENTITY(1,1) PRIMARY KEY,
    OfertaID INT,
    Operation VARCHAR(10),
    OldStatus VARCHAR(50),
    NewStatus VARCHAR(50),
    ChangeDate DATETIME DEFAULT GETDATE()
);

-- AFTER UPDATE - audyt zmian statusu oferty
CREATE TRIGGER trg_AuditOfertyUpdate
ON Oferty
AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;
    
    INSERT INTO AuditOferty (OfertaID, Operation, OldStatus, NewStatus, ChangeDate)
    SELECT
        i.OfertaID,
        'UPDATE',
        d.Status,
        i.Status,
        GETDATE()
    FROM
        inserted i
        INNER JOIN deleted d ON i.OfertaID = d.OfertaID
    WHERE
        ISNULL(i.Status, '') <> ISNULL(d.Status, '');
END;


-- AFTER INSERT - walidacja transakcji
CREATE TRIGGER trg_AfterInsertTransakcja
ON Transakcje
AFTER INSERT
AS
BEGIN
    DECLARE @OfertaID INT;
    DECLARE @StatusOferty VARCHAR(20);
    
    SELECT @OfertaID = OfertaID FROM INSERTED;
    
    SELECT @StatusOferty = Status 
    FROM Oferty 
    WHERE OfertaID = @OfertaID;
    
    IF @StatusOferty != 'Aktywna'
    BEGIN
        RAISERROR('Transakcja może być dodana tylko do aktywnej oferty!', 16, 1);
        ROLLBACK TRANSACTION;
    END;
END;


-- INSTEAD OF DELETE - ochrona przed usunięciem klienta
CREATE TRIGGER trg_InsteadOfDeleteKlient
ON Klienci
INSTEAD OF DELETE
AS
BEGIN
    DECLARE @KlientID INT;
    DECLARE @LiczbaOfert INT;
    
    SELECT @KlientID = KlientID FROM DELETED;
    
    SELECT @LiczbaOfert = COUNT(*)
    FROM Oferty
    WHERE KlientID = @KlientID;
    
    IF @LiczbaOfert > 0
    BEGIN
        RAISERROR('Nie można usunąć klienta z aktywnymi ofertami!', 16, 1);
    END
    ELSE
    BEGIN
        DELETE FROM Klienci WHERE KlientID = @KlientID;
    END;
END;
Test zmiany statusu oferty uruchamiający trigger audytowy
Test 1: zmiana statusu oferty – trigger zapisuje wpis w audycie.
Wynik zapytania do tabeli audytu AuditOferty
Widok danych w tabeli audytowej AuditOferty.
Aktualizacja bez zmiany statusu oferty
Test 2: aktualizacja bez zmiany statusu – trigger nie dodaje nowego wpisu do audytu.

Podsumowanie

Poznałeś pięć kluczowych narzędzi nowoczesnej bazy danych SQL:

Najlepsza praktyka

Połączenie tych mechanizmów tworzy solidny, wydajny i łatwy w utrzymaniu system bazodanowy. Strukturą zapewniasz spójność, widokami upraszczasz dostęp, funkcjami redukujesz duplikację kodu, triggerami automatyzujesz procesy, a procedurami centralizujesz logikę biznesową.