🧠 Ćwiczenia z SQL - Programowanie i Optymalizacja Baz Danych

Sekcja dla zaawansowanych: Transakcje, Kursory, Wyzwalacze, Funkcje Użytkownika i Optymalizacja!

1. 🧱 Struktura Bazy Danych (DDL) - Finalna Wersja

Zaktualizowano tabelę **`gracze`** o kolumny **`ranking`**, **`data_ostatniego_punkty`**, **`username`** i **`password`** do zaawansowanych ćwiczeń.

TabelaDodane/Zmienione KolumnyOpis
gracze**ranking** (INT), **data_ostatniego_punkty** (DATE), **username** (VARCHAR), **password** (VARCHAR)Niezbędne dla Zad. 2 i 8.
meczeid_gracza, id_druzyny

Skrypt Inicjalizacyjny (Tylko SQL)

Pokaż Skrypt Tworzenia Bazy (Schema + Przykładowe Dane)

-- DROP i CREATE TABLE dla czystego schematu (Wymagany do zaawansowanych ćwiczeń)

-- Tabela Gracze z nowymi kolumnami
CREATE TABLE gracze (
    id INT PRIMARY KEY AUTO_INCREMENT,
    nazwa VARCHAR(100) NOT NULL,
    nick VARCHAR(50) UNIQUE,
    punkty INT DEFAULT 0,
    id_druzyna INT,
    ranking INT, -- Nowa kolumna dla Zadania 2
    data_ostatniego_punkty DATE, -- Nowa kolumna dla Zadania 8
    username VARCHAR(50) UNIQUE, -- Nowa kolumna dla Zadania 10
    password VARCHAR(255) -- Nowa kolumna dla Zadania 10
);

-- Tabela Mecze z kluczem id_gracza
CREATE TABLE mecze (
    id INT PRIMARY KEY AUTO_INCREMENT,
    id_druzyny INT NOT NULL,
    id_gracza INT,
    wynik VARCHAR(50),
    wygrana BOOLEAN,
    FOREIGN KEY (id_gracza) REFERENCES gracze(id)
);

-- Przykładowe dane
INSERT INTO gracze (nazwa, nick, punkty, username, password, data_ostatniego_punkty) VALUES
('Jan Kowalski', 'Kowalski', 90, 'janek', 'haslo123', CURDATE()), -- Punkty < 100 dla testu transakcji
('Piotr Nowak', 'Piotrek', 1500, 'piotrek', 'bezpieczne', DATE_SUB(CURDATE(), INTERVAL 60 DAY)),
('Anna Lewa', 'Lewa_A', 900, 'anna', 'qwerty', CURDATE());

INSERT INTO mecze (id_gracza, wygrana) VALUES 
(1, 1), (1, 0), (2, 1); -- Przykładowe mecze
            

2. 🚀 Programowanie SQL (Zaawansowane)

Ćwicz tworzenie logiki zorientowanej na bazie danych.

2.1. Transakcja aktualizująca punkty (TRANSACTION)

Zadanie: Napisz transakcję, która zwiększy punkty gracza o **10** tylko wtedy, gdy jego aktualna liczba punktów jest **mniejsza niż 100**.

Rozwiązanie
START TRANSACTION;
-- Użyj konkretnego ID do testu, np. ID=1 (Kowalski, 90 pkt)
UPDATE gracze
SET punkty = punkty + 10
WHERE id = 1 AND punkty < 100;

-- Sprawdzenie, czy aktualizacja się powiodła
SELECT punkty FROM gracze WHERE id = 1; 

COMMIT; -- Zatwierdzenie zmian
-- ROLLBACK; -- Alternatywnie, wycofanie zmian

2.2. Złożona procedura rankingowa z Kursorem (PROCEDURE + CURSOR)

Zadanie: Utwórz procedurę aktualizuj_ranking, która przechodzi przez wszystkich graczy i ustawia ich **`ranking`** (kolumna z DDL) na podstawie punktów. Użyj kursora.

Rozwiązanie
DELIMITER //
CREATE PROCEDURE aktualizuj_ranking()
BEGIN
    DECLARE koniec INT DEFAULT 0;
    DECLARE id_gracza INT;
    
    -- Definicja Kursora
    DECLARE cur CURSOR FOR SELECT id FROM gracze ORDER BY punkty DESC;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET koniec = 1;

    SET @ranking = 1;
    OPEN cur;
    petla: LOOP
        FETCH cur INTO id_gracza;
        IF koniec THEN
            LEAVE petla;
        END IF;
        
        -- Aktualizacja rankingu
        UPDATE gracze SET ranking = @ranking WHERE id = id_gracza;
        SET @ranking = @ranking + 1;
    END LOOP;
    CLOSE cur;
END //
DELIMITER ;
-- Wywołanie: CALL aktualizuj_ranking();

2.3. Wyzwalacz blokujący zbyt wiele rozegranych meczów (TRIGGER + SIGNAL)

Zadanie: Utwórz wyzwalacz, który blokuje dodanie meczu, jeśli dany gracz rozegrał już **100 lub więcej** meczów (walidacja biznesowa).

