Перейти к содержимому

ORM и Query Builders в Go: sqlx, squirrel, sqlc, GORM, ent

Зачем знать: На middle 1 ваша команда решает: писать сырой SQL через pgx, использовать sqlx ради удобства, генерировать код через sqlc, или брать GORM. Каждый инструмент — компромисс между скоростью, читаемостью и магией. От правильного выбора зависит производительность, читаемость диффов и шансы выстрелить N+1 в проде. На собесе спросят: чем sqlc лучше GORM, что такое N+1, когда squirrel оправдан, какие проблемы с auto-migrate. Без этих знаний middle backend в РФ не работает.

  1. Базовая концепция
  2. Под капотом / Best practices
  3. Gotchas
  4. Производительность
  5. Вопросы на собеседовании
  6. Practice
  7. Источники

Спектр инструментов работы с БД в Go:

raw SQL → query builder → ORM
[pgx] [squirrel] [GORM, ent, bun]
[sqlx]
[sqlc]

Чем правее — больше абстракции, больше магии, больше overhead. Чем левее — больше контроля и предсказуемости.

ИнструментКатегорияМагияType safetyПопулярность 2026
pgx (raw)ДрайверНетЧерез ScanВысокая
sqlxРасширение stdlibНизкаяЧерез StructScanВысокая
squirrelQuery builderНизкаяНизкаяСредняя
sqlcCode generationCompile-timeВысокаяОчень высокая
GORMActive Record ORMВысокаяНизкая (reflect)Высокая, но критика
entSchema-as-code ORMВысокаяВысокаяРастёт
bunLightweight ORMСредняяСредняяСредняя

Главная мысль: Go-сообщество в 2026 предпочитает sqlc + pgx для большинства проектов, потому что:

  • Type-safe (compile-time).
  • Близко к SQL (видно, что выполняется).
  • Без магии, легко дебажить.
  • Производительность как у raw.

GORM остаётся в legacy и в стартапах, где нужен быстрый CRUD. Но в production-системах от него часто уходят.


Уже разобрано в 15-database-sql-pgx.md. Кратко:

var u User
err := pool.QueryRow(ctx, `SELECT id, name FROM users WHERE id=$1`, 42).
Scan(&u.ID, &u.Name)

Плюсы: полный контроль, нулевой overhead. Минусы: много boilerplate’а, при изменении схемы — менять все Scan’ы вручную.

Расширение database/sql: добавляет StructScan, Get, Select, named queries.

import "github.com/jmoiron/sqlx"
db, _ := sqlx.Open("pgx", dsn)
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
}
// Один row → struct
var u User
err := db.GetContext(ctx, &u, `SELECT id, name FROM users WHERE id=$1`, 42)
// Много rows → slice
var users []User
err := db.SelectContext(ctx, &users, `SELECT id, name FROM users WHERE age > $1`, 18)
// Named query
type Filter struct{ Min int `db:"min"` }
rows, err := db.NamedQueryContext(ctx,
`SELECT * FROM users WHERE age > :min`, Filter{Min: 18})

StructScan использует reflection, чтобы матчить колонки SELECT id, name с полями User{ID, Name} по тэгу db. Магии немного — только reflection при чтении.

Плюсы: меньше boilerplate, обычные SQL-запросы. Минусы: reflection (медленнее на ~10-20%, но обычно неважно), runtime ошибки при опечатках в SQL.

Query builder с fluent API:

import sq "github.com/Masterminds/squirrel"
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
q := psql.Select("id", "name").
From("users").
Where(sq.Eq{"age": 18}).
OrderBy("name").
Limit(10)
sql, args, err := q.ToSql()
// sql: SELECT id, name FROM users WHERE age = $1 ORDER BY name LIMIT 10
// args: [18]
rows, err := db.QueryContext(ctx, sql, args...)

Композиция:

q := psql.Select("*").From("users")
if filter.MinAge > 0 {
q = q.Where(sq.GtOrEq{"age": filter.MinAge})
}
if filter.Name != "" {
q = q.Where(sq.Like{"name": "%" + filter.Name + "%"})
}

