Irbisa · Шпаргалка
Базы данных и SQL в Go
Пул database/sql, транзакции, NULL-значения, pgx, миграции
В чём разница между
WHEREиHAVINGи междуINNER JOINиLEFT JOIN?-- customers with more than 5 paid orders this yearSELECT c.id, c.email, count(*) AS paid_ordersFROM customers cJOIN orders o ON o.customer_id = c.id -- INNER: customers with no orders drop outWHERE o.status = 'paid' -- row filter, runs before GROUP BY AND o.created_at >= '2026-01-01'…Что на самом деле делает
sql.Openи почему он не возвращает ошибку, когда база недоступна?func openDB(ctx context.Context, dsn string) (*sql.DB, error) { // No network traffic here. An error means an unknown driver or a // malformed DSN, never "the database is down". db, err := sql.Open("pgx", dsn) if err != nil { return nil, fmt.Errorf("open: %w", err)…Когда нужны
Exec,QueryиQueryRowи что сломается, если выполнитьDELETEчерезQuery?// wrong: Query on a write. The rows are never closed, so the connection never// returns to the pool. The DELETE itself succeeds, which hides the leak.rows, err := db.QueryContext(ctx, "DELETE FROM sessions WHERE expires_at < now()")if err != nil { return err}…Почему отсутствующая строка приходит как
sql.ErrNoRowsизScan, а не из самогоQueryRow, и где эту ошибку обрабатывать?var ErrUserNotFound = errors.New("user not found") func (r *Repo) User(ctx context.Context, id int64) (User, error) { var u User err := r.db.QueryRowContext(ctx, "SELECT id, email, created_at FROM users WHERE id = $1", id,…Колонка может быть NULL. Во что её сканировать, как записать NULL обратно и что меняет
COALESCE?func profile(ctx context.Context, db *sql.DB, id int64) error { // wrong: var nickname string // sql: Scan error on column index 0, name "nickname": // converting NULL to string is unsupported var nickname *string // nil means NULL; marshals to JSON null…Этот экспорт иногда выгружает меньше строк, чем есть в таблице, и в логе никогда нет ошибки. Чего не хватает?
func exportUsers(ctx context.Context, db *sql.DB, w io.Writer) error { rows, err := db.QueryContext(ctx, "SELECT id, email FROM users ORDER BY id") if err != nil { return err } defer rows.Close() for rows.Next() { var id int64 var email string if err := rows.Scan(&id, &email); err != nil { return err } fmt.Fprintf(w, "%d,%s\n", id, email) } return nil}Что означает ACID и какие аномалии допускает каждый из уровней Read Committed, Repeatable Read и Serializable?
Какие типы индексов есть в PostgreSQL, когда составной индекс помогает запросу и как читать
EXPLAIN ANALYZE?CREATE INDEX CONCURRENTLY orders_user_created_idx ON orders (user_id, created_at DESC); -- uses it: equality on the leading column, range and order on the secondEXPLAIN (ANALYZE, BUFFERS)SELECT id, total FROM orders…Эта функция оформляет заказ в транзакции и проходит тесты. Какие две ошибки проявятся в продакшене?
func (s *Store) PlaceOrder(ctx context.Context, o Order) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } if _, err := tx.ExecContext(ctx, insertOrderSQL, o.ID, o.UserID); err != nil { return err } if _, err := tx.ExecContext(ctx, reserveStockSQL, o.SKU, o.Qty); err != nil { tx.Rollback() return err } tx.Commit() return nil}Эта функция поиска уязвима для SQL-инъекции. Плейсхолдер чинит одну из двух подстановок. Что делать со второй?
func (r *Repo) Search(ctx context.Context, q, sortBy string) (*sql.Rows, error) { query := fmt.Sprintf( "SELECT id, name FROM docs WHERE name ILIKE '%%%s%%' ORDER BY %s LIMIT 50", q, sortBy) return r.db.QueryContext(ctx, query)}Как работают
SetMaxOpenConns,SetMaxIdleConnsиSetConnMaxLifetimeи как подобрать размер пула для нагруженного сервиса?func configurePool(db *sql.DB) { // Per pod. Postgres max_connections is shared by every pod, every cron // job and whatever a human has open in psql. db.SetMaxOpenConns(20) db.SetMaxIdleConns(20) // equal, or the pool re-dials under load db.SetConnMaxLifetime(30 * time.Minute) // recycle by age: DNS/failover changes, proxy max age…Что происходит с выполняющимся запросом, когда контекст запроса отменён, и зачем всё равно нужен
statement_timeout?func (r *Repo) TopSellers(ctx context.Context, since time.Time) ([]Row, error) { // A slice of the request's budget: one slow report cannot hold a pool // connection for a minute, and a client disconnect cancels it too. ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) defer cancel()…Как провести одну транзакцию через несколько методов репозитория, не протаскивая
*sql.Txв сервисный слой?// Implemented by *sql.DB, *sql.Tx and *sql.Conn.type DBTX interface { ExecContext(context.Context, string, ...any) (sql.Result, error) QueryContext(context.Context, string, ...any) (*sql.Rows, error) QueryRowContext(context.Context, string, ...any) *sql.Row}…Чем отличаются
database/sql,pgx,sqlx,sqlcи GORM и как выбрать одно из них для нового сервиса?type Order struct { ID int64 `db:"id"` UserID int64 `db:"user_id"` Total int64 `db:"total"`}…Что такое проблема N+1 запросов и как исправить её в Go без ORM?
// Two queries total, however many orders came back.func (r *Repo) OrdersWithItems(ctx context.Context, userID int64) ([]Order, error) { orders, err := r.orders(ctx, userID) if err != nil || len(orders) == 0 { return orders, err }…Как эффективно вставить из Go 200 000 строк и что ограничивает многострочный
INSERT?// pgx native: one statement, one stream, no per-row protocol overheadfunc loadEvents(ctx context.Context, pool *pgxpool.Pool, events []Event) (int64, error) { return pool.CopyFrom(ctx, pgx.Identifier{"events"}, []string{"user_id", "kind", "at"}, pgx.CopyFromSlice(len(events), func(i int) ([]any, error) {…Чем оптимистичная блокировка отличается от пессимистичной и как построить очередь задач на Postgres с
SKIP LOCKED?// Optimistic: no lock held; the version check catches a concurrent writer.func (r *Repo) Rename(ctx context.Context, id, version int64, title string) error { res, err := r.db.ExecContext(ctx, ` UPDATE docs SET title = $1, version = version + 1 WHERE id = $2 AND version = $3`, title, id, version) if err != nil {…Два перевода между одними и теми же счетами падают с
40P01 deadlock detected. Почему так происходит и как это предотвратить, а не просто ретраить?// wrong: lock order follows the transfer direction, so A->B and B->A deadlock// SELECT ... WHERE id = $from FOR UPDATE; then SELECT ... WHERE id = $to FOR UPDATE // right: one deterministic order for every transaction in the systemfunc transfer(ctx context.Context, tx *sql.Tx, from, to, amount int64) error { ids := []int64{from, to}…Найдите баг: под нагрузкой
CreateOrderзависает до таймаута запроса, хотя сама база почти простаивает.func (s *Store) CreateOrder(ctx context.Context, o Order) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() if _, err := tx.ExecContext(ctx, insertOrderSQL, o.ID, o.UserID, o.Total); err != nil { return err } if _, err := s.db.ExecContext(ctx, insertAuditSQL, o.ID, "order_created"); err != nil { return err } return tx.Commit()}Что означает
db.Prepare, когда под ним пул соединений, и почему общий*sql.Stmtможет ломаться за PgBouncer?// wrong: a process-wide Stmt over a pool: re-prepared on every connection it// touches, and broken behind a transaction-mode pooler.// var getUser *sql.Stmt // prepared in main(), used from every handler // right: just pass the args; pgx caches the statement per connection itselffunc (r *Repo) User(ctx context.Context, id int64) (User, error) {…Как работает MVCC в PostgreSQL и почему одна долгая транзакция может раздуть таблицы для всех?
Как ретраить транзакцию, которая падает с SQLSTATE
40001на уровнеSerializable, и чего ни в коем случае не должно быть внутри цикла повторов?func (r *Repo) InSerializableTx(ctx context.Context, fn func(DBTX) error) error { const maxAttempts = 4 var err error for attempt := 1; attempt <= maxAttempts; attempt++ { err = r.runOnce(ctx, fn) // the whole transaction, re-read from scratch if err == nil || !isRetryable(err) {…Как изменить схему в продакшене без простоя (например, переименовать колонку) и где здесь место goose или golang-migrate?
// Release 1 code: still reads name, writes both, so old pods keep working.func (r *Repo) SetName(ctx context.Context, id int64, name string) error { _, err := r.db.ExecContext(ctx, "UPDATE users SET name = $1, full_name = $1 WHERE id = $2", name, id) return err}…В чём разница между репликацией и шардированием и когда выбирать синхронную репликацию вместо асинхронной?