SQL & PostgreSQL
PostgreSQL je nejpokročilejší open-source relační databáze. Pro webové aplikace je zlatý standard — ACID transakce, JSON podpora, full-text search, dobré škálování.
| Databáze | Typ | Kdy použít |
|---|---|---|
| PostgreSQL | Relační (SQL) | Většina aplikací — strukturovaná data, relace, transakce |
| MySQL / MariaDB | Relační (SQL) | Tradiční PHP stack, méně features než PG |
| MongoDB | Dokumentová (NoSQL) | Flexibilní schema, vnořené dokumenty, časté změny struktury |
| Redis | In-memory K/V | Cache, sessions, real-time, rate limiting |
| SQLite | Relační (soubor) | Vývoj, embedded, malé aplikace, edge |
Základní SQL — rychlý přehled
-- CREATE TABLE
CREATE TABLE products (
id SERIAL PRIMARY KEY,
nazev VARCHAR(100) NOT NULL,
cena DECIMAL(10,2) NOT NULL,
dostupny BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- INSERT
INSERT INTO products (nazev, cena) VALUES ('Victorinox', 299.00);
-- SELECT s filtrem
SELECT id, nazev, cena
FROM products
WHERE dostupny = true AND cena < 500
ORDER BY cena ASC
LIMIT 10 OFFSET 0;
-- UPDATE
UPDATE products SET cena = 349.00 WHERE id = 1;
-- DELETE
DELETE FROM products WHERE id = 1;
-- JOIN
SELECT p.nazev, c.text AS komentar
FROM products p
JOIN comments c ON c.product_id = p.id
WHERE p.id = 42;
Prisma — setup a konfigurace
Prisma je TypeScript-first ORM — místo SQL píšete typované JS metody. Automaticky generuje typy z databázového schématu. Tři komponenty: Prisma Client, Prisma Migrate, Prisma Studio.
// src/lib/prisma.ts — Singleton (důležité pro Next.js/dev!)
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// Proč singleton? Next.js hot reload vytváří nové instance —
// bez singletonu dojde k "too many connections" chybě
Prisma Schema — datový model
Prisma Schema je "pravda" o struktuře databáze — definuje modely, typy, relace a indexy. Z něj se generují migrace i TypeScript typy.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Enum
enum Role { USER ADMIN EDITOR }
enum OrderStatus { PENDING PAID SHIPPED DELIVERED CANCELLED }
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
reviews Review[]
@@index([email]) // Index pro rychlé vyhledávání
}
model Product {
id Int @id @default(autoincrement())
nazev String
slug String @unique
cena Decimal @db.Decimal(10, 2)
popis String? @db.Text
dostupny Boolean @default(true)
kategorie String?
imageUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orderItems OrderItem[]
reviews Review[]
@@index([nazev])
@@index([kategorie])
}
model Order {
id Int @id @default(autoincrement())
userId Int
status OrderStatus @default(PENDING)
total Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
items OrderItem[]
}
model OrderItem {
id Int @id @default(autoincrement())
orderId Int
productId Int
quantity Int
price Decimal @db.Decimal(10, 2)
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id])
}
model Review {
id Int @id @default(autoincrement())
userId Int
productId Int
rating Int
text String? @db.Text
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
product Product @relation(fields: [productId], references: [id])
@@unique([userId, productId]) // Jeden review per user/product
}
// Kvíz: Co dělá @updatedAt v Prisma schématu?
Migrace — verzování schématu
Migrace jsou versioned SQL soubory popisující změny databázového schématu. Prisma Migrate je generuje automaticky z rozdílů ve schema.prisma.
// prisma/seed.ts — testovací data
import { prisma } from '../src/lib/prisma';
import bcrypt from 'bcryptjs';
async function main() {
// Admin uživatel
const admin = await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
name: 'Admin',
password: await bcrypt.hash('admin123', 12),
role: 'ADMIN',
},
});
// Produkty
const produkty = [
{ nazev: 'Victorinox Classic', slug: 'victorinox-classic', cena: 299 },
{ nazev: 'Gerber Paraframe', slug: 'gerber-paraframe', cena: 549 },
];
for (const p of produkty) {
await prisma.product.upsert({
where: { slug: p.slug },
update: {},
create: { ...p, cena: p.cena },
});
}
console.log('Seed dokončen');
}
main().catch(console.error).finally(() => prisma.$disconnect());
// package.json
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
CRUD dotazy s Prisma Client
import { prisma } from '../lib/prisma';
// ── CREATE ────────────────────────────────────────────────
const product = await prisma.product.create({
data: {
nazev: 'Victorinox Classic',
slug: 'victorinox-classic',
cena: 299,
dostupny: true,
},
});
// ── READ — findMany (seznam) ──────────────────────────────
const products = await prisma.product.findMany({
where: {
dostupny: true,
cena: { lte: 500 }, // less than or equal
nazev: { contains: 'nůž', mode: 'insensitive' },
},
orderBy: { cena: 'asc' },
skip: (page - 1) * limit,
take: limit,
select: { id: true, nazev: true, cena: true }, // Jen potřebná pole
});
// ── READ — findUnique (jeden) ─────────────────────────────
const product = await prisma.product.findUnique({
where: { id: 42 },
});
// or by unique field:
const product = await prisma.product.findUnique({
where: { slug: 'victorinox-classic' },
});
// ── UPDATE ────────────────────────────────────────────────
const updated = await prisma.product.update({
where: { id: 42 },
data: { cena: 349 },
});
// Upsert — vytvoř nebo aktualizuj
const upserted = await prisma.product.upsert({
where: { slug: 'victorinox' },
update: { cena: 349 },
create: { nazev: 'Victorinox', slug: 'victorinox', cena: 349 },
});
// ── DELETE ────────────────────────────────────────────────
await prisma.product.delete({ where: { id: 42 } });
// ── COUNT ─────────────────────────────────────────────────
const total = await prisma.product.count({
where: { dostupny: true },
});
// ── TRANSAKCE ─────────────────────────────────────────────
const result = await prisma.$transaction(async (tx) => {
// Všechny operace v jedné atomické transakci
const order = await tx.order.create({
data: { userId, total, status: 'PENDING' }
});
await tx.orderItem.createMany({
data: items.map(i => ({
orderId: order.id, productId: i.id,
quantity: i.qty, price: i.cena
}))
});
// Sníž sklad
for (const item of items) {
await tx.product.update({
where: { id: item.id },
data: { sklad: { decrement: item.qty } }
});
}
return order;
});
// Kvíz: Proč je Prisma $transaction důležitý pro vytváření objednávky (order + items + update skladu)?
Relace & Include / Select
Prisma umí načítat propojená data přes include (přidá do výsledku) nebo select (přesný výběr polí). Pozor na N+1 problem!
// include — načte propojené záznamy
const orderWithItems = await prisma.order.findUnique({
where: { id: orderId },
include: {
user: true, // Celý user objekt
items: {
include: {
product: { // Vnořené include
select: { nazev: true, imageUrl: true }
}
}
}
}
});
// orderWithItems.items[0].product.nazev ✅
// select — přesný výběr (lepší výkon)
const products = await prisma.product.findMany({
select: {
id: true,
nazev: true,
cena: true,
_count: { select: { reviews: true } }, // Počet reviews
reviews: {
select: { rating: true },
orderBy: { createdAt: 'desc' },
take: 3, // Jen poslední 3
}
}
});
// Agregace
const stats = await prisma.product.aggregate({
_avg: { cena: true },
_min: { cena: true },
_max: { cena: true },
_count: { id: true },
where: { dostupny: true },
});
// stats._avg.cena, stats._count.id
// groupBy
const byCategory = await prisma.product.groupBy({
by: ['kategorie'],
_count: { id: true },
_avg: { cena: true },
orderBy: { _count: { id: 'desc' } },
});
// N+1 problém — ŠPATNĚ
const products = await prisma.product.findMany();
for (const p of products) {
// ❌ Nový DB dotaz pro každý produkt!
p.reviews = await prisma.review.findMany({ where: { productId: p.id } });
}
// ✅ Správně — jeden dotaz s include
const products = await prisma.product.findMany({
include: { reviews: true } // JOIN v jednom dotazu
});
Optimalizace dotazů
// 1. INDEXY — kritické pro vyhledávání
model Product {
// ...
@@index([kategorie]) // filtr podle kategorie
@@index([nazev]) // full-text search
@@index([dostupny, cena]) // compound index pro filtr + sort
}
// 2. SELECT jen potřebná pole (neposílat hesla, velké texty)
const users = await prisma.user.findMany({
select: { id: true, email: true, name: true }
// NIKDY select: true pro User — vrátí heslo!
});
// 3. Cursor-based pagination (lepší než offset pro velké datasety)
const products = await prisma.product.findMany({
take: 20,
cursor: lastId ? { id: lastId } : undefined,
skip: lastId ? 1 : 0, // Přeskočit cursor item
orderBy: { id: 'asc' },
});
// 4. Raw SQL pro komplexní dotazy (full-text search, custom funkce)
const results = await prisma.$queryRaw`
SELECT id, nazev, ts_rank(search_vector, query) AS rank
FROM products,
to_tsquery('czech', ${searchTerm}) query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10
`;
// 5. Connection pooling (produkce)
// Použijte PgBouncer nebo Prisma Accelerate
// DATABASE_URL=prisma://accelerate.prisma-data.net/?api_key=...
// 6. Soft delete pattern
model Product {
// ...
deletedAt DateTime? // null = aktivní, DateTime = smazáno
@@index([deletedAt]) // rychlý filtr
}
// Dotaz:
where: { deletedAt: null } // Jen nesmazané
// Kvíz: Co je N+1 problém v kontextu ORM a databáze?
Cvičení — Blog datový model
Navrhněte kompletní Prisma schema pro blog systém: uživatelé, příspěvky (s tagy), komentáře, lajky, kategorie. Přidejte správné indexy, relace a enums.
// prisma/schema.prisma — doplňte:
model User {
id Int @id @default(autoincrement())
email String @unique
username String @unique
// TODO: role enum (USER, AUTHOR, ADMIN)
// TODO: relace na Post, Comment, Like
}
model Post {
id Int @id @default(autoincrement())
title String
slug String @unique
// TODO: content (Text), published (Boolean), authorId (FK)
// TODO: relace na User, Comment, Tag, Like
// TODO: indexy pro slug a authorId
}
model Tag {
// TODO: many-to-many s Post
// Nápověda: v Prisma se M:N definuje přes pole na obou stranách
// Post { tags Tag[] } a Tag { posts Post[] }
}
model Comment {
// TODO: author, post, text, parentId (vnořené komentáře?)
}
model Like {
// TODO: unikátní kombinace userId + postId
@@unique([userId, postId])
}
// Zamyslete se: jak implementovat počet lajků efektivně?
// a) prisma._count na každý post (extra dotaz)
// b) denormalizace — likesCount column na Post
// c) agregační dotaz
Database Engineer
Zvládáte Prisma schema, migrace, CRUD operace, relace a optimalizaci dotazů.
// Taháček
// Prisma CRUD
await prisma.model.create({ data: {...} });
await prisma.model.findMany({ where, orderBy, skip, take, select });
await prisma.model.findUnique({ where: { id } });
await prisma.model.update({ where: { id }, data: {...} });
await prisma.model.upsert({ where, create, update });
await prisma.model.delete({ where: { id } });
await prisma.model.count({ where });
// Relace
include: { relation: true } // načíst vše
include: { relation: { select: {} }} // načíst výběr
select: { _count: { select: { rel: true } }} // počet
// Transakce
await prisma.$transaction(async tx => {
await tx.a.create({...});
await tx.b.update({...});
});
// Migrace
npx prisma migrate dev --name popis // vývoj
npx prisma migrate deploy // produkce
npx prisma db seed // seed data
npx prisma studio // GUI editor