Полный гайд по реальным SQL-вопросам с собеседований

Введение. Собеседования на позиции, связанные с данными (аналитики, инженеры, ученые данных), всё чаще включают нестандартные и продвинутые вопросы по SQL. Большие технологические компании (Google, Amazon и др.) предъявляют высокие требования: важна не только правильность запроса, но и умение оптимизировать его и разбираться в реальных бизнес-данных[1][2]. В этом гайде мы разберем категории наиболее распространенных сложных SQL-задач с реальных собеседований – от платформ вроде DataLemur, LeetCode, StrataScratch – и подробно поясним решения. Каждая задача сопровождена анализом: условие, оптимальный подход, используемые SQL-конструкции, возможные ошибки и финальное решение (для PostgreSQL и MySQL, с указанием различий где необходимо). В конце добавлен отдельный раздел о современных базах данных, включая векторные БД (Pinecone, Weaviate, Milvus и др.), с примерами того, что могут спросить про них на собеседовании и как выглядят SQL-подобные запросы для работы с векторами.
У меня в Телеграм 1900 вопросов с собеседований по анализу данных, машинному обучению, sql. А здесь я собрал лучшие ресурсы для аналитиков данных по самым широким темам.
Агрегатные функции и группировка данных
Агрегация – основа многих аналитических запросов. Интервьюеры проверяют умение использовать COUNT, SUM, AVG и другие агрегаты с GROUP BY и фильтрацией результатов. Задачи этого типа требуют вычислить метрики по группам (например, суммарные продажи по категориям, средний чек по клиентам и т.п.) и правильно применить условия отбора результатов через HAVING. Важный навык – понимать разницу между фильтрацией до агрегирования (WHERE) и после агрегирования (HAVING).
- Пример 1 (группировка с условием): «Типы заявок». Дан таблица заявок loans с полем type (тип заявки). Нужно вывести user_id тех пользователей, которые сделали как минимум одну заявку типа “Refinance” и минимум одну типа “InSchool”[3][4]. Оптимальное решение – отфильтровать записи нужных типов и сгруппировать по пользователю, требуя наличия обоих типов:
SELECT user_id
FROM loans
WHERE type IN ('Refinance', 'InSchool')
GROUP BY user_id
HAVING COUNT(DISTINCT type) = 2;
Здесь мы сначала ограничиваем строки двумя нужными значениями type (WHERE), затем группируем по user_id и в условии HAVING проверяем, что в каждой группе присутствуют оба типа (через COUNT(DISTINCT type) = 2). Возможная ошибка: использовать WHERE вместо HAVING для проверки количества – это не сработает, потому что условие на агрегат можно задать только после группировки. Также важно использовать COUNT(DISTINCT …), иначе если в таблице есть дубли, простой COUNT(*) мог бы дать ложный результат. В нашем запросе WHERE фильтрует нужные типы до агрегирования, а HAVING гарантирует наличие обоих типов в группе[5]. Такой запрос корректно выполняется и в PostgreSQL, и в MySQL.
- Пример 2 (агрегат и подзапрос): «Города с самыми дорогими домами». Эта задача (реальный вопрос от Zillow) требует найти названия городов, где средняя цена дома выше среднерыночной по стране[6][7]. У нас есть таблица продаж zillow_transactions(city, mkt_price) и нужно сравнить среднюю цену по городу со средней ценой по всей таблице. Решение – использовать подзапрос в секции HAVING:
SELECT city
FROM zillow_transactions AS z
GROUP BY city
HAVING AVG(z.mkt_price) > (
SELECT AVG(mkt_price)
FROM zillow_transactions
)
ORDER BY city;
Во внутреннем субзапросе вычисляем общенациональную среднюю цену (SELECT AVG(mkt_price) FROM …), а во внешнем запросе выбираем те city, для которых средняя цена больше этой величины[8]. Здесь подзапрос выполняется один раз для всего запроса (не коррелированный), что эффективно. Возможные ошибки: забыть сгруппировать по city или попытаться применить WHERE для сравнения со средним (опять же нужно HAVING, потому что фильтр по агрегату). Данный запрос универсален для SQL-92, поддерживается и PostgreSQL, и MySQL без изменений.
- Пример 3 (условия на агрегаты): «Сумма по чётным и нечётным измерениям». Вопрос из интервью Google: есть таблица показаний датчика, где каждое измерение имеет уникальный measurement_id и значение. Требуется вывести для каждого дня сумму значений по нечётным и по чётным ID измерений отдельно. Решение – использовать агрегирование с условием через CASE (или фильтр в Postgres):
SELECT DATE(measurement_time) AS measurement_day,
SUM(CASE WHEN measurement_id % 2 = 1 THEN measurement_value ELSE 0 END) AS odd_sum,
SUM(CASE WHEN measurement_id % 2 = 0 THEN measurement_value ELSE 0 END) AS even_sum
FROM measurements
GROUP BY DATE(measurement_time);
Здесь мы группируем по дню (обрезая время), и вычисляем две суммы: одна суммирует значение только там, где measurement_id нечётный, другая – где чётный. В PostgreSQL ту же логику можно записать короче с помощью модификатора FILTER: SUM(measurement_value) FILTER (WHERE measurement_id % 2 = 1) (но в MySQL синтаксиса FILTER нет, поэтому применяем CASE как выше). Замечание: убедитесь, что используете правильную функцию для извлечения даты – например, DATE() или CAST … AS DATE – чтобы группировка восприняла значения как одинаковые для всех записей данного дня.

