SQL - контракт, який переживе ваш ORM

2026-09-10

Кожен продукт, який я відвантажив, пережив хоча б одне рішення про фреймворк. База даних пережила всі. Версії Next.js змінюються, ORM міняють, команда ротується - рядки лишаються, а разом з ними кожне припущення, зашите в схему. Тому SQL для мене - справжній контракт продукту, а не деталь реалізації за класом-репозиторієм.

Prisma закриває більшу частину мого дня: типізований CRUD, міграції в репозиторії, схема, яку прочитає новий розробник. Але SQL, який вона генерує, я читаю. Коли сторінка списку робить запит на кожен рядок, коли звіту потрібна віконна функція, коли дашборду треба GROUP BY через три джойни - пишу запит руками через $queryRaw або кладу у view. Перш ніж хтось додасть Redis, відкриваю EXPLAIN ANALYZE. Більшість тікетів «нам потрібен кеш» - це відсутній індекс.

Обмеження - найдешевше QA, яке у вас буде. NOT NULL, UNIQUE, FOREIGN KEY і CHECK спрацьовують на кожен запис, з кожного сервісу, включно зі скриптом, який хтось запускає руками в п’ятницю. Zod стереже API, база стереже правду. Гроші - в цілих мінорних одиницях або NUMERIC, ніколи не float. Вебхуки платежів приходять двічі, тож ключ ідемпотентності отримує унікальний індекс, а не коментар у хендлері.

Конкурентність - там, де живуть FinTech-баги. Два запити читають баланс, обидва вирішують, що вистачає, обидва пишуть - це lost update, і жодна валідація на фронті його не виправить. Віддаю перевагу одній атомарній інструкції - UPDATE ... SET balance = balance - $1 WHERE id = $2 AND balance >= $1, а потім перевірка, що змінився рівно один рядок - або транзакції з SELECT ... FOR UPDATE, плюс append-only таблиця леджера, щоб кожен рух можна було пояснити потім. На системах з $1.5M+ processed volume «пояснити потім» - це і є вся робота.

Індекси йдуть за запитами, які ви реально виконуєте, а не за колонками, що виглядають важливими. Складені індекси: спершу колонки з рівністю, в кінці діапазон чи сортування; часткові індекси для «активних» рядків; keyset-пагінація замість OFFSET 50000. Міграції на живій таблиці - кроками: додати колонку як nullable, заповнити батчами, потім додати обмеження; індекс будувати CONCURRENTLY. Міграція, яка блокує таблицю замовлень опівдні, - це аварія з прикріпленим пул-реквестом.

Що відмовляюсь робити: «гнучка» таблиця entity-attribute-value замість схеми, JSONB як схованка для ключових полів, які ніхто не хоче моделювати, soft delete всюди без думки про унікальні обмеження, і кеш перед запитом, для якого ніхто не робив EXPLAIN. Якщо шлете повільну сторінку чи схему, в якій не впевнені - надішліть запит і кількість рядків. Відповім планом, індексом і одним обмеженням, яке додав би першим.

Дайте наступному продукту того, хто лишається.

Готовий стартувати зараз і лишитись. Довготермінові ретейнери, не злив за два тижні. Напишіть - обговоримо проєкт, вимоги і як рухатись далі.

Запитайте про досвід чи послуги