https://leetcode.com/problems/product-price-at-a-given-date/
테이블 ``Product``는 어떤 상품의 새로운 가격과 변경된 날짜의 정보를 갖고 있다.
``2019-08-16``의 모든 상품의 가격 정보를 가져와야 한다.
만약 변경 이력이 없을 경우 10으로 간주한다.
문제에서 준 예시는 이렇다.
이걸 보면서 생각을 해 보자.
- 일단 ``2019-08-16`` 후의 날짜는 다 쳐내야 한다
- ``change_date``를 기준하여 내림차순으로 정렬
- 그리고 ``product_id`` 별 가장 빠른 날자의 가격을 확인
- 여기까지 별도의 쿼리로 둔다 - 이러면 기준 날짜 뒤에만 변경 로그가 있는 상품은 모르게 된다
- 기준 날짜 이전에 변경 로그가 없는 상품 아이디를 찾아 가격에 ``10``을 매칭
- 이제 이 쿼리와 이전의 쿼리를 합침
- 아예 별도의 쿼리로 생각했기 때문에 ``UNION ALL``이 될 것이다.
그럼 최신 가격을 걸러내는 것부터.
사실 이걸 어떻게 해야 하나 싶었다.
아이디어가 너무 떠오르지 않아서...
CTE로 가져올 수 있지만, 쿼리문이 너무 길어져 가독성이 떨어지고, 그 성능에 의심이 있었다.
셀프 조인도 시간 복잡도가 $O(n^2)$기 때문에 지양해야 했다.
그럼 대체 어떻게 해야 하나... 하면서 해메이다 보니,
``ROW_NUBER()``라는 윈도우 함수가 등장하는 것이 아닌가.
윈도우 함수는 SQL에서 꽤 효율적으로 동작하는 함수이기도 하고,
쿼리문이 간결해진다는 장점이 있다.
이를 다음과 같이 작성할 수 있다.
# change_date 별로 내림차순 정렬
# 가장 최근의 가격 가져옴
# ROW_NUMBER()로 번호 매겨서 나중에 WHERE 절에서 1만 가져오게 함
SELECT product_id, new_price,
ROW_NUMBER() OVER (
PARTITION BY product_id
ORDER BY change_date DESC
) AS is_changed
FROM Products
# 여기서 기준 날짜 기준으로 필터링
WHERE change_date <= '2019-08-16'
``product_id``로 잡아주고 날짜 기준으로 내림차순 정렬을 한다.
그럼 각 ``product_id``마다, 가장 최신의 갱신 이력이 가장 위로 올라가게 된다.
날짜까지 필터링이 완료가 됐으니 필요한 데이터는 다 가져왔다.
이제 최상위 데이터만 가져오면 된다.
# 가격 변동 있는 id와 그 가격을 구함
SELECT product_id,
new_price AS price
FROM (
# ...
) prices
# 만족하는 행이 없는 것도 필터링 함
# 행 번호가 가장 빠른 걸 가져오는 것이지만,
# 행의 유무도 판단되기에 BOOLEAN 처럼 취급
WHERE is_changed = 1
``is_changed``가 ``1``인 것만 가져온다.
사실 각 ``product_id``에 대해 단일 결과만이 존재하고,
이건 있다, 없다 2택으로 가를 수 있는 부분이다.
따라서 네이밍도 ``bool`` 변수를 사용하듯이 했다.
기간 내에 가격 변경 이력이 없는 상품들은 다음과 같이 가져올 수 있다.
# 가격 변경 없는 id를 구함
# 기준 날짜 뒤의 변경 사항을 걸러내는 것
SELECT DISTINCT product_id, 10 AS price
FROM Products
WHERE product_id NOT IN (
SELECT DISTINCT product_id FROM Products WHERE change_date <= '2019-08-16'
);
셀렉트문을 ``SELECT DISTINCT product_id, 10 AS price``로 작성했다.
여기서 왜 ``DISTINCT``가 필요하냐는 의문을 가질 수 있다.
기준 날짜 뒤에 변경 이력이 하나만 있을 거라는 보장이 있는가?
없기 때문에 필요한 것이다.
만약 저기서 중복을 걸러주지 못한다면 이렇게 된다.
나도 막상 돌려보니 저기서 막혀서 뭐가 잘못됐는지 계속 찾다 보니 그렇더라.
``WHERE``절에서 걸러줘서 끝인 줄 알았더니 하나가 더 있었다.
그리고 이 2개의 결과를 ``UNION ALL``로 합친다.
양 쿼리 결과에 대해 중복이 없어서 그냥 합치면 끝이다.
그냥 ``UNION``을 사용하게 되면, 중복 검사에 대한 추가적인 비용이 들기 때문에 여기선 비효율적이다.
따라서 다음의 최종 결과가 나온다.
# 가격 변동 있는 id와 그 가격을 구함
SELECT product_id,
new_price AS price
FROM (
# change_date 별로 내림차순 정렬
# 가장 최근의 가격 가져옴
# ROW_NUMBER()로 번호 매겨서 나중에 WHERE 절에서 1만 가져오게 함
SELECT product_id, new_price,
ROW_NUMBER() OVER (
PARTITION BY product_id
ORDER BY change_date DESC
) AS is_changed
FROM Products
# 여기서 기준 날짜 기준으로 필터링
WHERE change_date <= '2019-08-16'
) prices
# 만족하는 행이 없는 것도 필터링 함
# 행 번호가 가장 빠른 걸 가져오는 것이지만,
# 행의 유무도 판단되기에 BOOLEAN 처럼 취급
WHERE is_changed = 1
UNION ALL
# 가격 변경 없는 id를 구함
# 기준 날짜 뒤의 변경 사항을 걸러내는 것
SELECT DISTINCT product_id, 10 AS price
FROM Products
WHERE product_id NOT IN (
SELECT DISTINCT product_id FROM Products WHERE change_date <= '2019-08-16'
);
사실 이 결과라는 게 돌릴 때마다 왔다 갔다 하지만...
그래도 이 정도면 효율적으로 동작한다고 봐도 되지 않을까?
'Camp > T.I.L.' 카테고리의 다른 글
[TIL #25] 전력망을 둘로 나누기 (0) | 2024.10.16 |
---|---|
[TIL #24] 행렬 테두리 회전하기 (0) | 2024.10.14 |
[TIL #22] 실시간으로 달리는 공룡 (0) | 2024.10.07 |
[TIL #21] 큰 수 만들기 (0) | 2024.10.04 |
[TIL #20] 쿼드압축 후 개수 세기 (0) | 2024.10.02 |