database/sql и pgx: PostgreSQL в Go
Зачем знать: На уровне middle 1 вы пишете не toy-приложение, а сервис, который делает 10⁴-10⁶ запросов в секунду к Postgres. От того, как вы настроили pool, обработали ошибки, передали context — зависят latency и стабильность. На собесе спросят: разницу
Query/QueryRow/Exec, что делаетSetMaxOpenConns, чем pgx лучше lib/pq, как обнаружить deadlock черезpgErr.Code. Это базовый уровень для backend-инженера в РФ — Postgres стек, не Mongo.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Под капотом / Архитектура
- Gotchas
- Производительность
- Вопросы на собеседовании
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»В Go два уровня работы с БД:
database/sql— стандартный интерфейс stdlib. Не реализация: чтобы работать с конкретной БД, нужен драйвер.pgx(jackc/pgx) — Postgres-native драйвер, доступен и черезdatabase/sql, и как нативный API (pgxpool).
В 2026 ландшафт для Postgres:
jackc/pgx/v5— современный стандарт. Умеет: native binary протокол, JSONB/UUID/arrays типы, batch, listen/notify, COPY FROM.lib/pq— устарел. Maintainer официально отметил deprecated (2024). Новые проекты не выбирают.
Минимальный пример (database/sql + pgx как драйвер)
Заголовок раздела «Минимальный пример (database/sql + pgx как драйвер)»package main
import ( "context" "database/sql" "fmt" "log" "time"
_ "github.com/jackc/pgx/v5/stdlib" // регистрация драйвера "pgx")
func main() { dsn := "postgres://user:pass@localhost:5432/app?sslmode=disable" db, err := sql.Open("pgx", dsn) if err != nil { log.Fatal(err) } defer db.Close()
db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(30 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
if err := db.PingContext(ctx); err != nil { log.Fatal(err) }
var name string err = db.QueryRowContext(ctx, `SELECT name FROM users WHERE id = $1`, 42).Scan(&name) if err != nil { log.Fatal(err) } fmt.Println(name)}Минимальный пример (нативный pgxpool)
Заголовок раздела «Минимальный пример (нативный pgxpool)»import "github.com/jackc/pgx/v5/pgxpool"
ctx := context.Background()pool, err := pgxpool.New(ctx, dsn)if err != nil { log.Fatal(err)}defer pool.Close()
var name stringerr = pool.QueryRow(ctx, `SELECT name FROM users WHERE id = $1`, 42).Scan(&name)pgxpool даёт прямой доступ к pgx features (JSONB, arrays, batch) и обычно используется в новых проектах.
2. Под капотом / Архитектура
Заголовок раздела «2. Под капотом / Архитектура»2.1 database/sql как интерфейс
Заголовок раздела «2.1 database/sql как интерфейс»database/sql — это набор интерфейсов и pool/connection manager, который драйверы реализуют. Цепочка:
ваш код ↓database/sql (DB, Conn, Tx, Stmt, Rows) ↓driver.Driver (driver.Open) ↓драйвер (pgx, mysql, sqlite3, ...) ↓сеть/файл/TCP к БДРегистрация драйвера — sql.Register("pgx", &pgxDriver{}). Обычно через _ "github.com/jackc/pgx/v5/stdlib" (init() пакета вызовет Register).
2.2 sql.Open vs Ping
Заголовок раздела «2.2 sql.Open vs Ping»db, err := sql.Open("pgx", dsn) // НЕ подключается, только парсит dsn и создаёт DB// err тут — только parse error
err = db.PingContext(ctx) // вот тут реально открывается connectionПравило: после Open всегда Ping (с context!) — иначе ошибка соединения вылезет на первом запросе.
2.3 Connection pool
Заголовок раздела «2.3 Connection pool»*sql.DB — это не одно соединение, а пул. Под капотом:
- Список свободных connection’ов (
freeConn). - Список занятых (
numOpen). - Очередь ожидающих (
connRequests).
db.SetMaxOpenConns(25) // max соединений (по умолчанию unlimited!)db.SetMaxIdleConns(5) // max idle (по умолчанию 2)db.SetConnMaxLifetime(30*time.Minute) // max возрастdb.SetConnMaxIdleTime(5*time.Minute) // max idle timeДефолты:
MaxOpenConns = 0→ бесконечность. ОПАСНО в production (можно исчерпать Postgresmax_connections).MaxIdleConns = 2. Часто мало для высокого RPS.ConnMaxLifetime = 0→ коннекты живут навсегда. В k8s это плохо: Postgres перезапустился, а коннект подвис.ConnMaxIdleTime = 0→ idle не закрываются.
Рекомендуемая конфигурация для production
Заголовок раздела «Рекомендуемая конфигурация для production»db.SetMaxOpenConns(25) // или 50, 100 — зависит от Postgres max_connectionsdb.SetMaxIdleConns(25) // = MaxOpen для прогретого пулаdb.SetConnMaxLifetime(30*time.Minute) // против stale connectionsdb.SetConnMaxIdleTime(10*time.Minute) // балансирует idle vs reconnect overheadПравило: MaxOpen ≤ Postgres max_connections / число инстансов сервиса. Если max_connections=200 и 10 подов, на под — 20.
2.4 Query / QueryRow / Exec
Заголовок раздела «2.4 Query / QueryRow / Exec»| Метод | Возвращает | Использовать когда |
|---|---|---|
QueryContext | *sql.Rows | SELECT с N строками |
QueryRowContext | *sql.Row | SELECT с одной строкой (или сразу .Scan()) |
ExecContext | sql.Result | INSERT/UPDATE/DELETE без возврата строк |
rows, err := db.QueryContext(ctx, `SELECT id, name FROM users WHERE age > $1`, 18)if err != nil { return err}defer rows.Close()
var users []Userfor rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Name); err != nil { return err } users = append(users, u)}if err := rows.Err(); err != nil { // ВАЖНО! Ошибки итерации return err}Критично:
defer rows.Close()— иначе утечка connection.rows.Err()после цикла — итерация могла прерваться по ошибке.
QueryRow
Заголовок раздела «QueryRow»var name stringerr := db.QueryRowContext(ctx, `SELECT name FROM users WHERE id = $1`, 42).Scan(&name)if err == sql.ErrNoRows { return ErrUserNotFound}if err != nil { return err}QueryRow не возвращает err сразу — ошибка приходит в Scan. sql.ErrNoRows — если строк 0.
res, err := db.ExecContext(ctx, `UPDATE users SET name = $1 WHERE id = $2`, "alice", 42)if err != nil { return err}rows, err := res.RowsAffected()if err != nil { return err}if rows == 0 { return ErrUserNotFound}Result.LastInsertId() работает только в MySQL/SQLite. В Postgres используйте RETURNING id:
var id int64err := db.QueryRowContext(ctx, `INSERT INTO users (name) VALUES ($1) RETURNING id`, "alice").Scan(&id)2.5 Context — обязательно
Заголовок раздела «2.5 Context — обязательно»Всегда передавайте context. Без него:
- Запрос не отменится при timeout HTTP-запроса.
- Запрос не отменится при
Ctrl+Cсервиса. - Невозможно реализовать deadline.
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)defer cancel()
var v interr := db.QueryRowContext(ctx, "SELECT pg_sleep(1)").Scan(&v)// err: context deadline exceededПравило: в production коде нет методов без Context (Query, Exec, Begin без Ctx — антипаттерн в новом коде).
В pgx нативном API Context всегда первый аргумент — нет варианта без него.
2.6 Prepared statements
Заголовок раздела «2.6 Prepared statements»Ручной prepare
Заголовок раздела «Ручной prepare»stmt, err := db.PrepareContext(ctx, `SELECT name FROM users WHERE id = $1`)if err != nil { return err}defer stmt.Close()
for _, id := range ids { var name string if err := stmt.QueryRowContext(ctx, id).Scan(&name); err != nil { return err }}При множественных вызовах — быстрее (Postgres не парсит SQL каждый раз).
Auto-prepare в pgx
Заголовок раздела «Auto-prepare в pgx»pgx по умолчанию кэширует prepared statements per connection через QueryExecModeCacheStatement (v5 default).
// Тоже самое, но без явного Prepare:for _, id := range ids { db.QueryRowContext(ctx, `SELECT name FROM users WHERE id = $1`, id).Scan(&name)}// pgx внутри: первый запрос — Prepare + Execute, остальные — Execute по cached statement.Это часто быстрее, чем lib/pq, который не кэширует. Конфигурация в pgxpool:
config, _ := pgxpool.ParseConfig(dsn)config.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeCacheStatement // default// Альтернативы: QueryExecModeSimpleProtocol, QueryExecModeExec, QueryExecModeDescribeExec2.7 Scan: nullable, sql.Null*
Заголовок раздела «2.7 Scan: nullable, sql.Null*»var name sql.NullStringerr := db.QueryRowContext(ctx, "SELECT middle_name FROM users WHERE id=$1", 42).Scan(&name)if name.Valid { fmt.Println(name.String)}sql.Null* типы:
NullString,NullBool,NullInt64,NullInt32,NullInt16,NullByteNullFloat64,NullTime(Go 1.13+)
Альтернатива: указатели *string, *int64. Тогда nil = NULL. Многие предпочитают указатели — они лучше работают с JSON Marshal.
2.8 sql.RawBytes vs []byte
Заголовок раздела «2.8 sql.RawBytes vs []byte»sql.RawBytes — указатель на внутренний буфер. Действителен только до следующего rows.Next()! Не копирует данные:
var raw sql.RawBytesfor rows.Next() { rows.Scan(&raw) // НЕ держите raw долго! Он перепишется на следующем Next().}[]byte копирует. Безопаснее, но дороже.
2.9 pgx нативные фичи
Заголовок раздела «2.9 pgx нативные фичи»Type mapping
Заголовок раздела «Type mapping»pgx из коробки умеет:
uuid.UUID→ Postgresuuidtime.Time→timestamp/timestamptz[]int→int[](массивы)map[string]any→jsonb(через json.Marshal)pgtype.Numeric→numericбез потери точности
lib/pq всё это плохо умел — приходилось вручную писать Scanner/Valuer.
batch := &pgx.Batch{}for _, u := range users { batch.Queue(`INSERT INTO users (name) VALUES ($1)`, u.Name)}br := pool.SendBatch(ctx, batch)defer br.Close()for range users { if _, err := br.Exec(); err != nil { return err }}Все запросы шлются одним пакетом, ответы читаются последовательно. На сети — огромная экономия (1 RTT вместо N).
CopyFrom (bulk insert)
Заголовок раздела «CopyFrom (bulk insert)»rows := [][]any{ {"alice", 30}, {"bob", 25}, // ... тысячи строк}_, err := pool.CopyFrom(ctx, pgx.Identifier{"users"}, []string{"name", "age"}, pgx.CopyFromRows(rows))COPY FROM — самый быстрый bulk insert (5-10x быстрее VALUES).
Listen/Notify
Заголовок раздела «Listen/Notify»conn, err := pool.Acquire(ctx)defer conn.Release()_, err = conn.Exec(ctx, "LISTEN events")
for { n, err := conn.Conn().WaitForNotification(ctx) if err != nil { return err } fmt.Println("event:", n.Payload)}Pub/Sub из Postgres. Полезно для invalidation кэша или sync между процессами.
2.10 lib/pq vs pgx
Заголовок раздела «2.10 lib/pq vs pgx»| Свойство | lib/pq | pgx/v5 |
|---|---|---|
| Поддержка | Deprecated (2024) | Активно (jackc/pgx) |
| Binary protocol | Только text | Binary (быстрее) |
| Prepared cache | Нет | Да (per-conn) |
| UUID, JSONB, arrays | Нужны custom Scanner/Valuer | Из коробки |
| Batch | Нет | Да |
| Listen/Notify | Базовый | Полноценный |
| COPY FROM | Базовый | Optimized |
| pgxpool | Нет (только database/sql) | Да (нативный) |
| Доступ через database/sql | Да | Да (через pgx/stdlib) |
Вердикт 2026: новые проекты — pgx/v5 (нативно или через stdlib).
2.11 Reconnect handling
Заголовок раздела «2.11 Reconnect handling»Если Postgres перезапустился, существующий connection становится мёртвым. Что происходит:
database/sqlловит ошибку, помечает conn какbad, удаляет из pool.- Следующий запрос возьмёт новый conn (или создаст).
Но! Если есть connection_failure посередине транзакции — её не восстановить, нужно повторить.
SetConnMaxLifetime(30m) уменьшает шансы попасть на stale conn после restart’а Postgres: соединение проактивно закроется.
2.12 Errors
Заголовок раздела «2.12 Errors»sql.ErrNoRows
Заголовок раздела «sql.ErrNoRows»Sentinel error, который QueryRow.Scan возвращает, если 0 строк:
err := db.QueryRowContext(ctx, "SELECT ...").Scan(&v)if errors.Is(err, sql.ErrNoRows) { return ErrNotFound}pgconn.PgError
Заголовок раздела «pgconn.PgError»Конкретные коды ошибок Postgres:
import "github.com/jackc/pgx/v5/pgconn"
var pgErr *pgconn.PgErrorif errors.As(err, &pgErr) { switch pgErr.Code { case "23505": // unique_violation return ErrDuplicate case "23503": // foreign_key_violation return ErrInvalidReference case "40P01": // deadlock_detected return ErrDeadlock }}Коды: https://www.postgresql.org/docs/current/errcodes-appendix.html
if err != nil { return fmt.Errorf("get user %d: %w", id, err)}%w сохраняет цепочку для errors.Is/As.
2.13 Logging queries
Заголовок раздела «2.13 Logging queries»pgx tracelogger
Заголовок раздела «pgx tracelogger»import "github.com/jackc/pgx/v5/tracelog"
config.ConnConfig.Tracer = &tracelog.TraceLog{ Logger: tracelog.LoggerFunc(...), LogLevel: tracelog.LogLevelDebug,}Логирует каждый запрос с args и timing. На production — INFO или WARN, иначе диск утонет.
database/sql + hooks
Заголовок раздела «database/sql + hooks»Через sqlhooks, ocsql (OpenCensus) или собственный wrapper. На уровне пула middleware redacts чувствительные args.
2.14 Tracing (OpenTelemetry)
Заголовок раздела «2.14 Tracing (OpenTelemetry)»import ( "github.com/exaring/otelpgx" // или otelsql для database/sql)
config, _ := pgxpool.ParseConfig(dsn)config.ConnConfig.Tracer = otelpgx.NewTracer()Каждый запрос — span. Видно в Jaeger/Tempo.
2.15 Read replicas
Заголовок раздела «2.15 Read replicas»type Repo struct { write *pgxpool.Pool read *pgxpool.Pool}
func (r *Repo) GetUser(ctx context.Context, id int64) (User, error) { return loadFromPool(ctx, r.read, id)}func (r *Repo) CreateUser(ctx context.Context, u User) error { return insertToPool(ctx, r.write, u)}Простой подход: два пула, выбираем по типу операции. Внимание к replication lag — после INSERT’а read replica может не сразу видеть данные.
3. Gotchas
Заголовок раздела «3. Gotchas»3.1 Не Open + использовать, без Ping
Заголовок раздела «3.1 Не Open + использовать, без Ping»db, _ := sql.Open("pgx", "badurl")db.Query("...") // здесь упадёт впервыеOpen не подключается. Всегда Ping.
3.2 Утечка rows
Заголовок раздела «3.2 Утечка rows»rows, _ := db.Query("...")for rows.Next() { if cond { return } // НЕ закрыли rows — leak!}rows.Close()Решение: defer rows.Close() сразу после Query.
3.3 SQL injection
Заголовок раздела «3.3 SQL injection»// ПЛОХОdb.Query("SELECT * FROM users WHERE name = '" + name + "'")
// ХОРОШОdb.Query("SELECT * FROM users WHERE name = $1", name)Параметризованные запросы — единственная защита.
3.4 N+1
Заголовок раздела «3.4 N+1»for _, userID := range ids { db.QueryRow("SELECT * FROM orders WHERE user_id=$1", userID) // N запросов}
// Лучше:rows, _ := db.Query("SELECT * FROM orders WHERE user_id = ANY($1)", pq.Array(ids))3.5 Default MaxOpenConns = 0
Заголовок раздела «3.5 Default MaxOpenConns = 0»Без SetMaxOpenConns приложение может открыть тысячи conn’ов и убить Postgres.
3.6 Long-running query в одной conn
Заголовок раздела «3.6 Long-running query в одной conn»Если есть один тяжёлый запрос (минуты), он держит conn. Если MaxOpen=10, остальные 9 на месте, и приложение зависнет под нагрузкой.
Решение: timeout’ы через context, либо отдельный pool для аналитики.
3.7 sql.RawBytes lifetime
Заголовок раздела «3.7 sql.RawBytes lifetime»var raw sql.RawBytesfor rows.Next() { rows.Scan(&raw) go process(raw) // ОПАСНО: на след. Next() raw перепишется}Копируйте: data := append([]byte(nil), raw...).
3.8 Time zone
Заголовок раздела «3.8 Time zone»PostgreSQL timestamp без зоны vs timestamptz — разное. pgx по умолчанию возвращает timestamptz как time.Time в UTC, timestamp как time.Time с Local.
В коде — всегда работайте с UTC, конвертируйте на границе.
3.9 Float и numeric
Заголовок раздела «3.9 Float и numeric»numeric(10,2) — точный десятичный тип. В Go float64 теряет точность. Используйте pgtype.Numeric или shopspring/decimal:
var price decimal.Decimalpool.QueryRow(ctx, "SELECT price FROM products WHERE id=$1", id).Scan(&price)3.10 IN clause с переменным числом аргументов
Заголовок раздела «3.10 IN clause с переменным числом аргументов»? placeholders не поддерживают slice. Подходы:
// Postgres: используйте ANYdb.Query("SELECT * FROM users WHERE id = ANY($1)", pq.Array([]int{1,2,3}))// или pgx: pgxpool с pgx.QueryExecModeCacheStatementpool.Query(ctx, "SELECT * FROM users WHERE id = ANY($1)", []int{1,2,3})3.11 Transactions держат conn
Заголовок раздела «3.11 Transactions держат conn»db.BeginTx берёт conn из пула. До Commit/Rollback он не возвращается. Если внутри tx делаете долгие операции — пул быстро исчерпается.
3.12 prepared statement в pgx pool
Заголовок раздела «3.12 prepared statement в pgx pool»Кэш prepared per connection. При reconnect — кэш теряется. Это нормально.
В database/sql: prepared statement, созданный через db.Prepare, может потеряться, если drainнули pool. *sql.Stmt сам перевыполнит prepare на другом conn.
3.13 BatchResults.Close обязательно
Заголовок раздела «3.13 BatchResults.Close обязательно»br := pool.SendBatch(ctx, batch)defer br.Close() // обязательноИначе conn не вернётся в пул.
3.14 lib/pq и SSL
Заголовок раздела «3.14 lib/pq и SSL»В lib/pq dsn sslmode=disable — но в проде это плохо. Используйте sslmode=require или verify-full.
3.15 LISTEN/NOTIFY и conn lifetime
Заголовок раздела «3.15 LISTEN/NOTIFY и conn lifetime»LISTEN живёт на одном conn. Если этот conn закроется, notifications потеряются. Нужен retry loop.
3.16 Default search_path
Заголовок раздела «3.16 Default search_path»SET search_path действует на conn. Если используете несколько schema, лучше fully-qualify: myschema.users.
3.17 Encoding
Заголовок раздела «3.17 Encoding»Postgres по умолчанию UTF-8. Если БД в WIN1251, pgx может дать сюрпризы. Проверьте SHOW server_encoding.
3.18 Maximum identifier length
Заголовок раздела «3.18 Maximum identifier length»Postgres NAMEDATALEN=64. Имена таблиц/колонок/индексов длиннее 63 символов обрезаются. Тестируйте миграции.
4. Производительность
Заголовок раздела «4. Производительность»4.1 Pool sizing
Заголовок раздела «4.1 Pool sizing»Формула (от Brendan Burns, Kubernetes):
connections = ((core_count * 2) + effective_spindle_count)Для PG на SSD: cores * 2 — норма. Если 8 ядер БД — ~16 conn в пуле на сервис достаточно.
В микросервисах: instances * MaxOpen ≤ max_connections - reserved.
4.2 PgBouncer
Заголовок раздела «4.2 PgBouncer»Внешний connection pooler. Уменьшает число физических conn’ов к Postgres:
- transaction mode — самый эффективный, conn возвращается в пул после COMMIT. Но: prepared statements и LISTEN не работают.
- session mode — conn держится всю сессию клиента. Работают prepared, но меньше выигрыш.
В 2026 для high-RPS сервисов почти всегда есть PgBouncer перед Postgres.
С PgBouncer + pgx нужна осторожность с prepared statements: либо QueryExecModeSimpleProtocol, либо включить prepared statements в PgBouncer (если transaction mode — отключите кэш в pgx).
4.3 Latency vs throughput
Заголовок раздела «4.3 Latency vs throughput»- Один запрос:
SELECT id FROM users WHERE id=1~1ms (network + parse + execute + return). - 1000 запросов sequential: ~1s.
- С Batch: 1000 запросов ~10-50ms (один пакет).
- С CopyFrom: 100к строк ~100ms.
Если важна latency — минимизируйте RTT (batch, prepared, local conn).
4.4 Prepared statement выигрыш
Заголовок раздела «4.4 Prepared statement выигрыш»Без prepared: parse + plan + execute. Каждый раз. С prepared: parse + plan один раз, потом только execute.
Выигрыш — 10-30% для простых запросов, до 5x для сложных с большим plan’ом.
4.5 Binary protocol
Заголовок раздела «4.5 Binary protocol»pgx использует binary вместо text — int/timestamps передаются как 4/8 байт, не как строки. Экономия CPU и сети.
4.6 Bulk insert: VALUES vs COPY
Заголовок раздела «4.6 Bulk insert: VALUES vs COPY»| Метод | Скорость (10k строк) |
|---|---|
| INSERT row-by-row | ~10 сек |
| INSERT с multi-VALUES (10k штук) | ~1 сек |
| Batch (pgx.Batch) | ~500 ms |
| COPY FROM | ~50-100 ms |
COPY — победитель для bulk. VALUES имеет лимит на количество параметров (~65535 в Postgres).
4.7 Index hits
Заголовок раздела «4.7 Index hits»Запросы без индексов — full scan. EXPLAIN ANALYZE:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'a@b.com';Если Seq Scan на большой таблице — добавляйте индекс.
4.8 Connection lifetime в k8s
Заголовок раздела «4.8 Connection lifetime в k8s»Pod restarts → нужно правильно настроить:
SetConnMaxLifetime(30m)— соединение не живёт вечно.SetConnMaxIdleTime(10m)— idle закрывается.- Graceful shutdown:
db.Close()дожидается активных запросов.
4.9 Метрики pool
Заголовок раздела «4.9 Метрики pool»stats := db.Stats()log.Printf("open=%d in_use=%d idle=%d wait=%d wait_time=%v", stats.OpenConnections, stats.InUse, stats.Idle, stats.WaitCount, stats.WaitDuration)В Prometheus: экспортируйте Stats() как gauge’и. Высокий WaitCount = пул исчерпан, увеличьте MaxOpen.
В pgxpool: pool.Stat().
4.10 RowAffected vs Returning
Заголовок раздела «4.10 RowAffected vs Returning»Exec().RowsAffected() дешевле, чем Query + Scan для случаев, когда возвращаемые данные не нужны.
4.11 Pagination
Заголовок раздела «4.11 Pagination»Cursor-based (WHERE id > $1 ORDER BY id LIMIT 100) быстрее OFFSET 10000 LIMIT 100. Offset проходит N строк, cursor — ничего.
4.12 Slow query log
Заголовок раздела «4.12 Slow query log»В Postgres: log_min_duration_statement = 500ms — будут логироваться запросы дольше 500ms. Анализируйте регулярно.
5. Вопросы на собеседовании
Заголовок раздела «5. Вопросы на собеседовании»1. В чём разница sql.Open и db.Ping?
Open парсит DSN, не подключается. Ping устанавливает реальное соединение.
2. Что такое *sql.DB?
Не одно соединение, а пул. Управляет открытием/закрытием conn’ов автоматически.
3. Зачем SetMaxOpenConns?
По умолчанию 0 (unlimited) — приложение может убить Postgres. Production — обязательно ставить (например, 25-100).
4. Что делает SetConnMaxLifetime?
Закрывает соединение после N минут. Защищает от stale conn’ов (DNS rebalance, перезапуск БД).
5. Что произойдёт, если не вызвать rows.Close()?
Connection не вернётся в пул → leak → пул исчерпан → новые запросы блокируются.
6. Чем Query отличается от QueryRow?
Query возвращает *sql.Rows (итератор по строкам). QueryRow — одну строку (или sql.ErrNoRows).
7. Что возвращает Exec?
sql.Result с методами LastInsertId() (только MySQL/SQLite) и RowsAffected().
8. Зачем context в запросах? Cancel при timeout/shutdown. Без context запрос продолжит выполняться, даже если HTTP handler уже вернул response.
9. Что такое prepared statement? Парсенный и распланированный запрос на стороне БД. Повторное выполнение пропускает parse/plan.
10. Кэширует ли pgx prepared statements?
Да, по умолчанию через QueryExecModeCacheStatement (per connection).
11. Что такое sql.ErrNoRows?
Sentinel error, возвращается QueryRow.Scan при отсутствии строк. Проверяется через errors.Is.
12. Как обработать unique_violation?
var pgErr *pgconn.PgErrorif errors.As(err, &pgErr) && pgErr.Code == "23505" { ... }13. Чем pgx лучше lib/pq? Binary protocol, native UUID/JSONB/arrays, prepared cache, batch, CopyFrom, активная поддержка. lib/pq deprecated в 2024.
14. Что такое pgxpool? Native pool в pgx (без database/sql прослойки). Дает прямой доступ к pgx фичам.
15. Как делать bulk insert?
CopyFrom (быстрее всего), Batch (1 RTT), multi-VALUES (стандартный SQL).
16. Что такое sql.Null* типы?
Обёртки {Value, Valid} для nullable колонок. Альтернатива — указатели.
17. Как защититься от SQL injection?
Параметризованные запросы ($1, $2). Никогда конкатенация строк.
18. Что такое sql.RawBytes?
Указатель на внутренний буфер драйвера. Не копирует, но действителен только до следующего Next().
19. Что такое read replica и как с ним работать? Реплика для read-only запросов. Два пула: write (master), read (replica). Внимание к replication lag.
20. Что такое PgBouncer?
Внешний pooler перед Postgres. Уменьшает число conn’ов. Transaction mode — самый эффективный, но ограничивает prepared statements.
21. Что произойдёт, если Postgres restart’нётся?
Существующие conn’ы становятся bad. database/sql их выкинет из пула, следующий запрос откроет новый. Активные tx — потеряются.
22. Что такое N+1 и как избежать?
Цикл с запросом внутри: 1 главный + N для каждой строки. Лечится JOIN’ом или WHERE id = ANY($1).
23. Что делать с long-running query? Timeout через context, отдельный pool для аналитики (чтобы не блокировать OLTP), или вынести в отдельный воркер.
24. Когда использовать LISTEN/NOTIFY? Light pub/sub из Postgres: cache invalidation, sync между процессами. Не для high-throughput — Kafka/Redis лучше.
25. Чем COPY FROM лучше INSERT? В разы быстрее: bypass’ит SQL parsing, использует binary format. Для bulk-загрузки — выбор #1.
26. Что такое idle vs in_use в pool?
OpenConnections = InUse + Idle. InUse — занятые сейчас, Idle — свободные, готовые к выдаче.
27. Что такое WaitCount в db.Stats?
Сколько раз приложение ждало свободный conn (когда пул исчерпан). Высокий — увеличить MaxOpen.
28. Как трассировать SQL запросы?
OpenTelemetry: otelsql для database/sql, otelpgx для pgxpool. Каждый запрос — span с SQL текстом (sanitized).
29. Как логировать SQL? pgx tracelog, или обернуть driver. В проде — только slow queries или DEBUG.
30. Что такое RETURNING в Postgres?
INSERT ... RETURNING id возвращает значения. Используется вместо LastInsertId(), которого в Postgres нет.
6. Practice
Заголовок раздела «6. Practice»Задача 1: CRUD на pgx
Заголовок раздела «Задача 1: CRUD на pgx»Создайте UserRepo с методами Create/Get/Update/Delete, используя pgxpool. Покройте unit-тестами с testcontainers.
Задача 2: Pool tuning
Заголовок раздела «Задача 2: Pool tuning»Запустите два процесса, делающих 1000 запросов параллельно. Замерьте latency при MaxOpen=5/25/100. Постройте график.
Задача 3: Обработка unique_violation
Заголовок раздела «Задача 3: Обработка unique_violation»Реализуйте CreateUser, обрабатывающий дубликат email через pgErr.Code == "23505". Возвращайте свою ошибку ErrEmailTaken.
Задача 4: Bulk insert через CopyFrom
Заголовок раздела «Задача 4: Bulk insert через CopyFrom»Загрузите 100k строк в таблицу. Сравните: row-by-row, multi-VALUES, Batch, CopyFrom. Замерьте время.
Задача 5: Context cancellation
Заголовок раздела «Задача 5: Context cancellation»Запустите SELECT pg_sleep(10) с context.WithTimeout(1s). Убедитесь, что запрос отменился, проверьте сообщение об ошибке.
Задача 6: Read replica
Заголовок раздела «Задача 6: Read replica»Реализуйте Repo с двумя pgxpool (write/read). Замоксайте через testcontainers два postgres контейнера. Проверьте, что Get идёт на read, Create на write.
Задача 7: LISTEN/NOTIFY
Заголовок раздела «Задача 7: LISTEN/NOTIFY»Реализуйте уведомление о новых users: при INSERT триггер шлёт NOTIFY, отдельная goroutine LISTEN’ит и обновляет кеш.
Задача 8: Метрики
Заголовок раздела «Задача 8: Метрики»Экспортируйте db.Stats() или pool.Stat() как Prometheus gauges. Что увидите при нагрузке: WaitCount?
7. Источники
Заголовок раздела «7. Источники»- database/sql docs: https://pkg.go.dev/database/sql.
- jackc/pgx v5: https://github.com/jackc/pgx, https://pkg.go.dev/github.com/jackc/pgx/v5.
- Why pgx: https://github.com/jackc/pgx/wiki — рассказ автора о Migration с lib/pq.
- Don’t Defer Your Errors (Mat Ryer): лекция о rows.Err().
- Postgres error codes: https://www.postgresql.org/docs/current/errcodes-appendix.html.
- PgBouncer docs: https://www.pgbouncer.org/usage.html.
- otelpgx: https://github.com/exaring/otelpgx.
- Brendan Burns “Sizing connections” — для формулы пула.
- Книга “Postgres High Performance” Gregory Smith.
- Книга “Learning Go” Bodner (2nd ed., 2024) — глава по работе с БД.