Использовать: для динамических запросов (фильтры из UI). Альтернатива — ручная конкатенация SQL, что чревато ошибками.

Не использовать: для статичных запросов — sqlc или sqlx читаемее.

Генерация Go-кода из SQL-файлов. Самый рекомендуемый подход в 2026.

Окно терминала
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
version: "2"
sql:
- engine: "postgresql"
queries: "internal/db/queries"
schema: "internal/db/migrations"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5" # генерировать под pgx
emit_interface: true # генерировать Querier интерфейс
emit_json_tags: true
emit_prepared_queries: true # авто-prepare
-- name: GetUser :one
SELECT id, name, age FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT id, name, age FROM users WHERE age > $1 ORDER BY name LIMIT $2;
-- name: CreateUser :one
INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id;
-- name: UpdateUser :exec
UPDATE users SET name = $1 WHERE id = $2;
-- name: DeleteUser :execrows
DELETE FROM users WHERE id = $1;

Аннотации:

  • :one — одна строка, возвращает struct.
  • :many — slice struct’ов.
  • :exec — без результата.
  • :execrows — возвращает rowsAffected.
  • :batchexec, :batchone, :batchmany — pgx batch.
Окно терминала
sqlc generate

Создаёт internal/db/queries.sql.go с функциями:

type User struct {
ID int64
Name string
Age int32
}
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRow(ctx, getUser, id)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Age)
return u, err
}
import "myapp/internal/db"
pool, _ := pgxpool.New(ctx, dsn)
q := db.New(pool)
user, err := q.GetUser(ctx, 42)

sqlc читает schema/ файлы (DDL) и проверяет, что SELECT’ы валидны. Если в SQL опечатка — sqlc generate упадёт с ошибкой.

  • Type-safe на compile time.
  • Видишь SQL, который выполняется.
  • Близко к 0% overhead vs raw pgx.
  • Интегрируется с pgx, lib/pq, mysql.
  • IDE автокомплит на сгенерированных функциях.
  • Динамические queries сложны (фильтры — нужен squirrel рядом).
  • Сложные joins требуют ручного prep.
  • Каждое изменение SQL — sqlc generate (можно через go:generate).

С sql_package: "pgx/v5" sqlc генерирует код, работающий с pgxpool.Pool / pgx.Tx. Это даёт доступ к pgx-фичам: pgxpool, batch, типам.

Полноценный ORM в стиле Active Record. Magic-heavy.

import "gorm.io/gorm"
import "gorm.io/driver/postgres"
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Age int
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // soft delete
}
db, _ := gorm.Open(postgres.Open(dsn))
db.AutoMigrate(&User{}) // ОПАСНО в prod, см. ниже
// Create
db.Create(&User{Name: "alice", Age: 30})
// Read
var u User
db.First(&u, 42)
db.Where("age > ?", 18).Find(&users)
// Update
db.Model(&u).Update("name", "alice2")
// Delete
db.Delete(&u, 42)
type User struct {
ID uint
Orders []Order // HasMany
Profile Profile // HasOne
}
type Order struct {
ID uint
UserID uint
User User
}
db.Preload("Orders").First(&u, 42) // загрузит user + его orders
func (u *User) BeforeSave(tx *gorm.DB) error {
u.Name = strings.ToLower(u.Name)
return nil
}

Запускается автоматически.

db.AutoMigrate(&User{}, &Order{})

ОПАСНО в production:

  • Не удаляет колонки.
  • Не делает миграции данных.
  • Изменения схемы непредсказуемы.
  • Нет version control.

Используйте для локальной разработки и тестов, в prod — golang-migrate/atlas.

С полем DeletedAt gorm.DeletedAt GORM не удаляет, а ставит deleted_at = NOW(). Все Find’ы автоматически добавляют WHERE deleted_at IS NULL. Удобно, но магия — забываешь и удивляешься “почему данные есть, но не находятся”.

  • N+1: db.Find(&users) потом цикл с db.Model(&u).Association("Orders").Find(&orders) — N+1 запрос. Лечится Preload.
  • Reflection на каждый запрос — overhead 10-30%.
  • Hooks в цикле — вызываются на каждый объект.
  • Lazy loading ассоциаций — легко словить N+1.

