SQL 면접 질문
SQL 면접은 데이터 질문을 정확하고 효율적으로 표현할 수 있는지 테스트합니다. 라이브로 쿼리를 작성해야 하며, 종종 조인, 집계, 윈도우 함수가 포함됩니다.
SQL 면접에서 다루는 내용
조인 및 필터링
INNER/LEFT/RIGHT/FULL 조인, WHERE 대 HAVING, 셀프 조인.
집계
GROUP BY, 집계 함수 및 그룹화 함정(NULL, 중복).
윈도우 함수
ROW_NUMBER, RANK, LAG/LEAD, 누적 합계, 그룹별 상위 N.
성능
인덱스, 쿼리 계획, 검색 가능성 및 쿼리가 느린 이유.
샘플 SQL 면접 질문
- 테이블에서 두 번째로 높은 급여를 찾으세요.좋은 답변이 다루는 것
- LIMIT/OFFSET 사용법
- 서브쿼리와 MAX 함수 조합
- DENSE_RANK 윈도우 함수 사용
- NULL 처리 주의
- 중복 급여가 있을 때의 정확한 결과
샘플 답변 보기
두 번째로 높은 급여를 찾는 문제는 SQL 면접에서 자주 나오는 고전적인 질문입니다. 가장 간단한 방법은 `ORDER BY salary DESC LIMIT 1 OFFSET 1`을 사용하는 것이지만, 이는 중복 급여가 있을 때 두 번째로 높은 급여가 아닌 두 번째 행의 급여를 반환할 수 있습니다. 더 정확한 방법은 `DENSE_RANK()` 윈도우 함수를 사용하여 순위를 매긴 후 순위가 2인 값을 가져오는 것입니다. 또는 서브쿼리로 최대 급여를 찾고, 그보다 작은 값 중에서 최대값을 찾는 방법도 있습니다. 이때 `WHERE salary < (SELECT MAX(salary) FROM Employee)`를 사용합니다. 주의할 점은 급여가 모두 같거나 턱없이 적어서 두 번째 값이 없을 때 `NULL`을 반환해야 한다는 것입니다. `LIMIT/OFFSET` 방식은 NULL 대신 빈 결과셋을 반환하므로 오해의 소지가 있습니다. 실무에서는 인덱스가 급여 컬럼에 걸려 있어야 성능이 좋습니다. 시간 복잡도는 인덱스가 있을 경우 O(log n) 수준입니다.
참고 코드sql -- 방법 1: DENSE_RANK 사용 (중복 급여 처리에 가장 안전) SELECT DISTINCT salary FROM ( SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM Employee ) ranked WHERE rnk = 2; -- 방법 2: 서브쿼리와 MAX (명확하고 널리 호환됨) SELECT MAX(salary) AS SecondHighestSalary FROM Employee WHERE salary < (SELECT MAX(salary) FROM Employee); -- 방법 3: LIMIT/OFFSET (간단하지만 중복 급여를 무시) SELECT salary FROM Employee ORDER BY salary DESC LIMIT 1 OFFSET 1; - 그룹별 상위 N개 행을 가져오세요(예: 카테고리별 상위 3개 제품).좋은 답변이 다루는 것
- ROW_NUMBER() vs DENSE_RANK() 선택
- PARTITION BY와 ORDER BY 사용법
- 동점 처리에 따른 결과 차이
- 인덱스 활용도
- 성능 고려 (파티션 수가 많을 때)
샘플 답변 보기
그룹별 상위 N개 행을 가져오는 것은 윈도우 함수 `ROW_NUMBER()` 또는 `DENSE_RANK()`로 해결할 수 있습니다. 예를 들어 카테고리별로 가격이 높은 상위 3개 제품을 구하려면, 먼저 각 카테고리 내에서 원하는 순서로 순위를 매깁니다. `ROW_NUMBER()`는 동점인 경우에도 무조건 고유한 순서를 부여하므로 동점이 있을 때 원하는 결과가 아닐 수 있습니다. `DENSE_RANK()`는 동점일 때 같은 순위를 부여하지만, 그 다음 순위는 건너뛰지 않습니다. 문제의 의도에 따라 적절히 선택해야 합니다. 쿼리는 서브쿼리로 순위를 계산한 후 메인 쿼리에서 N 이하의 순위를 필터링합니다. 인덱스는 `PARTITION BY` 컬럼과 `ORDER BY` 컬럼을 포함한 복합 인덱스가 효과적입니다. 파티션이 많은 테이블에서는 각 파티션마다 정렬이 발생하므로 성능에 주의해야 합니다. 시간 복잡도는 일반적으로 O(n log n)이며, 인덱스가 있으면 더 빨라질 수 있습니다.
참고 코드sql -- 카테고리별 가격 상위 3개 제품 (동점시 같은 순위 허용, 다음 순위 건너뜀 없음) WITH RankedProducts AS ( SELECT product_id, category, price, DENSE_RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rnk FROM Products ) SELECT product_id, category, price FROM RankedProducts WHERE rnk <= 3 ORDER BY category, rnk; -- ROW_NUMBER()를 사용하면 동점이 있어도 무조건 3개만 반환 WITH RankedProducts AS ( SELECT product_id, category, price, ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rn FROM Products ) SELECT product_id, category, price FROM RankedProducts WHERE rn <= 3; - WHERE와 HAVING의 차이점을 설명하세요.좋은 답변이 다루는 것
- WHERE: 행 단위 필터링, 집계 전 적용
- HAVING: 그룹 단위 필터링, 집계 후 적용
- WHERE는 인덱스 활용 가능, HAVING은 불가능
- HAVING은 GROUP BY와 함께 사용
- HAVING에서 집계 함수 사용 가능
샘플 답변 보기
`WHERE`와 `HAVING`의 가장 큰 차이는 필터링이 적용되는 시점입니다. `WHERE`는 테이블에서 행을 읽을 때 각 행에 대해 조건을 검사하여 집계 연산 전에 행을 걸러냅니다. 반면, `HAVING`은 `GROUP BY`로 그룹화되고 집계 함수가 적용된 후에 그룹 전체에 대해 조건을 검사합니다. 따라서 `WHERE`는 개별 행의 속성에 대한 조건(예: `salary > 5000`)을, `HAVING`은 그룹의 집계 결과에 대한 조건(예: `AVG(salary) > 5000`)을 기술하는 데 사용합니다. `WHERE`는 인덱스를 적극적으로 활용할 수 있어 성능 우위가 있지만, `HAVING`은 집계 결과를 메모리에 올린 후 필터링하므로 인덱스를 사용할 수 없습니다. 또한 `HAVING`은 반드시 `GROUP BY`와 함께 사용해야 하지만, `WHERE`는 단독으로도 사용 가능합니다. 실무에서는 가능한 많은 필터링을 `WHERE`에서 처리하여 데이터 양을 줄인 후 집계하는 것이 좋습니다. 예를 들어 '부서별 평균 급여가 5000 이상인 부서'를 찾을 때, '급여가 5000 이상인 직원만으로 그룹화'하려면 `WHERE salary > 5000`을 먼저 적용하고 그 후에 `GROUP BY`와 `HAVING`으로 평균을 다시 체크할 수 있습니다.
- 중복 행을 찾아 하나만 남기는 쿼리를 작성하세요.좋은 답변이 다루는 것
- GROUP BY와 COUNT(*)로 중복 식별
- ROW_NUMBER()로 중복 표시 후 삭제
- 자기 조인(self join) 사용
- DELETE 문법 주의 (MySQL 등에서)
- 기본키 보존 전략
샘플 답변 보기
중복 행을 찾아 하나만 남기는 작업은 SQL에서 흔히 필요한 작업입니다. 가장 일반적인 방법은 먼저 어떤 컬럼을 기준으로 중복을 판단할지 정의하는 것입니다. 보통 특정 컬럼(들)이 같은 행을 중복으로 간주합니다. 중복을 찾으려면 해당 컬럼으로 `GROUP BY`하고 `COUNT(*) > 1`로 필터링하면 됩니다. 하나만 남기고 삭제할 때는 각 그룹에서 하나의 행을 유지하는 방법이 필요합니다. `ROW_NUMBER()` 윈도우 함수를 사용하여 각 그룹 내에서 순번을 매긴 후 순번이 1인 행만 남기고 나머지를 삭제하는 것이 안전합니다. 단, MySQL 등 일부 DBMS에서는 서브쿼리에서 바로 `DELETE`를 허용하지 않으므로 임시 테이블을 활용하거나 `JOIN`을 사용해야 합니다. 또 다른 방법은 자기 조인을 통해 같은 값을 가진 더 작은 ID(또는 다른 고유 키)를 가진 행을 찾아 삭제하는 것입니다. 주의할 점은 인덱스가 걸려 있지 않은 컬럼으로 중복을 찾으면 성능이 매우 느릴 수 있으므로, 작업 전에 인덱스를 고려해야 합니다. 다음 예제는 `email` 컬럼이 중복된 고객 중 `id`가 가장 작은 하나만 남깁니다.
참고 코드sql -- 중복 확인 SELECT email, COUNT(*) FROM Customers GROUP BY email HAVING COUNT(*) > 1; -- 중복 제거 (하나만 남김): ROW_NUMBER() 사용 (권장) WITH Duplicates AS ( SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM Customers ) DELETE FROM Customers WHERE id IN (SELECT id FROM Duplicates WHERE rn > 1); -- MySQL에서 위 DELETE가 안 될 경우: 임시 테이블 사용 CREATE TEMPORARY TABLE to_delete AS SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM Customers ) t WHERE rn > 1; DELETE FROM Customers WHERE id IN (SELECT id FROM to_delete); -- 자기 조인 방식 (단, 성능 주의) DELETE c1 FROM Customers c1 INNER JOIN Customers c2 ON c1.email = c2.email AND c1.id > c2.id; - 일일 수익의 누적 합계를 계산하세요.좋은 답변이 다루는 것
- SUM() OVER (ORDER BY date) 누적 합계
- PARTITION BY를 이용한 그룹별 누계
- 프레임(frame) 지정 (ROWS BETWEEN)
- NULL 처리와 ORDER BY 정렬 순서
- 인덱스 활용 성능
샘플 답변 보기
일일 수익의 누적 합계는 윈도우 함수 `SUM()`에 `OVER` 절을 사용하여 쉽게 계산할 수 있습니다. 기본적으로 `SUM(profit) OVER (ORDER BY date)`는 현재 행까지의 모든 행의 profit 합계를 반환합니다. 이때 `ORDER BY date`는 누적 순서를 정의합니다. 만약 동일한 날짜에 여러 개의 수익이 있다면, 기본적으로 `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` 프레임이 적용되어 같은 날짜의 모든 행을 포함하므로 예상치 못한 결과가 나올 수 있습니다. 이를 방지하려면 `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`를 명시적으로 지정하여 행 단위로 누적하거나, 먼저 일별로 그룹화한 후 누적 합계를 구하는 것이 좋습니다. 또한 `PARTITION BY`를 사용하면 부서별, 지점별 등 그룹별 누적 합계도 가능합니다. 쿼리 성능을 위해 `date` 컬럼에 인덱스가 필요하며, 큰 테이블에서는 파티션 단위로 계산하는 것이 도움이 됩니다. 시간 복잡도는 윈도우 함수가 정렬을 수행하므로 O(n log n)입니다.
참고 코드sql -- 일별 수익을 그룹화한 후 누적 합계 (권장) WITH DailyProfit AS ( SELECT date, SUM(profit) AS daily_profit FROM Sales GROUP BY date ) SELECT date, daily_profit, SUM(daily_profit) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_profit FROM DailyProfit ORDER BY date; -- 원본 데이터에 바로 누적 적용 (동일 날짜 여러 행이면 주의) SELECT date, profit, SUM(profit) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING) AS running_total FROM Sales; -- 그룹별 누적 (지점별) SELECT branch, date, profit, SUM(profit) OVER (PARTITION BY branch ORDER BY date) AS branch_cumulative FROM Sales; - 느린 쿼리를 어떻게 빠르게 만들겠습니까? 쿼리 계획이 무엇을 알려주나요?좋은 답변이 다루는 것
- 쿼리 계획(EXPLAIN) 해석: 스캔 vs 탐색
- 인덱스 추가 (복합 인덱스 포함)
- 쿼리 재작성 (서브쿼리 → JOIN, OR → UNION)
- 불필요한 함수 제거 (컬럼 가공 금지)
- 통계 정보 업데이트와 파티셔닝
샘플 답변 보기
느린 쿼리를 개선하기 위해 가장 먼저 할 일은 `EXPLAIN` 또는 `EXPLAIN ANALYZE`로 쿼리 실행 계획을 확인하는 것입니다. 실행 계획은 테이블에 접근하는 방식(Full Table Scan vs Index Seek), 조인 순서, 사용된 인덱스, 필터링 조건의 효율성 등을 보여줍니다. 예를 들어 `type`이 `ALL`이면 전체 테이블 스캔으로 느리다는 의미이므로 인덱스를 고려해야 합니다. 그 다음으로는 인덱스 전략을 점검합니다. `WHERE` 절에 자주 사용되는 컬럼, 조인 컬럼, `ORDER BY` 컬럼에 인덱스를 추가하고, 여러 컬럼을 동시에 사용하면 복합 인덱스를 고려합니다. 단, 인덱스가 너무 많으면 쓰기 성능이 떨어지므로 균형이 중요합니다. 쿼리 자체를 최적화하는 방법도 있습니다. 예를 들어 `WHERE YEAR(hire_date) = 2020` 대신 `WHERE hire_date BETWEEN '2020-01-01' AND '2020-12-31'`로 함수를 제거하면 인덱스를 활용할 수 있습니다. `OR` 조건이 많으면 `UNION ALL`로 분리하는 것이 더 빠를 수 있고, 서브쿼리 대신 `JOIN`이 더 효율적인 경우가 많습니다. 또한 통계 정보가 오래되었으면 `ANALYZE TABLE`로 갱신하여 옵티마이저가 더 나은 계획을 선택하도록 해야 합니다. 대용량 테이블에서는 파티셔닝을 통해 스캔 범위를 줄일 수 있습니다. 마지막으로 `LIMIT`을 사용하여 반환되는 행 수를 제한하거나, 필요한 컬럼만 선택하는 등 데이터 양을 줄이는 것도 중요합니다.
준비 방법
- 직접 쿼리를 작성하는 연습을 하세요 — 많은 면접에서 자동 완성이 없는 공유 편집기를 사용합니다.
- 윈도우 함수를 마스터하세요; '그룹별 상위 N' 및 누적 합계 문제의 많은 부분을 해결할 수 있습니다.
- NULL 동작과 중복에 대해 명시적으로 생각하세요 — 흔한 함정 영역입니다.
- 쿼리 계획을 읽고 인덱스가 도움이 되는 이유를 설명할 준비를 하세요.
자주 묻는 질문
어떤 SQL 방언을 공부해야 하나요?
표준 SQL이 대부분의 면접을 커버합니다. 윈도우 함수와 CTE가 널리 지원된다는 점을 알아두세요; 일부 함수는 PostgreSQL, MySQL, SQL Server 간에 다릅니다.
SQL 면접에서 쿼리 최적화가 필요한가요?
데이터/백엔드 직무의 경우 네 — 인덱스, 쿼리 계획, 쿼리가 느린 이유에 대해 논의할 준비를 하세요.
윈도우 함수가 자주 나오나요?
매우 그렇습니다. ROW_NUMBER/RANK 및 누적 합계는 특히 데이터 엔지니어링 및 분석 직무에서 자주 등장합니다.
SQL 면접을 어떻게 연습할 수 있나요?
실제 스키마에 대해 쿼리 문제를 풀고 접근 방식을 설명하세요. Offersly는 SQL 관련 질문을 생성하고 추론을 채점할 수 있습니다.
즉각적인 AI 피드백으로 SQL 질문 연습하기
이력서를 업로드하고 맞춤형 모의 면접을 받아 무엇을 개선해야 할지 정확히 확인하세요 — 무료로 시작하세요.