Rozwiązanie
DELIMITER //
CREATE TRIGGER blokada_meczy
BEFORE INSERT ON mecze
FOR EACH ROW
BEGIN
    DECLARE rozegrane INT;
    
    SELECT COUNT(*) INTO rozegrane 
    FROM mecze 
    WHERE id_gracza = NEW.id_gracza;
    
    IF rozegrane >= 100 THEN
        -- Wygenerowanie błędu, który przerwie INSERT
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Gracz osiągnął limit 100 meczów.';
    END IF;
END //
DELIMITER ;

2.4. Funkcja zwracająca liczbę meczów (UDF)

Zadanie: Napisz **funkcję**, która przyjmuje **`id_gracza`** i zwraca liczbę rozegranych przez niego meczów.

Rozwiązanie
DELIMITER //
CREATE FUNCTION liczba_meczy(gracz_id INT) 
RETURNS INT
DETERMINISTIC
BEGIN
    DECLARE liczba INT;
    SELECT COUNT(*) INTO liczba FROM mecze WHERE id_gracza = gracz_id;
    RETURN liczba;
END //
DELIMITER ;
-- Wywołanie: SELECT liczba_meczy(1);

3. 📊 Optymalizacja i Zarządzanie

Ćwicz zagadnienia związane z wydajnością i zarządzaniem DDL/DML.

3.1. Optymalizacja zapytania rankingowego (INDEX)

Zadanie: Utwórz **indeks** na kolumnie punkty w tabeli gracze dla szybszych zapytań rankingowych (sortowanie malejąco).

Rozwiązanie
CREATE INDEX idx_punkty_desc ON gracze(punkty DESC);

3.2. Usunięcie gracza i powiązanych danych (CASCADING DELETE)

Zadanie: Usuń gracza o ID=1 z tabeli gracze oraz **wszystkie jego mecze**. (Najbezpieczniej jest najpierw usunąć rekordy zależne).

Rozwiązanie
-- 1. Usuń rekordy zależne
DELETE FROM mecze WHERE id_gracza = 1; 

-- 2. Usuń gracza
DELETE FROM gracze WHERE id = 1;

-- UWAGA: Aby usunięcie rekordów zależnych było automatyczne, 
-- należy dodać ON DELETE CASCADE do definicji klucza obcego przy tworzeniu tabeli.

4. 🌐 Integracja PHP/Bezpieczeństwo

Ćwicz bezpieczne łączenie SQL z kodem aplikacji.

4.1. Dynamiczny raport w PHP z filtrowaniem daty

Zadanie: Napisz skrypt PHP, który generuje raport z rankingiem graczy, filtrując tych, którzy uzyskali punkty w ostatnich **30 dniach** (wykorzystaj kolumnę **`data_ostatniego_punkty`**).

Rozwiązanie

<?php
// Zakładamy, że połączenie PDO jest już skonfigurowane
$pdo = new PDO('mysql:host=localhost;dbname=esport', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$czas = $_GET['czas'] ?? 30; // Domyślnie 30 dni, ale można podmienić

$stmt = $pdo->prepare("
    SELECT nazwa, punkty, data_ostatniego_punkty
    FROM gracze
    WHERE data_ostatniego_punkty >= DATE_SUB(NOW(), INTERVAL :czas DAY)
    ORDER BY punkty DESC
");

// Parametryzacja zapytania - BEZPIECZEŃSTWO
$stmt->bindParam(':czas', $czas, PDO::PARAM_INT);
$stmt->execute();

echo '<h2>Raport Rankingowy (Aktywni)';
echo '<table>';
echo '<tr><th>Nazwa<th>Punkty<th>Ostatni Punkt';
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    echo '<tr>';
    echo '<td>' . htmlspecialchars($row['nazwa']) . '';
    echo '<td>' . htmlspecialchars($row['punkty']) . '';
    echo '<td>' . htmlspecialchars($row['data_ostatniego_punkty']) . '';
    echo '</tr>';
}
echo '</table>';
?>

4.2. Bezpieczeństwo (SQL Injection) w PHP

Zadanie: Zabezpiecz fragment kodu logowania przed SQL Injection, stosując **przygotowane zapytania (Prepared Statements)** w PHP.

Rozwiązanie

<?php
// Zabezpieczony kod logowania z Prepared Statements
$pdo = new PDO('mysql:host=localhost;dbname=esport', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

// Użycie placeholderów (?) lub nazwanego placeholdera (:nazwa)
$stmt = $pdo->prepare('SELECT id FROM gracze WHERE username = :username AND password = :password');

// Właściwe zabezpieczenie: Wartości są przekazywane ODDZIELNIE od zapytania.
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
$stmt->execute();

if ($stmt->rowCount() > 0) {
    echo 'Zalogowano pomyślnie!';
} else {
    echo 'Błędne dane logowania.';
}
?>

4.3. Złożone złączenia (JOIN) - Podsumowanie

Zadanie: Napisz zapytanie, które połączy tabelę gracze z tabelą mecze i wyświetli nazwiska graczy oraz liczbę rozegranych meczów.

Rozwiązanie
SELECT g.nazwa, COUNT(m.id) AS liczba_meczy
FROM gracze g
LEFT JOIN mecze m ON g.id = m.id_gracza
GROUP BY g.nazwa;