SQL w praktyce - DDL i DML - CREATE, INSERT, SELECT, UPDATE, DELETE, JOIN
Praktyczny SQL dla programisty C# - podział na DDL/DML/DCL/TCL, CREATE TABLE z kluczami głównymi i obcymi, CRUD (Wielka Czwórka DML), INNER JOIN, klasyczne pułapki UPDATE/DELETE bez WHERE, mapowanie SQL na LINQ w C#. Dwudziesta trzecia część kursu C#.
W poprzednim wpisie Relacyjne bazy danych w C# poznałeś architekturę silnika bazy oraz zasady projektowania tabel. Teraz przechodzimy do języka, którym baza rozmawia z Twoim kodem: SQL (Structured Query Language). Bez względu na to, czy piszesz bezpośrednio przez ADO.NET, czy używasz ORM (Entity Framework Core), pod maską wykonuje się SQL - a jego znajomość odróżnia programistę junior od senior.
W tej części poznasz podział SQL na cztery kategorie (DDL/DML/DCL/TCL - w codziennej pracy używasz głównie dwóch), konkretne polecenia do tworzenia struktury (CREATE, ALTER, DROP) oraz Wielką Czwórkę operacji na danych (INSERT, SELECT, UPDATE, DELETE). Plus klasyczne pułapki produkcyjne i mapowanie SQL na LINQ w C#.
1. Cztery kategorie SQL #
Nie każde polecenie SQL to SELECT. Standard dzieli język na cztery kategorie o różnym przeznaczeniu:
| Kategoria | Rozszyfrowanie | Polecenia | Częstość użycia |
|---|---|---|---|
| DDL | Data Definition Language | CREATE, ALTER, DROP, TRUNCATE | Rzadko (setup schematu, migracje) |
| DML | Data Manipulation Language | INSERT, SELECT, UPDATE, DELETE | Codziennie (aplikacja biznesowa) |
| DCL | Data Control Language | GRANT, REVOKE, DENY | Rzadko (uprawnienia - zwykle DBA/DevOps) |
| TCL | Transaction Control Language | BEGIN, COMMIT, ROLLBACK, SAVEPOINT | Często (transakcje dla spójności) |
W codziennej pracy programisty 80% czasu spędzasz na DML. DDL to najczęściej migracje (dodanie kolumny, nowa tabela) - zwykle raz na tydzień/miesiąc. TCL implicite w każdej operacji zapisu (EF Core zawija SaveChanges w transakcję automatycznie).
2. DDL - definiowanie struktury #
Trzy najważniejsze polecenia DDL:
CREATE- tworzy nowy obiekt (tabelę, indeks, widok, procedurę)ALTER- modyfikuje istniejący (np. dodaje kolumnę)DROP- bezpowrotnie usuwa obiekt wraz z danymi
CREATE TABLE - pełny przykład #
Budujemy strukturę z wpisu wczorajszego - użytkownicy i zamówienia:
-- Tabela Users z Primary Key i podstawowymi kolumnami
CREATE TABLE Users (
Id INT IDENTITY(1,1) PRIMARY KEY, -- auto-inkrementacja od 1
Name NVARCHAR(50) NOT NULL, -- Unicode, wymagane
Email VARCHAR(100) UNIQUE NOT NULL, -- unikalny w skali tabeli
CreatedAt DATETIME2 DEFAULT SYSUTCDATETIME() -- domyślnie aktualny UTC
);
-- Tabela Orders z Foreign Key do Users
CREATE TABLE Orders (
Id INT IDENTITY(1,1) PRIMARY KEY,
UserId INT NOT NULL,
Product NVARCHAR(100) NOT NULL,
Price DECIMAL(18, 2) NOT NULL,
-- Klucz obcy z kaskadowym usuwaniem
CONSTRAINT FK_Orders_Users FOREIGN KEY (UserId)
REFERENCES Users(Id)
ON DELETE CASCADE
);
Rozbicie ważnych elementów:
IDENTITY(1,1)- automatyczna inkrementacja (pierwszy=1, każdy kolejny +1)NOT NULL- kolumna musi mieć wartość (bez tego domyślnieNULLdozwolone)UNIQUE- wartość unikalna w tabeli (dwaj użytkownicy nie mogą mieć tego samego emaila)DEFAULT- wartość, gdyINSERTjej nie podaCONSTRAINT FK_Orders_Users- nazwany klucz obcy (nazwa pomaga przy debugowaniu)ON DELETE CASCADE- usunięcie usera automatycznie usuwa jego orders
ALTER TABLE - modyfikacje #
-- Dodaj kolumnę
ALTER TABLE Users ADD DateOfBirth DATE NULL;
-- Zmień typ kolumny
ALTER TABLE Users ALTER COLUMN Name NVARCHAR(100) NOT NULL;
-- Dodaj indeks
CREATE INDEX IX_Users_Email ON Users(Email);
-- Usuń kolumnę
ALTER TABLE Users DROP COLUMN DateOfBirth;
W produkcji ALTER TABLE na dużej tabeli może blokować ją na godziny - miej to na uwadze przy migracjach.
DROP - ostateczne usunięcie #
DROP TABLE Orders; -- usuwa tabelę razem z danymi
DROP jest ostateczne. Bez backupu - nie ma powrotu. Reguła bezpieczeństwa: nigdy nie łącz się z produkcją jako użytkownik z prawem DROP. Użyj konta z tylko DML (SELECT/INSERT/UPDATE/DELETE) - DROP zostaw dla dedykowanego konta migracji.
3. DML - Wielka Czwórka operacji CRUD #
CRUD w REST API to dokładnie DML w SQL:
| REST | SQL | HTTP |
|---|---|---|
| Create | INSERT | POST |
| Read | SELECT | GET |
| Update | UPDATE | PUT/PATCH |
| Delete | DELETE | DELETE |
INSERT - dodawanie wierszy #
-- Podstawowa forma z jawną listą kolumn
INSERT INTO Users (Name, Email)
VALUES (N'Jan Kowalski', '[email protected]');
-- Wstawianie wielu wierszy naraz (efektywne)
INSERT INTO Users (Name, Email) VALUES
(N'Anna Nowak', '[email protected]'),
(N'Piotr Wiśniewski', '[email protected]'),
(N'Ola Kowalczyk', '[email protected]');
-- INSERT z pobraniem Id (SQL Server)
INSERT INTO Users (Name, Email)
OUTPUT INSERTED.Id
VALUES (N'Nowy User', '[email protected]');
Kluczowe niuanse:
N'text'- Unicode literal; bezNpolskie znaki mogą się zepsuć wNVARCHARIDENTITYkolumny pomijamy - baza sama wypełniDEFAULTkolumny (jakCreatedAt) też można pominąćOUTPUT INSERTED.Id- zwraca wygenerowany PK (przydatne w aplikacji)
SELECT - odczyt (najbardziej rozbudowane polecenie SQL) #
-- Wszystkie kolumny, wszystkie wiersze - unikaj w produkcji
SELECT * FROM Users;
-- Konkretne kolumny (lepsze)
SELECT Id, Name, Email FROM Users;
-- Filtrowanie WHERE
SELECT Id, Name FROM Users WHERE Email = '[email protected]';
-- Wiele warunków
SELECT * FROM Orders
WHERE Price > 100
AND UserId = 5;
-- Sortowanie ORDER BY
SELECT * FROM Orders
ORDER BY Price DESC, Product ASC;
-- Paginacja OFFSET/FETCH (SQL Server 2012+)
SELECT * FROM Users
ORDER BY Id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- strona 3, po 10 rekordów
-- Agregaty
SELECT COUNT(*) FROM Orders WHERE UserId = 1;
SELECT AVG(Price) FROM Orders;
SELECT MIN(Price), MAX(Price) FROM Orders;
-- Grupowanie GROUP BY
SELECT UserId, COUNT(*) AS OrderCount, SUM(Price) AS TotalSpent
FROM Orders
GROUP BY UserId;
-- HAVING - filtr na grupach
SELECT UserId, COUNT(*) AS OrderCount
FROM Orders
GROUP BY UserId
HAVING COUNT(*) > 5; -- tylko użytkownicy z więcej niż 5 zamówieniami
Uwaga: unikaj SELECT * w produkcji. Lepiej SELECT Id, Name, Email:
- Mniej danych przesyłanych z bazy do aplikacji
- Jasny kontrakt - dodanie kolumny
Passwordw bazie nie wysadza aplikacji - Lepsza wydajność - baza może użyć covering index (indeks zawierający potrzebne kolumny)
SELECT z INNER JOIN #
Łączenie dwóch tabel przez wspólny klucz:
-- Podstawowy INNER JOIN
SELECT u.Name, o.Product, o.Price
FROM Users u
INNER JOIN Orders o ON u.Id = o.UserId;
-- LEFT JOIN - wszyscy użytkownicy, nawet bez zamówień
SELECT u.Name, o.Product
FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId;
-- Dla użytkownika bez zamówień: o.Product będzie NULL
-- Wielotabelowy JOIN
SELECT u.Name, o.Product, c.Name AS Category
FROM Users u
INNER JOIN Orders o ON u.Id = o.UserId
INNER JOIN Categories c ON o.CategoryId = c.Id
WHERE o.Price > 1000;
Aliasy (u, o, c) - skróty dla nazw tabel; nie są wymagane, ale zawsze stosuj dla czytelności.
Różnica INNER kontra LEFT:
- INNER JOIN - tylko wiersze, gdzie istnieje dopasowanie w obu tabelach
- LEFT JOIN - wszystkie z lewej + dopasowane z prawej (brak = NULL)
- RIGHT JOIN - odwrotność LEFT (rzadko używany - zwykle piszemy jako LEFT z odwróconą kolejnością tabel)
- FULL OUTER JOIN - wszystko z obu tabel (brak = NULL po odpowiedniej stronie)
UPDATE - modyfikacja #
-- Podstawowa forma
UPDATE Users
SET Email = '[email protected]'
WHERE Id = 1;
-- Aktualizacja wielu kolumn
UPDATE Orders
SET Price = 2500, Product = N'Laptop Pro'
WHERE Id = 1;
-- UPDATE z warunkiem złożonym
UPDATE Orders
SET Price = Price * 1.1 -- podwyżka 10%
WHERE Product LIKE N'Laptop%'
AND CreatedAt > '2024-01-01';
KRYTYCZNIE WAŻNE: UPDATE bez WHERE aktualizuje cały dataset!
UPDATE Users SET Email = '[email protected]';
-- ⚠️ WSZYSCY użytkownicy dostają email '[email protected]'
Zasada bezpieczeństwa produkcyjnego:
- Napisz
SELECT * FROM Users WHERE ...z tym samym warunkiem - Sprawdź dokładnie ile wierszy zaznaczysz
- Dopiero potem zamień
SELECT *naUPDATE ... SET ...
DELETE - usuwanie wierszy #
-- Usunięcie konkretnego wiersza
DELETE FROM Orders WHERE Id = 5;
-- Usunięcie wielu wierszy z warunkiem
DELETE FROM Orders
WHERE CreatedAt < '2020-01-01'; -- stare zamówienia sprzed 2020
Analogicznie do UPDATE: DELETE FROM Orders; bez WHERE wyczyszcza całą tabelę.
Różnica DELETE kontra TRUNCATE:
DELETE FROM Table | TRUNCATE TABLE Table | |
|---|---|---|
Warunki WHERE | ✅ Można | ❌ Nie można |
| Wpisy do Transaction Log | Każdy wiersz | Minimalne |
Reset IDENTITY | Nie | Tak (do seed value) |
| Wywalane triggery | Tak | Nie (SQL Server) |
| Prawa | DELETE | ALTER (DDL!) |
| Odzysk przez rollback | Tak (w transakcji) | Zależy od bazy |
| Wydajność dla całej tabeli | Wolne | Błyskawiczne |
Dla wyczyszczenia całej tabeli w środowisku deweloperskim: TRUNCATE TABLE Orders; (setki razy szybsze niż DELETE FROM Orders).
4. Klasyczne pułapki produkcyjne #
Pułapka 1: UPDATE/DELETE bez WHERE #
Już wielokrotnie wspomniana - najsłynniejszy bug juniorów. Zawsze najpierw SELECT z tym samym warunkiem.
Pułapka 2: SELECT * w produkcji #
SELECT * FROM Users WHERE Id = 1;
-- Pobiera Password, ApiKey, PrivateSettings...
-- Kolumny dodane w przyszłości też będą się przesyłać
Explicit lista kolumn: jasny kontrakt między aplikacją a bazą.
Pułapka 3: N'text' kontra 'text' dla polskich znaków #
-- ZŁE
INSERT INTO Users (Name) VALUES ('Łódź'); -- może zostać zapisane jako '?ódź'
-- DOBRE
INSERT INTO Users (Name) VALUES (N'Łódź'); -- Unicode - bezbłędnie
Zawsze N'...' gdy kolumna to NVARCHAR.
Pułapka 4: brak indeksów #
SELECT * FROM Orders WHERE UserId = 42;
-- Bez indeksu na UserId: pełen skan tabeli (mogą być miliony wierszy)
-- Z indeksem: logarytmiczne wyszukanie (mikrosekundy)
Regułą kciuka: indeks dla każdej kolumny użytej w WHERE, JOIN, ORDER BY. Ale uwaga: indeksy spowalniają INSERT/UPDATE/DELETE - bilans.
Pułapka 5: N+1 problem #
Aplikacja pobiera 100 użytkowników, potem dla każdego osobne zapytanie o jego zamówienia = 101 zapytań zamiast 1. Rozwiązanie: JOIN lub Include w EF Core (omówione w wpisie o EF Core).
5. SQL kontra LINQ w C# #
Jeśli używasz Entity Framework Core, LINQ zamienia się na SQL automatycznie:
// C# LINQ
var expensiveOrders = context.Orders
.Where(o => o.Price > 1000)
.OrderByDescending(o => o.Price)
.Take(10)
.ToList();
// Wygenerowany SQL:
// SELECT TOP 10 *
// FROM Orders
// WHERE Price > 1000
// ORDER BY Price DESC
Mapowanie 1:1:
| LINQ | SQL |
|---|---|
.Where(...) | WHERE |
.OrderBy(...) | ORDER BY ASC |
.OrderByDescending(...) | ORDER BY DESC |
.Take(N) | TOP N (SQL Server) lub LIMIT N (PostgreSQL) |
.Skip(N) | OFFSET N ROWS |
.GroupBy(...) | GROUP BY |
.Join(...) | INNER JOIN |
.Select(...) | SELECT (kolumny) |
.Count() | COUNT(*) |
.Sum(x => x.Price) | SUM(Price) |
Znając SQL - znasz LINQ. Znając LINQ - łatwiej Ci będzie SQL. To dwa dialekty tej samej idei.
ADO.NET - klasyczny sposób #
Alternatywa dla EF Core - bezpośredni SQL przez ADO.NET:
using System.Data.SqlClient;
using var conn = new SqlConnection(connectionString);
conn.Open();
using var cmd = new SqlCommand(
"SELECT Id, Name, Email FROM Users WHERE Id = @id",
conn);
cmd.Parameters.AddWithValue("@id", 42);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
string email = reader.GetString(2);
Console.WriteLine($"{id}: {name} ({email})");
}
Kluczowe: zawsze używaj parametrów (@id), nigdy nie sklejaj SQL ze stringów:
// ❌ SQL Injection!
string sql = "SELECT * FROM Users WHERE Name = '" + userInput + "'";
// ✅ Bezpieczne - parametr
string sql = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.AddWithValue("@name", userInput);
Więcej o SQL Injection - w kolejnych wpisach o bezpieczeństwie.
Podsumowanie tematu #
W tej części kursu poznałeś:
- Cztery kategorie SQL - DDL (struktura), DML (dane), DCL (uprawnienia), TCL (transakcje); jako programista używasz głównie DDL i DML
- DDL -
CREATE TABLEz kluczami głównymi (IDENTITY), obcymi (FOREIGN KEY REFERENCES), ograniczeniami (NOT NULL,UNIQUE,DEFAULT);ALTER TABLEdla modyfikacji;DROPjako operacja ostateczna - Wielka Czwórka DML (CRUD) -
INSERTz listą kolumn i wartości,SELECTzWHERE/ORDER BY/GROUP BY/HAVING,UPDATEz krytyczną klauzuląWHERE,DELETEjako operacja usunięcia - JOINy -
INNER JOIN(tylko dopasowane),LEFT JOIN(wszystko z lewej + NULL dla braku), rzadkoRIGHT/FULL OUTER - Klasyczne pułapki - UPDATE/DELETE bez WHERE (najsłynniejszy bug juniora),
SELECT *w produkcji,N'...'dla polskich znaków, brak indeksów, N+1 problem - DELETE kontra TRUNCATE -
DELETEz WHERE i log-per-wiersz,TRUNCATEreset całej tabeli, wielokrotnie szybszy dla wyczyszczenia - SQL kontra LINQ - mapowanie 1:1 (Where/OrderBy/GroupBy/Join); EF Core zamienia LINQ na SQL automatycznie; ADO.NET dla bezpośrednich zapytań
- Bezpieczeństwo - zawsze parametry, nigdy stringi łączone z inputem użytkownika (SQL Injection)
W następnym wpisie Więzy integralności w SQL - Primary Key, Foreign Key, CASCADE, UNIQUE, CHECK zejdziemy głębiej w mechanizmy obronne bazy: strategie kaskadowego zachowania ON DELETE, wybór INT IDENTITY kontra GUID jako PK, oraz walidacja logiczna przez CHECK. Później kolejno: indeksy i plan zapytania, transakcje oraz zaawansowane JOINy.
Quiz i zadanie poniżej. Zadanie pokazuje kanoniczny odpowiednik SQL w C#: Listforeach jako SELECT, LINQ join jako INNER JOIN, First(...) + modification jako UPDATE WHERE. Znając te paralele, przechodzisz płynnie między dialektami.
Podobne wpisy
Więzy integralności w SQL - Primary Key, Foreign Key, CASCADE, UNIQUE, CHECK
Mechanizmy obronne bazy danych - Primary Key (INT IDENTITY kontra GUID), Foreign Key i integralność referencyjna, trzy strategie ON DELETE (CASCADE/SET NULL/NO ACTION), constraints UNIQUE i CHECK jako walidacja na poziomie bazy. Dwudziesta czwarta część kursu C#.
Relacyjne bazy danych w C# - architektura RDBMS, schemat tabel, mapowanie typów SQL
Architektura silnika RDBMS (Query Optimizer, Storage Engine, Transaction Log), projektowanie tabel z PK/FK, mapowanie typów SQL Server na typy C# (INT, NVARCHAR, DECIMAL, DATETIME2, DATE). Dwudziesta druga część kursu C#.
ArrayPool i MemoryPool w C# - wypożyczalnia tablic kontra LOH
Recykling dużych buforów dla hot path - LOH (Large Object Heap) i jego pułapki, ArrayPool<T>.Shared z Rent/Return, pułapka nadmiarowego rozmiaru, MemoryPool<T> z IMemoryOwner i using dla async. Dwudziesta pierwsza część kursu C#.
🔗 Linkują tu