26 вопросов
SQL - язык запросов к реляционным БД. JOIN'ы, агрегации, CTE, оконные функции, транзакции (ACID), индексы, нормализация, N+1 проблема. Обязательные знания для любого backend-разработчика.
LEFT JOIN возвращает ВСЕ строки из левой таблицы. Если в правой нет совпадений - поля правой заполняются NULL. INNER JOIN вернул бы пустой результат. RIGHT JOIN - все из правой. FULL JOIN - все из обеих.
Подробнее →ACID: Atomicity (атомарность - всё или ничего), Consistency (согласованность - данные валидны), Isolation (изоляция - транзакции не мешают друг другу), Durability (надёжность - зафиксированные данные не потеряются). Основа реляционных БД.
Подробнее →sql.DB - пул соединений, а не одно соединение. Автоматически управляет открытием, закрытием, переиспользованием. Не нужно создавать sql.DB для каждого запроса. Настройки: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime. Один sql.DB на всё приложение.
Подробнее →WHERE фильтрует строки до агрегации. HAVING фильтрует результаты GROUP BY. Пример: SELECT dept, COUNT(*) FROM employees WHERE salary > 50000 GROUP BY dept HAVING COUNT(*) > 5. WHERE убирает сотрудников с зарплатой <=50к, HAVING - отделы с <=5 сотрудниками.
Подробнее →UNION объединяет результаты и удаляет дубликаты (требует сортировки/хеширования - дороже). UNION ALL просто конкатенирует - быстрее. Если дубликаты невозможны или допустимы, всегда используйте UNION ALL.
Подробнее →FOREIGN KEY гарантирует, что значение ссылается на существующую запись в другой таблице. Нельзя вставить заказ с несуществующим customer_id. CASCADE - автоматическое удаление связанных записей. RESTRICT - запрет удаления при наличии ссылок. SET NULL - обнуление ссылки.
Подробнее →Миграции - SQL-скрипты с версиями (001_create_users.sql). Инструменты отслеживают применённые и выполняют новые по порядку. Версионирование схемы в git, воспроизведение БД с нуля, безопасное обновление production. В Go популярны goose и golang-migrate.
Подробнее →Конкатенация пользовательского ввода (+ id) - SQL-инъекция. Параметризованные запросы ($1, $2) безопасны - драйвер экранирует данные. Всегда используйте плейсхолдеры: $1 для PostgreSQL, ? для MySQL. Никогда не вставляйте данные через fmt.Sprintf в SQL.
Подробнее →QueryRow ожидает 0 или 1 строку. Не нужен Close(), ошибка возвращается через Scan(). Query возвращает sql.Rows, который нужно закрывать.
Подробнее →N+1: один запрос получает список (100 заказов), затем для каждого - отдельный запрос за деталями = 101 запрос. Решение: JOIN (один запрос), IN (... список ID), или preload в ORM. В Go решается через JOIN или batch-запросы.
Подробнее →CTE определяется через WITH: WITH active AS (SELECT * FROM users WHERE active) SELECT * FROM active. Улучшает читаемость, можно ссылаться несколько раз. Рекурсивные CTE (WITH RECURSIVE) позволяют обходить деревья и графы.
Подробнее →Оконные функции (ROW_NUMBER, RANK, SUM OVER, LAG, LEAD) вычисляют значения по группе строк (окну), сохраняя все строки в результате. В отличие от GROUP BY, не схлопывают строки. Пример: SUM(amount) OVER (PARTITION BY user_id ORDER BY date).
Подробнее →Нормализация разбивает данные по таблицам для устранения избыточности. 1NF - атомарные значения. 2NF - зависимость от всего ключа. 3NF - нет транзитивных зависимостей. Денормализация - обратный процесс для ускорения чтения.
Подробнее →Паттерн: tx, _ := db.Begin(); defer tx.Rollback(); ...; return tx.Commit(). defer tx.Rollback() безопасен - после успешного Commit() Rollback() ничего не делает (возвращает sql.ErrTxDone). Это гарантирует откат при panic, ранних return и любых ошибках.
Подробнее →SELECT COUNT(column) если column содержит NULL? 🟡 Средний
▶
COUNT(column) считает только НЕ-NULL значения. COUNT(*) считает все строки. SUM, AVG, MIN, MAX тоже игнорируют NULL. Частая ошибка: COUNT(nullable_column) < COUNT(*). Для подсчёта NULL: COUNT(*) - COUNT(column).
Подробнее →Каждый индекс - дополнительная структура, обновляемая при записи. INSERT добавляет записи во все индексы. UPDATE может затронуть индексы. DELETE помечает записи. 5-10 индексов на таблицу - ощутимая нагрузка на запись. Баланс: индексы ускоряют чтение, замедляют запись.
Подробнее →Партиционирование разбивает таблицу внутри одной БД (по дате, региону). Запросы и транзакции работают прозрачно. Шардирование - распределение между серверами. Даёт горизонтальное масштабирование, но усложняет JOIN, транзакции и агрегации между шардами.
Подробнее →Hash-индекс поддерживает только оператор =. B-tree поддерживает =, <, >, <=, >=, BETWEEN, IN, ORDER BY, LIKE 'prefix%'. Hash может быть чуть быстрее для = на очень больших таблицах, но на практике B-tree почти всегда предпочтительнее.
Подробнее →sql.DB - это пул соединений. SetMaxOpenConns ограничивает общее число (по умолчанию безлимитно), SetMaxIdleConns - сколько держать открытыми в ожидании (по умолчанию 2).
Подробнее →rows держит соединение из пула. Без Close() соединение не вернется. При MaxOpenConns это приведет к блокировке всех новых запросов.
Подробнее →Планировщик может предпочесть sequential scan: 1) запрос возвращает >5-10% таблицы - последовательное чтение быстрее random I/O; 2) таблица маленькая - помещается в пару страниц; 3) устаревшая статистика (нужен ANALYZE). EXPLAIN покажет выбранный план.
Подробнее →EXISTS останавливается при первом совпадении (short-circuit). IN загружает весь результат подзапроса. При большом подзапросе EXISTS эффективнее. NOT EXISTS лучше NOT IN из-за NULL-семантики: NOT IN с NULL в подзапросе ничего не вернёт.
Подробнее →Пессимистическая: SELECT ... FOR UPDATE блокирует строки до конца транзакции. Оптимистическая: читаем версию, при UPDATE проверяем WHERE version = old_version. Если кто-то изменил - повторяем. Оптимистическая лучше при редких конфликтах, пессимистическая - при частых.
Подробнее →(a, b, c) - какой запрос НЕ будет его использовать? 🔴 Сложный
▶
Составной индекс работает по принципу leftmost prefix - запрос должен начинаться с первого столбца. (a), (a,b), (a,b,c) - индекс будет использован. (b,c) без a - не будет, потому что индекс отсортирован сначала по a. Порядок столбцов критически важен.
Подробнее →Phantom read: транзакция выполняет SELECT WHERE дважды и во второй раз видит новые строки от другой транзакции. Dirty read - чтение незакоммиченных данных. Non-repeatable read - та же строка изменилась. Read Committed защищает от dirty read, Repeatable Read - от phantom read.
Подробнее →LATERAL позволяет подзапросу обращаться к столбцам таблиц, перечисленных ранее в FROM. SELECT * FROM users u, LATERAL (SELECT * FROM orders WHERE user_id = u.id LIMIT 3). Для каждой строки выполняет подзапрос. Мощная замена коррелированным подзапросам.
Подробнее →