Бенчмарки показывают: GORM ~3-5x медленнее sqlc/pgx на CRUD.

  1. Active Record паттерн плохо ложится на Go (нет inheritance, нужны workarounds).
  2. Errors не возвращаются из chaindb.Where(...).First(&u), чтобы поймать gorm.ErrRecordNotFound, нужно после .Error.
  3. Magic SQL — не видно, что выполняется (нужен logger).
  4. Trust issues — обновления могут менять поведение.

Многие команды мигрируют с GORM на sqlc после первого инцидента в проде.

Open source от Facebook. Schema-as-code в Go, генерирует typed API.

schema/user.go
package schema
import "entgo.io/ent"
type User struct{ ent.Schema }
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
field.Int("age").Positive(),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("orders", Order.Type),
}
}
Окно терминала
go generate ./ent

Генерирует:

u, err := client.User.Create().SetName("alice").SetAge(30).Save(ctx)
u2, err := client.User.Query().Where(user.Age(30)).Only(ctx)
  • Type-safe queries (compile-time).
  • Graph-like API (edges, traversal).
  • Schema valida tion.
  • Hooks, privacy, GraphQL integration.
  • Steep learning curve.
  • Очень магический code generation.
  • Большой объём сгенерированного кода.
  • Не для простых CRUD-проектов.

В Faceboook ent используется массово (Tao, etc.). В Go-сообществе — нишевый, для сложных доменов с графами.

Наследник go-pg. Lightweight ORM с близким к SQL API.

import "github.com/uptrace/bun"
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string
}
db := bun.NewDB(pgxstdlib, pgdialect.New())
var u User
err := db.NewSelect().Model(&u).Where("id = ?", 42).Scan(ctx)
err = db.NewInsert().Model(&User{Name: "alice"}).Exec(ctx)

Менее популярен, чем GORM, но более идиоматичен (SQL-первый подход).

СценарийРекомендация
Простой CRUD-сервис, мало queriessqlx
Type safety + сложные queriessqlc (default)
Динамические фильтры из UIsquirrel рядом с sqlc
Прототип, нет времени на SQLGORM (но осторожно)
Сложная graph-схема (соц. сеть)ent
Команда из Python/Ruby, хочет Active RecordGORM или bun
High-performance criticalpgx (raw) или sqlc

Default в 2026: sqlc + pgx + squirrel для динамики + golang-migrate для миграций.

Бенчмарки сообщества (relative):

ToolSELECT oneINSERTSELECT 1000
pgx (raw)1.0x1.0x1.0x
sqlc + pgx1.05x1.02x1.03x
sqlx1.1x1.05x1.15x
squirrel + pgx1.05x1.05x1.05x
bun1.4x1.3x1.5x
ent1.5x1.4x1.8x
GORM2.5x3.0x4.0x

(Это grossly approximate, зависит от запроса.)

Вывод: sqlc/pgx — почти zero-overhead. GORM — 3-5x медленнее.

Что:

users := db.Find(&users) // 1 запрос
for _, u := range users {
db.Where("user_id=?", u.ID).Find(&u.Orders) // N запросов
}
// Итого: 1 + N запросов

Решения:

  1. JOIN: SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id.
  2. Preload (GORM): db.Preload("Orders").Find(&users).
  3. WHERE IN: загрузить users, потом SELECT * FROM orders WHERE user_id = ANY($1).
  4. DataLoader: batch’инг асинхронных запросов (для GraphQL).

В sqlc — пишете JOIN явно, проблемы нет:

-- name: ListUsersWithOrders :many
SELECT u.*, o.* FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

ORM’ы используют *sql.DB или pgxpool.Pool под капотом. Pool настройка та же:

sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(25)

(GORM пример.)

