---
title: "Знакомство с sqlc: почему SQL-first"
description: "Что такое sqlc: генератор типобезопасного Go-кода из SQL-запросов. Сравнение sqlc vs GORM, философия SQL-first, когда sqlc — лучший выбор, а когда ORM."
course: "sqlc: типобезопасный код из SQL-запросов"
url: "https://golangmaster.ru/courses/sqlc/lessons/01-znakomstvo/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 15
---

# Знакомство с sqlc: почему SQL-first

*Курс «sqlc: типобезопасный код из SQL-запросов», урок 1 из 25. ~15 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/sqlc/lessons/01-znakomstvo/*

Представьте: в пятницу вечером падает эндпоинт с ошибкой `column "crated_at" does not exist` — опечатка внутри строки условия дожила до продакшена, потому что компилятор Go не заглядывает внутрь SQL-строк. Что такое sqlc: консольный генератор, который выносит SQL из строк в .sql-файлы, проверяет запросы по схеме базы и пишет за вас типобезопасный Go-код вместо ручного Scan. Разберём философию SQL-first, сравним sqlc vs GORM по этапу ловли ошибок и честно определим, когда sqlc — лучший выбор, а когда проще ORM.

Сценарий, знакомый каждому, кто писал бэкенд на Go. Пятница, вечер, в продакшене падает эндпоинт с сообщением `column "crated_at" does not exist`. Днём кто-то переименовал колонку в базе и поправил основной код — а одно условие, спрятанное строкой внутри вызова `Where(...)`, проверять было нечем: компилятор внутрь строк не смотрит, тест на этой ветке не гонялся. Код собрался, всё выглядело зелёным — до первого запроса.

Это история не про кривые руки, а про устройство категории инструментов: SQL, записанный строкой в Go-коде, невидим ни для компилятора, ни для рефакторингов, ни для автодополнения. Чем больше сервис, тем больше таких строк и тем дороже каждая. В этом уроке мы разберём sqlc — инструмент, который выносит SQL из строк в отдельные файлы, проверяет его по схеме базы и генерирует вместо вас всю нудную Go-обвязку. Связка «SQL-first плюс кодогенерация» — и есть ответ на вопрос, почему sqlc вообще появился.

### SQL в строках: что видит и чего не видит компилятор

```go
// GORM: условие — обычная строка
db.Where("user_id = ? AND crated_at > ?", uid, since).Find(&todos)

// database/sql: запрос — тоже строка, плюс ручной Scan по колонкам
rows, err := db.Query("SELECT id, titel FROM todos WHERE user_id = $1", uid)
```

Обе строки прекрасно компилируются: для Go это просто текст. Опечатка crated_at выстрелит в рантайме сообщением от драйвера, titel — тем же способом, только позже и, возможно, у пользователя. Линтеры и автодополнение сюда не заглядывают: строка есть строка. В среднем сервисе таких строк сотни. sqlc сделан ровно для того, чтобы вытащить их на свет — в .sql-файлы, где их можно проверить по схеме и превратить в типизированные функции.

### Что такое sqlc: генератор, а не библиотека

Что такое sqlc, проще всего объяснить через то, чем он не является. Это не ORM и не драйвер: в рантайме sqlc вообще не участвует в работе сервиса. Это консольный инструмент, который читает два входа — схему таблиц и аннотированные SQL-запросы в .sql-файлах — и пишет на выходе обычный Go-код: структуры для таблиц, методы для каждого запроса, аккуратный Scan внутри каждого метода. Рефлексии и struct-тегов в сгенерированном коде нет: это те же строки, которые вы писали бы руками, только без ошибок и опечаток.

Проект начал Кайл Конрой в 2019 году, в 2020-м sqlc дорос до версии 1.0 и с тех пор стал де-факто стандартом SQL-first-подхода в Go. Ставится одной командой: `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest`, есть и brew. Команд по сути три: `sqlc init` создаёт заготовку конфига sqlc.yaml, `sqlc generate` запускает генерацию, `sqlc vet` проверяет запросы правилами — до последней доберёмся в конце курса. Настройке конфига посвящён второй урок, сегодня важно само устройство конвейера.

### Философия SQL-first: источник правды — база

ORM переворачивает привычную работу с данными: источник правды — структура Go, а SQL библиотека сочиняет сама. sqlc ставит всё с ног на голову. Вы пишете SQL — такой, каким он уйдёт в базу: с джойнами, индексными условиями, оконными функциями, если они нужны. Гарантия «запрос делает ровно то, что написано» здесь бесплатна: вы видите текст целиком, а не догадываетесь, во что развернётся цепочка методов. Для человека, который дружит с SQL, это не ограничение, а облегчение: JOIN руками — не наказание, а десять строк прозрачного текста.

Отсюда и главная характеристика курса: sqlc — инструмент для тех, кто SQL уже знает. Синтаксис он не учит и плохой запрос не починит — просто честно сгенерирует код под него. Если SQL пока даётся с трудом, честный совет: начните с [курса GORM](/courses/gorm/), где слой данных спрятан за библиотекой, а сюда возвращайтесь, когда захочется контроля над каждым запросом.

### Вход 1: схема

```go
-- schema.sql
CREATE TABLE todos (
  id    BIGSERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  done  BOOLEAN NOT NULL DEFAULT false
);
```

Схема — отправная точка: без неё sqlc не знает, какие колонки существуют и какие у них типы. Это может быть один файл, как здесь, или папка с миграциями golang-migrate и goose — sqlc прочитает файлы в порядке имён и соберёт итоговую схему. Годится даже живая база, из которой схему можно вытащить напрямую — такой путь разбираем в уроках ближе к концу курса. В примерах всего курса возим таблицу todos: задачи трекера, до которого в итоговом проекте дойдём целиком.

### Вход 2: запросы с аннотациями

```go
-- query.sql

-- name: GetTodo :one
SELECT * FROM todos WHERE id = $1 LIMIT 1;

-- name: ListOpenTodos :many
SELECT * FROM todos WHERE done = false ORDER BY id;

-- name: CreateTodo :exec
INSERT INTO todos (title) VALUES ($1);
```

Перед каждым запросом — аннотация: имя будущей функции и форма возврата. `:one` вернёт одну строку (Todo, error), `:many` — срез ([]Todo, error), `:exec` — только error, для INSERT без возврата. Есть и другие формы: `:execrows` отдаёт число затронутых строк, `:execresult` — sql.Result, а для pgx есть `:copyfrom` и пакетные формы :batch. Знак $1 — плейсхолдер PostgreSQL; в MySQL и SQLite пишется вопросом. Звёздочку в SELECT sqlc раскроет в явный список колонок — как это делает сама база.

### Выход: типобезопасный Go-код

```go
// models.go — структура по таблице
type Todo struct {
        ID    int64
        Title string
        Done  bool
}

// query.sql.go — метод по запросу (сокращено)
const getTodo = `-- name: GetTodo :one
SELECT id, title, done FROM todos WHERE id = $1 LIMIT 1`

func (q *Queries) GetTodo(ctx context.Context, id int64) (Todo, error) {
        row := q.db.QueryRowContext(ctx, getTodo, id)
        var i Todo
        err := row.Scan(&i.ID, &i.Title, &i.Done)
        return i, err
}
```

BIGSERIAL превратился в int64, TEXT — в string, BOOLEAN — в bool. Nullable-колонки получили бы sql.NullString, указатели или типы pgtype — этому посвящён девятый урок. GetTodo принимает контекст и типизированный id int64: передать строку вместо числа или перепутать порядок аргументов не получится — не пустит компилятор. Внутри метода — тот самый Scan, который раньше вы писали руками. Сгенерированный код можно открыть и прочитать целиком: sqlc ничего не прячет, рядом лежит db.go с интерфейсом DBTX и методом WithTx для транзакций.

### Как этим пользоваться

```go
conn, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
        return err
}
defer conn.Close()

queries := db.New(conn) // db — сгенерированный пакет

todo, err := queries.GetTodo(ctx, 42)
if err != nil {
        return fmt.Errorf("получить задачу: %w", err)
}
fmt.Println(todo.Title) // автодополнение знает поля
```

db.New оборачивает соединение — *sql.DB или пул pgx — в структуру Queries. Дальше обычный Go: err, todo.Title, свои обёртки над ошибками. Если под условие не попало ни одной строки, вернётся sql.ErrNoRows, знакомый по database/sql. Так выглядит весь контракт sqlc: до генерации — SQL и схема, после — обычный код, который не отличить от рукописного.

### Что sqlc делает за вас

- Проверяет каждый запрос по схеме: таблицы, колонки, типы — до компиляции и тем более до продакшена
- Генерирует структуры таблиц в models.go и по одному файлу на каждый .sql с запросами
- Превращает плейсхолдеры в типизированные аргументы функции: $1 с типом bigint становится id int64
- Пишет Scan за вас: строки результата раскладываются по полям структур
- Работает с database/sql и с pgx/v4, pgx/v5 — переключается одной строкой в конфиге
- Держит транзакции на методе WithTx, который принимает *sql.Tx или pgx.Tx (урок 11)

### sqlc vs gorm: где ловится ошибка

Сравнение sqlc vs gorm удобнее всего вести по одной оси: на каком этапе проявится ошибка в слое данных. Возьмём переименование колонки. В GORM строка условия и поля структуры расходятся молча: компилятор всё устраивает, библиотека в рантайме отправит запрос со старым именем — и вы узнаете об этом от драйвера, лучше всего в пятницу вечером. У sqlc цикл другой: правите схему, запускаете `sqlc generate` — и если запрос разъехался со схемой, генерация падает с указанием файла и строки. А если запросы перегенерировались, но вызовы вы обновить забыли, их отсечёт уже компилятор Go: сигнатуры-то изменились. Двойной заслон вместо одного.

Оборотная сторона: GORM строит запросы в рантайме и потому умеет менять их на лету, sqlc работает только с тем, что зафиксировано в файлах. Так что sqlc — не «лучший GORM», а другой контракт работы с базой; выбирать нужно под тип задач, и следующая таблица в этом поможет.

### sqlc vs GORM: сравнение по критериям

| Критерий | sqlc | GORM |
| --- | --- | --- |
| Что пишет разработчик | SQL в .sql-файлах | структуры и цепочки методов |
| Кто собирает SQL | вы сами, руками | библиотека в рантайме |
| Ошибка в имени колонки | на sqlc generate, до компиляции | в рантайме |
| Рефлексия и struct-теги | нет — обычный Go-код | основа всей библиотеки |
| Динамические запросы | трудно: COALESCE, отдельные выборки | легко: Where() с условиями |
| Миграции | нет, снаружи (golang-migrate, goose) | AutoMigrate из коробки |
| Что нужно знать | уверенный SQL | API библиотеки |

### Когда sqlc — не лучший выбор

Обещал честность — вот мой случай. Первый проект, который я тащил на sqlc, начинался со страницы поиска: шесть опциональных фильтров, любая комбинация, сортировка на выбор. В GORM это три строки с Where(), в sqlc — полдень возни: либо один запрос с COALESCE-трюками, либо отдельная выборка под каждую комбинацию. Динамический SQL sqlc сознательно не умеет, и после того дня я знаю, где у него проходит граница.

Моё правило с тех пор такое: если схема ещё не устоялась и модель данных меняется каждую неделю, sqlc будет мешать — каждая правка колонки означает перегенерацию и волну ошибок компиляции. Это фича, а не баг, но в прототипе она бесит, и такой проект я спокойно веду на GORM. Не берите sqlc и там, где команда не знает SQL: инструмент не спасёт от плохих запросов, он их только зафиксирует. Зато когда схема стабильна, запросов много, а половина из них — отчёты и тяжёлые выборки, sqlc окупается с первой недели: типобезопасность и читаемость там решают. Развёрнутое сравнение с GORM, ent и squirrel — в двадцать четвёртом уроке.

> **Чего sqlc не делает.** Под ноль магии значит и под ноль удобств ORM. Миграций у sqlc нет — берите golang-migrate или goose (шестнадцатый урок). Связей между таблицами и жадной загрузки нет — JOIN пишете вы, а не библиотека. Динамические запросы собираются только средствами самого SQL: COALESCE, ANY, отдельные выборки. Это не список недостатков, а границы контракта: за ними начинается ваш SQL.

> **Песочница сайта не запускает sqlc.** Браузерный интерпретатор сайта исполняет чистый Go без сети и файловой системы, поэтому код с sqlc и базой в уроках не запускается в песочнице. Для практики нужен локальный Go, сам sqlc и база: проще всего SQLite (файл вместо сервера) или PostgreSQL в Docker. Интерактивы урока — тесты, «что выведет», карточки — работают прямо в браузере и дают XP.

> **Что нужно знать перед курсом.** Уверенный SQL (джойны, GROUP BY, базовые типы) и базовый Go: структуры, методы, ошибки, указатели — это разбирается в [базовом курсе](/courses/go-basics/), а типы и их нулевые значения — в уроке про [переменные и константы](/courses/go-basics/lessons/04-peremennye-konstanty/). Если вы уже проходили [курс GORM](/courses/gorm/), пара мест покажется знакомой: мы будем сравнивать подходы на тех же задачах.

### Проверка: что такое sqlc

**Вопрос:** Что именно делает sqlc?

- Библиотека, которая в рантайме по struct-тегам собирает SQL-запросы
- ✅ Консольный генератор: читает схему и аннотированные SQL-запросы, пишет типобезопасный Go-код
- Драйвер PostgreSQL с типизированными методами вместо строк
- Фреймворк миграций: создаёт таблицы по структурам Go

В рантайме sqlc не участвует вообще: сгенерированный код работает поверх обычного database/sql или pgx. Теги, рантайм-сборку запросов и AutoMigrate оставим ORM — это профиль GORM, а не sqlc.

### Проверка: ошибку в колонке

**Вопрос:** В запросе опечатка: SELECT titel FROM todos. Что произойдёт при sqlc generate?

- Код сгенерируется, а ошибка вылезет в рантайме при первом запросе
- ✅ Генерация упадёт с ошибкой: колонки titel нет в схеме
- Проект не соберётся: компилятор Go проверит колонки по models.go
- sqlc молча исправит опечатку и сгенерирует корректный код

sqlc сверяет каждый запрос с каталогом схемы: неизвестная таблица или колонка роняет генерацию с указанием файла и строки. Компилятор Go тут ни при чём — до него дело просто не доходит. У ORM та же опечатка в строке Where() дожила бы до рантайма.

### Прочитайте запрос как sqlc: какая сигнатура получится?

```go
-- schema.sql
CREATE TABLE todos (
  id    BIGSERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  done  BOOLEAN NOT NULL DEFAULT false
);

-- query.sql
-- name: CountOpenTodos :one
SELECT count(*) FROM todos WHERE done = false;
```

**Что выведет программа?**

- ✅ func (q *Queries) CountOpenTodos(ctx context.Context) (int64, error)
- func (q *Queries) CountOpenTodos(ctx context.Context, done bool) (int64, error)
- func (q *Queries) CountOpenTodos(ctx context.Context) ([]int64, error)
- func (q *Queries) CountOpenTodos() error

Разбираем по шагам: :one — одна строка, значит результат плюс error. В запросе нет плейсхолдеров — аргументов, кроме ctx, не будет. count(*) в PostgreSQL имеет тип bigint, а bigint sqlc отображает в int64. Вариант с []int64 дал бы :many, а параметр done появился бы только при плейсхолдере $1 в WHERE.

### Восстановите аннотации

Два запроса, две формы возврата: чтение одной записи и список. Впишите формы в аннотации.

```go
-- name: GetTodo ___
SELECT id, title, done FROM todos WHERE id = $1 LIMIT 1;

-- name: ListOpenTodos ___
SELECT id, title, done FROM todos WHERE done = false ORDER BY id;
```

Подсказка: Одна запись — :one, список строк — :many. Двоеточие обязательно.

Правильные ответы в пропусках: `:one`, `:many`

### Соберите рабочий цикл sqlc

Расставьте шаги от чистого репозитория до вызова сгенерированного метода.

Правильный порядок строк:

1. `sqlc init — создать заготовку sqlc.yaml`
2. `описать в sqlc.yaml движок, пути к схеме и запросам, пакет генерации`
3. `записать схему таблиц в schema.sql или в файлы миграций`
4. `написать запросы в query.sql с аннотациями -- name: ... :one/:many/:exec`
5. `запустить sqlc generate — появятся models.go, db.go и методы`
6. `вызвать сгенерированный метод; ошибки типов ловит компилятор`

### Карточки: словарь sqlc

- **sqlc** — CLI-генератор кода (Кайл Конрой, 2019): читает схему и аннотированные SQL-запросы из .sql-файлов, пишет типобезопасный Go-код. В рантайме не участвует.
- **SQL-first** — Подход, при котором источник правды — SQL, написанный руками: Go-код генерируется из запросов, а не запросы из структур и тегов.
- **:one, :many, :exec** — Формы возврата в аннотации -- name:. :one — одна строка (T, error), :many — срез ([]T, error), :exec — только error. Ещё есть :execrows, :execresult и pgx-формы.
- **Queries и DBTX** — Сгенерированный репозиторий: методы запросов поверх интерфейса DBTX (ExecContext, QueryContext и другие). Подойдёт *sql.DB, *sql.Tx через WithTx или пул pgx.
- **sqlc generate** — Команда генерации: сверяет запросы со схемой и пишет db.go, models.go и файлы запросов. Ошибка в колонке падает здесь, а не в продакшене.
- **Чем sqlc отличается от ORM** — Нет рефлексии, тегов и рантайм-сборки SQL: запросы пишете вы, sqlc — только Go-обвязку. Динамические запросы, миграции и связи — не его зона.

### Что дальше по курсу

Во втором уроке поставим sqlc и соберём конфиг: структура sqlc.yaml, версия, движки postgresql, mysql и sqlite, вывод пакетов. В третьем — первые запросы и генерация: напишем GetAuthor-подобные выборки и посмотрим на результат целиком. Весь план из 25 уроков — на [странице курса](/courses/sqlc/), курс ведём на сквозном примере трекера задач до итогового проекта включительно.

А если вы ещё не выбрали подход, полистайте вводный урок [курса GORM](/courses/gorm/lessons/01-znakomstvo/) — там та же работа со слоем данных показана с ORM-стороны. Держать перед глазами обе картины полезно: в двадцать четвёртом уроке будем выбирать между ними уже осознанно.

## Коротко: что запомнить

- sqlc — CLI-генератор, а не библиотека: в рантайме работает только сгенерированный обычный Go-код.
- SQL-first: запросы в .sql-файлах — источник правды, структуры и Scan генерируются из них.
- Аннотация -- name: задаёт имя функции и форму возврата: :one, :many, :exec и другие.
- sqlc проверяет запросы по схеме на этапе генерации — ошибка в колонке не доходит ни до компилятора, ни до продакшена.
- Работает с database/sql и pgx/v4, pgx/v5; переключение — одна строка в sqlc.yaml.
- Динамические запросы, миграции и связи — не зона sqlc: первое через COALESCE, второе снаружи (goose, golang-migrate).
- sqlc силён при стабильной схеме и уверенном SQL; в прототипе с плавающей моделью удобнее ORM.

## Домашнее задание

Поставьте sqlc локально (go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest), выполните sqlc init и соберите конфиг под движок sqlite (тип BIGSERIAL из урока замените на INTEGER PRIMARY KEY AUTOINCREMENT). Возьмите схему todos, добавьте колонку priority INTEGER NOT NULL DEFAULT 0, напишите три запроса с аннотациями :one, :many и :exec, запустите sqlc generate. Откройте сгенерированные файлы и найдите структуру Todo, константу с текстом запроса и метод со Scan. Затем переименуйте колонку в запросе и посмотрите на ошибку генерации — это то самое сообщение, которое в обычном проекте спасает продакшен.

## Вопросы и ответы по уроку

### Что такое sqlc простыми словами?

Программа, которая читает ваши SQL-запросы и схему базы, а затем пишет за вас обычный Go-код: структуры для таблиц и функции для запросов с готовым Scan. Вы отвечаете за SQL, sqlc — за типобезопасную обвязку.

### sqlc vs GORM: что выбрать?

Зависит от задач. Стабильная схема, много отчётов, команда, знающая SQL, — берите sqlc: ошибки ловятся на генерации. Быстрый прототип, частые смены модели, AutoMigrate и ассоциации из коробки — удобнее GORM. В одном проекте их можно совмещать.

### Работает ли sqlc только с PostgreSQL?

Нет. Кроме PostgreSQL есть движки MySQL и SQLite, а на стороне Go — database/sql (включая pgx в режиме stdlib) и нативные pgx/v4, pgx/v5. Движок и драйвер выбираются в sqlc.yaml.
