Go: Базы данных SQL

20 вопросов

1 Какие типы JOIN бывают в SQL? Как использовать в Go?

INNER JOIN - только совпадающие строки; LEFT JOIN - все из левой + совпадения справа (NULL при отсутствии); RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN. В Go пишут сырой SQL или через builder (squirrel, goqu). Параметры через плейсхолдеры $1, $2 (Postgres) или ? (MySQL), передают в db.QueryContext, db.ExecContext.

rows, err := db.QueryContext(ctx,
    "SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.id = $1", id)
Открыть отдельно →
2 Чем WHERE отличается от HAVING?

WHERE фильтрует строки до группировки и не может использовать агрегатные функции. HAVING фильтрует группы после GROUP BY и может использовать COUNT, SUM, AVG и т.д. Порядок выполнения: FROM - WHERE - GROUP BY - агрегаты - HAVING - ORDER BY - LIMIT.

SELECT department_id, COUNT(*) FROM employees
WHERE salary > 50000 GROUP BY department_id HAVING COUNT(*) > 5
Открыть отдельно →
3 Какие агрегатные функции использовать в SQL из Go?

COUNT, SUM, AVG, MIN, MAX - стандартные. В Postgres: STRING_AGG для склейки строк. Результат сканируют в переменные через row.Scan или rows.Scan. Для одной строки - db.QueryRowContext. Агрегаты без GROUP BY возвращают одну строку.

var count int
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE active = $1", true).Scan(&count)
Открыть отдельно →
4 Как работает GROUP BY? Ошибки с неагрегированными столбцами.

GROUP BY группирует строки по одинаковым значениям указанных столбцов. В SELECT допустимы только столбцы из GROUP BY или агрегатные функции. Иначе СУБД выдает ошибку (или неоднозначность в MySQL без ONLY_FULL_GROUP_BY). В Go запросы с GROUP BY пишут как обычный SQL, сканируют в структуры или переменные.

SELECT status, COUNT(*) FROM orders GROUP BY status
Открыть отдельно →
5 Что такое подзапросы (subqueries)? Примеры в Go.

Подзапрос - SELECT внутри другого запроса (в WHERE, FROM, SELECT). IN (SELECT ...), EXISTS (SELECT 1 ...), скалярный подзапрос в SELECT. В Go передают как одну строку SQL с плейсхолдерами; вложенность и параметры задают аккуратно. Коррелированный подзапрос выполняется для каждой строки внешнего запроса - может быть медленным.

rows, err := db.QueryContext(ctx,
    "SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > $1)", minTotal)
Открыть отдельно →
6 Как использовать EXPLAIN в Go для анализа запросов?

Выполняют EXPLAIN (ANALYZE) SELECT ... и читают результат как обычный запрос. В коде можно включить логирование запросов (pgx с логгером) или выполнять EXPLAIN вручную при отладке. План показывает тип доступа (seq scan, index scan), оценку строк, стоимость. Важно для поиска полных сканов и отсутствующих индексов.

rows, _ := db.QueryContext(ctx, "EXPLAIN ANALYZE SELECT * FROM users WHERE email = $1", email)
// разбор вывода плана
Открыть отдельно →
7 Оконные функции (window functions) в SQL. Поддержка в драйверах.

Оконные функции (ROW_NUMBER, RANK, SUM OVER (PARTITION BY ... ORDER BY ...)) вычисляют значение по "окну" строк без схлопывания в одну. Поддерживаются в Postgres, MySQL 8+, SQL Server. В Go пишут сырой SQL с оконными функциями, результат сканируют в структуры; иногда нужны отдельные поля для номера строки и агрегата по окну.

SELECT id, name, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employees
Открыть отдельно →
8 Что такое CTE (WITH)? Когда применять в Go?

CTE - именованное временное множество: WITH name AS (SELECT ...) SELECT ... FROM name. Улучшает читаемость, позволяет рекурсию (WITH RECURSIVE). В Go передают один большой запрос с WITH; параметры - через плейсхолдеры. Удобно для многошаговых выборок и рекурсивных структур (деревья).

q := "WITH recent AS (SELECT * FROM orders WHERE created_at > $1) SELECT u.name, COUNT(r.id) FROM users u LEFT JOIN recent r ON u.id = r.user_id GROUP BY u.id, u.name"
rows, err := db.QueryContext(ctx, q, since)
Открыть отдельно →
9 Зачем нужны prepared statements в Go? Как использовать?

Prepared statement - запрос разобран один раз, параметры подставляются безопасно. Защита от SQL-инъекций и переиспользование плана. В Go: db.PrepareContext(ctx, "SELECT ... WHERE id = $1"), затем stmt.QueryContext или ExecContext с аргументами. sql.DB по умолчанию кеширует подготовленные запросы. Не подставлять пользовательский ввод в строку запроса - только через аргументы.

stmt, err := db.PrepareContext(ctx, "SELECT name FROM users WHERE id = $1")
defer stmt.Close()
var name string
err = stmt.QueryRowContext(ctx, id).Scan(&name)
Открыть отдельно →
10 Что такое VIEW? Как читать из представления в Go?

VIEW - именованный запрос (виртуальная таблица). Данные не хранятся отдельно, при обращении выполняется лежащий SELECT. В Go обращаются к VIEW как к таблице: SELECT * FROM active_users WHERE .... Материализованное представление (MATERIALIZED VIEW) хранит результат и обновляется по расписанию - чтение быстрее, данные могут быть не самыми свежими.

rows, err := db.QueryContext(ctx, "SELECT * FROM active_users LIMIT 10")
Открыть отдельно →
11 Что такое триггеры? Нужны ли в приложении на Go?