В sqlc — берёте свой pgxpool и передаёте в New(pool).

Чтобы не привязываться к конкретному ORM, заверните в интерфейс:

type UserRepo interface {
Get(ctx context.Context, id int64) (User, error)
Create(ctx context.Context, u User) (int64, error)
}
// pgRepo использует sqlc внутри
type pgRepo struct { q *db.Queries }
func (r *pgRepo) Get(...) {...}

В тестах — мок репозитория, не БД.

Все ORM’ы умеют логировать. В sqlc + pgx — pgx tracelog. В GORM:

import "gorm.io/gorm/logger"
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})

Для prod: только slow queries и errors, не каждый запрос.

  • sqlc: передаёшь pgx.Tx в db.New(tx).
  • GORM: db.Transaction(func(tx *gorm.DB) error { ... }).
  • ent: tx, _ := client.Tx(ctx) затем tx.User.Create()....

См. 17-transactions-migrations.md.


Не делайте этого. Auto-migrate:

  • Не удаляет колонки.
  • Не делает rename.
  • Не делает backfill.
  • Не делает миграцию данных.

В проде — миграции через golang-migrate/atlas.

var u *User
db.First(u, 42) // PANIC: nil pointer

Передавайте &User{}, не nil.

err := db.First(&u, 42).Error
if errors.Is(err, gorm.ErrRecordNotFound) { ... }

В sqlc/pgx — pgx.ErrNoRows/sql.ErrNoRows.

db.Where(&User{Active: false}).Find(...) // WHERE active=false НЕ выполнится!

GORM игнорирует zero values в struct-фильтрах. Используйте map: db.Where(map[string]any{"active": false}).

По умолчанию squirrel генерирует ? (MySQL). Для Postgres:

psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

Иначе будут ? в SQL, и Postgres упадёт.

type Audit struct {
CreatedAt time.Time `db:"created_at"`
}
type User struct {
Audit
Name string
}

sqlx поддерживает embedded — выберет колонки рекурсивно. Но с конфликтами имён бороться нужно вручную.

sqlc парсит SQL и матчит типы из schema. Если используете extension (PostGIS, pg_trgm) — типы могут не определиться. Решение: :exec без возврата, или явные CAST.

-- name: ListUsers :many
SELECT * FROM users WHERE age > $1 AND (name = $2 OR name LIKE $3);

Невозможно сделать условный WHERE. Лечится:

  1. squirrel для динамики.
  2. SQL с условными COALESCE:
WHERE ($1::int IS NULL OR age > $1)

После go generate ./ent папка ent/ весит мегабайты. Это нормально, но в IDE можно тормозить.

GORM пишет CreatedAt/UpdatedAt в local time по умолчанию. В проде с разными TZ серверов будут проблемы. Принудительно ставьте UTC:

db.Config.NowFunc = func() time.Time { return time.Now().UTC() }
err := db.Get(&u, "SELECT id, name FROM users WHERE id=$1", 42)
// если 0 строк: err = sql.ErrNoRows

То же поведение, что и QueryRow.Scansql.ErrNoRows sentinel.

В Gorm v2 контекст передаётся через db.WithContext(ctx). Забыли — запрос не отменится. Лечение: middleware на router, всегда db.WithContext(ctx).

В Go типах:

  • NOT NULL колонка → string, int64, time.Time.
  • NULL колонка → sql.NullString, pgtype.Text (в pgx).

Можно настроить overrides в sqlc.yaml:

overrides:
- column: "users.middle_name"
go_type: "*string"
db.Preload("Orders.User").Find(&users)
// orders загрузит, потом для каждого order загрузит user — а это те же users.
// Циклический preload — лучше JOIN.
  • GORM: db.CreateInBatches(&users, 100) — 100 за раз.
  • sqlc: используйте :copyfrom или :batchexec (pgx).
  • pgx raw: pool.CopyFrom.
CREATE UNIQUE INDEX uniq_users_email ON users(email);

