1. Problem
하나의 상품(product_id)에 대해 여러 상점(store1, store2, store3)의 가격이 컬럼으로 나열된 와이드 포맷 데이터를, 각 상점별 가격이 행으로 쌓이는 롱 포맷으로 변환해야 한다. 이때 특정 상점에 가격 정보가 없는(NULL) 행은 결과에서 제외해야 한다는 제약 조건이 있다.
2. Solution
각 상점별 데이터를 독립적으로 추출한 후, 이를 수직으로 통합하는 UNION ALL 방식을 사용한다.
- 정적 리터럴 사용: 컬럼명인 store1 등을 문자열 'store1'으로 선언하여 store라는 새로운 컬럼의 데이터 값으로 활용한다.
- UNION ALL의 효율성: 각 서브쿼리가 추출하는 상점 도메인이 서로 다르므로 중복 제거 과정이 없는 UNION ALL을 사용하는 것이 연산 속도 면에서 최적이다.
3. Takeaway (데이터 모델링과 확장성)
데이터 구조를 변경하는 과정에서 분석가가 반드시 인지해야 할 모델링의 원리를 정립한다.
- CASE WHEN의 한계: 유진 님이 오답 쿼리에서 시도했던 CASE WHEN은 하나의 행에서 '하나의 값'만 선택하여 출력한다. 하지만 Unpivot은 하나의 행을 '여러 개의 행'으로 복제해야 하므로, 집합 자체를 합치는 UNION ALL 방식이 논리적으로 적합하다.
- 유연한 데이터 구조(Long Format): 와이드 포맷은 새로운 상점이 추가될 때마다 테이블 스키마를 변경(Add Column)해야 하지만, 롱 포맷은 데이터 행만 추가하면 되므로 분석 시스템의 확장성 측면에서 훨씬 객관적인 우위를 점한다.
- NULL 필터링의 중요성: WHERE ... IS NOT NULL 조건은 데이터의 노이즈를 제거하고 문제의 요구사항을 충족시키는 핵심 장치이다.
https://leetcode.com/problems/rearrange-products-table/
Table: Products
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| product_id | int |
| store1 | int |
| store2 | int |
| store3 | int |
+-------------+---------+
product_id is the primary key (column with unique values) for this table.
Each row in this table indicates the product's price in 3 different stores: store1, store2, and store3.
If the product is not available in a store, the price will be null in that store's column.
Write a solution to rearrange the Products table so that each row has (product_id, store, price). If a product is not available in a store, do not include a row with that product_id and store combination in the result table.
Return the result table in any order.
The result format is in the following example.
Example 1:
Input:
Products table:
+------------+--------+--------+--------+
| product_id | store1 | store2 | store3 |
+------------+--------+--------+--------+
| 0 | 95 | 100 | 105 |
| 1 | 70 | null | 80 |
+------------+--------+--------+--------+
Output:
+------------+--------+-------+
| product_id | store | price |
+------------+--------+-------+
| 0 | store1 | 95 |
| 0 | store2 | 100 |
| 0 | store3 | 105 |
| 1 | store1 | 70 |
| 1 | store3 | 80 |
+------------+--------+-------+
Explanation:
Product 0 is available in all three stores with prices 95, 100, and 105 respectively.
Product 1 is available in store1 with price 70 and store3 with price 80. The product is not available in store2.
📝 [SQL/Easy] Rearrange Products Table: 데이터 구조의 대전환
1. 문제 풀이의 핵심: "가로를 세로로 (Unpivot)"
하나의 행에 나열된 store1, store2, store3의 가격 정보를 각각 별개의 행으로 분리해야 합니다. 표준 SQL에서 이를 수행하는 가장 직관적인 방법은 각 스토어별로 데이터를 추출한 뒤 집합 연산자로 합치는 것입니다.
🛠 해결 전략
- 각 스토어별로 SELECT 문을 작성하여 product_id, 스토어 이름(문자열), 가격을 추출합니다.
- 가격을 추출할 때 해당 스토어의 값이 NULL인 행은 문제 조건에 따라 자동으로 제외됩니다 (기본적으로 WHERE price IS NOT NULL 효과 필요).
- UNION ALL을 사용하여 각 결과 집합을 하나로 수직 통합합니다.
2. 정답 쿼리 예시
-- Store 1 데이터 추출
SELECT product_id, 'store1' AS store, store1 AS price
FROM Products
WHERE store1 IS NOT NULL
UNION ALL
-- Store 2 데이터 추출
SELECT product_id, 'store2' AS store, store2 AS price
FROM Products
WHERE store2 IS NOT NULL
UNION ALL
-- Store 3 데이터 추출
SELECT product_id, 'store3' AS store, store3 AS price
FROM Products
WHERE store3 IS NOT NULL;
💡 왜 UNION ALL인가요?
UNION은 중복을 제거하는 과정을 거치지만, UNION ALL은 중복 체크 없이 모든 데이터를 그대로 합치기 때문에 연산 속도가 더 빠릅니다. 이 문제에서는 각 쿼리가 서로 다른 스토어(store1, store2, store3)를 추출하므로 결과가 중복될 리가 없으니 UNION ALL이 최적의 선택입니다.
3. 출제자의 의도 분석
이 문제는 지원자가 **'데이터 모델링'**의 기본 원리를 이해하고 있는지 테스트합니다.
① 와이드 포맷(Wide Format) vs 롱 포맷(Long Format)
- 와이드 포맷 (입력값): 사람이 보기는 편하지만, 스토어가 늘어날 때마다 컬럼을 계속 추가해야 하는 단점이 있습니다.
- 롱 포맷 (출력값): 기계와 분석 도구가 처리하기 가장 좋은 형태이며, 새로운 스토어가 추가되어도 행만 늘어날 뿐 테이블 구조는 변하지 않습니다.
- 의도: "유연한 데이터 분석을 위해 구조를 자유자재로 변경할 수 있는가?"
② 정적 값(Literal) 생성
컬럼명인 store1을 데이터 값인 'store1'이라는 문자열로 생성하여 출력할 수 있는지 확인합니다.
🚩 블로그 오답노트: "NULL 처리가 관건이다"
"없는 데이터는 보여주지 마라"
이 문제의 핵심 제약 조건은 NULL인 경우 결과에 포함하지 않는 것이다.
각 SELECT 문에 WHERE storeX IS NOT NULL 조건을 빼먹지 않았는지 확인하는 것이 'Easy' 문제에서 점수를 잃지 않는 비결입니다.
2. 나의 오답 쿼리
#쿼리 짜다가 포기함
SELECT product_id,
CASE
WHEN store1 IS NOT NULL THEN 'store1'
WHEN store2 IS NOT NULL THEN 'store2'
WHEN store3 IS NOT NULL THEN 'store3'
END AS store,
AS price