ORM, query builder, nebo čisté SQL? Výhody, pasti a jak volit
Srovnání ORM, query builderů a ručně psaného SQL na příkladech v Pythonu a TypeScriptu. Problém N+1, výkon, bezpečnost a kdy kombinovat přístupy.
Debata „ORM vs. SQL“ se často vede jako náboženská válka. Ve skutečnosti jde o tři nástroje s různými kompromisy a většina zdravých projektů používá kombinaci. Podívejme se, co který přístup přináší a kde se typicky láme.
Tři úrovně abstrakce
| Přístup | Příklady | Píšete |
|---|---|---|
| Čisté SQL | psycopg, pg (node-postgres), PDO |
SQL řetězce s parametry |
| Query builder | Knex, Kysely, Laravel Query Builder, SQLAlchemy Core | SQL skládané metodami |
| ORM | Eloquent, Django ORM, SQLAlchemy ORM, Prisma, Doctrine, Drizzle | práce s objekty a relacemi |
Stejný dotaz třemi způsoby
Úkol: deset zákazníků s nejvyšší útratou za letošní rok.
Čisté SQL (Python, psycopg 3)
rows = conn.execute(
"""
SELECT c.id, c.name, SUM(o.total) AS revenue
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.created_at >= %s
GROUP BY c.id, c.name
ORDER BY revenue DESC
LIMIT 10
""",
(date(2026, 1, 1),),
).fetchall()
Query builder (TypeScript, Kysely)
const rows = await db
.selectFrom("customers as c")
.innerJoin("orders as o", "o.customer_id", "c.id")
.select(["c.id", "c.name", (eb) => eb.fn.sum<number>("o.total").as("revenue")])
.where("o.created_at", ">=", new Date("2026-01-01"))
.groupBy(["c.id", "c.name"])
.orderBy("revenue", "desc")
.limit(10)
.execute();
ORM (Python, Django)
from django.db.models import Sum
top = (
Customer.objects
.filter(orders__created_at__gte=date(2026, 1, 1))
.annotate(revenue=Sum("orders__total"))
.order_by("-revenue")[:10]
)
Všechny tři vygenerují v podstatě stejný SQL dotaz. Rozdíl je v tom, co dostanete navíc a co ztratíte.
Co nabízí ORM
- Rychlý vývoj CRUD. Uložení formuláře je
Article.create(data), ne dvacet řádků SQL. - Relace a načítání –
article.author,with('comments'). - Migrace odvozené z modelů.
- Přenositelnost mezi databázemi (v praxi omezená – většina projektů databázi nikdy nemění).
- Bezpečnost – hodnoty se automaticky předávají jako parametry.
Kde ORM bolí
N+1 dotazy
for article in Article.objects.all()[:50]:
print(article.author.name) # 1 dotaz na články + 50 dotazů na autory
Oprava je jednoduchá, ale musíte o ní vědět:
Article.objects.select_related("author")[:50] # JOIN
Article.objects.prefetch_related("comments")[:50] # druhý dotaz s IN (...)
Ekvivalenty: with() v Eloquentu, include v Prismě, joinedload/selectinload v SQLAlchemy.
Složité dotazy
Window funkce, rekurzivní CTE, LATERAL JOIN nebo specifické funkce PostgreSQL se v ORM píší krkolomně nebo vůbec. Výsledný kód bývá méně čitelný než SQL, které nahrazuje.
Skrytý výkon
len(queryset) místo .count(), načtení celých objektů, když potřebujete dva sloupce, nebo uložení tisíců objektů v cyklu místo hromadného bulk_create. ORM nic z toho nezakazuje.
Kde bolí čisté SQL
- Opakující se kód – mapování řádků na objekty, skládání
INSERT/UPDATE. - Dynamické filtry – podmíněné skládání
WHEREřetězců vede k chybám a lákání ke konkatenaci. - Typy – bez dalších nástrojů nevíte, co dotaz vrací (pomáhají nástroje jako sqlc pro Go nebo PgTyped pro TypeScript, které typy generují ze SQL).
Bezpečnost: SQL injection
Žádný přístup vás neochrání, když hodnotu vložíte do řetězce:
# ŠPATNĚ – SQL injection
conn.execute(f"SELECT * FROM users WHERE email = '{email}'")
# SPRÁVNĚ – parametr
conn.execute("SELECT * FROM users WHERE email = %s", (email,))
ORM mají „únikové“ metody pro surové SQL (raw(), DB::raw(), $queryRawUnsafe v Prismě) – a tam platí totéž. Parametry nejdou použít pro názvy sloupců a tabulek; ty ověřujte proti pevnému seznamu povolených hodnot (allowlist).
Jak volit
ORM se hodí pro:
- typické webové aplikace s hodně CRUD operacemi,
- tým, který framework zná (Laravel, Django, Rails – ORM je jejich součástí),
- doménový model s relacemi.
Query builder se hodí pro:
- dynamicky skládané dotazy (filtry, vyhledávání),
- TypeScript projekty, kde chcete typovou kontrolu a blízkost SQL,
- knihovny a menší služby bez potřeby celého ORM.
Čisté SQL se hodí pro:
- reporty, analytiku, složité agregace,
- výkonově kritická místa,
- datové migrace a hromadné operace.
Doporučený kompromis
Používejte ORM pro 80 % běžné práce a bez výčitek sáhněte po SQL tam, kde je čitelnější nebo rychlejší. Dodržujte přitom tři pravidla:
- Surové SQL vždy s parametry.
- Složité dotazy dávejte do pojmenovaných funkcí nebo repozitářů, ne roztroušené po kontrolerech.
- Zapněte si ve vývoji logování dotazů (Django Debug Toolbar, Laravel Debugbar nebo Telescope,
echo=Truev SQLAlchemy) a pravidelně se na ně dívejte.
Kdo nevidí SQL, které ORM generuje, dříve či později narazí na problém s výkonem. Kdo píše všechno ručně, stráví čas kódem, který za něj mohl udělat nástroj.