С soft-delete: запись помечена удалённой, но email занят. Новая запись с тем же email не вставится. Решение: UNIQUE INDEX ... WHERE deleted_at IS NULL.

sqlc видит схему через DDL. Если migration сначала добавляет FK, потом sqlc generate, всё ок. Если порядок нарушен — generate упадёт.

GORM использует reflect почти на каждое поле. На больших struct’ах (50+ полей) overhead растёт. Sqlc — нет reflect, всё захардкожено.


ПодходLatency (median, локально)
pgx~150µs
sqlc~155µs
sqlx~165µs
GORM~400µs

Вставка 10к строк:

ПодходВремя
sqlc + pgx CopyFrom~80ms
sqlc + pgx Batch~250ms
sqlx ExecBatch~600ms
GORM CreateInBatches~1.5s
GORM Create в цикле~8s

Для 1000 users с orders:

  • N+1 (наивный): ~1.5s (1000 запросов).
  • JOIN: ~50ms.
  • Preload (GORM): ~80ms (2 запроса: users, orders WHERE IN).

GORM держит metadata о всех зарегистрированных моделях. На 50 моделей — десятки MB.

sqlc — только сгенерированные struct’ы, никакого runtime metadata.

sqlc генерация на 100 запросов — несколько секунд. ent на большом schema — минуты. GORM/sqlx — нет генерации.

pgx (под sqlc) — кэширует. GORM — тоже использует prepared (если не SimpleProtocol).

Все используют одинаковый pool (database/sql или pgxpool). Настройка MaxOpen/MaxIdle важна одинаково.

db.Where("a=?", 1).Where("b=?", 2).Where("c=?", 3).Find(&u)

GORM строит chain in-memory. Десятки Where — overhead. Лучше один Where("a=? AND b=? AND c=?", 1,2,3).

Для SELECT ... WHERE id = ANY($1) sqlc генерирует:

func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error)

Передавайте slice — pgx сериализует в array.

GORM по умолчанию SELECT *. Для оптимизации — db.Select("id, name").Find(&u). На таблицах с 30 колонками — экономия трафика.


1. Чем sqlx отличается от database/sql? sqlx — расширение, добавляет Get/Select/StructScan, named queries. Тот же *sql.DB под капотом.

2. Что такое sqlc и почему он популярен? Code generation из SQL: пишешь .sql, sqlc генерирует type-safe Go функции. Близко к raw pgx по скорости, ловит ошибки на compile time.

3. Чем sqlc лучше GORM? sqlc — без магии, видно SQL, compile-time safety, в 3-5x быстрее. GORM — Active Record, runtime reflect, скрытые N+1.

4. Что такое squirrel? Query builder с fluent API: Select("*").From("users").Where(sq.Eq{...}). Полезен для динамических queries.

5. Что такое N+1 проблема? Один запрос за главным списком + N запросов за деталями (по одному на каждый элемент). Лечится JOIN, Preload, WHERE IN.

6. Что такое Preload в GORM? Заранее загрузить ассоциированные данные одним дополнительным запросом (WHERE IN). Решает N+1, но не идеально.

7. Чем ent отличается от GORM? ent — schema-as-code, type-safe queries, edges (graph). GORM — Active Record, runtime reflection. ent сложнее, но безопаснее.

8. Когда использовать ORM, а когда raw SQL? ORM — когда CRUD простой и команда не любит SQL. Raw/sqlc — когда нужна производительность, контроль, type safety.

9. Почему AutoMigrate в проде — плохо? Не удаляет колонки, не делает rename, не делает backfill, не version-controlled. Используйте golang-migrate/atlas.

10. Что такое soft delete? Запись помечается deleted_at = NOW() вместо удаления. Все SELECT’ы фильтруют WHERE deleted_at IS NULL. В GORM — встроенное.

11. Какие минусы soft delete?

  • Unique constraints ломаются (email duplicate).
  • Запросы сложнее.
  • Размер таблицы растёт.
  • GDPR — иногда обязан удалять физически.

