복기
[SQL] LEETCODE SQL 50 문제풀이
nahowo
2025. 3. 6. 18:32
1193. Monthly Transactions I
SELECT
DATE_FORMAT(trans_date, '%Y-%m') AS month,
country,
COUNT(id) AS trans_count,
SUM(state = 'approved') AS approved_count,
SUM(amount) AS trans_total_amount,
SUM(IF(state = 'approved', amount, 0)) AS approved_total_amount
FROM
Transactions
GROUP BY
DATE_FORMAT(trans_date, '%Y-%m'), country
- 나라별/달별 group by를 통해 특정 조건의 개수/비율을 계산하는 문제이다. IF를 사용하는 방법을 몰라서 서브쿼리로 해결하려다 시간을 많이 썼다…
- IF(조건문, 참일 때의 값, 거짓일 때의 값)을 사용하면 서브쿼리 없이 집계함수만으로도 해결할 수 있다.
1174. Immediate Food Delivery II
https://leetcode.com/problems/immediate-food-delivery-ii/?envType=study-plan-v2&envId=top-sql-50
SELECT
ROUND(AVG(order_date = customer_pref_delivery_date) * 100, 2) AS immediate_percentage
FROM
Delivery
WHERE
(customer_id, order_date) IN
(
SELECT
customer_id,
MIN(order_date) AS order_date
FROM
Delivery
GROUP BY
customer_id
)
- 처음에 서브쿼리로 풀었는데 좋은 풀이가 있었다. IN을 사용할 때 컬럼을 여러 개 사용할 수 있다는 것을 배웠다.
550. Game Play Analysis IV
SELECT
ROUND(( COUNT(player_id) / (SELECT COUNT(DISTINCT player_id) FROM Activity)), 2) AS fraction
FROM
Activity
WHERE
(player_id, event_date) IN (
SELECT
player_id,
DATE_ADD(MIN(event_date), INTERVAL +1 DAY) AS event_date
FROM
Activity
GROUP BY
player_id
)
- 위 문제와 비슷한 방식으로 다시 풀어봤다. 앞으로도 pk별 MIN/MAX 등 특정 값을 요구하는 문제는 이렇게 풀어야겠다.
619. Biggest Single Number
SELECT
MAX(num) AS num
FROM (
SELECT
num
FROM
MyNumbers
GROUP BY
num
HAVING
COUNT(*) = 1
) AS a
- 만약 Single Number가 없다면 null을 출력해야 하는 문제였는데, 딱히 처리를 해주지 않아도 null이 출력되었다. min/max를 튜플이 없는 테이블에 적용하는 경우 null로 출력되는 것 같다.
1731. The Number of Employees Which Report to Each Employee
SELECT
a.employee_id,
a.name,
COUNT(*) AS reports_count,
ROUND(AVG(b.age), 0) AS average_age
FROM
Employees AS a
JOIN
Employees AS b
ON
a.employee_id = b.reports_to
GROUP BY
a.employee_id
ORDER BY
a.employee_id
1789. Primary Department for Each Employee
SELECT
employee_id,
department_id
FROM (
SELECT
employee_id,
department_id,
primary_flag,
COUNT(*) OVER (PARTITION BY employee_id) AS CNT
FROM Employee
) AS a
WHERE
CNT = 1
OR
primary_flag = 'Y'
- 처음에는 CNT가 하나인 튜플, flag가 Y인 튜플 따로 OR로 묶어서 연산했다.
- 집계 없이 COUNT만 수행한 뒤 갯수를 세면 된다.
180. Consecutive Numbers
SELECT
DISTINCT L1.num AS ConsecutiveNums
FROM
Logs as L1,
Logs as L2,
Logs as L3
WHERE
L1.id = L2.id - 1
AND
L1.num = L2.num
AND
L1.id = L3.id - 2
AND
L1.num = L3.num
- FROM에 여러 테이블을 함께 넣어서 동시에 처리할 수 있다. 이는 암시적 조인으로서 크로스 조인으로 작동한다.
SELECT
Today.id
FROM
Weather AS Yesterday,
Weather AS Today
WHERE
Yesterday.recordDate = DATE_ADD(Today.recordDate, INTERVAL -1 DAY)
AND
Yesterday.temperature < Today.temperature
- 이전에 풀었던 날짜 문제도 동일하게 해결할 수 있다.
1204. Last Person to Fit in the Bus
WITH RECURSIVE tw AS (
SELECT
0 AS total_weight,
0 AS turn
UNION ALL
SELECT
total_weight + (SELECT weight FROM Queue WHERE turn = tw.turn + 1) AS total_weight,
turn + 1
FROM
tw
WHERE
total_weight <= 1000
)
SELECT
person_name
FROM
Queue
WHERE
turn = (
SELECT
MAX(turn) - 1 AS turn
FROM
tw
)
- 순위별로 누적합을 구하기 위해 RECURSIVE를 사용했다.
SELECT
q1.person_name
FROM
Queue AS q1
JOIN
Queue AS q2
ON
q1.turn >= q2.turn
GROUP BY
q1.turn
HAVING
SUM(q2.weight) <= 1000
ORDER BY
SUM(q2.weight) DESC
LIMIT 1
- RECURSIVE를 사용하지 않는 풀이를 찾았다. 현재 turn 이하인 값들을 전부 현재 turn에 조인하고, 그 값들을 합하면 n부터 1까지의 누적합이 된다. 이 누적합이 1000 이하인 것들 중 최대값을 찾으면 된다.
1907. Count Salary Categories
WITH c AS (
SELECT
account_id,
CASE
WHEN income < 20000 THEN 'Low Salary'
WHEN income BETWEEN 20000 AND 50000 THEN 'Average Salary'
WHEN income > 50000 THEN 'High Salary'
END AS category
FROM
Accounts
UNION ALL
SELECT null, 'Low Salary' AS category
UNION ALL
SELECT null, 'Average Salary' AS category
UNION ALL
SELECT null, 'High Salary' AS category
)
SELECT
category,
IFNULL(COUNT(account_id), 0) AS accounts_count
FROM
c
GROUP BY
category
- UNION ALL을 이용해 각 항목별로 기본값을 갖도록 할 수 있다.
SELECT
'Low Salary' AS category,
SUM(IF(income < 20000, 1, 0)) AS accounts_count
FROM
Accounts
UNION ALL
SELECT
'Average Salary' AS category,
SUM(IF(income BETWEEN 20000 AND 50000, 1, 0)) AS accounts_count
FROM
Accounts
UNION ALL
SELECT
'High Salary' AS category,
SUM(IF(income > 50000, 1, 0)) AS accounts_count
FROM
Accounts
- 찾아본 다른 코드에서는 각 항목별로 카운트를 했다.
1164. Product Price at a Given Date
WITH t1 AS (
SELECT
DISTINCT product_id,
MAX(change_date) OVER(PARTITION BY product_id) AS last_changed
FROM
Products
WHERE
change_date <= '2019-08-16'
)
SELECT
t1.product_id,
p.new_price AS price
FROM
Products AS p
JOIN t1
ON
t1.product_id = p.product_id
AND
t1.last_changed = p.change_date
UNION
SELECT
DISTINCT product_id,
10
FROM
Products
WHERE
product_id NOT IN (
SELECT
product_id
FROM
t1
)
- 가장 최근 변경일이 기준일 이전인 데이터를 따로 뽑고, 그것의 차집합을 UNION해서 풀었다.
SELECT
product_id,
IFNULL(price, 10) AS price
FROM (
SELECT
DISTINCT product_id
FROM
Products
) AS uq
LEFT JOIN (
SELECT
DISTINCT product_id,
FIRST_VALUE(new_price) OVER(PARTITION BY product_id ORDER BY change_date DESC) AS price
FROM
Products
WHERE
change_date <= '2019-08-16'
) AS last_price
USING (product_id)
- 공식 leetcode 가이드는 WITH 대신 서브쿼리에 기준일 이전의 값들에 대해 최근 변경일을 구한 뒤, null값들에 대해 10으로 처리하는 식으로 나와있다.
- FIRST_VALUE, LAST VALUE, NTH_VALUE
- NTH_VALUE(new_price, 1)와 FIRST_VALUE(new_price, 1)는 동일하다.
- PARTITION BY: 그룹화할 대상 컬럼
- ORDER BY: 정렬화할 대상 컬럼
- 즉 product_id 기준 그룹화하고 같은 product_id를 가지는 튜플들을 change_date 기준 내림차순 정렬하고, 거기서 최대값을 가져온다.
윈도우 함수
- 윈도우 함수는 튜플들에 대한 집계 함수처럼 작동한다. 하지만 집계 함수는 특정 필드에 대해 하나의 튜플만 반환하는 반면에 윈도우 함수는 각 튜플별 결과를 생성한다.
- 윈도우 함수는 집계 윈도우 함수와 비집계 윈도우 함수로 나뉜다.
- 집계 윈도우 함수는 OVER 구문과 함께 쓰여야 윈도우 함수가 될 수 있다. MAX, MIN, SUM이 이에 해당한다. 만약 이 윈도우 함수들을 OVER 구문 없이 사용한다면 집계 함수처럼 작동하게 된다.
- LEAD, LAG, RANK, FIRST_VALUE 등의 비집계 윈도우 함수들은 OVER 구문과 함께 쓰여야 한다.
- 위의 내용이 leetcode 풀이에서 GROUP BY로 묶지 않고 PARTITION BY를 사용하는 이유이다.
626. Exchange Seats
https://leetcode.com/problems/exchange-seats/description/?envType=study-plan-v2&envId=top-sql-50
SELECT
CASE
WHEN
id % 2 = 1
AND
id != (SELECT MAX(id) FROM Seat)
THEN id + 1
WHEN id % 2 = 0 THEN id - 1
ELSE id
END AS id,
student
FROM
Seat
ORDER BY
id
- 연속된 두 학생의 자리를 스왑하는 문제였다. id가 홀수일 때는 +1을 하고, 짝수일 때는 -1을 하면 된다. 만약 전체 학생이 홀수명이라면 마지막 학생의 자리는 바꾸지 않는다.
1341. Movie Rating
https://leetcode.com/problems/movie-rating/description/?envType=study-plan-v2&envId=top-sql-50
(SELECT
u.name AS results
FROM (
SELECT
user_id,
COUNT(*) AS cnt
FROM
MovieRating
GROUP BY
user_id
) as r
JOIN
Users AS u
USING
(user_id)
ORDER BY
r.cnt DESC, u.name
LIMIT 1)
UNION ALL
(SELECT
m.title AS results
FROM (
SELECT
movie_id,
AVG(rating) AS rating
FROM
MovieRating
WHERE
MONTH(created_at) = '2'
AND
YEAR(created_at) = '2020'
GROUP BY
movie_id
) as r
JOIN
Movies AS m
USING
(movie_id)
ORDER BY
r.rating DESC, m.title
LIMIT 1)
- 왜 두 결과를 한 테이블에서 요구하는지 모르겠다…
- 둘 다 각각 답을 만든 뒤 UNION하면 되는데, 사람 이름과 영화 이름이 같을 수 있기 때문에 UNION ALL을 해줘야 한다.
- LIMIT을 사용하는 테이블을 UNION하려면 괄호로 묶어줘야 오류가 나지 않는다.
(
SELECT
name AS results
FROM
MovieRating
JOIN
Users
USING
(user_id)
GROUP BY
name
ORDER BY
COUNT(*) DESC, name
LIMIT 1
)
UNION ALL
(
SELECT
title AS results
FROM
MovieRating
JOIN
Movies
USING
(movie_id)
WHERE
MONTH(created_at) = '2'
AND
YEAR(created_at) = '2020'
GROUP BY
title
ORDER BY
AVG(rating) DESC, title
LIMIT 1
)
1321. Restaurant Growth
https://leetcode.com/problems/restaurant-growth/description/?envType=study-plan-v2&envId=top-sql-50
WITH d AS (
SELECT
visited_on,
SUM(amount) AS amount,
DENSE_RANK() OVER (ORDER BY visited_on) AS day
FROM
Customer
GROUP BY
visited_on
)
SELECT
c1.visited_on,
SUM(c2.amount) AS amount,
ROUND((SUM(c2.amount) / 7 ), 2) AS average_amount
FROM
d AS c1
JOIN
d AS c2
ON
c1.day >= c2.day
AND
c1.day <= c2.day + 6
AND
c1.day >= 7
GROUP BY
c1.day
ORDER BY
c1.visited_on
- DENSE_RANK를 통해 몇 번째 날인지 확인하고, 버스 문제처럼 셀프 조인을 통해 구간합을 찾았다.
1667. Fix Names in a Table
SELECT
user_id,
CONCAT(UPPER(SUBSTRING(name, 1, 1)), LOWER(SUBSTRING(name, 2))) AS name
FROM
Users
ORDER BY
user_id
- SUBSTRING(자를 문자열, 시작 위치, 자를 길이)에서 시작 위치는 1부터 시작한다…
1527. Patients With a Condition
SELECT
*
FROM
Patients
WHERE
conditions LIKE 'DIAB1%'
OR
conditions LIKE '% DIAB1%'
- REGEXP 쓰는 풀이도 있던데 그냥 LIKE 쓰는 게 편할 것 같다.
196. Delete Duplicate Emails
DELETE
p1
FROM
Person AS p1, Person AS p2
WHERE
p1.email = p2.email
AND
p1.id > p2.id
- DELETE를 사용하는 문제이다.
176. Second Highest Salary
SELECT
IF(COUNT(*) = 0, null, salary) AS SecondHighestSalary
FROM (
SELECT
salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS ranking
FROM
Employee
) AS a
WHERE
ranking = 2
- DENSE_RANK로 순위를 매긴 다음 IF문으로 없을 경우 null 처리를 해주면 된다.
SELECT
(
SELECT
DISTINCT Salary
FROM
Employee
ORDER BY
Salary DESC
LIMIT 1
OFFSET 1
) AS SecondHighestSalary
- SELECT문에 FROM이 없어도 되는걸 처음 알았다. 이렇게 작성하면 null 처리를 따로 해주지 않아도 된다.
- LIMIT a, b : a부터 b까지 가져오기
- OFFSET a : a부터 가져오기
1484. Group Sold Products By The Date
SELECT
DATE_FORMAT(sell_date, '%Y-%m-%d') AS sell_date,
COUNT(DISTINCT product) AS num_sold,
GROUP_CONCAT(DISTINCT product ORDER BY product) AS products
FROM
Activities
GROUP BY
sell_date
- GROUP_CONCAT을 통해 그루핑한 튜플을 연결할 수 있다.
- (SEPARAOTR ‘문자’)를 추가하면 컴마 말고 다른 문자열도 사용이 가능하다.
- 코드에 나와있듯이 ORDER BY, DISTINCT 설정이 가능하다.
585. Investments in 2016
SELECT
ROUND(SUM(tiv_2016), 2) AS tiv_2016
FROM
Insurance
WHERE
tiv_2015 IN (
SELECT
tiv_2015
FROM
Insurance
GROUP BY
tiv_2015
HAVING
COUNT(*) > 1
)
AND
(lat, lon) IN (
SELECT
lat,
lon
FROM
Insurance
GROUP BY
lat, lon
HAVING
COUNT(*) = 1
)
- 조건을 각각 서브쿼리로 만들어서 적용했다.
185. Department Top Three Salaries
https://leetcode.com/problems/department-top-three-salaries/description/
SELECT
d.name AS Department,
e.name AS Employee,
e.salary AS Salary
FROM (
SELECT
*,
DENSE_RANK() OVER(PARTITION BY departmentId ORDER BY salary DESC) AS tier
FROM
Employee
) AS e
JOIN
Department AS d
ON
d.id = e.departmentId
WHERE
e.tier <= 3
- DENSE_RANK로 미리 순위를 메긴 뒤에 그 순위가 3 이하인 것들만 선택했다.