DIAB1으로 시작하는(접두사) 제1형 당뇨병 환자 찾기초기에는 단순히 특정 문자열 포함 여부만 확인하면 된다고 생각해 정규표현식(REGEXP 'DIAB1')을 사용했다.
하지만 문제의 조건은 명확히 '접두사(prefix)'였다. 정규식을 단순하게 작성하니 DIAB1이 단어 중간에 포함된 예외 케이스(예: SADIAB100)까지 잡아내는 논리적 오류가 발생한다는 것을 깨달았다.
1. LIKE 연산자 사용 (실무 표준 ⭐️)
굳이 복잡한 정규표현식을 쓸 필요 없이, 실무에서 가장 권장하는 LIKE 연산자를 활용하는 것이 정석이었다. % 와일드카드를 사용한 검색은 데이터베이스의 인덱스를 탈 수 있어 성능(검색 속도) 면에서도 훨씬 유리하다.
# Write your MySQL query statement below
SELECT
patient_id,
patient_name,
conditions
FROM Patients
-- LIKE 연산자를 활용
WHERE conditions LIKE 'DIAB1%'
OR conditions LIKE '% DIAB1%';
-- REGEXP 연산자를 활용
-- WHERE conditions REGEXP '^(DIAB1| DIAB1)'
2. 정규표현식(REGEXP) 보완
만약 정규표현식을 끝까지 활용한다면, 문자열의 시작을 알리는 기호(^)와 파이프(|)를 사용하여 WHERE conditions REGEXP '^DIAB1| DIAB1' 형태로 고도화할 수 있다는 것도 새롭게 알게 되었다.
데이터의 차집합(Anti-Join)을 구하는 문제라 판단해, LEFT JOIN을 걸고 오른쪽 테이블의 결제 내역이 없는 값을 WHERE t.visit_id IS NULL로 찾아내도록 쿼리를 짰다.
결과는 정확했지만, 쿼리 작성 중 습관적으로 SELECT DISTINCT를 사용한 것이 아쉬웠다. GROUP BY를 쓰면 이미 고유하게 그룹화가 이루어지며 중복이 제거되는데, 여기에 DISTINCT까지 추가하면 데이터베이스가 불필요한 중복 제거 연산을 한 번 더 수행하게 되어 성능이 저하된다.
NOT EXISTS 활용 (대용량 데이터 최적화 ⭐️)
내가 작성한 LEFT JOIN + IS NULL 방식도 훌륭한 기법이지만, 실무나 대용량 데이터 환경에서는 NOT EXISTS를 활용하는 것이 성능 면에서 가장 우수하다고 한다. 서브쿼리를 사용하는 NOT IN 방식은 서브쿼리 내에 NULL이 포함되거나 데이터가 많아질 경우 연산 속도가 크게 떨어질 위험이 있어 지양해야겠다.
SELECT
DISTINCT v.customer_id,
COUNT(v.customer_id) as count_no_trans
FROM Visits v
LEFT JOIN Transactions t
ON t.visit_id = v.visit_id
-- v.visit_id가 T.visit_id에 포함되지 않는 조건
WHERE t.visit_id IS NULL
-- WHERE v.visit_id NOT IN (
-- SELECT visit_id
-- FROM Transactions
-- )
GROUP BY v.customer_id
NOT EXIST를 활용한 방법
SELECT
v.customer_id,
COUNT(v.customer_id) AS count_no_trans
FROM Visits v
WHERE NOT EXISTS (
SELECT 1
FROM Transactions t
WHERE t.visit_id = v.visit_id
)
GROUP BY v.customer_id;
JOIN으로 두 테이블을 결합하고, SUM 함수로 잔액을 계산한 뒤 GROUP BY로 계좌를 묶었다. 그리고 마지막에 HAVING 절로 10000 초과 조건을 걸어 문제를 통과했다.
이 문제를 풀며 WHERE와 HAVING의 차이를 머릿속으로 확실하게 정리할 수 있었다. WHERE는 데이터 그룹화 이전 개별 행에 대한 필터링이고, HAVING은 그룹화 및 계산이 끝난 요약 결과값에 대한 필터링이다.
내가 작성한 방식이 이 문제의 가장 깔끔한 정답이었다. 특히 MySQL 환경에서는 표준 SQL과 다르게 편의상 HAVING 절에 SELECT에서 선언한 별칭(Alias, balance)을 바로 가져다 쓸 수 있어 쿼리의 가독성을 크게 높일 수 있었다.
SELECT
u.name,
SUM(t.amount) as balance
FROM Users u
JOIN Transactions t
ON u.account = t.account
GROUP BY t.account
HAVING balance > 10000
초기에는 두 테이블을 단순히 JOIN한 뒤, COUNT(r.user_id) / COUNT(u.user_id)를 하면 분자와 분모가 알맞게 계산될 것이라 생각했다.
하지만 큰 착각이었다. 두 테이블을 INNER JOIN하게 되면, 이미 '해당 대회에 등록한 유저'들만의 교집합이 만들어진다. 결국 분자와 분모의 숫자가 동일해져 모든 대회의 등록 비율이 100%가 나오는 논리 오류를 범했다. 게다가 기준을 묶어주는 GROUP BY 구문마저 누락했었다.
스칼라 서브쿼리 (Scalar Subquery) 활용 ⭐️
애초에 무거운 JOIN 연산을 사용할 필요가 없는 문제였다. SELECT 절 내부에 (SELECT COUNT(*) FROM Users)와 같이 스칼라 서브쿼리를 작성하면, 데이터베이스가 전체 유저 수를 고정된 상수 값으로 뱉어준다. 분모를 정확하게 고정하는 매우 유용한 테크닉을 배웠다.
SELECT
contest_id,
ROUND(COUNT(user_id) / (SELECT COUNT(*) FROM Users) * 100, 2) AS percentage
FROM Register
GROUP BY contest_id
ORDER BY
percentage DESC,
contest_id ASC;
activity_type이 start인 경우와 end인 경우를 WITH 구문(CTE)을 사용해 각각 별개의 가상 테이블로 분리하려는 논리로 접근했다. 하지만 이렇게 쪼갠 두 테이블을 다시 어떤 조건으로 JOIN 해야 동일한 프로세스로 정확하게 묶이는지, 연결고리를 설정하는 구문 작성이 헷갈렸다.
# Write your MySQL query statement below
-- activity type이 start인 table
WITH start_activity AS (
SELECT machine_id, process_id, timestamp AS start_time
FROM Activity
WHERE activity_type = 'start'
),
-- activity type이 end인 table
end_activity AS (
SELECT machine_id, process_id, timestamp AS end_time
FROM Activity
WHERE activity_type = 'end'
)
SELECT
s.machine_id,
ROUND(AVG(e.end_time - s.start_time), 3) AS processing_time
FROM start_activity s
JOIN end_activity e
ON s.machine_id = e.machine_id
AND s.process_id = e.process_id
GROUP BY s.machine_id;
Self JOIN 활용 (코드 단축 ⭐️)
굳이 WITH 구문으로 테이블을 명시적으로 분리할 필요 없이, 원본 테이블을 두 번 불러와(a1, a2) 조인 조건에서 바로 start와 end의 역할을 부여하는 Self JOIN 방식이 훨씬 간결하고 직관적이었다. 동일한 기계(machine_id)와 프로세스(process_id)를 기준으로 묶어주니 직관적으로 평균을 계산할 수 있었다.
SELECT
a1.machine_id,
ROUND(AVG(a2.timestamp - a1.timestamp), 3) AS processing_time
FROM Activity a1
JOIN Activity a2
ON a1.machine_id = a2.machine_id
AND a1.process_id = a2.process_id
AND a1.activity_type = 'start'
AND a2.activity_type = 'end'
GROUP BY a1.machine_id;
추가로, JOIN 없이 수학적인 원리를 이용해 CASE WHEN 절로 end 시간은 더하고 start 시간은 빼서 테이블을 단 한 번만 스캔하는 최적화 기법도 존재한다는 것을 알게 되었다. 추후 대용량 데이터를 처리할 때 꼭 적용해 봐야겠다.