12. Что такое hooks в GORM? BeforeSave, AfterCreate etc. — функции, вызываемые автоматически. Удобно, но магия (поведение зависит от типа).

13. Как избежать N+1 в sqlc? Писать SQL с JOIN или WHERE IN явно. В sqlc видно, что выполняется, проблем меньше, чем в GORM.

14. Что такое sqlc.yaml? Конфигурационный файл sqlc: engine, paths, output package, overrides типов, sql_package (pgx/v5).

15. Что такое emit_interface в sqlc? Генерирует Go interface Querier со всеми методами. Удобно для моков в тестах.

16. Чем squirrel отличается от sqlc? squirrel — runtime построение query (динамическое). sqlc — compile-time (статическое). Можно использовать вместе.

17. Что такое bun? Lightweight ORM, наследник go-pg. SQL-first подход, более идиоматичный, чем GORM.

18. Какой ORM используется в Booking, Avito, Wildberries (по слухам)? Чаще: sqlx или sqlc + pgx. GORM реже, обычно в стартапах или старых проектах.

19. Что такое Active Record в Go? Паттерн ORM, где struct = строка БД, методы на struct’е (.Save(), .Delete()). В Go плохо ложится из-за отсутствия наследования.

20. Что лучше для type safety: sqlc или ent? Оба type-safe на compile time. sqlc проще, ent — больше фичей (edges, schema validation).

21. Может ли sqlc работать с MySQL? Да, поддерживает PostgreSQL, MySQL, SQLite.

22. Что такое overrides в sqlc? Замена типа Go для конкретной колонки: users.idMyUserID вместо int64.

23. Что такое prepared statements в ORM? Закэшированные на сервере (PG) SQL-запросы. ORM может авто-кэшировать (pgx, GORM с prepared=true).

24. Можно ли смешивать sqlc и raw pgx? Да. sqlc для статических queries, pgx для динамики/спецслучаев (LISTEN, COPY).

25. Какие минусы code generation подхода?

  • Лишний шаг в build (go generate).
  • Большой объём сгенерированного кода в git.
  • При изменении схемы — re-generate. Но плюсы (type safety, скорость) обычно перевешивают.

Возьмите простой проект на GORM (User CRUD), перепишите на sqlc + pgx. Замерьте: latency, время компиляции, размер бинаря.

Реализуйте ListUsers(ctx, filter) с опциональными name, min_age, max_age. Используйте squirrel для построения query.

Установите sqlc, создайте sqlc.yaml, опишите 5 запросов в queries.sql, сгенерируйте код, напишите main, выполните.

Создайте User has many Order. Сначала наивный код с N+1 (логируйте queries), потом с Preload. Сравните latency.

Создайте User schema в ent, сгенерируйте код, выполните CRUD. Сравните UX с sqlc.

Заверните sqlc-генерированные методы в свой UserRepo интерфейс. Замокируйте в unit-тестах через testify/mock.

Напишите бенчмарки на одно и то же действие (SELECT user by id × 10000) для: pgx raw, sqlc, sqlx, GORM. Получите цифры.

Реализуйте users с soft delete + unique email. Удалите пользователя, попробуйте создать с тем же email. Найдите проблему, исправьте через partial index.


  1. sqlc: https://sqlc.dev/, GitHub: https://github.com/sqlc-dev/sqlc.
  2. sqlx: https://github.com/jmoiron/sqlx, docs http://jmoiron.github.io/sqlx/.
  3. squirrel: https://github.com/Masterminds/squirrel.
  4. GORM: https://gorm.io/, docs.
  5. ent: https://entgo.io/.
  6. bun: https://bun.uptrace.dev/.
  7. Why sqlc (статьи): https://kyleconroy.com/blog/sqlc.
  8. GORM критика в Go community: HackerNews threads, https://www.reddit.com/r/golang.
  9. Benchmarks: https://github.com/uptrace/go-orm-benchmarks.
  10. Книга “Learning Go” Bodner (2nd ed., 2024) — глава по БД.
  11. Talk “Hello, sqlc” Kyle Conroy на GopherCon.