Триггер - процедура в БД, выполняемая при INSERT/UPDATE/DELETE. Используют для аудита, денормализации, проверок. В приложении на Go логику часто выносят в код (события, очереди), чтобы тестировать и версионировать. Триггеры удобны для гарантированной консистентности на уровне БД и для legacy-систем.

-- в БД: CREATE TRIGGER ... AFTER UPDATE ON orders FOR EACH ROW EXECUTE ...
Открыть отдельно →
12 Внешние ключи (foreign keys) и ограничения в Go.

Foreign key гарантирует ссылочную целостность: значение столбца должно существовать в другой таблице. ON DELETE/UPDATE: CASCADE, SET NULL, RESTRICT. В Go миграции (goose, golang-migrate) создают ограничения; приложение получает ошибки при нарушении (например, вставка несуществующего user_id). Индекс на столбце FK улучшает производительность JOIN и CASCADE.

ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
Открыть отдельно →
13 Какие ограничения (constraints) бывают в SQL?

PRIMARY KEY (уникальность + NOT NULL), UNIQUE, NOT NULL, CHECK (условие на значение), FOREIGN KEY, DEFAULT. В Go схему задают миграциями; при нарушении СУБД возвращает ошибку, ее обрабатывают в коде. CHECK полезен для статусов, диапазонов (price >= 0).

CHECK (status IN ('draft','paid','shipped'))
CHECK (price >= 0)
Открыть отдельно →
14 Как в SQL и Go работать с NULL?

NULL - отсутствие значения. Сравнение с NULL дает NULL (не true/false): нужны IS NULL, IS NOT NULL. Агрегаты игнорируют NULL. В Go сканируют в sql.NullString, sql.NullInt64 или указатели (*string). COALESCE(col, default) в SQL подставляет значение по умолчанию. При маппинге в структуры удобны типы с указателями или sql.Null*.

var name sql.NullString
err := row.Scan(&name)
if name.Valid { use(name.String) }
Открыть отдельно →
15 Проблемы LIMIT/OFFSET при пагинации. Что делать в Go?

OFFSET заставляет БД "пройти" первые N строк, поэтому при больших OFFSET запрос замедляется. Для пагинации предпочтительна курсорная: WHERE id > :last_id ORDER BY id LIMIT 20. В Go хранят last_id с предыдущей страницы и передают в запрос. Стабильная производительность и нет "прыжков" при появлении новых строк.

rows, err := db.QueryContext(ctx,
    "SELECT * FROM orders WHERE id > $1 ORDER BY id LIMIT $2", lastID, pageSize)
Открыть отдельно →
16 Что такое курсорная пагинация (keyset)? Как реализовать в Go?

Вместо номера страницы используют курсор - значение ключа последней полученной строки. Следующая страница: WHERE (created_at, id) < (:last_created, :last_id) ORDER BY created_at DESC, id DESC LIMIT 20. В Go передают last_created и last_id с предыдущего ответа. Нельзя перейти на произвольную страницу по номеру, только вперед/назад. Подходит для бесконечной ленты и API.

q := "SELECT * FROM events WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT $3"
rows, _ := db.QueryContext(ctx, q, lastCreated, lastID, limit)
Открыть отдельно →
17 Как безопасно добавить колонку в большую таблицу?

В Postgres 11+ ADD COLUMN с DEFAULT без переписывания таблицы (для не-volatile default). Без DEFAULT или с NOT NULL может потребоваться полное переписывание. Подход: добавить колонку nullable без default, заполнить батчами в фоне, затем добавить NOT NULL/default. В Go миграции делают по шагам; для больших таблиц используют CREATE INDEX CONCURRENTLY и избегают долгих блокировок.

-- шаг 1: ADD COLUMN new_col TYPE NULL
-- шаг 2: батчевое обновление в приложении
-- шаг 3: ALTER COLUMN new_col SET NOT NULL
Открыть отдельно →
18 Как оптимизировать медленный запрос из Go?

1) EXPLAIN ANALYZE - план и фактические строки. 2) Искать full scan - добавить индекс под WHERE/ORDER BY/JOIN. 3) Уменьшить выборку (нужные столбцы), убрать лишние JOIN. 4) Разбить запрос (CTE, батчи). 5) Обновить статистику (ANALYZE в Postgres). 6) В коде: пул соединений, таймауты, избегать N+1 (один запрос с IN или JOIN вместо цикла запросов).

// вместо N+1:
ids := extractIDs(users)
rows, _ := db.QueryContext(ctx, "SELECT * FROM orders WHERE user_id = ANY($1)", pq.Array(ids))
Открыть отдельно →
19 Что такое N+1 проблема? Как избежать в Go?

N+1: один запрос за списком сущностей и N запросов за связями (например, заказы по каждому пользователю). Решение: один запрос с JOIN или два запроса - список id и один SELECT ... WHERE id IN (...). В Go собирают id, затем db.QueryContext(ctx, "SELECT ... WHERE user_id = ANY($1)", pq.Array(ids)) и мапят по user_id. ORM (GORM) решают через Preload.

users, _ := getUsers(ctx, db)
ids := userIDs(users)
orders, _ := getOrdersByUserIDs(ctx, db, ids)
byUser := groupOrdersByUserID(orders)
Открыть отдельно →
20 Что такое LATERAL join? Пример в Postgres и Go.

LATERAL позволяет подзапросу в FROM ссылаться на столбцы предыдущих таблиц в том же FROM; подзапрос выполняется для каждой строки. Удобно для "top-N по группе" без оконных функций. В Go пишут обычный SQL с LATERAL.

q := "SELECT u.name, o.* FROM users u, LATERAL (SELECT * FROM orders WHERE user_id = u.id ORDER BY created_at DESC LIMIT 5) o"
rows, err := db.QueryContext(ctx, q)
Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.