Programování

Návrh databázového schématu: normalizace, klíče a typy prakticky

Jak navrhnout databázové schéma, které vydrží: entity a vztahy, normalizace bez dogmat, cizí klíče, správné datové typy a časté chyby začátečníků.

12. 10. 2026 · 4 min čtení

Špatně navržené schéma se dá opravit, ale bolí to – migrace dat, přepis dotazů, výpadky. Pár rozhodnutí na začátku přitom ušetří hodně práce. Projdeme si návrh na příkladu jednoduchého e-shopu a ukážeme principy, které platí v PostgreSQL i MySQL.

Začněte entitami a vztahy

Než napíšete první CREATE TABLE, sepište si podstatná jména z domény a vztahy mezi nimi:

  • Zákazník má více objednávek (1:N).
  • Objednávka obsahuje více produktů a produkt je ve více objednávkách (M:N).
  • Produkt patří do jedné kategorie (N:1).

Vztah M:N se v relační databázi vždy řeší spojovací tabulkou.

Schéma

CREATE TABLE customers (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email      TEXT NOT NULL UNIQUE,
  name       TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE categories (
  id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);

CREATE TABLE products (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  category_id BIGINT NOT NULL REFERENCES categories(id),
  sku         TEXT NOT NULL UNIQUE,
  name        TEXT NOT NULL,
  price_cents INTEGER NOT NULL CHECK (price_cents >= 0)
);

CREATE TABLE orders (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers(id),
  status      TEXT NOT NULL DEFAULT 'new'
              CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
  order_id         BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
  product_id       BIGINT NOT NULL REFERENCES products(id),
  quantity         INTEGER NOT NULL CHECK (quantity > 0),
  unit_price_cents INTEGER NOT NULL,
  PRIMARY KEY (order_id, product_id)
);

CREATE INDEX ON orders (customer_id);
CREATE INDEX ON order_items (product_id);
CREATE INDEX ON products (category_id);

Syntaxe je pro PostgreSQL; v MySQL 8 nahraďte GENERATED ALWAYS AS IDENTITY za AUTO_INCREMENT, TIMESTAMPTZ za DATETIME a TEXT u unikátních sloupců za VARCHAR(255).

Proč je to navržené takhle

Cena v položce objednávky

order_items.unit_price_cents vypadá jako duplicita k products.price_cents. Není. Cena produktu se mění, ale objednávka musí navždy zachovat cenu v okamžiku nákupu. Totéž platí pro doručovací adresu – kopírujte ji do objednávky, neodkazujte na aktuální adresu zákazníka.

Peníze v celých číslech

Nikdy neukládejte částky jako FLOAT nebo DOUBLE – binární plovoucí čárka neumí přesně vyjádřit 0,1. Použijte buď celé číslo v haléřích/centech, nebo NUMERIC(12,2) / DECIMAL(12,2).

Omezení v databázi, ne jen v kódu

NOT NULL, UNIQUE, CHECK a cizí klíče jsou poslední obrana proti chybným datům. Validace v aplikaci se dá obejít – importním skriptem, ručním dotazem, chybou v novém endpointu. Databáze ne.

Indexy na cizí klíče

PostgreSQL index na cizí klíč nevytváří automaticky. Bez něj je pomalý JOIN i mazání nadřazeného řádku (databáze musí zkontrolovat odkazy).

Normalizace bez dogmat

Normalizace znamená, že každá informace je uložena na jednom místě. Prakticky stačí hlídat tři věci:

  1. Žádné seznamy v jednom sloupci. tags = 'php,mysql,api' nejde rozumně indexovat ani filtrovat. Udělejte tabulku product_tags.
  2. Žádné opakující se sloupce. phone1, phone2, phone3 jsou signál pro samostatnou tabulku.
  3. Žádné odvozené údaje, které se mohou rozejít. Pokud ukládáte orders.total vedle položek, musíte zajistit, že se vždy přepočítá.

Denormalizace je v pořádku, když je vědomá: kvůli výkonu, historii (cena v objednávce) nebo reportingu. Zapište si důvod do komentáře u sloupce.

Volba primárního klíče

Typ Výhody Nevýhody
BIGINT identity malý, rychlý, seřazený odhaluje počet záznamů v URL
UUID v4 lze generovat v aplikaci, nehádatelný 16 bajtů, náhodné vkládání do indexu
UUID v7 časově seřazený, nehádatelný podpora generování se teprve rozšiřuje

Rozumný kompromis: interně BIGINT, navenek (v URL a API) samostatný unikátní sloupec s UUID. Funkce uuidv7() je v PostgreSQL vestavěná až od verze 18; ve starších verzích UUID v7 generujte v aplikaci.

JSON sloupce

PostgreSQL JSONB i MySQL JSON jsou užitečné pro data s proměnlivou strukturou – parametry produktu, odpověď z externího API. Nepoužívejte je ale jako náhradu schématu. Jakmile podle hodnoty v JSONu filtrujete, řadíte nebo na ni odkazujete, patří do vlastního sloupce.

Konvence pojmenování

  • Tabulky v množném čísle (orders) nebo jednotném – hlavně konzistentně.
  • snake_case, bez diakritiky a mezer.
  • Cizí klíč <tabulka_jednotne>_id: customer_id.
  • Časové sloupce s příponou _at, logické s předponou is_.

Soft delete?

Sloupec deleted_at místo skutečného mazání zní bezpečně, ale každý dotaz pak musí obsahovat WHERE deleted_at IS NULL a unikátní omezení přestanou dávat smysl. Pokud potřebujete historii, zvažte raději archivní tabulku nebo auditní log.

Ověření návrhu

Než schéma nasadíte, zkuste nad ním napsat pět nejčastějších dotazů aplikace (detail objednávky, objednávky zákazníka, tržby za měsíc…). Pokud je některý krkolomný, schéma pravděpodobně neodpovídá tomu, jak data používáte.

Popis domény a návrh tabulek můžete dát ColdHost AI v režimu Programování s modelem Polaris a nechat si schéma oponovat – hledat chybějící omezení a indexy.