Irbisa · Шпаргалка

Базы данных и SQL в Go

Пул database/sql, транзакции, NULL-значения, pgx, миграции

Middle-разработчик · 24 вопроса

  1. 01

    В чём разница между WHERE и HAVING и между INNER JOIN и LEFT JOIN?

    WHERE фильтрует строки до группировки, а HAVING фильтрует группы после того, как посчитаны агрегаты.

    -- 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'…
  2. 02

    Что на самом деле делает sql.Open и почему он не возвращает ошибку, когда база недоступна?

    sql.Open вообще не ходит в сеть. Он создаёт *sql.DB (пул, в котором поначалу ноль соединений), а первое соединение открывает ваш первый запрос или Ping.

    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)…
  3. 03

    Когда нужны Exec, Query и QueryRow и что сломается, если выполнить DELETE через Query?

    Exec нужен для запросов, которые не возвращают строк, Query для набора строк, а QueryRow для не более чем одной строки.

    // 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}…
  4. 04

    Почему отсутствующая строка приходит как sql.ErrNoRows из Scan, а не из самого QueryRow, и где эту ошибку обрабатывать?

    QueryRow сам не возвращает ошибку, поэтому откладывает её, и Scan возвращает сентинел sql.ErrNoRows. Переведите его один раз, на границе репозитория, в собственный ErrUserNotFound через errors.Is.

    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,…
  5. 05

    Колонка может быть NULL. Во что её сканировать, как записать NULL обратно и что меняет COALESCE?

    В Go-шном string нет места для NULL, поэтому Scan падает с converting NULL to string is unsupported.

    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…
  6. 06

    Этот экспорт иногда выгружает меньше строк, чем есть в таблице, и в логе никогда нет ошибки. Чего не хватает?

    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}

    После цикла не хватает rows.Err(). Next возвращает false и когда строки кончились, и когда обход сломался, поэтому соединение, оборвавшееся на 4000-й строке, выглядит ровно как нормальное завершение.

  7. 07

    Что означает ACID и какие аномалии допускает каждый из уровней Read Committed, Repeatable Read и Serializable?

    ACID: транзакция выполняется целиком или никак, сохраняет ограничения, изолирована от параллельных и после коммита переживает падение.

  8. 08

    Какие типы индексов есть в PostgreSQL, когда составной индекс помогает запросу и как читать EXPLAIN ANALYZE?

    Индекс позволяет найти строки, не читая всю таблицу; B-tree служит индексом по умолчанию для равенства, диапазонов и ORDER BY.

    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…
  9. 09

    Эта функция оформляет заказ в транзакции и проходит тесты. Какие две ошибки проявятся в продакшене?

    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}

    Ранний return после первого ExecContext не откатывает транзакцию, и она продолжает держать соединение из пула и блокировки строк; а ошибка tx.Commit() проигнорирована, поэтому неудавшийся коммит выдаётся за оформленный заказ.

  10. 10

    Эта функция поиска уязвима для 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)}

    Значения передаются плейсхолдерами, а идентификаторы нет. $1 делает текст поиска безопасным, а имя колонки в ORDER BY безопасно, только если вы выбираете его из собственного белого списка.

  11. 11

    Как работают SetMaxOpenConns, SetMaxIdleConns и SetConnMaxLifetime и как подобрать размер пула для нагруженного сервиса?

    SetMaxOpenConns ограничивает число соединений (по умолчанию без лимита), SetMaxIdleConns держит часть из них открытыми между всплесками (по умолчанию 2), а 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…
  12. 12

    Что происходит с выполняющимся запросом, когда контекст запроса отменён, и зачем всё равно нужен statement_timeout?

    С QueryContext отмена заставляет database/sql перестать ждать, а драйвер попросить Postgres отменить запрос; с обычным Query ничего не подключено, и запрос продолжает работать.

    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()…
  13. 13

    Как провести одну транзакцию через несколько методов репозитория, не протаскивая *sql.Tx в сервисный слой?

    Пусть функции репозитория принимают маленький интерфейс, которому удовлетворяют и *sql.DB, и *sql.Tx, а у сервиса будет один InTx(ctx, func(q DBTX) error), который начинает, коммитит и откатывает транзакцию.

    // 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}…
  14. 14

    Чем отличаются database/sql, pgx, sqlx, sqlc и GORM и как выбрать одно из них для нового сервиса?

    database/sql предоставляет стандартный API с пулом и Scan; pgx является драйвером Postgres с более богатым собственным API; sqlx убирает рутину со Scan; sqlc генерирует типизированный Go из написанного вами SQL; GORM пишет SQL за вас.

    type Order struct {	ID     int64 `db:"id"`	UserID int64 `db:"user_id"`	Total  int64 `db:"total"`}…
  15. 15

    Что такое проблема N+1 запросов и как исправить её в Go без ORM?

    N+1 означает один запрос за списком, а затем ещё по одному запросу на каждый элемент внутри цикла.

    // 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	}…
  16. 16

    Как эффективно вставить из Go 200 000 строк и что ограничивает многострочный INSERT?

    INSERT на каждую строку тратит время на 200 000 round trip'ов, а не на работу базы.

    // 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) {…
  17. 17

    Чем оптимистичная блокировка отличается от пессимистичной и как построить очередь задач на Postgres с SKIP LOCKED?

    Пессимистичная блокировка берёт лок через `SELECT ...

    // 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 {…
  18. 18

    Два перевода между одними и теми же счетами падают с 40P01 deadlock detected. Почему так происходит и как это предотвратить, а не просто ретраить?

    Каждая транзакция блокирует строки в порядке перевода, поэтому A→B и B→A держат по одной блокировке и ждут друг друга; Postgres находит цикл через deadlock_timeout (1 с) и откатывает одну из них.

    // 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}…
  19. 19

    Найдите баг: под нагрузкой 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()}

    Вставка в аудит идёт через s.db, а не через tx, поэтому берёт из пула второе соединение и коммитится сама по себе.

  20. 20

    Что означает 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) {…
  21. 21

    Как работает MVCC в PostgreSQL и почему одна долгая транзакция может раздуть таблицы для всех?

    Postgres не обновляет строку на месте: UPDATE пишет новую версию, а старую помечает мёртвой, и транзакция видит версии, разрешённые её снимком.

  22. 22

    Как ретраить транзакцию, которая падает с SQLSTATE 40001 на уровне Serializable, и чего ни в коем случае не должно быть внутри цикла повторов?

    Повторяйте всю транзакцию, а не упавший запрос: ошибка сериализации откатывает всё, поэтому функция заново запускается с BeginTx и заново читает все значения.

    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) {…
  23. 23

    Как изменить схему в продакшене без простоя (например, переименовать колонку) и где здесь место goose или golang-migrate?

    Expand, migrate, contract: во время rolling-деплоя старый и новый код работают с одной базой, и каждый шаг схемы должен подходить обоим.

    // 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}…
  24. 24

    В чём разница между репликацией и шардированием и когда выбирать синхронную репликацию вместо асинхронной?

    Репликация держит копии данных на нескольких серверах ради отказоустойчивости и масштабирования чтения; шардирование делит данные по ключу между серверами, что масштабирует запись.