Полный гайд по реальным SQL задачам
Почему это важно: Умение комбинировать агрегаты с фильтрацией и подзапросами показывает, что кандидат может вычислять нетривиальные метрики. В Big Tech-интервью могут спросить что-то вроде «Найдите медианный показатель X по группам Y» или «Сколько пользователей выполнили определённые типы действий», проверяя знание HAVING, DISTINCT и вложенных запросов. Например, Google в реальном интервью спрашивал нахождение медианы числа поисковых запросов на пользователя, зная распределение частот[9][10]. Такие задачи требуют творчески применять агрегаты: медиану можно получить, вычислив кумулятивное распределение или используя специальные функции. В PostgreSQL есть агрегатные функции для перцентилей (например, percentile_cont(0.5)), но в MySQL пришлось бы вручную выбирать среднее двух центральных значений или писать подзапрос с порядком. Главное – понимать принцип и описать его собеседнику.
Оконные функции (Window Functions)
Оконные функции – мощный инструмент для продвинутого анализа данных. Они позволяют вычислять скользящие агрегаты, ранги и показатели по «окну» строк, сохраняя при этом все исходные строки. Вопросы на собеседовании часто проверяют знание ROW_NUMBER(), RANK(), DENSE_RANK(), а также функций с сдвигом (LAG()/LEAD()) и рамок (PARTITION BY и рамки ROWS BETWEEN). В Big Tech задачах оконные функции применяются, например, для нахождения рекордов, рейтингов, top-N результатов по группам или во всем наборе данных, без потери подробностей по строкам.
Сравнение функций ранжирования (ROW_NUMBER, RANK, DENSE_RANK) в SQL.
Прежде чем перейти к примерам, кратко вспомним разницу между основными функциями ранжирования[11]. Пусть у нас есть отсортированный список значений (например, оценки пользователей).
- ROW_NUMBER() присваивает уникальный порядковый номер каждой строке в пределах партиции (без пропусков).
- RANK() присваивает одинаковый ранг равным значениям, пропуская следующее значение ранга после группы равных (то есть, если два элемента поделили 1-е место, следующий получит ранг 3).
- DENSE_RANK() тоже дает равным значениям один ранг, но не пропускает последующий номер (в том же случае следующий после двух первых также получит ранг 2).
Например, для значений [100, 90, 90, 80] ROW_NUMBER() даст [1, 2, 3, 4]; RANK() – [1, 2, 2, 4]; DENSE_RANK() – [1, 2, 2, 3].
Рассмотрим теперь задачи, где без оконных функций не обойтись:
- Пример 4 (ранжирование и выбор топ-N): «Самые дальние поездки (топ-10 пользователей)». В реальном интервью Lyft кандидату дали две таблицы: lyft_rides_log(user_id, distance) – лог поездок с длинами, и lyft_users(id, name) – данные пользователей. Попросили найти 10 пользователей, которые суммарно проехали наибольшее расстояние, и вывести их id, имя и общую дистанцию[12][13]. Этот запрос сочетает JOIN, агрегирование и оконную функцию ранжирования:
SELECT user_id, name, traveled_distance
FROM (
SELECT lu.id AS user_id,
lu.name,
SUM(lr.distance) AS traveled_distance,
RANK() OVER (ORDER BY SUM(lr.distance) DESC) AS rnk
FROM lyft_users AS lu
JOIN lyft_rides_log AS lr
ON lu.id = lr.user_id
GROUP BY lu.id, lu.name
) AS sub
WHERE rnk <= 10;
Здесь внутри подзапроса мы соединили таблицы, сгруппировали по пользователю и посчитали SUM(distance) для каждого. Затем с помощью RANK() OVER (ORDER BY SUM(distance) DESC) присвоили ранг – 1 для самого большого пробега, 2 для следующего и т.д.[14]. Во внешнем запросе выбрали только строки с rnk <= 10 (первые 10 рангов). Обратите внимание: мы не указали PARTITION BY, значит ранжирование идет по всему набору записей (в данном случае по всем пользователям). Почему не ROW_NUMBER()? В этой задаче, если несколько пользователей проехали одинаковое расстояние, они получат одинаковый ранг и вместе войдут в топ-10, потенциально вернув немного больше 10 результатов. Если же требовалось строго 10 записей, независимо от равенства, можно было взять ROW_NUMBER() – тогда при совпадающих суммах кто-то бы выпал из десятки. Собеседник обычно уточняет, как поступить с равными значениями. Различия СУБД: и PostgreSQL, и MySQL (начиная с 8.0) поддерживают оконные функции идентично. В MySQL нужно убедиться, что версия >= 8.0, иначе такой запрос не выполнится, т.к. более старые версии оконные функции не поддерживают.
- Пример 5 (ранги с условием сортировки): «Рейтинг активности пользователей почты». Вопрос из интервью Google: дана таблица google_gmail_emails(from_user, to_user) с сообщениями. Требуется вывести для каждого отправителя общее число отправленных писем и его ранг активности (1 – у самого активного)[15][16]. Причем, если несколько пользователей отправили одинаковое число писем, сортировать их по имени (алфавиту), но присвоить им разные ранги (то есть не иметь “ничьих” по рангу). Данный случай – классический паттерн: нужно использовать ROW_NUMBER(), но вторым условием сортировки учесть имя, чтобы разрывать возможные ничьи по количеству. Запрос:
SELECT
from_user,
COUNT(*) AS total_emails,
ROW_NUMBER() OVER (
ORDER BY COUNT(*) DESC, from_user ASC
) AS activity_rank
FROM google_gmail_emails
GROUP BY from_user;
Здесь в оконной функции ROW_NUMBER() мы указали сразу два критерия упорядочивания окна: сперва по убыванию количества писем, а затем по имени пользователя возрастанию[16]. Это гарантирует уникальный ранг даже при равном COUNT(*). Обратите внимание: в секции ORDER BY оконной функции можно использовать агрегат COUNT(*) напрямую, хотя в самом SELECT вне оконной функции агрегаты должны быть либо внутри агрегатной функции, либо в GROUP BY. SQL оптимизатор понимает, что окно вычисляется после GROUP BY. Альтернативы: можно было вычислить ранги через подзапрос с DENSE_RANK() и затем для одинаковых значений вручную поправить, но использование ROW_NUMBER с дополнительной сортировкой проще.
- Пример 6 (поиск максимума с использованием оконной функции): «Максимальное число побед». Вопрос от Netflix: дана таблица oscar_nominees(nominee, winner) с информацией по наградам, где winner = TRUE для выигранных номинаций. Нужно найти, кто получил больше всего премий Оскар (то есть максимальное число winner=TRUE), и вывести имя и количество[17][18]. Задачу можно решить подзапросом с агрегатами или даже одним запросом с ORDER BY LIMIT 1, но интервьюер мог ожидать решение с оконной функцией RANK() для демонстрации навыков:
WITH wins AS (
SELECT nominee,
COUNT(CASE WHEN winner = TRUE THEN 1 END) AS wins_count,
RANK() OVER (ORDER BY COUNT(CASE WHEN winner = TRUE THEN 1 END) DESC) AS rnk
FROM oscar_nominees
GROUP BY nominee
)
SELECT nominee, wins_count
FROM wins
WHERE rnk = 1;
Здесь мы используем COUNT с условием (через CASE) или можно было SUM(CASE WHEN winner THEN 1 ELSE 0 END) для подсчета побед. Применяя RANK() OVER (ORDER BY … DESC), присваиваем ранг 1 обладателям наибольшего числа побед. Затем во внешнем запросе фильтруем WHERE rnk = 1 – это может вернуть несколько имен, если несколько человек лидируют по числу наград. Обсуждение: интервьюер может спросить, чем отличается использование ROW_NUMBER() – в данном случае ROW_NUMBER() вернул бы только одного человека (при одинаковом кол-ве побед выбрал бы кого-то по неявному порядку), что неверно по смыслу. DENSE_RANK() тоже подошел бы – дал бы 1 для лидеров, 2 для следующих и т.д., – но раз нам нужны только лидеры, достаточно RANK(). В решении от Netflix было отмечено, что ROW_NUMBER() не подходит, а RANK() или DENSE_RANK() – корректны[19][20].
Оконные функции и производительность. В больших компаниях могут обратить внимание, что при чтении огромных таблиц использование оконных функций без партиционирования может быть тяжёлым. Поэтому, если уместно, стоит применять PARTITION BY для разбивки на группы (например, ранжирование внутри каждой категории). Также важно знать, что в MySQL 8+ и PostgreSQL оконные функции реализованы эффективно, но всё же индексы на поля сортировки могут помочь. Однако индексы не всегда используются для окон (особенно если есть PARTITION BY), так что иногда запрос с оконной функцией трудно оптимизировать. На собеседовании достаточно показать понимание, что происходит – что СУБД должна отсортировать данные в указанном порядке для вычисления окна, и это может быть дорого.
Подзапросы и CTE (вложенные запросы)
Подзапросы проверяют вашу способность разбить сложную задачу на части. Они бывают некоррелированные (выполняются независимо) и коррелированные (зависят от текущей строки внешнего запроса). Также сюда относятся CTE (Common Table Expressions, “связанные таблицы” после WITH), которые часто улучшают читаемость решения.
Вопросы из этой области могут требовать: – использовать подзапрос для вычисления промежуточного результата (например, общая средняя величина, как мы делали выше в примере с городами), – или коррелированный подзапрос для фильтрации по условию на каждую строку (например, «найдите записи, которые являются самыми последними по времени для каждой категории» – можно решить коррелированным подзапросом с WHERE … < (SELECT …)).
Также интервьюеры любят спросить «что такое коррелированный подзапрос» в теории.
Коррелированный подзапрос – это подзапрос, который выполняется для каждой строки внешнего запроса, используя ее значения[21]. Например, применение: найти работников, чья зарплата выше средней по их отделу – можно реализовать коррелированным подзапросом, сравнивающим зарплату сотрудника с SELECT AVG(salary) FROM employees WHERE dept_id = outer.dept_id. Такой подзапрос будет пересчитываться для каждой строки внешней таблицы. Это менее эффективно, чем объединение через JOIN или использование агрегатов с группировкой, но иногда удобнее.
- Пример 7 (коррелированный подзапрос): «Сотрудники с зарплатой выше, чем у их менеджера». Классическая задача (есть на LeetCode): в таблице Employee(emp_id, name, salary, manager_id) нужно выбрать имена тех сотрудников, чья salary больше, чем salary их менеджера. Одно из решений – коррелированный подзапрос:
SELECT e.name
FROM Employee AS e
WHERE e.salary > (
SELECT m.salary
FROM Employee AS m
WHERE m.emp_id = e.manager_id
);
Здесь внутренний запрос выбирает зарплату менеджера текущего сотрудника e. Он выполняется для каждой строки e во внешнем запросе. Замечание: если у сотрудника нет менеджера (manager_id = NULL), подзапрос вернет NULL и сравнение > NULL даст FALSE, такая строка просто отбрасывается, что логично. В интервью ожидают понимания, что эту же задачу можно решить самообъединением (self-JOIN): соединить таблицу Employee сама с собой по e.manager_id = m.emp_id и сравнить e.salary > m.salary. Такой JOIN-подход часто предпочтительнее по эффективности, так как вместо многих подзапросов выполняется одно соединение.
- Пример 8 (несколько уровней вложенности): Иногда встречаются задачи, где нужно использовать несколько подзапросов. Например, на платформе StrataScratch был вопрос: «Найдите пользователя, сделавшего вторую по количеству покупок на сайте». Решение: сперва получить агрегированную таблицу (пользователь -> число покупок), затем взять ранг по количеству и выбрать ранг 2. Это можно сделать через два подзапроса: внутренний для агрегации и внешний для фильтрации по рангу, либо через оконную функцию. Подзапросы могут вкладываться и глубже, но важно не переусложнять – в реальном интервью спрашивают до 2-3 уровней вложенности максимум. Если видите, что вложенность становится сложно читать, лучше предложить альтернативу с CTE.
- Когда лучше CTE: Common Table Expression, объявляемая через WITH, полезна, когда нужно использовать результат подзапроса несколько раз или просто сделать запрос читаемее. Например, ту же задачу с Оскарами (Пример 6) мы решили через CTE wins для понятности. В PostgreSQL CTE по умолчанию материализуется как временный результат (начиная с версии 12, оптимизатор может “разворачивать” простые CTE), а вот MySQL 8.0 обрабатывает WITH как оптимизируемый субзапрос. На собеседовании от вас не ждут глубокого знания внутренних различий реализации CTE, но важно правильно использовать синтаксис и уместно объяснить, что CTE улучшает структуру запроса.
Совет: если задают открытый теоретический вопрос типа «Что такое коррелированный подзапрос и когда его применять?», стоит коротко ответить (как в тексте выше) и привести небольшой пример сценария[21]. Обычно этого достаточно.
Объединение таблиц (JOINS) и сложные источники данных
Практически любая реальная аналитическая задача затрагивает данные из нескольких таблиц. Поэтому на интервью часто дают задачи на JOIN разных типов: внутреннее соединение (INNER JOIN), внешние (LEFT/RIGHT JOIN), полное (FULL JOIN), а также самосоединения. Кандидат должен понимать, как работают эти соединения и в каких случаях их применять. Также могут проверить знание ключевых моментов: что такое Cartesian product (декартово произведение), чем отличается WHERE vs ON при соединении, и как объединять данные, чтобы не потерять записи.
- Пример 9 (множественный JOIN и фильтрация): «Уникально закрепленные консультанты». Задача (DataLemur, интервью Accenture): есть две таблицы – staffing(employee_id, engagement_id) (какой сотрудник на каком проекте задействован) и engagement(engagement_id, client_name). Требуется вывести для каждого клиента: общее число консультантов, у него занятых, и сколько из них работают только на этого клиента (эксклюзивно)[22][23]. Решение предусматривает два соединения и группировку. Алгоритм: соединить таблицы по engagement_id чтобы проставить каждому engagement его client_name, потом для каждого клиента посчитать (a) число уникальных сотрудников и (b) число уникальных сотрудников, которые имеют только одну запись и она принадлежит этому клиенту.
Один из подходов – использовать подзапрос для (b): сначала определить всех сотрудников, занятых более чем на одном проекте, и исключить их. Однако можно и одним запросом с агрегатами и условием:
SELECT e.client_name,
COUNT(DISTINCT s.employee_id) AS total_staffed,
COUNT(DISTINCT CASE
WHEN emp.engagement_count = 1 THEN s.employee_id
END) AS exclusive_staffed
FROM staffing AS s
JOIN engagement AS e
ON s.engagement_id = e.engagement_id
JOIN (
SELECT employee_id, COUNT(DISTINCT engagement_id) AS engagement_count
FROM staffing
GROUP BY employee_id
) AS emp
ON s.employee_id = emp.employee_id
GROUP BY e.client_name
ORDER BY e.client_name;
Здесь через вложенный подзапрос emp мы вычисляем, сколько проектов у каждого сотрудника. При соединении и финальной группировке по клиенту используем условный COUNT с CASE: считаем сотрудника в exclusive_staffed только если engagement_count = 1 (т.е. он числится ровно в одном проекте)[24][25]. Такой запрос работает и в PostgreSQL, и в MySQL. Замечание: в MySQL можно было вместо COUNT(DISTINCT CASE …) использовать SUM(CASE … THEN 1 ELSE 0 END), но с DISTINCT тоже поддерживается.
Возможные ошибки: забыть DISTINCT – тогда если сотрудник задействован на проекте у клиента два раза, его посчитают дважды. Или неправильно соединить таблицы: тут нужно staffing→engagement по engagement_id, а также подключить подзапрос по employee_id. Важно понять, что последовательность JOIN-ов влияет на итог только через то, какие строки отсеются или дублируются, но логически это одно выражение. Например, если бы в подзапросе emp мы не использовали DISTINCT, то лишние дубли могли исказить результат.
- Пример 10 (LEFT JOIN vs INNER JOIN): «Пользователи без активных подписок». Типичный вопрос (вариации есть на LeetCode): есть таблица Users и таблица Subscriptions, нужно выбрать всех пользователей, у которых нет активной подписки. Решение: левым соединением получить пользователей, у которых не находится строк в подписках с условием активности. Например:
SELECT u.user_id, u.name
FROM Users AS u
LEFT JOIN Subscriptions AS s
ON u.user_id = s.user_id AND s.status = 'active'
WHERE s.user_id IS NULL;
Здесь условие s.status = ‘active’ поставлено в секции ON так, что если для пользователя нет активных подписок, в результирующем JOIN-строке поля из s будут NULL, и мы отфильтруем таких по WHERE s.user_id IS NULL. Это паттерн «anti-join» (левой джойн + проверка на NULL). Ошибка: иногда кандидаты ставят условие status=’active’ в WHERE – тогда даже при LEFT JOIN это превратится фактически в INNER (потому что WHERE отсеет все NULL). Правильное место условия – в ON, если хотим сохранить пользователей без активных подписок. Этот пример иллюстрирует понимание, как фильтры при соединении влияют на результат.
- Пример 11 (CROSS JOIN подвох): Иногда дают каверзный вопрос на понимание соединений. Классический пример: «Есть таблица Product с 8 записями и таблица Manufacturer с 4 записями. Что вернет запрос SELECT * FROM Product, Manufacturer;?» Многие по невнимательности не вспоминают, что без явного условия это произведет декартово произведение 8×4 = 32 строк[26][27]. Правильный ответ: 32 строки, так как при отсутствии JOIN … ON или WHERE SQL выполнит CROSS JOIN всех записей[27]. На собеседовании такой вопрос проверяет, знает ли кандидат, что забытый ON или условие соединения – распространенная ошибка, приводящая к множению строк. Это больше теоретический вопрос, но уместно упомянуть.
Объединения и СУБД: PostgreSQL и MySQL имеют одинаковый синтаксис JOIN. Стоит помнить, что MySQL historically допускает указание соединений через запятую в FROM (как в примере выше), но это не рекомендовано – лучше писать явные JOIN. Также MySQL позволяет использовать USING(col) сокращение, а PostgreSQL – нет (только в стандартном SQL-стиле). Эти нюансы на интервью обычно не фокусируются, важно корректно соединить и выбрать нужные данные.
Работа с датами, временем и последовательностями событий
Задачи, связанные с временем – частый гость на интервью среднего и продвинутого уровня. Вас могут спросить вычислить разницу дат, найти записи в пределах временного окна, определить последовательность событий (например, сессии начала/окончания). Здесь пригодятся функции работы с датами (DATEDIFF, DATE_TRUNC, INTERVAL и т.п.), а также оконные функции LAG/LEAD для анализа последовательностей. Рассмотрим пару примеров из реальных задач:
- Пример 12 (интервал между событиями): «Повторяющиеся платежи». Вопрос (SQL-интервью в Stripe): дана таблица транзакций с полями transaction_id, merchant_id, credit_card_id, amount, transaction_timestamp. Требуется найти случаи повторной оплаты – то есть когда в одном и том же магазине (merchant_id) с той же картой (credit_card_id) и на ту же сумму происходит два платежа в течение 10 минут, и вывести общее число таких повторных платежей[28][29]. Звучит сложно, но решение сводится к сравнению каждой транзакции с другими по этим ключам и времени. Подходов два:
- Самосоединение (self-join): соединить таблицу транзакций сама с собой по условию совпадения merchant_id, credit_card_id, amount и проверить, что время t2.timestamp лежит в пределах 10 минут после t1.timestamp. То есть условие t2.transaction_timestamp BETWEEN t1.transaction_timestamp AND t1.transaction_timestamp + INTERVAL ’10 minute’ (в PostgreSQL) или t1.transaction_timestamp >= t2.transaction_timestamp AND TIMESTAMPDIFF(MINUTE, t1.transaction_timestamp, t2.transaction_timestamp) <= 10 (в MySQL). Нужно также исключить совпадение одной и той же записи (например, t1.id <> t2.id). Затем сгруппировать такие пары по идентификатору, посчитать количество. Это громоздко, но работает.
- Оконная функция LAG: более изящно можно отсортировать записи по времени в рамке тех же merchant, card, amount и вычислить LAG(transaction_timestamp) OVER (PARTITION BY merchant_id, credit_card_id, amount ORDER BY transaction_timestamp) – время предыдущей транзакции с теми же параметрами. Затем в SELECT сравнить текущую метку времени с лагом: если разница <= 600 секунд (10 минут), то считать это повтором. И наконец агрегировать суммарно количество таких случаев.
В контексте интервью, если кандидат предлагает решение через LAG, это демонстрирует владение продвинутыми возможностями. Углы: обязательно учесть, что если две транзакции произошли в 8 минут разницы, а следующая через еще 8, то первая и третья уже не в 10-минутном окне друг с другом, но обе являются повтором с промежуточной. Как считать такие серии – обычно считают каждую последующую оплату после первой как повторную, поэтому итог в примере Stripe был 1 (как число случаев повторной оплаты)[29][30]. Реализация может сложиться из условий, но важно проговорить логику правильно.
PostgreSQL vs MySQL: в Postgres можно легко прибавлять интервал (t1_time + INTERVAL ’10 minute’), а в MySQL используются функции типа TIMESTAMPDIFF или оператор DATE_ADD(t1_time, INTERVAL 10 MINUTE). Оконная функция LAG доступна и там, и там (с MySQL 8+). Однако, MySQL не поддерживает тип INTERVAL в LAG напрямую – нужно вычитать значения или использовать TIMEDIFF. Эти детали не критичны, главное – общее решение.
- Пример 13 (вычисление времени работы): «Суммарное время работы серверов (в днях)». Вопрос из интервью Amazon: имеется лог включения/выключения серверов – таблица server_log(server_id, status_time, session_status)[31][32]. Каждая запись – либо start, либо stop для определенного сервера. Нужно посчитать суммарное время (в днях), которое весь парк серверов был включен, учитывая, что серверов много и каждый может включаться и выключаться несколько раз[33][34]. По сути, требуется суммировать периоды работы каждого сервера и результат выразить в днях (целое число полных дней).
Решение: сначала рассчитать время работы для каждого отдельного интервала start-stop каждого сервера, а потом суммировать. Проще всего – воспользоваться оконной функцией LEAD (или LAG):
WITH server_sessions AS (
SELECT
server_id,
status_time AS start_time,
LEAD(status_time) OVER (PARTITION BY server_id ORDER BY status_time) AS end_time
FROM server_log
WHERE session_status = 'start'
ORDER BY server_id, status_time
)
SELECT
FLOOR(SUM(EXTRACT(EPOCH FROM (end_time - start_time))) / 86400) AS total_uptime_days
FROM server_sessions;
Здесь CTE server_sessions берет для каждой записи start следующую запись по времени как end_time (которая должна быть соответствующим stop). Предполагается, что данные корректны и у каждого start будет свой stop. Затем во внешнем запросе суммируем разницы end_time – start_time по всем серверам (в PostgreSQL EXTRACT(EPOCH FROM interval) выдает количество секунд) и делим на 86400 (число секунд в дне), берем FLOOR для полного числа дней. Получаем искомый суммарный аптайм в днях.
В MySQL эквивалент: можно использовать TIMESTAMPDIFF(SECOND, start_time, end_time) и затем делить на 86400. Оконная функция LEAD поддерживается, так что запрос почти не меняется, за исключением синтаксиса приведения типов.
Проверка на примере: Если сервер 1 работал с 2 августа по 4 августа (2 дня), а сервер 2 – с 17 по 24 августа (7 дней), суммарно получим 9. (В примере DataLemur было получено 21, что вызывает вопросы – возможно, там были другие данные или учитывались частичные дни округлением вверх. На интервью стоит уточнить требование округления: тут мы взяли полные дни).
Замечание: Такие задачи проверяют навыки работы с датами. Интервьюер может поинтересоваться, что если промежутки накладываются (например, если несколько серверов работают одновременно – надо ли их времена суммировать просто арифметически или считать общее покрытие по времени?). В нашей формулировке явно сказано суммировать uptime каждого сервера, так что накладки не уменьшают счетчик – просто складываем время всех серверов[35]. Этот момент нужно понять из требования.
Вопросы на даты могут также включать: «Найдите процент увеличения чего-либо по месяцам», «Сравните показатели за соседние дни» (тогда применяется LAG/LEAD для предыдущего дня), «Найдите максимальную последовательность ежедневных действий пользователя» (нужны даты и разница в один день – тут полезен трюк DATEDIFF(day, prev_date, curr_date) или использование переменных/окон). При ответе важно показать уверенность в функциях конкретной СУБД для работы со временем. Например, упомянуть, что в MySQL есть DATEDIFF(date1, date2) (разница в днях), а в Postgres можно просто вычитать даты или использовать AGE().
Аналитические и сценарные задачи (Open-Ended)
Отдельная категория – открытые вопросы, где нет чётко заданной метрики, которую нужно вычислить. Кандидату дают некоторую бизнес-ситуацию и ожидают, что он сам придумает, какие показатели вычислить и как, чтобы ответить на вопрос. Часто это встречается для опытных ролей (Senior Data Analyst, Data Scientist). Здесь важны не только SQL-навыки, но и понимание данных. Тем не менее, написать SQL всё равно придётся.
Отличие от обычных задач: вам не говорят прямо “посчитай X”, а формулируют проблему. Например: «Упали доходы, как узнать, какой процент выручки потерян из-за незавершённых заказов?» или «Как измерить эффективность кампании запуска нового продукта?». Нужно придумать метрику, а потом выразить её на SQL.
- Пример 14 (open-ended, определение метрик): «Процент потерянной выручки». Вопрос (реальное интервью Uber): дана таблица заказов uber_orders с колонками: service_name (тип сервиса), number_of_orders, monetary_value, status_of_order (завершён/не завершён/и т.п.). Нужно для каждого сервиса посчитать: (a) процент незавершённых заказов и (b) процент упущенной выручки из-за незавершённых заказов[36][37]. Получается, сперва надо понять, как определить “незавершён” и “упущенную выручку”. Решение (предложенное на StrataScratch) использовало сразу несколько современных фишек SQL – фильтрацию в агрегатах (Postgres) или CASE – и оформление через подзапрос:
SELECT service_name,
(lost_orders_number * 100.0 / total_orders) AS orders_loss_percent,
(lost_profit * 100.0 / possible_profit) AS profit_loss_percent
FROM (
SELECT service_name,
SUM(number_of_orders) AS total_orders,
SUM(number_of_orders) FILTER (WHERE status_of_order != 'Completed') AS lost_orders_number,
SUM(monetary_value) AS possible_profit,
SUM(monetary_value) FILTER (WHERE status_of_order != 'Completed') AS lost_profit
FROM uber_orders
GROUP BY service_name
) AS sub;
Во внутреннем запросе мы группируем по service_name и вычисляем: общее число заказов, количество незавершённых (все, где status_of_order != ‘Completed’), полная возможная выручка (сумма всех monetary_value) и упущенная выручка (сумма по незавершённым)[38][39]. В PostgreSQL для этого элегантно используются SUM(…) FILTER (WHERE …). В MySQL эквивалент будет SUM(CASE WHEN status_of_order != ‘Completed’ THEN monetary_value ELSE 0 END) и аналогично для подсчёта заказов. Во внешнем запросе просто считаем проценты.
Обсуждение метрик: Процент незавершённых = (не завершённые / все)*100. Процент потери выручки = (упущенная выручка / вся возможная выручка)*100. В решении указано, что надо явно оговорить, что считаем заказ незавершённым, если статус не ‘Completed’ (может быть ‘Cancelled’, ‘Other’ и т.п.)[40]. Такие нюансы – именно то, что проверяется в open-ended вопросах. Собеседник хочет увидеть, что вы мыслите критически: “Статус ‘Other’ – считать ли его завершённым? Может, нет? Надо уточнить.” В нашем случае разработчик решения предположил, что все отличные от ‘Completed’ – это проваленные заказы[40], но признаёт, что это допущение, и на интервью кандидат должен это проговорить.
Возможные ошибки: Посчитать процент не от общего, а, скажем, от завершённых – невнимательность к определению метрики. Или забыть умножить на 100 (просят же процент). Ещё – неправильно округлить, но обычно формат вывода не критичен, главное значение.
Open-ended вопросы могут включать много аспектов. Их решение часто предполагает многошаговый SQL или даже несколько запросов. Поэтому интервьюер может разрешить вам не писать сразу весь код, а сперва описать подход: какие таблицы соединить, какие поля агрегировать, какие фильтры применить. Отличный ответ включает обсуждение различных интерпретаций и чёткое обоснование выбранной.
Если вдруг попадается open-ended вопрос уровня «Как бы вы определили метрику успеха X и получили её из базы?», структура ответа должна быть такая: 1. Определяете понятие успеха/метрики своими словами. 2. Предлагаете набор данных/полей, нужных для расчёта. 3. Описываете, какие SQL-конструкции применить (JOIN, агрегаты, окна и пр.). 4. При необходимости, приводите упрощённый синтаксис запроса или фрагменты.
Иногда вместо конкретного SQL-кода интервьюеру достаточно вашей способности логически разбить задачу и знание функций/синтаксиса для реализации.
Оптимизация запросов и продвинутые концепции
На более высоких позициях (и всё чаще даже на средних) работодатели хотят уверенности, что кандидат не только напишет работающий SQL, но и оптимальный SQL. Вопросы по оптимизации могут быть теоретическими: «Как работает индекс?», «Когда лучше использовать денормализацию?», «Почему этот запрос выполняется медленно и как ускорить?» – или практическими: вам могут показать запрос и спросить, как его улучшить.
Индексы и эффективность. Стоит повторить основы: индекс – структура данных, ускоряющая поиск по определённому столбцу (аналогично указателю или оглавлению книги). Польза: быстрее выборка по индексируемому полю, особенно на больших таблицах[41]. Цена: медленнее операции вставки/обновления и дополнительное место на диске[42]. Интервьюер может спросить, какие типы индексов вы знаете (B-Tree – самый распространённый, хэш-индексы, GiST/GIN для Postgres и т.п.). Но чаще достаточно общих фраз: «Создаем индекс, если часто ищем по полю, и он значительно ускоряет SELECT, но надо быть осторожным – много индексов замедляют INSERT/UPDATE»[41].
Пример вопроса: «Объясните, как работает индекс и какие плюсы/минусы даёт» – можно ответить примерно так: “Индекс – это дополнительная структура (например, сбалансированное дерево), хранящая значения из выбранных столбцов и указатели на соответствующие строки. При запросе по условию на эти столбцы СУБД может искать в индексе, а не сканировать всю таблицу – поэтому запросы ускоряются[41]. Минусы: при обновлении данных индекс тоже надо обновлять, что замедляет запись; плюс индекс занимает место. В некоторых случаях (маленькая таблица, или нужно прочитать большую часть строк) индекс не даёт выигрыша.” Этого уровня ответа достаточно. Можно добавить, что в MySQL движок InnoDB использует B-Tree индексы для большинства случаев, и что кластерный индекс по первичному ключу, но это детали для особо интересующихся.
Денормализация и нормализация. Такие концептуальные вопросы проверяют понимание дизайна БД. Нормализация – приведение структуры данных к устранению избыточности (1НФ, 2НФ, 3НФ и т.д.), а денормализация – сознательное отступление от нормализации для повышения скорости чтения (храним дубли или предвычисленные агрегаты, чтобы не делать сложные JOINы на лету)[43][44]. Google, например, может спросить: «Что такое денормализация и когда вы бы ее использовали?». Ответ: “Денормализация – это добавление избыточных данных в схему, отклонение от правил нормальных форм. Делается для ускорения чтения – например, храним суммарные ежемесячные продажи прямо в таблице, вместо вычисления через JOINы каждый раз. Применимо в системах аналитической отчетности, хранилищах данных, где нужна быстрая выборка, а дополнительная сложность поддержания данных в актуальном состоянии оправдана. Минус – риск несогласованности данных и сложность обновлений.”[45].
Хранимые процедуры vs функции (UDF). Такой вопрос тоже прозвучал в DataLemur: «В чем разница между stored procedure и user-defined function?». Коротко: хранимая процедура – выполняет набор SQL-операций, может возвращать наборы результатов, поддерживает транзакции и изменения данных, но не может вызываться в SELECT как выражение. UDF (скалярная или табличная функция) – возвращает значение (или таблицу), может использоваться в запросах как часть выражения, но обычно не должна менять данные. Процедуры используются для операций, действий, UDF – для вычислений. Кроме того, в некоторых СУБД (например, SQL Server) есть различия в плане, что UDF можно в SELECT, а процедуры нет. Если не уверены, можно ответить на уровне: “Оба механизма позволяют сохранить на стороне БД кусок логики. Процедуры часто для целых бизнес-операций (могут менять данные), а функции – для вычисления и могут использоваться как часть запроса. Выбор зависит от задачи: сложный транзакционный процесс – процедура, простое вычисление значения – функция.”[46].
DELETE vs TRUNCATE. Казалось бы, базовый вопрос, но на интервью любят его задавать, чтобы увидеть, знает ли кандидат детали работы БД. DELETE – языковой оператор DML, удаляет строки, можно с условием WHERE; каждое удаление пишется в журнал транзакций (лог), триггеры ON DELETE срабатывают[47]. TRUNCATE – командa DDL, мгновенно очищает всю таблицу, обычно путем сброса данных сегмента; нельзя выбрать отдельные строки – всегда всё; журналирует лишь факт удаления страницы/таблицы, а не каждую строку, поэтому работает гораздо быстрее на больших объёмах[47][48]. Но у TRUNCATE есть ограничения: нельзя, если есть внешний ключ на таблицу; не вызывает обычных триггеров (в некоторых СУБД), и откатить его сложнее (в транзакции не всегда можно, например, в MySQL TRUNCATE не транзакционен).
На интервью достаточно сказать: “DELETE – более гибкий, можно удалять выборочно, но медленнее для больших таблиц; TRUNCATE – быстрый сброс всех данных, но без условий, фактически переСоздает пустую таблицу.”[47]. Этим вы покажете внимание к деталям.
Другие продвинутые темы: Могут спросить про VIEW (представления), триггеры, транзакции (свойства ACID). Например, «Что такое ACID?» – простой вопрос, ответ: Atomicity, Consistency, Isolation, Durability – 4 свойства транзакций[49]. Atomicity – либо все изменения внутри транзакции применяются, либо ни одного (все или ничего)[50]. Consistency – транзакция переводит базу из одного согласованного состояния в другое (целостность данных не нарушается правилами)[51]. Isolation – параллельные транзакции не мешают друг другу (эффект как будто выполнялись последовательно, при определенном уровне изоляции)[51]. Durability – после подтверждения транзакции (COMMIT) изменения сохраняются навсегда, даже если сбой системы случится[52]. Этот вопрос теоретический, в SQL-коде не проявляется, но важно знать.
Подводя итог: секция оптимизации – ваш шанс блеснуть широтой знаний. Желательно упомянуть личный опыт оптимизации (если есть), например: “Сталкивался с долгими запросами, использовали EXPLAIN чтобы понять план, добавляли нужные индексы, переписывали подзапрос на JOIN и добились ускорения.” – такой рассказ производит хорошее впечатление. Но не выдумывайте, если не спрашивают – отвечайте по сути заданного вопроса.
Современные базы данных и векторные БД
Сфера данных постоянно развивается, и в последние годы появились новые типы СУБД и расширения классических БД. На собеседованиях (особенно в компаниях, связанных с ML/AI) могут спросить про векторные базы данных или другие современные хранилища. Векторные БД (например, Pinecone, Weaviate, Milvus, Qdrant, Vespa и др.) предназначены для хранения и поиска по векторным представлениям данных – обычно эмбеддингов из нейросетей. Например, мы можем сохранять эмбеддинг изображения или текста как массив чисел размерностью 100–1000, и хотеть быстро находить “ближайшие” векторы (то есть схожие объекты).
Что могут спросить: – Основы: «Что такое векторная база и зачем она нужна?». Ответ: это база, оптимизированная под поиск ближайших соседей в высокомерном пространстве. Она позволяет по входному вектору быстро найти k самых похожих из миллионов хранимых (например, поиск похожих продуктов по эмбеддингу описания). Классические СУБД не справляются с таким поиском эффективно, поэтому появились специализированные. – Принцип работы: могут поинтересоваться методами, как достигается скорость. Ключевые слова: использование Approximate Nearest Neighbor (ANN) алгоритмов вместо точного поиска, чтобы ускорить за счёт приближения[53]. Также специальные индексы: графовые (HNSW), деревья и инвертированные файлы (IVF), локально чувствительные хэширования (LSH) и т.д.[54]. Они уменьшают объём сравнений – не приходится сравнивать с каждым вектором в базе, что позволило бы искать по миллиардным наборам быстро[54][55]. – Метрики сходства: часто упоминают cosine similarity (косинусное сходство) и евклидово расстояние. Cosine similarity популярна для текстовых эмбеддингов, потому что она смотрит на угол между векторами (суть – насколько они направлены похоже, игнорируя масштаб)[56]. Евклидово расстояние – прямое расстояние в пространстве, тоже используется. Могут спросить: «Какие метрики поддерживаются?» – можно перечислить: cosine, euclidean, иногда dot-product (скалярное произведение) – в зависимости от движка. – Пример задачи: «Вот у нас есть таблица с эмбеддингами товаров, как найти 5 самых похожих к заданному товару?». От вас ожидают хотя бы рассуждение: “Я бы использовал vector similarity search: вычисляем эмбеддинг заданного товара, затем ищем 5 ближайших по расстоянию в пространстве эмбеддингов.” Если спрашивают, как это выглядит в SQL-подобном синтаксисе, можно привести пример с pgvector (расширение PostgreSQL для векторов). В pgvector определён оператор <-> (или <=>) для вычисления расстояния между векторами. Например, запрос на топ-3 похожих документа может выглядеть так:
SELECT content
FROM embeddings
ORDER BY embedding <-> '[0.12, 0.25, ..., -0.07]'::vector
LIMIT 3;
Здесь embedding – колонка типа VECTOR, а ‘<…>’::vector – литерал искомого вектора. Оператор <-> вычисляет расстояние (по умолчанию, евклидово или косинусное, в зависимости от настроек) и ORDER BY сортирует от самого близкого (наименьшее расстояние) к дальше. Примечание: в цитируемом примере pgvector используется оператор <=> для косинусной меры[57][58], но суть аналогична. Такой синтаксис – SQL-подобный запрос к векторной базе внутри PostgreSQL.
Специализированные векторные БД (Pinecone, Weaviate) чаще не используют SQL. Например, Pinecone – это облачный сервис с собственным API: запрос отправляется через REST или gRPC, где вы передаете вектор и получаете список ближайших. Weaviate предоставляет GraphQL-интерфейс для семантического поиска. Однако, тренд идет к унификации: есть проекты, позволяющие писать на SQL поверх векторного поиска (например, в Singlestore добавляют функции для этого, или совместное использование PostgreSQL + pgvector, как показано выше).
- Что ещё могут упомянуть: гибридный поиск – когда сочетается условие по вектору и обычное фильтр-поле. Например: «Найдите похожие документы, но только в категории ‘спорт’.» В SQL с pgvector это было бы сочетание WHERE category = ‘sport’ и ORDER BY embedding <-> :vec. В некоторых движках (Weaviate) есть прямой параметр фильтра в запросе. Шардинг векторов – как масштабируется (через разбиение по ID или по clusters). Это уже глубокие детали, маловероятно для интервью, но если спросили – можно упомянуть, что при распределении нужно поддерживать баланс, а поиск может требовать обратиться ко многим узлам, поэтому часто используют приближенный поиск плюс распределение.
- Вопрос на опыт: «Использовали ли вы векторные базы? Для чего?» – здесь хорошо бы ответить, если знаете, реальные примеры: поиск похожих изображений, семантический поиск FAQ (когда у пользователя вопрос, а вы векторизуете вопрос и ищете векторно похожие существующие ответы), рекомендационные системы (по схожести предпочтений, тоже через эмбеддинги). Даже если не использовали, можно сказать: “Я знаком с концепцией, читал о них. Типичное применение – функции поиска в приложениях с AI, например, для чата с контекстом (Retrieval-Augmented Generation), когда сохраняют эмбеддинги документов и по вопросу пользователя ищут релевантные документы, чтобы подтянуть информацию.”[59][60].
- Про интеграцию: возможно, спросят, как интегрировать векторное хранилище в существующую архитектуру. Ответ: либо использовать гибридную СУБД (например, PostgreSQL с расширением – тогда можно в одном запросе сочетать обычные данные и векторный поиск), либо использовать отдельный сервис (например, Pinecone) и в приложении сначала искать там top-K идентификаторов, потом идти в основную БД за полными данными. Тут нет правильного/неправильного ответа, важно понимание, что векторный поиск – дополнительный шаг.
Совет: если вакансия не предполагает работы с ML, то вероятность глубоких вопросов про векторы невысока. Но базово знать, что это, – полезно. Также могут спросить про NoSQL базы, NewSQL, Hadoop и т.п., но это выходит за рамки чисто SQL-интервью. В контексте нашего вопроса ограничимся векторными, как самых наболевших сейчас.
Пример вопроса на SQL-подобный синтаксис для векторов: Интервьюер: «Допустим, у нас PostgreSQL с pgvector. Покажите запрос, который находит ближайших соседей данного вектора в таблице.» – Вы можете написать, как выше, и пояснить, что создав индекс HNSW по колонке embedding, мы обеспечим быстрый ANN-поиск[61][62]. Стоит упомянуть, что для точного поиска можно и без индекса, но это медленно; обычно используют ANN и получают почти точные результаты значительно быстрее[53].
Векторные БД – это про similarity search. Поэтому ключевые слова: ближайший сосед, cosine similarity, высокая размерность, approximate search.
Подготовившись по этим пунктам, вы успешно ответите на современные вопросы по БД, покажете свою актуальность в 2025 году и произведете впечатление на интервьюеров!
Заключение. Этот гайд охватил широкий спектр тем: от группировок и оконных функций до индексов и vector search. Конечно, список вопросов не исчерпывающий, но принципы разбора применимы ко многим другим задачам. Практикуйтесь на упомянутых платформах (DataLemur, LeetCode, StrataScratch) – там собраны сотни реальных вопросов от крупных компаний. Внимательно читайте условия, разбивайте проблему на шаги, сначала убеждайтесь в корректности подхода, а уже потом пишите финальный SQL. И не забывайте объяснять ваши решения “словами” – на собеседовании важно показать ход мыслей. Удачи в покорении SQL-интервью!
Источник использованных примеров и материалов: реальные интервью-вопросы из DataLemur[28][9], StrataScratch[6][8], статьи и обсуждения на Medium и профильных ресурсах[54][63].
[1] [2] [3] [4] [5] [6] [7] [8] [12] [13] [14] [15] [16] [17] [18] [19] [20] [26] [27] [36] [37] [38] [39] [40] [47] [48] SQL Interview Questions You Must Prepare: The Ultimate Guide – StrataScratch
[9] [10] [21] [22] [23] [24] [25] [28] [29] [30] [31] [32] [33] [34] [35] [41] [42] [46] [49] [50] [51] [52] 20 Advanced SQL Interview Questions (With Answers!)
[11] What I’ve Learned Solving All 44 SQL Leetcode Hard Questions in 7 Days | by chinhau | Medium
[43] [44] [45] 14 Google SQL Interview Questions (Updated 2025)
[53] [54] [55] [56] [63] Top 25 Vector Database Interview Questions and Answers | by Sanjay Kumar PhD | Medium
[57] [58] [59] [60] [61] [62] PostgreSQL as a Vector Database: A Pgvector Tutorial | TigerData



