Programování

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.

12. 10. 2026 · 4 min čtení

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:

  1. Surové SQL vždy s parametry.
  2. Složité dotazy dávejte do pojmenovaných funkcí nebo repozitářů, ne roztroušené po kontrolerech.
  3. Zapněte si ve vývoji logování dotazů (Django Debug Toolbar, Laravel Debugbar nebo Telescope, echo=True v 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.