PHP i baza danych: od pierwszego echo do 101 arkuszy
W większości arkuszy praktycznych INF.03 trzeba napisać cztery zapytania SQL i skrypt PHP, który łączy stronę z bazą danych. Ten kurs uczy obu rzeczy od zera, a potem prowadzi przez 101 arkuszy z lat 2018–2026 (INF.03 oraz starsze EE.09 i E.14).
Kod piszesz i uruchamiasz na tej stronie. PHP działa tu w przeglądarce, a baza danych zachowuje się jak MySQL z XAMPP: możesz wysyłać formularze, dodawać wiersze i oglądać wynik w podglądzie strony.
Co jest w kursie
| Część | Zawartość |
|---|---|
| PHP od zera | Jedenaście lekcji: znaczniki PHP, zmienne, warunki, pętle, tablice, funkcje, formularze, pola wyboru (radio, lista, checkbox), daty, ciasteczka i pliki tekstowe. |
| SQL | Jedenaście lekcji, które obejmują wszystkie rodzaje zapytań z arkuszy: od SELECT przez daty, JOIN i LEFT JOIN, grupowanie, INSERT, UPDATE, ALTER i CREATE TABLE po konta użytkowników i uprawnienia (CREATE USER, GRANT, REVOKE). |
| PHP i baza | Siedem lekcji z tym, co powtarza się w arkuszach: połączenie, wyniki w liście i tabeli, formularz dodający i szukający, zmiana i usuwanie danych (także odnośnikiem przy wierszu), przekierowanie po zapisie. |
| Arkusze | 101 arkuszy. W każdym zapytania do napisania, skrypt do uzupełnienia na stronie z arkusza i opublikowane rozwiązanie do porównania. |
ALTER, CREATE USER, GRANT), więc takie zapytania kurs porównuje ze wzorem jako tekst. Zamiast obrazów widzisz kolorowe karty z nazwą pliku.PHP i SQL na egzaminie praktycznym
Na wykonanie zadania jest 150 minut. Arkusz z PHP ma zawsze te same części, a każda jest oceniana jako osobny rezultat.
| Część arkusza | Co robisz | Czego dotyczy ten kurs |
|---|---|---|
| Operacje na bazie danych | Importujesz bazę w phpMyAdmin, piszesz cztery zapytania do pliku kwerendy.txt i robisz zrzuty ekranu z wynikami. | Część „SQL” i zapytania w każdym arkuszu. |
| Witryna internetowa | Budujesz stronę w HTML i CSS według opisu i ilustracji. | Stronę dostajesz gotową, żeby skupić się na skrypcie. |
| Skrypt połączenia z bazą | W wyznaczonych miejscach strony wstawiasz kod PHP, który wysyła zapytania i wypisuje wyniki. | Części „PHP od zera” i „PHP i baza” oraz skrypt w każdym arkuszu. |
Wymagania, które powtarzają się w każdym arkuszu
- Skrypt łączy się z serwerem na
localhost, użytkownikrootbez hasła, baza o nazwie podanej w arkuszu. - Na końcu działania skrypt zamyka połączenie z serwerem. Brak
mysqli_close()to stracony punkt. - Nazwy zmiennych i funkcji mają być znaczące, po polsku albo po angielsku.
- Wynik jest wypisywany dokładnie według wzoru z arkusza, znak po znaku: przecinki, dwukropki, jednostki.
- Zapytania w skrypcie to te same zapytania, które zapisujesz w pliku
kwerendy.txt, czasem zmodyfikowane o wartość z formularza.
Jakie zapytania były na egzaminach
Zestawienie 403 zapytań z rozwiązań arkuszy w tym kursie. Jedno zapytanie może należeć do kilku wierszy.
| Rodzaj zapytania | Ile razy | Lekcja |
|---|---|---|
SELECT z WHERE, sortowaniem, LIKE, IN, BETWEEN | 300 | SELECT i WHERE, Sortowanie i wzorce |
złączenie tabel (JOIN, w tym 2 razy LEFT JOIN) | 88 | JOIN, LEFT JOIN |
funkcje COUNT, AVG, MIN, MAX, SUM, grupowanie | 44 | Funkcje i grupowanie |
INSERT | 32 | INSERT, UPDATE, DELETE |
UPDATE | 23 | INSERT, UPDATE, DELETE |
ALTER TABLE | 22 | ALTER TABLE |
CREATE USER, GRANT | 11 | Konta użytkowników, GRANT i REVOKE |
daty i czas: CURDATE, NOW, DATEDIFF, YEAR, MONTH | 8 | Daty i obliczenia |
CREATE TABLE | 7 | CREATE, DROP, TRUNCATE |
DELETE | 5 | INSERT, UPDATE, DELETE |
DROP TABLE, TRUNCATE | 3 | CREATE, DROP, TRUNCATE |
Co oddajesz
00000000000/ folder o nazwie numeru zdającego index.php strona ze skryptem (nazwę pliku podaje arkusz) styl.css kwerendy.txt cztery zapytania kw1.png … kw4.png zrzuty ekranu z wynikami zapytań obrazy z archiwum
Pierwszy skrypt: PHP w środku strony
Plik .php to zwykła strona HTML, w której między znacznikami <?php i ?> stoi kod. Serwer wykonuje ten kod, a do przeglądarki wysyła już tylko wynik. Wszystko poza znacznikami przechodzi bez zmian.
<h1>Biblioteka</h1> <p> <?php echo "Dzisiaj otwarte do 18:00"; // echo wypisuje tekst w tym miejscu strony ?> </p>
Przeglądarka dostaje <p>Dzisiaj otwarte do 18:00</p> i nie wie, że tekst wypisał skrypt. Dlatego echo może wypisywać także znaczniki HTML:
<?php echo "<li>Lalka</li>"; echo "<li>Solaris</li>"; ?>
- Każda instrukcja kończy się średnikiem.
- Tekst stoi w cudzysłowie albo w apostrofach.
- Komentarz zaczyna się od
//albo#, a dłuższy stoi między/*i*/. - Plik musi mieć rozszerzenie
.phpi być otwierany przez serwer (localhost/folder/plik.php), a nie dwuklikiem.
Zmienne, obliczenia i sklejanie tekstu
Nazwa zmiennej zawsze zaczyna się od znaku dolara. Zmienna powstaje przy pierwszym przypisaniu, a typu się nie podaje.
<?php $tytul = "Lalka"; // tekst $strony = 680; // liczba całkowita $cena = 39.90; // liczba z ułamkiem, zawsze z kropką $dostepna = true; // wartość logiczna $rabat = $cena * 0.1; $doZaplaty = $cena - $rabat; ?>
Sklejanie: kropka, nie plus
Teksty łączy się kropką. Plus w PHP służy tylko do dodawania liczb.
echo "Tytuł: " . $tytul . ", stron: " . $strony; // Tytuł: Lalka, stron: 680 echo "Tytuł: $tytul, stron: $strony"; // to samo: zmienna w cudzysłowie zamienia się na wartość echo 'Tytuł: $tytul'; // w apostrofach NIE: wypisze dosłownie $tytul
Gdy w cudzysłowie stoi element tablicy albo zaraz po zmiennej jest litera, otocz zmienną klamrami: "{$wiersz['tytul']} zł".
| Zapis | Wynik | Uwagi |
|---|---|---|
7 + 2, 7 - 2, 7 * 2, 7 / 2 | 9, 5, 14, 3.5 | |
7 % 2 | 1 | Reszta z dzielenia. |
$licznik++, $suma += 5 | Zwiększenie o 1 i o 5. | |
$tekst .= "!" | Doklejenie na końcu tekstu. | |
"5" + "10" | 15 | Plus zamienia teksty na liczby. Inaczej niż w JavaScripcie. |
Warning: Undefined variable $tytul oznacza literówkę w nazwie albo użycie zmiennej, zanim dostała wartość. Wielkość liter ma znaczenie: $Tytul i $tytul to dwie różne zmienne.Warunki
if ($punkty >= 75) {
echo "Zdane";
} elseif ($punkty >= 50) {
echo "Blisko";
} else {
echo "Niezdane";
}
| Zapis | Znaczenie |
|---|---|
==, != | równe, różne |
<, >, <=, >= | porównania liczb |
&&, ||, ! | i, lub, nie |
=== | równe i tego samego typu: 5 == "5" jest prawdą, 5 === "5" nie |
Warunek w środku HTML
Blok if może obejmować zwykły kod strony. Znaczniki PHP zamykasz i otwierasz, a HTML między nimi pojawi się tylko wtedy, gdy warunek jest prawdziwy.
<?php if ($dostepna) { ?>
<p class="ok">Książka jest na półce</p>
<?php } else { ?>
<p class="brak">Wypożyczona</p>
<?php } ?>
= w warunku. if ($punkty = 75) niczego nie porównuje, tylko wpisuje 75 do zmiennej, więc warunek jest zawsze prawdziwy.Pętle
Pętla powtarza ten sam kod. W arkuszach najczęściej wypisuje kolejne wiersze z bazy, ale zasada jest ta sama dla liczb.
for ($i = 1; $i <= 5; $i++) {
echo "<li>Półka nr $i</li>";
}
W nawiasie są trzy części: wartość początkowa licznika, warunek dalszego działania i zmiana licznika po każdym obrocie.
while: dopóki warunek jest prawdziwy
$zostalo = 3;
while ($zostalo > 0) {
echo "Zostało: $zostalo<br>";
$zostalo--;
}
To właśnie while spotkasz przy bazie danych: „dopóki jest następny wiersz, wypisz go”.
Zbieranie wyniku
$suma = 0;
for ($i = 1; $i <= 10; $i++) {
$suma += $i;
}
echo "Suma: $suma"; // 55
Maximum execution time exceeded, tak jak na serwerze.Tablice
Tablica przechowuje wiele wartości pod jedną nazwą. W PHP są dwa rodzaje i oba spotkasz przy bazie danych.
Tablica z numerami
$tytuly = ["Lalka", "Solaris", "Ferdydurke"]; echo $tytuly[0]; // Lalka, numery liczy się od zera echo count($tytuly); // 3 $tytuly[] = "Quo vadis"; // dopisanie na końcu
Tablica z nazwami (asocjacyjna)
$ksiazka = ["tytul" => "Lalka", "autor" => "Bolesław Prus", "rok" => 1890];
echo $ksiazka["tytul"]; // Lalka
echo "{$ksiazka['autor']} ({$ksiazka['rok']})"; // w cudzysłowie: klamry i apostrofy
Dokładnie taką tablicę dostajesz dla każdego wiersza z bazy: nazwami są nazwy kolumn.
foreach: przejście po wszystkich elementach
foreach ($tytuly as $tytul) {
echo "<li>$tytul</li>";
}
foreach ($ksiazka as $nazwa => $wartosc) {
echo "$nazwa: $wartosc<br>";
}
Undefined array key oznacza numer spoza tablicy albo literówkę w nazwie. Nazwa klucza jest tekstem, więc stoi w cudzysłowie lub apostrofach: $ksiazka[tytul] bez nich to błąd.Funkcje
Funkcja to nazwany fragment kodu, który możesz wywołać wiele razy z różnymi danymi. Arkusze rzadko jej wymagają, ale oceniają nazwy, a dobrze nazwana funkcja porządkuje dłuższy skrypt.
function cenaZRabatem($cena, $procent) {
return $cena - $cena * $procent / 100; // return oddaje wynik
}
echo cenaZRabatem(40, 25); // 30
- Parametry to zmienne, które dostają wartości podane przy wywołaniu, w tej samej kolejności.
returnkończy funkcję i oddaje wartość.echowewnątrz funkcji tylko wypisuje, a nic nie oddaje.- Zmienne spoza funkcji nie są w niej widoczne. Wszystko, czego funkcja potrzebuje, przekaż w parametrach.
Funkcje wbudowane, które przydają się w arkuszach
| Funkcja | Co robi |
|---|---|
strlen($tekst) | liczba znaków tekstu |
strtoupper($tekst), strtolower($tekst) | wielkie i małe litery |
round($liczba, 2) | zaokrąglenie do dwóch miejsc po przecinku |
number_format($liczba, 2, ",", " ") | liczba jako tekst: dwa miejsca, przecinek, spacja co trzy cyfry |
rand(1, 6) | losowa liczba całkowita od 1 do 6 |
isset($zmienna), empty($zmienna) | czy zmienna istnieje; czy jest pusta |
Formularze: dane od użytkownika
Formularz wysyła dane do pliku z atrybutu action. Skrypt odbiera je z tablicy $_POST albo $_GET, zależnie od atrybutu method. Kluczem jest atrybut name pola.
<form action="karta.php" method="post">
<input type="text" name="imie">
<input type="number" name="wiek">
<button type="submit">Wyślij</button>
</form>
<?php
if (isset($_POST["imie"])) { // czy formularz w ogóle został wysłany
$imie = $_POST["imie"];
$wiek = $_POST["wiek"];
echo "Witaj $imie, masz $wiek lat";
}
?>
Przy pierwszym wejściu na stronę formularz nie był jeszcze wysłany i tablica $_POST jest pusta. Odczyt $_POST["imie"] bez sprawdzenia skończyłby się ostrzeżeniem Undefined array key. Dlatego kod obsługi stoi zawsze w warunku.
| Warunek | Kiedy prawdziwy |
|---|---|
isset($_POST["imie"]) | pole przyszło z formularza, nawet puste |
!empty($_POST["imie"]) | pole przyszło i coś w nim wpisano |
$_SERVER["REQUEST_METHOD"] == "POST" | strona została otwarta przez wysłanie formularza metodą POST |
POST czy GET
Metoda get dokleja dane do adresu (plik.php?imie=Ola) i czytasz je z $_GET. Tak samo działają odnośniki z parametrem: <a href="plik.php?id=3">. Metoda post wysyła dane niewidocznie. Arkusz zwykle mówi, której użyć.
empty.Pola wyboru: radio, lista i checkbox
Pole tekstowe wysyła to, co wpisano. Pola wyboru wysyłają zawartość atrybutu value zaznaczonej opcji, a nie tekst widoczny obok niej. Arkusze INF.03 z lat 2024–2026 kilka razy każą odczytać przycisk radio, więc warto wiedzieć, co dokładnie przychodzi do skryptu.
Przyciski radio: jedna wartość z kilku
<form action="index.php" method="post">
<label><input type="radio" name="oprawa" value="twarda"> Twarda</label>
<label><input type="radio" name="oprawa" value="miekka"> Miękka</label>
<button type="submit">Zamów</button>
</form>
<?php
if (isset($_POST["oprawa"])) {
echo "Wybrano oprawę: " . $_POST["oprawa"]; // twarda albo miekka
}
?>
Wszystkie przyciski jednej grupy mają to samo name, a różne value. Gdy nic nie zaznaczono, pole w ogóle nie przychodzi, więc isset jest tu konieczny. Przycisk z atrybutem checked jest zaznaczony od początku.
Lista rozwijana
<select name="dostawa"> <option value="kurier">Kurier</option> <option value="paczkomat" selected>Paczkomat</option> </select>
Lista zawsze coś wysyła: zaznaczoną opcję albo pierwszą, gdy nic nie wybrano. Gdy opcja nie ma value, wysyłany jest jej tekst. Opcje wczytane z bazy poznasz w lekcji „Lista z bazy”.
Checkbox: zero, jedna albo kilka wartości
Pojedynczy checkbox działa jak przełącznik: gdy jest zaznaczony, przychodzi jego value, gdy nie, nie przychodzi nic.
<label><input type="checkbox" name="regulamin" value="tak"> Akceptuję regulamin</label>
<?php
if (isset($_POST["regulamin"])) { echo "Dziękujemy"; } else { echo "Zaakceptuj regulamin"; }
?>
Grupa checkboxów, z których można zaznaczyć kilka, dostaje nazwę z nawiasami kwadratowymi. PHP składa wtedy wszystkie zaznaczone wartości w tablicę.
<label><input type="checkbox" name="dodatki[]" value="zakładka"> Zakładka</label>
<label><input type="checkbox" name="dodatki[]" value="folia"> Folia ochronna</label>
<label><input type="checkbox" name="dodatki[]" value="dedykacja"> Dedykacja</label>
<?php
if (isset($_POST["dodatki"])) {
$dodatki = $_POST["dodatki"]; // na przykład ["zakładka", "dedykacja"]
echo "Liczba dodatków: " . count($dodatki);
foreach ($dodatki as $dodatek) {
echo "<li>$dodatek</li>";
}
echo implode(", ", $dodatki); // zakładka, dedykacja
}
?>
W $_POST klucz nazywa się dodatki, już bez nawiasów. Funkcja implode skleja elementy tablicy w jeden tekst z podanym separatorem.
Zaznaczenie zostaje po wysłaniu
Po wysłaniu formularza strona ładuje się od nowa i wszystkie pola wracają do stanu z HTML. Arkusz INF.03 ze stycznia 2026 wymaga, żeby wybrany przycisk radio pozostał zaznaczony. Skrypt dopisuje wtedy checked do przycisku, którego wartość przyszła z formularza:
<?php
$oprawa = "twarda"; // wybór domyślny przy pierwszym wejściu
if (isset($_POST["oprawa"])) { $oprawa = $_POST["oprawa"]; }
?>
<input type="radio" name="oprawa" value="twarda" <?php if ($oprawa == "twarda") echo "checked"; ?>> Twarda
<input type="radio" name="oprawa" value="miekka" <?php if ($oprawa == "miekka") echo "checked"; ?>> Miękka
@$a = $_POST["pole"];. Małpa wycisza ostrzeżenie o brakującym polu, ale problem zostaje: zmienna jest pusta, a dalszy kod wykonuje się, jakby wszystko było w porządku. Sprawdzenie isset mówi wprost, co ma się stać, gdy pola nie ma.Data, liczby i teksty
Bieżąca data
echo date("Y-m-d"); // 2026-10-12, w takim formacie daty trzyma baza
echo date("d.m.Y"); // 12.10.2026
echo date("H:i"); // 14:05
echo date("Y") + 1; // następny rok
| Znak | Znaczenie | Znak | Znaczenie |
|---|---|---|---|
Y | rok, cztery cyfry | H | godzina 00–23 |
m | miesiąc 01–12 | i | minuty |
d | dzień 01–31 | s | sekundy |
N | dzień tygodnia 1–7 | t | liczba dni w miesiącu |
Polska nazwa dnia i miesiąca
date("l") i date("F") zwracają nazwy po angielsku (Wednesday, October). Polskie nazwy bierzesz z własnej tablicy, a numer dnia albo miesiąca służy jako indeks:
$dni = ["", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota", "niedziela"];
$miesiace = ["", "stycznia", "lutego", "marca", "kwietnia", "maja", "czerwca",
"lipca", "sierpnia", "września", "października", "listopada", "grudnia"];
echo "Dziś jest " . $dni[date("N")]; // N: 1 = poniedziałek … 7 = niedziela
echo date("j") . " " . $miesiace[date("n")]; // 23 października
Pusty tekst na początku tablicy zajmuje indeks 0, dzięki czemu poniedziałek ma numer 1, tak jak zwraca go date("N"). Litery j i n to dzień i miesiąc bez zera na początku. Ten sam efekt da tablica z kluczami po angielsku: ["Monday" => "poniedziałek", …] i odczyt $dni[date("l")].
Data przesunięta o kilka dni
$zwrot = date("Y-m-d", strtotime("+14 days")); // za dwa tygodnie
$zwrot = date("Y-m-d", strtotime("2026-01-10 +7 days")); // tydzień po podanej dacie
Liczby i teksty w komunikatach
$cena = 12.5;
echo number_format($cena, 2); // 12.50
echo round(10 / 3, 2); // 3.33
echo intval("42 zł"); // 42, tekst na liczbę całkowitą
echo strtoupper("uwaga"); // UWAGA
echo str_replace("-", ".", "2026-10-12"); // 2026.10.12
Ciasteczka i sesja
Skrypt PHP zaczyna każde wejście na stronę od zera i niczego nie pamięta. Żeby rozpoznać powracającego użytkownika, zapisuje dane w ciasteczku (w przeglądarce) albo w sesji (na serwerze).
Ciasteczko
<?php
if (isset($_COOKIE["wizyta"])) {
echo "Miło nam, że nas znowu odwiedziłeś";
} else {
setcookie("wizyta", "tak", time() + 3600); // nazwa, wartość, ważne przez godzinę
echo "Dzień dobry! Strona wykorzystuje ciasteczka";
}
?>
setcookiemusi zostać wywołane przed pierwszym znakiem HTML wysłanym do przeglądarki, czyli na samej górze pliku.- Ustawione ciasteczko widać w
$_COOKIEdopiero przy następnym wejściu na stronę. - Trzeci argument to chwila wygaśnięcia:
time()podaje bieżący czas w sekundach, a do niego dodajesz czas życia.
Sesja
<?php
session_start(); // na samej górze każdej strony, która korzysta z sesji
if (!isset($_SESSION["licznik"])) {
$_SESSION["licznik"] = 0;
}
$_SESSION["licznik"]++;
echo "Odsłona nr " . $_SESSION["licznik"];
?>
Sesja działa jak tablica, która przetrwa między odsłonami. Tak pamięta się zalogowanego użytkownika: po poprawnym logowaniu zapisujesz $_SESSION["login"], a na pozostałych stronach sprawdzasz, czy istnieje.
Pliki tekstowe
Skrypt może zapisać dane do zwykłego pliku obok strony i odczytać je przy następnym wejściu. Arkusz INF.03 ze stycznia 2025 każe na przykład wypisać na stronie harmonogram szkoleń z bazy i jednocześnie zapisać każdą linię do pliku harmonogram.txt.
Otwarcie, zapis, zamknięcie
$plik = fopen("harmonogram.txt", "w"); // otwórz do zapisu
fwrite($plik, "2026-11-04 Bezpieczeństwo w sieci\n");
fwrite($plik, "2026-11-18 Arkusze kalkulacyjne\n");
fclose($plik); // zamknij, gdy skończysz
fopen zwraca uchwyt pliku, który przekazujesz do pozostałych funkcji, tak jak połączenie z bazą. "\n" to znak końca linii; działa tylko w cudzysłowie.
| Tryb | Co robi |
|---|---|
"r" | tylko odczyt; plik musi istnieć |
"w" | zapis od zera: kasuje dotychczasową treść, a gdy pliku nie ma, tworzy go |
"a" | dopisywanie na końcu; gdy pliku nie ma, tworzy go |
"x" | tworzy nowy plik; gdy już istnieje, zgłasza błąd |
"r+" | odczyt i zapis istniejącego pliku |
Odczyt linia po linii
if (file_exists("harmonogram.txt")) {
$plik = fopen("harmonogram.txt", "r");
while (!feof($plik)) { // dopóki nie koniec pliku
$linia = trim(fgets($plik)); // jedna linia, bez znaku końca linii
if ($linia != "") echo "<li>$linia</li>";
}
fclose($plik);
} else {
echo "Brak harmonogramu";
}
fgets czyta jedną linię razem ze znakiem \n, a trim go obcina. Ostatnia linia pliku zakończonego znakiem nowej linii jest pusta, dlatego warunek ją pomija. fread($plik, 100) czyta zamiast linii podaną liczbę bajtów.
Krótsze zapisy
| Zapis | Co robi |
|---|---|
file_put_contents("plik.txt", $tekst) | otwiera, zapisuje od zera i zamyka |
file_put_contents("plik.txt", $tekst, FILE_APPEND) | to samo, ale dopisuje na końcu |
file_get_contents("plik.txt") | cała treść pliku jako jeden tekst |
file("plik.txt", FILE_IGNORE_NEW_LINES) | tablica linii, bez znaków końca linii |
file_exists("plik.txt") | czy plik istnieje |
unlink("plik.txt") | usuwa plik |
htdocs albo jego podfolder. Na tej stronie pliki istnieją do następnego kliknięcia „Uruchom”, które zaczyna ćwiczenie od zera.SELECT: wybieranie kolumn i wierszy
Lekcje o SQL korzystają z bazy biblioteka. Jej tabele zobaczysz pod każdym ćwiczeniem.
SELECT tytul, autor FROM ksiazki; -- dwie kolumny, wszystkie wiersze SELECT * FROM ksiazki; -- wszystkie kolumny SELECT tytul FROM ksiazki WHERE rok > 1950; -- tylko wiersze spełniające warunek
Arkusz pisze „wybierające jedynie pola tytuł i autor”. Słowo „jedynie” znaczy, że gwiazdka jest błędem: wymieniasz dokładnie te kolumny, w tej kolejności.
| Warunek | Znaczenie |
|---|---|
rok = 1890, rok != 1890 | równe, różne (różne zapisuje się też <>) |
cena <= 30 | porównanie liczb |
gatunek = 'powieść' | tekst zawsze w apostrofach |
rok > 1900 AND cena < 40 | oba warunki naraz |
gatunek = 'poezja' OR gatunek = 'dramat' | przynajmniej jeden |
rok BETWEEN 1900 AND 1950 | od 1900 do 1950 włącznie |
gatunek IN ('poezja', 'dramat') | jedna z wymienionych wartości |
data_zwrotu IS NULL | pole jest puste; = NULL nie działa |
'2026-03-15', a godzinę jako '10:30:00'. Tak samo działa czas BETWEEN '10:00:00' AND '10:59:00' z arkusza EE.09.Pusty tekst to nie NULL
Pole bez wartości może mieć NULL albo pusty tekst ''. To dwie różne rzeczy i arkusz mówi, o którą chodzi.
SELECT pseudonim FROM zawodnicy WHERE tytul != ''; -- pole tytul nie jest pustym tekstem DELETE FROM nagrody WHERE cena IS NULL OR cena = 0; -- „cena nie jest podana lub wynosi 0”
AND i OR razem: nawiasy
AND wiąże mocniej niż OR, tak jak mnożenie mocniej niż dodawanie. Gdy w warunku są oba, otocz nawiasem to, co ma być sprawdzone razem.
SELECT tytul FROM ksiazki WHERE (gatunek = 'poezja' OR gatunek = 'dramat') AND cena < 25; SELECT nazwa, opis FROM abonamenty WHERE opis LIKE '%zdrowie%' OR opis LIKE '%opieką%'; -- dwa wzorce: OR
Cztery grupy poleceń SQL
Polecenia SQL dzieli się według tego, czego dotyczą. Podział przydaje się w części teoretycznej egzaminu i przy uprawnieniach: prawa do danych to co innego niż prawa do struktury.
| Grupa | Do czego | Polecenia |
|---|---|---|
| DQL, język zapytań | odczyt danych | SELECT |
| DML, operowanie danymi | zmiana zawartości tabel | INSERT, UPDATE, DELETE |
| DDL, definiowanie danych | budowa bazy: tabele, kolumny, klucze | CREATE, ALTER, DROP, TRUNCATE |
| DCL, kontrola dostępu | uprawnienia użytkowników | GRANT, REVOKE |
Część podręczników nie wydziela DQL i zalicza SELECT do DML. Spotkasz też grupę TCL z poleceniami transakcji COMMIT i ROLLBACK.
Sortowanie, ograniczanie i wzorce
SELECT tytul, rok FROM ksiazki ORDER BY rok; -- rosnąco SELECT tytul, rok FROM ksiazki ORDER BY rok DESC; -- malejąco SELECT tytul, cena FROM ksiazki ORDER BY cena DESC LIMIT 3; -- trzy najdroższe
„Pięć pierwszych wierszy o najwyższej punktacji” z arkusza to zawsze ORDER BY … DESC LIMIT 5. Kolejność słów w zapytaniu jest stała: SELECT … FROM … WHERE … ORDER BY … LIMIT ….
„Posortowane według nazwiska, a przy tym samym nazwisku według imienia” to dwie kolumny po przecinku. Kierunek podajesz osobno dla każdej: ORDER BY nazwisko, imie albo ORDER BY rok DESC, tytul.
LIKE: tekst pasujący do wzorca
| Wzorzec | Co pasuje |
|---|---|
nazwisko LIKE 'K%' | zaczyna się na K |
nazwisko LIKE '%ski' | kończy się na „ski” |
tytul LIKE '%pan%' | zawiera „pan” w dowolnym miejscu |
kod LIKE '4_-___' | podkreślnik zastępuje dokładnie jeden znak |
Bez powtórzeń
SELECT DISTINCT gatunek FROM ksiazki; -- każdy gatunek tylko raz
Funkcje i grupowanie
SELECT COUNT(*) FROM ksiazki; -- liczba wierszy SELECT AVG(cena), MIN(cena), MAX(cena) FROM ksiazki; SELECT SUM(cena) FROM ksiazki WHERE gatunek = 'powieść';
Funkcja agregująca robi z wielu wierszy jedną wartość. Z GROUP BY liczy ją osobno dla każdej grupy:
SELECT gatunek, COUNT(*) FROM ksiazki GROUP BY gatunek; -- ile książek w każdym gatunku SELECT gatunek, AVG(cena) AS srednia FROM ksiazki GROUP BY gatunek;
AS nadaje kolumnie własną nazwę. W PHP odczytasz ją potem jako $wiersz["srednia"]. Warunek na wynik funkcji agregującej zapisuje się po grupowaniu słowem HAVING, a nie WHERE:
SELECT gatunek, COUNT(*) FROM ksiazki GROUP BY gatunek HAVING COUNT(*) > 2;
Nazwa kolumny ze spacją albo polską literą
Arkusz każe czasem nazwać kolumny wyniku, na przykład „średnia”, „najmniej”, „Średnia cena”. Nazwę ze spacją albo polskimi znakami bierzesz w cudzysłów:
SELECT AVG(cena) AS "Średnia cena", MAX(cena) AS "Maksymalna cena" FROM pojazdy WHERE model = 'meta'; SELECT AVG(liczba_pracownikow) AS srednia, MIN(liczba_pracownikow) AS najmniej, MAX(liczba_pracownikow) AS najwiecej FROM wykonawcy;
Funkcje na pojedynczych wartościach
| Funkcja | Wynik |
|---|---|
YEAR(data), MONTH(data), DAY(data) | część daty jako liczba |
CURDATE(), NOW() | dzisiejsza data; data z godziną |
DATEDIFF(data1, data2) | liczba dni między datami |
CONCAT(imie, ' ', nazwisko) | sklejenie tekstów |
LEFT(opis, 100) | pierwsze 100 znaków |
UPPER(nazwisko), LOWER(nazwisko) | wielkie i małe litery |
ROUND(cena, 1) | zaokrąglenie |
Funkcje na napisach
| Funkcja | Przykład i wynik |
|---|---|
CHAR_LENGTH(tekst) | CHAR_LENGTH('Wiedźmin') → 8, liczba znaków |
LENGTH(tekst) | LENGTH('Wiedźmin') → 9, liczba bajtów: polska litera zajmuje dwa |
SUBSTRING(tekst, od, ile) | SUBSTRING('Solaris', 1, 3) → Sol; znaki liczy się od 1 |
LEFT(tekst, n), RIGHT(tekst, n) | n znaków od początku, od końca |
REPLACE(tekst, co, na_co) | REPLACE('2026-10-23', '-', '.') → 2026.10.23 |
TRIM(tekst) | usuwa spacje z początku i końca |
LOCATE(czego, w_czym) | LOCATE('a', 'Lalka') → 2, miejsce pierwszego wystąpienia; 0, gdy brak |
CONCAT_WS(', ', a, b, c) | skleja wartości, wstawiając między nie separator |
Funkcji można używać w SELECT, w WHERE i w ORDER BY, a także zagnieżdżać:
SELECT CONCAT(LEFT(imie, 1), '. ', UPPER(nazwisko)) AS czytelnik FROM czytelnicy; -- O. NOWAK SELECT tytul FROM ksiazki ORDER BY CHAR_LENGTH(tytul) DESC LIMIT 1; -- najdłuższy tytuł
Losową kolejność daje ORDER BY RAND(), na przykład SELECT tytul FROM ksiazki ORDER BY RAND() LIMIT 1 wybiera jedną losową książkę. Takiego zapytania nie da się sprawdzić porównaniem wyników, bo za każdym razem zwraca co innego.
Daty, czas i obliczenia w zapytaniu
Baza sama zna dzisiejszą datę i umie liczyć na kolumnach. Arkusze korzystają z tego w zapytaniach wybierających, ale też przy wstawianiu i zmianie danych.
Dziś i teraz
| Zapis | Wynik | Przykład |
|---|---|---|
CURDATE(), CURRENT_DATE | dzisiejsza data | 2026-10-23 |
CURTIME(), CURRENT_TIME | bieżąca godzina | 14:05:12 |
NOW() | data z godziną | 2026-10-23 14:05:12 |
INSERT INTO rejestr VALUES (NULL, CURDATE(), 1, 14); -- „z dzisiejszą datą” INSERT INTO zgloszenia VALUES (NULL, 1, 4, 'Warszawa, Staszica 34/5', 1, NOW()); -- data i godzina zgłoszenia UPDATE wagi SET dzien = CURRENT_DATE, czas = CURRENT_TIME WHERE lokalizacje_id BETWEEN 2 AND 4;
CURRENT_DATE możesz pisać z nawiasami albo bez. Funkcja nie stoi w apostrofach: 'CURDATE()' byłoby zwykłym tekstem.
Części daty
SELECT dataZadania, wpis FROM zadania WHERE MONTH(dataZadania) = 7; -- lipiec dowolnego roku SELECT id, nazwisko FROM pracownicy WHERE YEAR(dataUr) BETWEEN 1970 AND 1979; -- urodzeni w latach 70.
Obliczenia na datach
SELECT nick, YEAR(NOW()) - rejestracja AS latNaForum FROM konta; -- ile lat minęło od roku rejestracji SELECT pseudonim, DATEDIFF(CURDATE(), data_zdobycia) AS dni FROM zawodnicy; -- ile dni minęło od daty SELECT DATE_ADD(data_wypozyczenia, INTERVAL 30 DAY) FROM wypozyczenia; -- data o 30 dni późniejsza
DATEDIFF(późniejsza, wcześniejsza) zwraca liczbę dni. Zamiana kolejności daje wynik ujemny.
Obliczenia na liczbach
SELECT tytul, cena * 1.23 AS brutto FROM ksiazki; -- działanie na każdej wartości kolumny SELECT ROUND(AVG(temperatura), 2) FROM pomiary; -- średnia z dwoma miejscami po przecinku SELECT ROUND(AVG(liczba_doroslych)) FROM wycieczki; -- bez drugiego argumentu: do liczby całkowitej UPDATE ksiazki SET cena = cena - 2 WHERE rok < 1900; -- nowa wartość liczona ze starej
Arkusz INF.03 z czerwca 2024 każe wstawić losową wagę od 1 do 10: FLOOR(RAND() * 10 + 1). RAND() daje ułamek od 0 do 1, mnożenie i dodanie przesuwa go do przedziału 1–10,99, a FLOOR obcina część ułamkową.
CURDATE().JOIN: dane z dwóch tabel
Tabela wypozyczenia nie zawiera tytułu książki, tylko jej numer w kolumnie id_ksiazki. Żeby zobaczyć tytuł, trzeba połączyć ją z tabelą ksiazki. Arkusz mówi o tym słowami „zapytanie wykorzystuje relację”.
SELECT ksiazki.tytul, wypozyczenia.data_wypozyczenia FROM wypozyczenia JOIN ksiazki ON wypozyczenia.id_ksiazki = ksiazki.id;
Po ON stoi warunek łączący: klucz obcy z jednej tabeli równa się kluczowi głównemu drugiej. Gdy nazwa kolumny występuje w obu tabelach (zwykle id), poprzedzasz ją nazwą tabeli i kropką.
Trzy tabele i dodatkowy warunek
SELECT czytelnicy.nazwisko, ksiazki.tytul FROM wypozyczenia JOIN ksiazki ON wypozyczenia.id_ksiazki = ksiazki.id JOIN czytelnicy ON wypozyczenia.id_czytelnika = czytelnicy.id WHERE wypozyczenia.data_zwrotu IS NULL;
To samo można zapisać starszym sposobem, z tabelami po przecinku i warunkiem łączącym w WHERE. Klucze oceniania uznają oba zapisy:
SELECT ksiazki.tytul, wypozyczenia.data_wypozyczenia FROM wypozyczenia, ksiazki WHERE wypozyczenia.id_ksiazki = ksiazki.id;
INNER JOIN to dokładnie to samo co JOIN; w rozwiązaniach arkuszy spotkasz oba zapisy. Dodatkowy warunek możesz dopisać w WHERE albo po AND w samym ON:
SELECT klienci.imie, opinie.opinia FROM klienci JOIN opinie ON klienci.id = opinie.klienci_id AND opinie.Typy_id IN (2, 3);
Złączenie z grupowaniem
SELECT ksiazki.gatunek, COUNT(*) AS wypozyczen FROM wypozyczenia JOIN ksiazki ON wypozyczenia.id_ksiazki = ksiazki.id GROUP BY ksiazki.gatunek;
Rodzaje relacji i tabela pośrednicząca
| Relacja | Znaczenie | Jak zapisana |
|---|---|---|
| 1:1, jeden do jednego | uczeń i jego legitymacja | klucz obcy w jednej z tabel, z wartościami bez powtórzeń |
| 1:n, jeden do wielu | jeden gatunek, wiele książek | klucz obcy po stronie „wielu”: w tabeli książek kolumna id_gatunku |
| n:m, wiele do wielu | czytelnik wypożycza wiele książek, książkę wypożycza wielu czytelników | osobna tabela z dwoma kluczami obcymi |
Relacji wiele do wielu nie da się zapisać jedną kolumną, bo w jednej komórce musiałoby stać kilka numerów. Dlatego powstaje tabela pośrednicząca, w której każdy wiersz to jedna para. W bazie biblioteka jest nią właśnie wypozyczenia: kolumny id_ksiazki i id_czytelnika wskazują książkę i czytelnika, a przy okazji tabela przechowuje dane samej pary, czyli daty. Zapytanie o „kto co wypożyczył” łączy więc trzy tabele, z tabelą pośredniczącą w środku, tak jak w przykładzie wyżej.
Tabelę pośredniczącą rozpoznasz w arkuszu po tym, że ma dwa klucze obce do dwóch różnych tabel. Klucze obce zdradza przedrostek albo przyrostek id w nazwie kolumny, na przykład id_ksiazki albo Ksiazki_id.
LEFT JOIN: także wiersze bez pary
Zwykły JOIN zwraca tylko te wiersze, które mają parę w drugiej tabeli. Czytelnik, który nigdy nic nie wypożyczył, nie pojawi się w złączeniu czytelników z wypożyczeniami. Gdy arkusz chce wszystkich, także tych bez pary, potrzebny jest LEFT JOIN. Pojawił się w dwóch arkuszach INF.03 z 2026 roku.
SELECT czytelnicy.nazwisko, wypozyczenia.data_wypozyczenia FROM czytelnicy LEFT JOIN wypozyczenia ON czytelnicy.id = wypozyczenia.id_czytelnika;
Tabela przed LEFT JOIN (lewa) daje wszystkie swoje wiersze. Tam, gdzie w prawej tabeli nie ma pary, kolumny prawej tabeli mają wartość NULL. RIGHT JOIN działa odwrotnie, ale ten sam wynik daje zamiana tabel miejscami.
Wiersze, które nie mają pary
„Pracownicy, którzy nie mają żadnego wpisu w rejestrze”: złącz przez LEFT JOIN i zostaw tylko wiersze z NULL po prawej stronie.
SELECT personel.id, personel.nazwisko FROM personel LEFT JOIN rejestr ON personel.id = rejestr.id_personel WHERE rejestr.id_personel IS NULL; -- arkusz INF.03, styczeń 2026
Liczenie z zerami
Przy LEFT JOIN licz konkretną kolumnę z prawej tabeli, a nie gwiazdkę. COUNT(*) policzy też wiersz z samymi NULL i da 1 zamiast 0.
SELECT czytelnicy.nazwisko, COUNT(wypozyczenia.id) FROM czytelnicy LEFT JOIN wypozyczenia ON czytelnicy.id = wypozyczenia.id_czytelnika GROUP BY czytelnicy.id, czytelnicy.nazwisko;
Trzy tabele, z których jedna bywa pusta
SELECT imie, nazwisko, Zlecenie.data, Wynik.data, plik FROM Pacjent JOIN Zlecenie ON Zlecenie.idPacjenta = Pacjent.id LEFT JOIN Wynik ON Zlecenie.id = Wynik.idZlecenia; -- zlecenia bez wyniku też są na liście
JOIN zgubiłby właśnie te wiersze.INSERT, UPDATE, DELETE
Dodanie wiersza
INSERT INTO czytelnicy (imie, nazwisko, miasto) VALUES ('Ewa', 'Lis', 'Opole');
INSERT INTO czytelnicy VALUES (NULL, 'Ewa', 'Lis', 'Opole'); -- bez listy kolumn: wszystkie wartości po kolei
Gdy arkusz pisze „klucz główny nadawany automatycznie”, w miejscu id wpisujesz NULL albo pomijasz tę kolumnę na liście. Baza sama nada kolejny numer.
Zmiana danych
UPDATE ksiazki SET cena = 45 WHERE id = 3; UPDATE ksiazki SET cena = cena * 1.1, dostepna = 1 WHERE gatunek = 'poezja';
Usunięcie wierszy
DELETE FROM wypozyczenia WHERE data_zwrotu IS NOT NULL;
UPDATE ksiazki SET cena = 0 zmieni cenę wszystkich książek, a DELETE FROM ksiazki usunie wszystkie. Zawsze najpierw sprawdź warunek zwykłym SELECT.W ćwiczeniach z tymi poleceniami kurs porównuje zawartość tabel po Twoim zapytaniu i po zapytaniu wzorcowym. Każde sprawdzenie zaczyna od świeżej kopii bazy.
Zmiana tabeli: ALTER TABLE
Czwarte zapytanie w arkuszu często nie dotyczy danych, tylko budowy bazy albo kont użytkowników. Najczęściej to dodanie kolumny: takie zapytanie było w 19 arkuszach. Tych poleceń ta strona nie wykonuje, więc ćwiczenia porównują Twój zapis ze wzorem. Wielkość liter słów kluczowych, odstępy, słowo COLUMN i końcowy średnik nie mają znaczenia.
Typy kolumn
| Arkusz mówi | Typ w MySQL |
|---|---|
| typ numeryczny, całkowity | INT |
| liczba całkowita bez znaku, dodatnia | INT UNSIGNED |
| mała liczba całkowita (do 255 bez znaku) | TINYINT, TINYINT UNSIGNED |
| typ napisowy o maksymalnej długości 50 znaków | VARCHAR(50) |
| typ napisowy, długi tekst, bez podanej długości | TEXT |
| liczba z dwoma miejscami po przecinku | DECIMAL(6,2) albo FLOAT |
| typ logiczny | BOOLEAN (w MySQL to samo co TINYINT(1)) |
| data; godzina; data i czas | DATE; TIME; DATETIME |
Dodanie kolumny
ALTER TABLE ksiazki ADD wydawnictwo VARCHAR(50); -- na końcu tabeli ALTER TABLE ksiazki ADD COLUMN wydawnictwo VARCHAR(50); -- to samo, słowo COLUMN można dopisać ALTER TABLE wycieczki ADD COLUMN liczbaDni INT UNSIGNED AFTER dataWyjazdu; -- „umieszczona po kolumnie dataWyjazdu” ALTER TABLE ksiazka ADD COLUMN rezerwacja TINYINT(1) DEFAULT 0; -- „z wartością domyślną 0” ALTER TABLE ksiazki ADD numer INT FIRST; -- jako pierwsza kolumna
AFTER i FIRST stoją na samym końcu, po typie i po DEFAULT. Bez nich kolumna trafia na koniec tabeli.
Usunięcie, zmiana nazwy, zmiana typu
ALTER TABLE wycieczki DROP COLUMN dataWyjazdu; -- usunięcie kolumny ALTER TABLE zapisy CHANGE COLUMN Id_klienta Id_sluchacza INT; -- nowa nazwa: stara, nowa, typ ALTER TABLE ksiazki MODIFY cena DECIMAL(6,2); -- ta sama nazwa, nowy typ ALTER TABLE ksiazki RENAME COLUMN rok TO rok_wydania; -- sama zmiana nazwy (MySQL 8) ALTER TABLE ksiazki RENAME TO katalog; -- zmiana nazwy tabeli
Przy CHANGE podajesz typ nawet wtedy, gdy zmienia się tylko nazwa. Arkusz INF.03 ze stycznia 2025 chciał właśnie tego.
Klucz główny w istniejącej tabeli
ALTER TABLE zamowienia ADD PRIMARY KEY (id); -- arkusz E.14, czerwiec 2020 ALTER TABLE zamowienia MODIFY id INT UNSIGNED NOT NULL AUTO_INCREMENT; -- i numeracja automatyczna
CREATE TABLE, DROP i TRUNCATE
Siedem arkuszy każe utworzyć tabelę. Opis kolumn w arkuszu zamieniasz na listę w nawiasie: nazwa, typ, a po nim cechy kolumny. Kolumny oddzielasz przecinkami, po ostatniej przecinka nie ma.
CREATE TABLE klienci ( id_klienta INT UNSIGNED PRIMARY KEY AUTO_INCREMENT NOT NULL, imie VARCHAR(20), nazwisko VARCHAR(40), data_ur DATE ); -- arkusz INF.03, czerwiec 2025
| Arkusz mówi | Cecha kolumny |
|---|---|
| klucz główny | PRIMARY KEY |
| z automatycznym numerowaniem, autoinkrementacją | AUTO_INCREMENT (tylko przy kluczu) |
| niepusta, wymagana, nie może przyjmować wartości NULL | NOT NULL |
| może być pusta | NULL albo nic |
| liczba bez znaku, nieujemna | UNSIGNED, zaraz po typie |
| z wartością domyślną | DEFAULT 0, DEFAULT 'brak' |
Kolejność cech po typie jest dowolna: PRIMARY KEY AUTO_INCREMENT NOT NULL i NOT NULL AUTO_INCREMENT PRIMARY KEY to to samo. Tylko UNSIGNED musi stać tuż po typie.
Klucz główny osobno i klucz obcy
CREATE TABLE wpisy ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, wpis TEXT, uzytkownik_id INT UNSIGNED NOT NULL, PRIMARY KEY (id), -- klucz na końcu listy FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) -- relacja z tabelą uzytkownicy );
Klucza obcego żaden z arkuszy tego kursu nie wymagał w zapytaniu, ale tak wygląda relacja, którą widzisz w zrzutach baz. Klucz obcy musi mieć ten sam typ co klucz, na który wskazuje.
Usunięcie tabeli albo tylko danych
DROP TABLE liga; -- tabela znika razem z danymi i strukturą TRUNCATE rozgrywka; -- tabela zostaje, wszystkie wiersze znikają, numeracja id wraca do 1 TRUNCATE TABLE rozgrywka; -- to samo DELETE FROM rozgrywka; -- też usuwa wszystkie wiersze, ale numeracja id idzie dalej
„Usuwające tabelę” to DROP TABLE. „Usuwające wszystkie rekordy z tabeli” albo „czyszczące tabelę” to TRUNCATE (przyjmowane jest też DELETE FROM bez warunku).
CREATE TABLE zrób zrzut ekranu struktury nowej tabeli z phpMyAdmin (zakładka Struktura), bo samo polecenie nie zwraca wierszy. Arkusz zwykle mówi, co ma być widać na zrzucie.Konta użytkowników
Serwer MySQL ma własną listę kont, niezależną od tabel w bazach. Skrypty z arkuszy łączą się jako root, który może wszystko. Arkusz każe czasem założyć konto dla kogoś, kto ma mieć mniej możliwości: tak wygląda zapytanie „tworzące użytkownika anna z hasłem Anna4!”. Takie zapytanie jest w dziewięciu arkuszach z tego kursu, a w trzech z nich następne zapytanie nadaje temu kontu uprawnienia.
Nazwa konta: użytkownik i host
Konto to para: nazwa użytkownika i komputer (host), z którego wolno mu się łączyć. Zapisuje się je w dwóch osobnych apostrofach połączonych znakiem @.
| Zapis | Kto może się zalogować |
|---|---|
'anna'@'localhost' | anna, tylko z tego samego komputera, na którym działa serwer (tak jest w XAMPP) |
'anna'@'%' | anna, z dowolnego komputera |
'anna' | to samo co 'anna'@'%': gdy hosta nie podasz, MySQL dopisze % |
'anna'@'192.168.1.20' | anna, tylko z komputera o tym adresie |
Arkusz mówi „na localhost” albo „na serwerze localhost”, kiedy chce @'localhost'. Gdy o hoście nie wspomina, zostaw samą nazwę. 'anna'@'localhost' i 'anna'@'%' to dla serwera dwa różne konta, więc host w GRANT musi być ten sam co w CREATE USER.
Tworzenie konta z hasłem
CREATE USER 'anna' IDENTIFIED BY 'Anna4!'; -- arkusz nie podaje hosta CREATE USER 'anna'@'localhost' IDENTIFIED BY 'anna123'; -- „na localhost” CREATE USER IF NOT EXISTS 'anna'@'localhost' IDENTIFIED BY 'anna123'; -- bez błędu, gdy konto już jest
- Hasło stoi w apostrofach i jest przepisywane znak po znaku. Wielkość liter ma znaczenie, a znaki
@ ! # &w haśle są zwykłymi znakami:'n@JEM1@!'. - Nazwy użytkowników też rozróżniają wielkość liter:
'KatarzynaNowak'i'katarzynanowak'to dwa konta. - Nowe konto nie ma żadnych praw do baz. Może się zalogować, ale nic nie zobaczy, dopóki nie dostanie uprawnień (następna lekcja).
Zmiana hasła, zmiana nazwy, usunięcie
ALTER USER 'anna'@'localhost' IDENTIFIED BY 'Nowe#2026'; -- nowe hasło SET PASSWORD FOR 'anna'@'localhost' = 'Nowe#2026'; -- to samo, starszy zapis RENAME USER 'anna'@'localhost' TO 'anna.nowak'@'localhost'; -- nowa nazwa DROP USER 'anna'@'localhost'; -- usunięcie konta razem z jego prawami
Jakie konta są na serwerze
SELECT user, host FROM mysql.user;
Lista kont jest w tabeli user systemowej bazy mysql. W phpMyAdmin ten sam widok jest w zakładce Konta użytkowników, a przycisk Dodaj konto użytkownika otwiera formularz, który tworzy konto bez pisania SQL.
kwerendy.txt i wykonujesz je w zakładce SQL w phpMyAdmin. Zrzut ekranu pokazuje komunikat o wykonaniu polecenia (MySQL zwrócił pusty wynik), bo CREATE USER nie zwraca wierszy. Drugie wykonanie tego samego zapytania kończy się błędem Operation CREATE USER failed, bo konto już istnieje: wtedy usuń je przez DROP USER i spróbuj jeszcze raz.Uprawnienia: GRANT i REVOKE
Uprawnienie mówi, co konto może zrobić i z czym. GRANT je nadaje, REVOKE odbiera. Zapytanie ma zawsze trzy części: jakie prawa, do czego i dla kogo.
GRANT SELECT, INSERT ON biblioteka.wypozyczenia TO 'anna'@'localhost'; -- jakie prawa do czego dla kogo
Jakie prawa: słowa z arkusza
Arkusz opisuje prawa słowami, a Ty zamieniasz je na nazwy z MySQL. Kolejność praw na liście nie ma znaczenia.
| Arkusz mówi | Prawo |
|---|---|
| wybierania, przeglądania, odczytu danych | SELECT |
| wstawiania, dodawania, wpisywania rekordów | INSERT |
| aktualizacji, modyfikacji, zmiany danych | UPDATE |
| usuwania rekordów, usuwania danych | DELETE |
| tworzenia obiektu, tworzenia tabel | CREATE |
| edycji struktury obiektu, zmiany struktury tabeli | ALTER |
| usuwania obiektu, usuwania tabel | DROP |
| tworzenia indeksów | INDEX |
| wszystkie prawa, pełne uprawnienia | ALL PRIVILEGES (krócej ALL) |
Uważaj na słowo „usuwania”. „Usuwania rekordów” to DELETE, bo dotyczy danych. „Usuwania obiektu” albo „tabeli” to DROP, bo dotyczy struktury.
Do czego: zakres po ON
| Zapis | Zakres |
|---|---|
ON biblioteka.ksiazki | jedna tabela w jednej bazie |
ON biblioteka.* | wszystkie tabele bazy biblioteka, także te utworzone później |
ON *.* | wszystkie bazy na serwerze; tak wygląda konto administratora |
„Do tabel bazy sklep” albo „do bazy sklep” to sklep.*. „Dla tabeli zgloszenia” to nazwa_bazy.zgloszenia: dopisz nazwę bazy z arkusza, nawet jeśli w treści zapytania jej nie ma.
Dla kogo: konto po TO
Konto zapisujesz tak samo jak przy CREATE USER. Gdy arkusz założył konto 'anna' bez hosta, w GRANT też piszesz 'anna'.
CREATE USER 'anna' IDENTIFIED BY 'Anna4!'; GRANT INSERT, UPDATE, SELECT ON ratownictwo.zgloszenia TO 'anna'; -- arkusz EE.09, czerwiec 2019 CREATE USER 'adam' IDENTIFIED BY 'Adam6&'; GRANT CREATE, ALTER, DROP ON ee09.ratownicy TO 'adam'; -- arkusz EE.09, styczeń 2022 GRANT INSERT ON sklep.* TO 'klient'; -- „jedynie do wpisywania rekordów do tabel bazy sklep”
Odbieranie i sprawdzanie praw
REVOKE UPDATE ON biblioteka.ksiazki FROM 'anna'@'localhost'; -- FROM zamiast TO REVOKE ALL PRIVILEGES ON biblioteka.* FROM 'anna'@'localhost'; -- wszystkie prawa do bazy SHOW GRANTS FOR 'anna'@'localhost'; -- lista praw konta
REVOKE odbiera dokładnie to, co wymienisz, i w tym samym zakresie. Prawa nadanego na biblioteka.* nie da się odebrać dla jednej tabeli: trzeba odebrać całość i nadać od nowa to, co ma zostać.
Prawo do przekazywania praw
GRANT SELECT ON biblioteka.* TO 'anna'@'localhost' WITH GRANT OPTION;
Z dopiskiem WITH GRANT OPTION anna może sama nadawać innym te prawa, które dostała. Dopisz go, gdy zadanie mówi o możliwości przekazywania albo nadawania uprawnień innym użytkownikom.
GRANT, REVOKE i CREATE USER działają od razu. FLUSH PRIVILEGES jest potrzebne tylko wtedy, gdy ktoś zmienia tabele w bazie mysql zwykłym INSERT albo UPDATE.Połączenie, zapytanie, zamknięcie
Każdy skrypt z arkusza składa się z tych samych czterech kroków. Funkcje są wypisane w tabeli na końcu arkusza, więc nie trzeba ich pamiętać dokładnie, ale trzeba wiedzieć, w jakiej kolejności ich użyć.
<?php
$polaczenie = mysqli_connect("localhost", "root", "", "biblioteka"); // 1. serwer, użytkownik, hasło, baza
$zapytanie = "SELECT COUNT(*) FROM ksiazki";
$wynik = mysqli_query($polaczenie, $zapytanie); // 2. wysłanie zapytania
$wiersz = mysqli_fetch_array($wynik); // 3. pobranie wiersza z wyniku
echo "W bibliotece jest $wiersz[0] książek";
mysqli_close($polaczenie); // 4. zamknięcie połączenia
?>
mysqli_connectzwraca połączenie, które przekazujesz do pozostałych funkcji.mysqli_queryzwraca wynik zapytania, ale jeszcze nie dane. Dane wyciąga dopieromysqli_fetch_array, po jednym wierszu.mysqli_fetch_arrayzwraca tablicę z wartościami wiersza. Do kolumn sięgasz numerem ($wiersz[0]) albo nazwą ($wiersz["tytul"]).mysqli_closestoi na samym końcu, po ostatnim zapytaniu na stronie. To osobno punktowane wymaganie.
Trzy sposoby pobrania wiersza
| Funkcja | Kolumny dostępne jako |
|---|---|
mysqli_fetch_array($wynik) | numer i nazwa: $wiersz[0] albo $wiersz["tytul"] |
mysqli_fetch_row($wynik) | tylko numer: $wiersz[0], $wiersz[1]… |
mysqli_fetch_assoc($wynik) | tylko nazwa: $wiersz["tytul"] |
Wszystkie trzy działają w pętli while tak samo. Numery kolumn idą w kolejności z SELECT, więc przy SELECT tytul, autor numer 0 to tytuł. Numer w tekście w cudzysłowie piszesz bez klamer: "<td>$wiersz[0]</td>". Wiele rozwiązań arkuszy używa mysqli_fetch_row; nazwy kolumn są czytelniejsze, ale oba zapisy dają ten sam wynik.
Unknown database to literówka w nazwie bazy, Access denied to zły użytkownik lub hasło, a You have an error in your SQL syntax wskazuje miejsce błędu w zapytaniu. Zapytanie najlepiej najpierw sprawdzić osobno, a dopiero potem wkleić do PHP.Wiele wierszy: pętla while
Zapytanie zwykle zwraca wiele wierszy. Każde wywołanie mysqli_fetch_array oddaje kolejny, a gdy wiersze się skończą, zwraca pustą wartość i pętla się kończy.
<ul>
<?php
$polaczenie = mysqli_connect("localhost", "root", "", "biblioteka");
$wynik = mysqli_query($polaczenie, "SELECT tytul, autor FROM ksiazki");
while ($wiersz = mysqli_fetch_array($wynik)) {
echo "<li>" . $wiersz["tytul"] . ", " . $wiersz["autor"] . "</li>";
}
mysqli_close($polaczenie);
?>
</ul>
Znaczniki <ul> stoją poza pętlą, bo lista jest jedna. W pętli powstaje tylko to, co się powtarza: element listy, wiersz tabeli, blok z obrazem.
Wzór z arkusza
Arkusz podaje format tak: „<tytul>, autor: <autor>, gdzie nawiasy <> oznaczają wartość pobraną z bazy”. Wszystko poza nawiasami przepisujesz dosłownie, razem z przecinkami i spacjami.
echo "<li>{$wiersz['tytul']}, autor: {$wiersz['autor']}</li>";
Obraz z nazwą pliku z bazy
echo "<img src='{$wiersz['okladka']}' alt='{$wiersz['tytul']}'>";
Wewnątrz tekstu w cudzysłowie atrybuty HTML zapisujesz w apostrofach. Inaczej cudzysłów atrybutu zakończyłby tekst PHP.
Tabela, licznik wierszy i brak wyników
Wiersze tabeli
<table>
<tr><th>Tytuł</th><th>Rok</th></tr>
<?php
while ($wiersz = mysqli_fetch_array($wynik)) {
echo "<tr><td>{$wiersz['tytul']}</td><td>{$wiersz['rok']}</td></tr>";
}
?>
</table>
Wiersz nagłówkowy jest w HTML, poza pętlą. Każdy obrót pętli dopisuje jeden wiersz <tr> z komórkami.
Ile wierszy zwróciło zapytanie
$ile = mysqli_num_rows($wynik); echo "Znaleziono: $ile";
Gdy nie ma żadnego wiersza
if (mysqli_num_rows($wynik) == 0) {
echo "Brak książek";
} else {
while ($wiersz = mysqli_fetch_array($wynik)) {
echo "<li>{$wiersz['tytul']}</li>";
}
}
Licznik w pętli
Numer porządkowy, którego nie ma w bazie, liczysz własną zmienną zwiększaną w każdym obrocie.
$numer = 1;
while ($wiersz = mysqli_fetch_array($wynik)) {
echo "<tr><td>$numer</td><td>{$wiersz['tytul']}</td></tr>";
$numer++;
}
Formularz, który dodaje wiersz
Wartości z formularza wklejasz do zapytania INSERT. Teksty muszą trafić do zapytania w apostrofach, liczby bez.
<?php
if (!empty($_POST["imie"]) && !empty($_POST["nazwisko"])) {
$imie = $_POST["imie"];
$nazwisko = $_POST["nazwisko"];
$miasto = $_POST["miasto"];
$polaczenie = mysqli_connect("localhost", "root", "", "biblioteka");
$zapytanie = "INSERT INTO czytelnicy VALUES (NULL, '$imie', '$nazwisko', '$miasto')";
mysqli_query($polaczenie, $zapytanie);
mysqli_close($polaczenie);
echo "Czytelnik $imie $nazwisko został dodany";
}
?>
- Warunek z
emptypilnuje, żeby przy pierwszym wejściu i przy pustych polach nic się nie dodało. - Zapytanie jest zwykłym tekstem PHP. Zmienne w cudzysłowie zamieniają się na wartości, więc baza dostaje gotowe polecenie.
- Kolejność wartości odpowiada kolejności kolumn w tabeli.
NULLna początku to automatyczny klucz główny.
Jak sprawdzić, co poszło do bazy
echo $zapytanie; // wypisz zapytanie zamiast je wysyłać i wklej je do phpMyAdmin
O'Hara zamknie tekst w zapytaniu za wcześnie i baza zgłosi błąd składni. Na egzaminie nikt tego nie sprawdza, ale w prawdziwej aplikacji wartości przepuszcza się przez mysqli_real_escape_string($polaczenie, $tekst).Formularz, który wyszukuje
Arkusz mówi wtedy: „skrypt wysyła zapytanie 2 zmodyfikowane tak, że wybierany jest wiersz o id podanym w polu edycyjnym”. Bierzesz zapytanie z pliku kwerendy.txt i w miejsce stałej wartości wstawiasz zmienną.
<?php
if (!empty($_POST["rok"])) {
$rok = $_POST["rok"];
$polaczenie = mysqli_connect("localhost", "root", "", "biblioteka");
$wynik = mysqli_query($polaczenie, "SELECT tytul, autor FROM ksiazki WHERE rok > $rok");
while ($wiersz = mysqli_fetch_array($wynik)) {
echo "<p>{$wiersz['tytul']} ({$wiersz['autor']})</p>";
}
mysqli_close($polaczenie);
}
?>
Tekst w warunku
$miasto = $_POST["miasto"]; $zapytanie = "SELECT imie, nazwisko FROM czytelnicy WHERE miasto = '$miasto'"; // tekst w apostrofach $zapytanie = "SELECT tytul FROM ksiazki WHERE tytul LIKE '%$fraza%'"; // zawiera frazę
Odnośnik zamiast formularza
Tak samo działa wybór przez kliknięcie odnośnika. Wartość przychodzi wtedy w adresie i czytasz ją z $_GET.
echo "<a href='ksiazka.php?id={$wiersz['id']}'>{$wiersz['tytul']}</a>";
// a po kliknięciu:
if (isset($_GET["id"])) { $id = $_GET["id"]; … }
Zmiana i usuwanie danych, przekierowanie
<?php
if (!empty($_POST["id"])) {
$id = $_POST["id"];
$polaczenie = mysqli_connect("localhost", "root", "", "biblioteka");
mysqli_query($polaczenie, "UPDATE ksiazki SET dostepna = 0 WHERE id = $id");
if (mysqli_affected_rows($polaczenie) == 0) {
echo "Nie ma książki o takim numerze";
} else {
echo "Książka nr $id została wypożyczona";
}
mysqli_close($polaczenie);
}
?>
mysqli_affected_rows mówi, ile wierszy zmieniło ostatnie polecenie INSERT, UPDATE albo DELETE. Zero oznacza, że żaden wiersz nie pasował do warunku.
Kolejność na stronie ma znaczenie
Skrypt wykonuje się od góry do dołu. Jeśli lista książek jest wypisywana wyżej niż kod, który zmienia dane, strona pokaże jeszcze stary stan. Są dwa wyjścia: umieścić zmianę nad listą albo po zmianie wczytać stronę jeszcze raz.
mysqli_query($polaczenie, "DELETE FROM wypozyczenia WHERE id = $id");
mysqli_close($polaczenie);
header("Location: index.php"); // przeglądarka otwiera stronę od nowa, już z nowymi danymi
Odnośnik „usuń” przy każdym wierszu
Zamiast wpisywać numer w formularz, można przy każdym wierszu listy wypisać odnośnik z jego kluczem głównym. Kliknięcie otwiera tę samą stronę z parametrem w adresie, a skrypt na górze strony go odczytuje.
<?php
$polaczenie = mysqli_connect("localhost", "root", "", "biblioteka");
if (isset($_GET["usun"])) { // kliknięto odnośnik: czytelnicy.php?usun=3
$id = $_GET["usun"];
mysqli_query($polaczenie, "DELETE FROM czytelnicy WHERE id = $id");
}
$wynik = mysqli_query($polaczenie, "SELECT id, imie, nazwisko FROM czytelnicy");
while ($wiersz = mysqli_fetch_row($wynik)) {
echo "<li>$wiersz[1] $wiersz[2] <a href='czytelnicy.php?usun=$wiersz[0]'>usuń</a></li>";
}
mysqli_close($polaczenie);
?>
Usuwanie stoi przed wypisaniem listy, więc lista od razu pokazuje stan po zmianie. Tak samo buduje się odnośniki „edytuj” albo „szczegóły”, które przekazują id do innej strony.
Przekierowanie po wysłaniu formularza
Po dodaniu wiersza formularzem odświeżenie strony klawiszem F5 wysyła formularz drugi raz i w bazie pojawia się duplikat. Zapobiega temu przekierowanie zaraz po zapisie, nazywane wzorcem PRG (wysłanie, przekierowanie, pobranie):
<?php
if (!empty($_POST["nazwisko"])) {
// … INSERT …
header("Location: " . $_SERVER["PHP_SELF"]); // ta sama strona, już bez danych formularza
exit; // nie wykonuj dalszej części skryptu
}
?>
$_SERVER["PHP_SELF"] to adres bieżącego skryptu, więc kod działa bez zmian w każdym pliku. exit zatrzymuje skrypt: przeglądarka i tak za chwilę otworzy stronę od nowa. Po przekierowaniu nie da się już wypisać komunikatu „dodano”, więc gdy arkusz wymaga komunikatu, zostań przy wersji bez przekierowania.
Lista rozwijana z bazy i dwa zapytania na stronie
Opcje listy rozwijanej często pochodzą z tabeli. W atrybucie value trafia klucz główny, a tekstem opcji jest nazwa widoczna dla użytkownika.
<select name="czytelnik">
<?php
$polaczenie = mysqli_connect("localhost", "root", "", "biblioteka");
$wynik = mysqli_query($polaczenie, "SELECT id, imie, nazwisko FROM czytelnicy");
while ($wiersz = mysqli_fetch_array($wynik)) {
echo "<option value='{$wiersz['id']}'>{$wiersz['imie']} {$wiersz['nazwisko']}</option>";
}
?>
</select>
Po wysłaniu formularza w $_POST["czytelnik"] jest numer wybranego czytelnika, gotowy do użycia w warunku WHERE albo w zapytaniu INSERT.
Jedno połączenie, kilka skryptów
Arkusz zwykle wymaga dwóch albo trzech skryptów w różnych miejscach strony. Połączenie otwierasz raz, w pierwszym z nich, i zamykasz raz, w ostatnim. Zmienna $polaczenie jest widoczna we wszystkich blokach PHP na tej samej stronie.
<?php $polaczenie = mysqli_connect("localhost", "root", "", "biblioteka"); ?>
…
<?php $wynik = mysqli_query($polaczenie, "SELECT …"); /* skrypt 1 */ ?>
…
<?php $wynik = mysqli_query($polaczenie, "SELECT …"); /* skrypt 2 */ ?>
…
<?php mysqli_close($polaczenie); ?>
Wszystkie arkusze z PHP
101 arkuszy praktycznych z lat 2018–2026. Każdy ma zapytania do napisania i stronę, w której uzupełniasz skrypty PHP. Kliknij kod arkusza, żeby go otworzyć.
Ściąga
Szkielet skryptu
$polaczenie = mysqli_connect("localhost", "root", "", "nazwa_bazy");
$wynik = mysqli_query($polaczenie, "SELECT kolumna1, kolumna2 FROM tabela");
while ($wiersz = mysqli_fetch_array($wynik)) {
echo "<li>{$wiersz['kolumna1']}, {$wiersz['kolumna2']}</li>";
}
mysqli_close($polaczenie);
PHP
| Co chcesz zrobić | Zapis |
|---|---|
| Wypisać tekst ze zmienną | echo "Cena: $cena zł"; |
| Wypisać element tablicy w tekście | echo "{$wiersz['tytul']}"; |
| Skleić teksty | $a . " " . $b |
| Sprawdzić, czy formularz wysłano | if (isset($_POST["pole"])) |
| Sprawdzić, czy pole wypełniono | if (!empty($_POST["pole"])) |
| Odczytać parametr z adresu | $_GET["id"] |
| Policzyć zwrócone wiersze | mysqli_num_rows($wynik) |
| Policzyć zmienione wiersze | mysqli_affected_rows($polaczenie) |
| Jeden wiersz bez pętli | $wiersz = mysqli_fetch_array($wynik); |
| Wiersz tylko z numerami kolumn | $wiersz = mysqli_fetch_row($wynik); → $wiersz[0] |
| Odczytać zaznaczony przycisk radio | if (isset($_POST["grupa"])) $wybor = $_POST["grupa"]; |
| Odczytać kilka checkboxów | name="dodatki[]" → foreach ($_POST["dodatki"] as $d) |
| Skleić tablicę w tekst | implode(", ", $tablica) |
| Polska nazwa dnia tygodnia | $dni[date("N")], tablica od indeksu 1 |
| Dopisać linię do pliku | file_put_contents("plik.txt", "$tekst\n", FILE_APPEND); |
| Zapisać plik od zera | $f = fopen("plik.txt", "w"); fwrite($f, $tekst); fclose($f); |
| Linie pliku jako tablica | file("plik.txt", FILE_IGNORE_NEW_LINES) |
| Odnośnik usuwający wiersz | echo "<a href='plik.php?usun=$id'>usuń</a>"; |
| Przekierować na tę samą stronę | header("Location: " . $_SERVER["PHP_SELF"]); exit; |
| Dzisiejsza data | date("Y-m-d") |
| Dwa miejsca po przecinku | number_format($cena, 2) |
| Ustawić ciasteczko na godzinę | setcookie("nazwa", "wartosc", time() + 3600); |
| Wczytać stronę od nowa | header("Location: plik.php"); |
SQL
| Arkusz mówi | Zapytanie |
|---|---|
| wybierające jedynie pola a i b | SELECT a, b FROM tabela |
| dla wierszy, w których… | WHERE warunek |
| posortowane malejąco według… | ORDER BY kolumna DESC |
| pięć pierwszych wierszy | LIMIT 5 |
| zaczynające się na literę K | WHERE kolumna LIKE 'K%' |
| liczbę wierszy, średnią, sumę | COUNT(*), AVG(k), SUM(k) |
| dla każdego … osobno | GROUP BY kolumna |
| zapytanie wykorzystuje relację | FROM a JOIN b ON a.id_b = b.id |
| także te bez pary; które nie mają wpisu | FROM a LEFT JOIN b ON … WHERE b.kolumna IS NULL |
| kolumna nazwana „Średnia cena” | AVG(cena) AS "Średnia cena" |
| z dzisiejszą datą; z bieżącą godziną | CURDATE(); CURTIME(); razem NOW() |
| liczba dni od daty do dziś | DATEDIFF(CURDATE(), data) |
| w lipcu; w latach 1970–1979 | MONTH(data) = 7; YEAR(data) BETWEEN 1970 AND 1979 |
| pole nie jest puste (tekst) | WHERE pole != '' |
| wstawiające wiersz, klucz automatyczny | INSERT INTO tabela VALUES (NULL, …) |
| zmieniające wartość pola | UPDATE tabela SET pole = wartość WHERE … |
| usuwające wiersze | DELETE FROM tabela WHERE … |
| dodające kolumnę | ALTER TABLE tabela ADD kolumna TYP |
| … umieszczoną po kolumnie x; z wartością domyślną | … ADD kolumna TYP DEFAULT 0 AFTER x |
| usuwające kolumnę | ALTER TABLE tabela DROP COLUMN kolumna |
| zmieniające nazwę kolumny | ALTER TABLE tabela CHANGE stara nowa TYP |
| tworzące tabelę z kluczem | CREATE TABLE t (id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT NOT NULL, …) |
| usuwające tabelę; wszystkie rekordy | DROP TABLE t; TRUNCATE t |
| tworzące użytkownika z hasłem | CREATE USER 'nazwa' IDENTIFIED BY 'hasło' |
| … na localhost | CREATE USER 'nazwa'@'localhost' IDENTIFIED BY 'hasło' |
| zmieniające hasło | ALTER USER 'nazwa'@'localhost' IDENTIFIED BY 'nowe' |
| usuwające użytkownika | DROP USER 'nazwa'@'localhost' |
| nadające prawa do tabeli | GRANT SELECT, INSERT ON baza.tabela TO 'nazwa'@'localhost' |
| … do wszystkich tabel bazy | GRANT ALL PRIVILEGES ON baza.* TO 'nazwa'@'localhost' |
| odbierające prawa | REVOKE UPDATE ON baza.tabela FROM 'nazwa'@'localhost' |
| wybierania / wstawiania / aktualizacji / usuwania rekordów | SELECT / INSERT / UPDATE / DELETE |
| tworzenia / edycji struktury / usuwania obiektu | CREATE / ALTER / DROP |
| liczba znaków; fragment tekstu | CHAR_LENGTH(k); SUBSTRING(k, 1, 3), LEFT(k, 3) |
| losowy wiersz | ORDER BY RAND() LIMIT 1 |
| grupy poleceń: DQL / DML / DDL / DCL | SELECT / INSERT UPDATE DELETE / CREATE ALTER DROP / GRANT REVOKE |
Zanim oddasz pracę
- Nazwa bazy, pliku i tabel dokładnie jak w arkuszu.
- Każdy komunikat i każdy wiersz wypisany znak po znaku według wzoru.
- Na końcu skryptu jest
mysqli_close. - Zmienne mają znaczące nazwy:
$polaczenie,$wynik,$wiersz, a nie$a,$x. - Plik
kwerendy.txtma wszystkie zapytania, a zrzuty ekranu mają nazwy z arkusza.
Quiz
Pytania o PHP, SQL i wymagania z arkuszy. Po kliknięciu odpowiedzi zobaczysz wyjaśnienie.