<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>cheonroro_.log</title>
        <link>https://velog.io/</link>
        <description>데이터를 향해, 한 걸음씩 천천히.</description>
        <lastBuildDate>Sat, 02 May 2026 09:55:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>cheonroro_.log</title>
            <url>https://velog.velcdn.com/images/cheonroro_/profile/bb6f12f7-8f36-4a6c-abbf-8b1904da1cd9/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. cheonroro_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/cheonroro_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 (#1527, #1581, #1587, #1663, #1661)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-1527-1581-1587-1663-1661</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-1527-1581-1587-1663-1661</guid>
            <pubDate>Sat, 02 May 2026 09:55:32 GMT</pubDate>
            <description><![CDATA[<h1 id="1527-patients-with-a-condition">1527. Patients With a Condition</h1>
<h3 id="문제설명">문제설명</h3>
<ul>
<li><strong>문제 링크:</strong> <a href="https://leetcode.com/problems/patients-with-condition/">LeetCode 1527</a></li>
<li><strong>요구사항:</strong> 질병 코드가 <code>DIAB1</code>으로 시작하는(접두사) 제1형 당뇨병 환자 찾기</li>
</ul>
<h3 id="📝-나의-풀이-및-고민했던-점">📝 나의 풀이 및 고민했던 점</h3>
<p>초기에는 단순히 특정 문자열 포함 여부만 확인하면 된다고 생각해 정규표현식(<code>REGEXP &#39;DIAB1&#39;</code>)을 사용했다. 
하지만 문제의 조건은 명확히 <strong>&#39;접두사(prefix)&#39;</strong>였다. 정규식을 단순하게 작성하니 <code>DIAB1</code>이 단어 중간에 포함된 예외 케이스(예: <code>SADIAB100</code>)까지 잡아내는 논리적 오류가 발생한다는 것을 깨달았다.</p>
<h3 id="💡-다른-접근법-및-배운-점">💡 다른 접근법 및 배운 점</h3>
<p><strong>1. <code>LIKE</code> 연산자 사용 (실무 표준 ⭐️)</strong>
굳이 복잡한 정규표현식을 쓸 필요 없이, 실무에서 가장 권장하는 <code>LIKE</code> 연산자를 활용하는 것이 정석이었다. <code>%</code> 와일드카드를 사용한 검색은 데이터베이스의 인덱스를 탈 수 있어 성능(검색 속도) 면에서도 훨씬 유리하다.</p>
<pre><code class="language-sql"># Write your MySQL query statement below

SELECT 
    patient_id,
    patient_name,
    conditions
FROM Patients

-- LIKE 연산자를 활용
WHERE conditions LIKE &#39;DIAB1%&#39; 
   OR conditions LIKE &#39;% DIAB1%&#39;;
-- REGEXP 연산자를 활용
-- WHERE conditions REGEXP &#39;^(DIAB1| DIAB1)&#39;</code></pre>
<p><strong>2. 정규표현식(REGEXP) 보완</strong></p>
<p>만약 정규표현식을 끝까지 활용한다면, 문자열의 시작을 알리는 기호(^)와 파이프(|)를 사용하여 <code>WHERE conditions REGEXP &#39;^DIAB1| DIAB1&#39;</code> 형태로 고도화할 수 있다는 것도 새롭게 알게 되었다.</p>
<hr>
<h1 id="1581-customer-who-visited-but-did-not-make-any-transactions">1581. Customer Who Visited but Did Not Make Any Transactions</h1>
<h3 id="문제설명-1">문제설명</h3>
<ul>
<li><strong>문제 링크:</strong> <a href="https://leetcode.com/problems/customer-who-visited-but-did-not-make-any-transactions/description/">LeetCode 1581</a></li>
<li><strong>요구사항:</strong> 쇼핑몰에 방문은 했지만, 결제는 한 번도 하지 않은 고객의 ID와 그 횟수 구하기</li>
</ul>
<h3 id="📝-나의-풀이-및-고민했던-점-1">📝 나의 풀이 및 고민했던 점</h3>
<p>데이터의 차집합(Anti-Join)을 구하는 문제라 판단해, LEFT JOIN을 걸고 오른쪽 테이블의 결제 내역이 없는 값을 <code>WHERE t.visit_id IS NULL</code>로 찾아내도록 쿼리를 짰다.
결과는 정확했지만, 쿼리 작성 중 습관적으로 SELECT DISTINCT를 사용한 것이 아쉬웠다. GROUP BY를 쓰면 이미 고유하게 그룹화가 이루어지며 중복이 제거되는데, 여기에 DISTINCT까지 추가하면 데이터베이스가 불필요한 중복 제거 연산을 한 번 더 수행하게 되어 성능이 저하된다.</p>
<h3 id="💡-다른-접근법-및-배운-점-1">💡 다른 접근법 및 배운 점</h3>
<p><code>NOT EXISTS</code> 활용 (대용량 데이터 최적화 ⭐️)
내가 작성한 <code>LEFT JOIN + IS NULL</code> 방식도 훌륭한 기법이지만, 실무나 대용량 데이터 환경에서는 <code>NOT EXISTS</code>를 활용하는 것이 성능 면에서 가장 우수하다고 한다. 서브쿼리를 사용하는 NOT IN 방식은 서브쿼리 내에 NULL이 포함되거나 데이터가 많아질 경우 연산 속도가 크게 떨어질 위험이 있어 지양해야겠다.</p>
<pre><code class="language-sql">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</code></pre>
<p><code>NOT EXIST</code>를 활용한 방법</p>
<pre><code class="language-sql">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;</code></pre>
<hr>
<h1 id="1587-bank-account-summary-ii">1587. Bank Account Summary II</h1>
<h3 id="문제설명-2">문제설명</h3>
<ul>
<li><strong>문제 링크:</strong> <a href="https://leetcode.com/problems/bank-account-summary-ii/">LeetCode 1587</a></li>
<li><strong>요구사항:</strong> 모든 거래를 합산한 잔액이 10,000을 초과하는 유저의 이름과 잔액 출력</li>
</ul>
<h3 id="📝-나의-풀이-및-고민했던-점-2">📝 나의 풀이 및 고민했던 점</h3>
<p><code>JOIN</code>으로 두 테이블을 결합하고, <code>SUM</code> 함수로 잔액을 계산한 뒤 <code>GROUP BY</code>로 계좌를 묶었다. 그리고 마지막에 <code>HAVING</code> 절로 10000 초과 조건을 걸어 문제를 통과했다.
이 문제를 풀며 <code>WHERE</code>와 <code>HAVING</code>의 차이를 머릿속으로 확실하게 정리할 수 있었다. <code>WHERE</code>는 데이터 그룹화 이전 개별 행에 대한 필터링이고, <code>HAVING</code>은 그룹화 및 계산이 끝난 요약 결과값에 대한 필터링이다.</p>
<h3 id="💡-다른-접근법-및-배운-점-2">💡 다른 접근법 및 배운 점</h3>
<p>내가 작성한 방식이 이 문제의 가장 깔끔한 정답이었다. 특히 MySQL 환경에서는 표준 SQL과 다르게 편의상 <code>HAVING</code> 절에 <code>SELECT</code>에서 선언한 별칭(Alias, balance)을 바로 가져다 쓸 수 있어 쿼리의 가독성을 크게 높일 수 있었다.</p>
<pre><code class="language-sql">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 &gt; 10000</code></pre>
<hr>
<h1 id="1633-percentage-of-users-attended-a-contest">1633. Percentage of Users Attended a Contest</h1>
<h3 id="문제설명-3">문제설명</h3>
<ul>
<li><strong>문제 링크</strong>: <a href="https://leetcode.com/problems/percentage-of-users-attended-a-contest/">LeetCode 1633</a></li>
<li><strong>요구사항:</strong> 각 대회별 유저 등록 비율을 소수점 둘째 자리까지 구하기</li>
</ul>
<h3 id="📝-나의-풀이-및-고민했던-점-3">📝 나의 풀이 및 고민했던 점</h3>
<p>초기에는 두 테이블을 단순히 <code>JOIN</code>한 뒤, <code>COUNT(r.user_id) / COUNT(u.user_id)</code>를 하면 분자와 분모가 알맞게 계산될 것이라 생각했다.
하지만 큰 착각이었다. 두 테이블을 <code>INNER JOIN</code>하게 되면, 이미 &#39;해당 대회에 등록한 유저&#39;들만의 교집합이 만들어진다. 결국 분자와 분모의 숫자가 동일해져 모든 대회의 등록 비율이 100%가 나오는 논리 오류를 범했다. 게다가 기준을 묶어주는 <code>GROUP BY</code> 구문마저 누락했었다.</p>
<h3 id="💡-다른-접근법-및-배운-점-3">💡 다른 접근법 및 배운 점</h3>
<p><strong>스칼라 서브쿼리 (Scalar Subquery) 활용 ⭐️</strong>
애초에 무거운 <code>JOIN</code> 연산을 사용할 필요가 없는 문제였다. <code>SELECT</code> 절 내부에 <code>(SELECT COUNT(*) FROM Users)</code>와 같이 스칼라 서브쿼리를 작성하면, 데이터베이스가 전체 유저 수를 고정된 상수 값으로 뱉어준다. 분모를 정확하게 고정하는 매우 유용한 테크닉을 배웠다.</p>
<pre><code class="language-sql">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;</code></pre>
<hr>
<h1 id="1661-average-time-of-process-per-machine">1661. Average Time of Process per Machine</h1>
<h3 id="문제설명-4">문제설명</h3>
<ul>
<li><strong>문제 링크:</strong> <a href="https://leetcode.com/problems/average-time-of-process-per-machine/">LeetCode 1661</a></li>
<li><strong>요구사항:</strong> 각 기계별 프로세스의 평균 처리 시간(end - start) 구하기</li>
</ul>
<h3 id="📝-나의-풀이-및-고민했던-점-4">📝 나의 풀이 및 고민했던 점</h3>
<p>activity_type이 start인 경우와 end인 경우를 WITH 구문(CTE)을 사용해 각각 별개의 가상 테이블로 분리하려는 논리로 접근했다. 하지만 이렇게 쪼갠 두 테이블을 다시 어떤 조건으로 <code>JOIN</code> 해야 동일한 프로세스로 정확하게 묶이는지, 연결고리를 설정하는 구문 작성이 헷갈렸다.</p>
<pre><code class="language-sql"># 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 = &#39;start&#39;
),

-- activity type이 end인 table
end_activity AS (
    SELECT machine_id, process_id, timestamp AS end_time
    FROM Activity
    WHERE activity_type = &#39;end&#39;
)

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;</code></pre>
<h3 id="💡-다른-접근법-및-배운-점-4">💡 다른 접근법 및 배운 점</h3>
<p><code>Self JOIN</code> 활용 (코드 단축 ⭐️)
굳이 WITH 구문으로 테이블을 명시적으로 분리할 필요 없이, 원본 테이블을 두 번 불러와(a1, a2) 조인 조건에서 바로 start와 end의 역할을 부여하는 <code>Self JOIN</code> 방식이 훨씬 간결하고 직관적이었다. 동일한 기계(machine_id)와 프로세스(process_id)를 기준으로 묶어주니 직관적으로 평균을 계산할 수 있었다.</p>
<pre><code class="language-sql">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 = &#39;start&#39; 
  AND a2.activity_type = &#39;end&#39;
GROUP BY a1.machine_id;</code></pre>
<p>추가로, <code>JOIN</code> 없이 수학적인 원리를 이용해 <code>CASE WHEN</code> 절로 end 시간은 더하고 start 시간은 빼서 테이블을 단 한 번만 스캔하는 최적화 기법도 존재한다는 것을 알게 되었다. 추후 대용량 데이터를 처리할 때 꼭 적용해 봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 (#1378, #1407, #1484, #1517)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-1378-1407-1484-1517</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-1378-1407-1484-1517</guid>
            <pubDate>Wed, 29 Apr 2026 07:48:22 GMT</pubDate>
            <description><![CDATA[<h1 id="1378-replace-employee-id-with-the-unique-identifier">1378. Replace Employee ID With The Unique Identifier</h1>
<h3 id="문제설명">문제설명</h3>
<pre><code>Write a solution to show the unique ID of each user, If a user does not have a 
unique ID replace just show null. Return the result table in any order.</code></pre><p>두 테이블을 <code>LEFT JOIN</code>을 진행하면 되는 간단한 문제였다.</p>
<pre><code class="language-sql">SELECT 
    uni.unique_id, 
    e.name
FROM Employees e
LEFT JOIN EmployeeUNI uni
ON e.id = uni.id</code></pre>
<hr>
<h1 id="1407-top-travellers">1407. Top Travellers</h1>
<h3 id="문제설명-1">문제설명</h3>
<pre><code>Write a solution to report the distance traveled by each user.
Return the result table ordered by travelled_distance in descending order, 
if two or more users traveled the same distance, order them by their name in 
ascending order.</code></pre><p>각 유저(Users)별로 차량 탑승 기록(Rides)의 총 이동 거리를 구하는 문제이다. </p>
<pre><code class="language-sql">SELECT 
    u.name,
    IFNULL(SUM(r.distance), 0) AS travelled_distance
FROM 
    Users u
LEFT JOIN 
    Rides r ON u.id = r.user_id
GROUP BY 
    u.id
ORDER BY 
    travelled_distance DESC, 
    u.name ASC;</code></pre>
<h3 id="오늘의-핵심-개념-ifnull-vs-coalesce">오늘의 핵심 개념: <code>IFNULL</code> vs <code>COALESCE</code></h3>
<p>위 1407번 문제에서 탑승 기록이 없어 distance 합계가 null이 되는 경우를 0으로 바꿔주기 위해 <code>IFNULL</code>을 사용했다. 다른 분들의 풀이를 보니 <code>COALESCE</code>를 많이 사용하길래, 두 함수의 차이점을 정리했다.</p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>IFNULL</th>
<th>COALESCE</th>
</tr>
</thead>
<tbody><tr>
<td>작동 원리</td>
<td>1번째 값이 NULL이면 2번째 값 반환</td>
<td>괄호 안의 값 중 첫 번째로 NULL이 아닌 값 반환</td>
</tr>
<tr>
<td>인자 개수</td>
<td>딱 2개 (값, 대체값)</td>
<td>2개 이상 제한 없음 (값1, 값2, 값3...)</td>
</tr>
<tr>
<td>표준 여부</td>
<td>MySQL 등 일부 DB 전용 함수</td>
<td>표준 SQL (Oracle, PostgreSQL 등 대부분 지원)</td>
</tr>
</tbody></table>
<h3 id="요약-및-적용">요약 및 적용</h3>
<ul>
<li><strong>단순 대체 (<code>IFNULL</code>)</strong>: 
  지금처럼 단순히 값이 NULL일 때 0으로 바꾸는 등의 1차원적인 작업에서는 코드가 직관적인 <code>IFNULL</code>을 써도 무방하다.</li>
<li><strong>다중 조건 &amp; 호환성 (<code>COALESCE</code>)</strong>
만약 여러 컬럼을 순차적으로 확인해야 하거나(ex: 사내번호 -&gt; 휴대폰번호 -&gt; &#39;없음&#39; 순으로 데이터 추출), 향후 다양한 DB 환경을 고려한다면 표준 문법인 COALESCE를 사용하는 것이 더 좋은 습관이다.</li>
</ul>
<p><em>SQL-- COALESCE 활용 예시</em>
<code>SELECT COALESCE(NULL, NULL, 10, 20); -- 결과: 10</code></p>
<hr>
<h1 id="1484-group-sold-products-by-the-date">1484. Group Sold Products By The Date</h1>
<h3 id="문제설명-2">문제설명</h3>
<pre><code>Write a solution to find for each date the number of different products sold and 
their names. The sold products names for each date should be sorted lexicographically.
Return the result table ordered by sell_date.</code></pre><p>날짜별로 판매된 상품들을 그룹화하고, 하나의 문자열로 묶어서 출력하는 문제이다.</p>
<h3 id="트러블-슈팅-기존-쿼리의-문제점-분석">트러블 슈팅: 기존 쿼리의 문제점 분석</h3>
<p>제가 처음에 작성했던 오답 쿼리와 발생한 문제점은 다음과 같다.</p>
<p><strong>[초기 쿼리]</strong></p>
<pre><code class="language-sql">SELECT 
    sell_date,
    COUNT(product) as num_sold,
    GROUP_CONCAT(product) as products
FROM Activities 
GROUP BY sell_date
ORDER BY 
    sell_date ASC,
    products ASC;</code></pre>
<p><strong>[문제점]</strong></p>
<ul>
<li><p>중복 값이 그대로 출력됨: 
동일한 날짜에 &#39;Mask&#39;가 두 번 팔렸다면, num_sold도 1개가 더 카운트되고 products 문자열에도 &#39;Mask&#39;가 두 번 들어가게 된다.</p>
</li>
<li><p>문자열 내부 정렬 안 됨: 
쿼리 맨 마지막에 있는 <code>ORDER BY products ASC</code>는 결과로 나온 행(Row) 전체의 순서를 정렬할 뿐, <code>GROUP_CONCAT</code>으로 묶인 단어들 내부의 순서(사전순)를 정렬해 주지 못한다.</p>
</li>
</ul>
<h3 id="해결-방법-및-정답-쿼리">해결 방법 및 정답 쿼리</h3>
<p>이 문제를 해결하려면 <code>COUNT()</code>와 <code>GROUP_CONCAT()</code> 함수 내부에서 직접 <code>DISTINCT</code>와 <code>ORDER BY</code>를 사용해야 한다.</p>
<p>num_sold: <code>COUNT(DISTINCT product)</code>를 사용하여 중복을 제거한 고유 상품 개수만 센다.</p>
<p>products: <code>GROUP_CONCAT(DISTINCT product ORDER BY product ASC)</code>를 사용하여 결합하기 전에 미리 중복을 제거하고 사전순으로 정렬되도록 처리한다.</p>
<pre><code class="language-sql">SELECT 
    sell_date,
    COUNT(DISTINCT product) AS num_sold,
    GROUP_CONCAT(DISTINCT product ORDER BY product ASC SEPARATOR &#39;,&#39;) AS products
FROM 
    Activities
GROUP BY 
    sell_date
ORDER BY 
    sell_date ASC;</code></pre>
<h3 id="핵심-개념-group_concat-함수-파헤치기">핵심 개념: GROUP_CONCAT 함수 파헤치기</h3>
<p>MySQL에서 여러 행의 데이터를 하나의 문자열로 결합할 때 사용하는 굉장히 유용한 함수이다. 데이터를 요약해서 보여주어야 할 때 자주 활용된다.</p>
<p><strong>1. 기본 문법</strong></p>
<pre><code class="language-sql">GROUP_CONCAT(
    [DISTINCT] 컬럼명
    [ORDER BY 정렬할_컬럼명 ASC/DESC]
    [SEPARATOR &#39;구분자&#39;]
)</code></pre>
<p><strong>2. 주요 옵션 설명</strong> </p>
<ul>
<li><p>DISTINCT: 결합할 데이터 중 중복되는 값을 하나로 합쳐준다.</p>
</li>
<li><p>ORDER BY: 문자열로 결합되기 전에 괄호 안에서 데이터들의 순서를 먼저 정렬한다. (전체 쿼리 끝에 쓰는 ORDER BY와는 적용 시점과 대상이 다르다!)</p>
</li>
<li><p>SEPARATOR: 단어와 단어 사이를 이어줄 구분자를 지정한다. </p>
</li>
<li><p><em>기본값은 쉼표(,)*</em>이다. 만약 띄어쓰기나 슬래시 등으로 연결하고 싶다면 SEPARATOR &#39; / &#39; 와 같이 지정할 수 있다.</p>
</li>
</ul>
<p><strong>3. 활용 예시 비교</strong></p>
<p>만약 묶어줄 데이터가 [A, B, A, C] 순서로 존재한다고 가정.</p>
<ul>
<li>기본 결합: <code>GROUP_CONCAT(col)</code> $\rightarrow$ &quot;A,B,A,C&quot;</li>
<li>중복 제거: <code>GROUP_CONCAT(DISTINCT col)</code> $\rightarrow$ &quot;A,B,C&quot;</li>
<li>중복 제거 &amp; 역순 정렬: <code>GROUP_CONCAT(DISTINCT col ORDER BY col DESC)</code> $\rightarrow$ &quot;C,B,A&quot;</li>
<li>구분자 변경: <code>GROUP_CONCAT(col SEPARATOR &#39; | &#39;)</code> $\rightarrow$ &quot;A | B | A | C&quot;</li>
</ul>
<hr>
<h1 id="1517-find-users-with-valid-e-mails">1517. Find Users With Valid E-Mails</h1>
<h3 id="문제설명-3">문제설명</h3>
<pre><code>Write a solution to find the users who have valid emails.
A valid e-mail has a prefix name and a domain where:
The prefix name is a string that may contain letters (upper or lower case), digits, 
underscore &#39;_&#39;, period &#39;.&#39;, and/or dash &#39;-&#39;. The prefix name must start with a letter.
The domain is &#39;@leetcode.com&#39;. Return the result table in any order.</code></pre><p>정규표현식(Regular Expression)을 활용하여 복잡한 문자열 조건을 필터링하는 문제이다. 
정규식 문법뿐만 아니라, MySQL의 대소문자 구분(Case Sensitivity) 처리와 버전별 호환성 에러까지 해결해야 하는 꽤 까다롭고 배울 점이 많은 문제였다.</p>
<h3 id="📌-문제-이해-및-정규표현식-설계">📌 문제 이해 및 정규표현식 설계</h3>
<p>유효한 이메일인지 판별하기 위해 문제에서 제시한 규칙을 정규표현식 기호로 하나씩 치환했다</p>
<ol>
<li><strong>무조건 문자로 시작 (대문자 or 소문자)</strong><ul>
<li><code>^[a-zA-Z]</code> : <code>^</code>는 문자열의 시작을 의미한다.</li>
</ul>
</li>
<li><strong><code>@</code> 앞에는 문자, 숫자, 특수문자(<code>_</code>, <code>.</code>, <code>-</code>)만 허용</strong><ul>
<li><code>[a-zA-Z0-9_.-]*</code> : 괄호 안의 문자들이 0개 이상(<code>*</code>) 올 수 있음을 의미한다.</li>
</ul>
</li>
<li><strong>도메인은 무조건 <code>@leetcode.com</code>으로 끝날 것</strong><ul>
<li><code>@leetcode\\.com$</code> : 마침표(<code>.</code>)는 정규식에서 &#39;아무 문자 1개&#39;를 의미하므로, 진짜 마침표를 표현하기 위해 이스케이프(<code>\\</code>) 처리한다. <code>$</code>는 문자열의 끝을 의미한.</li>
</ul>
</li>
</ol>
<p><strong>[완성된 기본 정규표현식]</strong>
<code>&#39;^[a-zA-Z][a-zA-Z0-9_.-]*@leetcode\\.com$&#39;</code></p>
<hr>
<h3 id="트러블-슈팅-1-대소문자-구분의-함정">트러블 슈팅 1: 대소문자 구분의 함정</h3>
<p>위에서 만든 정규표현식을 <code>WHERE mail REGEXP</code> 구문에 넣고 돌렸더니, <code>winston@leetcode.COM</code> 이라는 오답 데이터가 검색 결과에 포함되었다.</p>
<ul>
<li><strong>원인:</strong> MySQL의 <code>REGEXP</code> 연산자는 기본적으로 <strong>대소문자를 구분하지 않는다(Case-insensitive).</strong> 따라서 소문자로 <code>@leetcode.com</code>이라 적었어도 대문자인 <code>COM</code>까지 모두 유효하다고 판단한 것이다.</li>
<li><strong>1차 해결 시도:</strong> 정규식에서 대소문자를 엄격하게 구분하도록 <code>REGEXP</code> 뒤에 <strong><code>BINARY</code></strong> 키워드를 추가하였다. (<code>WHERE mail REGEXP BINARY &#39;정규식&#39;</code>)</li>
</ul>
<hr>
<h3 id="트러블-슈팅-2-문자열-셋character-set-충돌-에러">트러블 슈팅 2: 문자열 셋(Character set) 충돌 에러</h3>
<p><code>BINARY</code> 키워드를 넣었더니 이번에는 아래와 같은 끔찍한 에러가 발생했습니다.</p>
<blockquote>
<p><code>Character set &#39;utf8mb3_general_ci&#39; cannot be used in conjunction with &#39;binary&#39; in call to regexp_like.</code></p>
</blockquote>
<ul>
<li><strong>원인:</strong> LeetCode의 테이블 컬럼은 대소문자를 무시하는 문자열 셋(<code>utf8mb3_general_ci</code>)을 사용 중인데, 쿼리에서는 바이트 단위로 비교하는 <code>BINARY</code>를 강제로 적용하려고 하니 MySQL 내부에서 충돌이 일어난 것이다.</li>
</ul>
<hr>
<h3 id="최종-해결-regexp_like-함수와-매칭-옵션">최종 해결: REGEXP_LIKE 함수와 매칭 옵션</h3>
<p>최신 MySQL 8.0 이상 환경에서는 이 문제를 아주 깔끔하게 해결할 수 있는 <strong><code>REGEXP_LIKE</code></strong> 전용 함수와 매칭 옵션을 제공한다.</p>
<ul>
<li><strong>문법:</strong> <code>REGEXP_LIKE(컬럼명, &#39;정규식&#39;, &#39;매칭옵션&#39;)</code></li>
<li>매칭 옵션에 <strong><code>&#39;c&#39;</code> (Case-sensitive)</strong>를 주면, 대소문자를 아주 엄격하게 구분하여 매칭해주고, 반대로 대소문자를 무시하고 싶다면 <code>&#39;i&#39;</code>를 사용한다.</li>
</ul>
<h3 id="💻-최종-정답-쿼리">💻 최종 정답 쿼리</h3>
<p>```sql
SELECT 
    user_id, 
    name, 
    mail
FROM 
    Users
WHERE 
    REGEXP_LIKE(mail, &#39;^[a-zA-Z][a-zA-Z0-9_.-]*@leetcode\.com$&#39;, &#39;c&#39;);</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 (#1148, #1179, #1211, #1251)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-1148-1179-1211-1251</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-1148-1179-1211-1251</guid>
            <pubDate>Tue, 28 Apr 2026 09:26:49 GMT</pubDate>
            <description><![CDATA[<h1 id="1148-article-views-i">1148. Article Views I</h1>
<h3 id="문제설명">문제설명</h3>
<pre><code>Write a solution to find all the authors that viewed at least one of their own 
articles. Return the result table sorted by id in ascending order.</code></pre><p>최소 1번 이상 author_id와 viewer_id가 같은 author_id 출력하는 문제이다.</p>
<pre><code class="language-sql">SELECT DISTINCT author_id as id

FROM Views

WHERE author_id = viewer_id

ORDER BY 1 ASC;</code></pre>
<h1 id="1179-reformat-department-table">1179. Reformat Department Table</h1>
<h3 id="문제-설명">문제 설명</h3>
<pre><code>Reformat the table such that there is a department id column and a revenue column 
for each month.    Return the result table in any order.</code></pre><p>다음과 같은 <code>Department</code> table에서 id와 month 컬럼을 분리하여 테이블을 Reformat 하는 문제이다.</p>
<p><strong>Input: Department table</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>revenue</th>
<th>month</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>8000</td>
<td>Jan</td>
</tr>
<tr>
<td>2</td>
<td>9000</td>
<td>Jan</td>
</tr>
<tr>
<td>3</td>
<td>10000</td>
<td>Feb</td>
</tr>
<tr>
<td>1</td>
<td>7000</td>
<td>Feb</td>
</tr>
<tr>
<td>1</td>
<td>6000</td>
<td>Mar</td>
</tr>
</tbody></table>
<p>Reformat 해야 하는 결과 형태는 다음과 같다.</p>
<p><strong>Output:</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>Jan_Revenue</th>
<th>Feb_Revenue</th>
<th>Mar_Revenue</th>
<th>...</th>
<th>Dec_Revenue</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>8000</td>
<td>7000</td>
<td>6000</td>
<td>...</td>
<td>null</td>
</tr>
<tr>
<td>2</td>
<td>9000</td>
<td>null</td>
<td>null</td>
<td>...</td>
<td>null</td>
</tr>
<tr>
<td>3</td>
<td>null</td>
<td>10000</td>
<td>null</td>
<td>...</td>
<td>null</td>
</tr>
</tbody></table>
<p>(Month의 Value들은 다음과 같다. <code>&quot;Jan&quot;, &quot;Feb&quot;, &quot;Mar&quot;, &quot;Apr&quot;, &quot;May&quot;, &quot;Jun&quot;, &quot;Jul&quot;, &quot;Aug&quot;, &quot;Sep&quot;, &quot;Oct&quot;, &quot;Nov&quot;, &quot;Dec&quot;</code>)</p>
<hr>
<h3 id="첫-접근과-깨달음-이런-노가다-문제였다니-">첫 접근과 깨달음: &quot;이런 노가다 문제였다니.. ^^&quot;</h3>
<p>처음 문제를 접했을 때는 세로로 긴 데이터를 가로로 펼쳐야 하길래, 당연히 복잡한 윈도우 함수나 내가 모르는 특별한 내장 함수가 있을 줄 알았다. 하지만 막상 파고들어 보니 생각보다 정직한(?) 노가다를 요구하는 문제였다.</p>
<p>이러한 작업을 데이터베이스 용어로 <strong>&#39;피벗(Pivot)&#39;</strong> 또는 <strong>&#39;행-열 변환&#39;</strong>이라고 부른다. 기계가 저장하기 좋은 형태(세로)를 사람이 보기 좋은 형태(가로)로 바꾸는 작업이다. Oracle이나 MS SQL 같은 곳에서는 <code>PIVOT</code>이라는 전용 함수를 지원하지만, 아쉽게도 MySQL에서는 이를 직접 구현해야 한다. </p>
<p>이를 위해 사용하는 핵심 기술이 바로 <strong>조건부 집계(Conditional Aggregation)</strong>다.</p>
<h4 id="조건부-집계의-3요소">조건부 집계의 3요소</h4>
<ol>
<li><strong><code>GROUP BY</code></strong>: 결과 테이블은 <code>id</code>별로 한 줄씩 나와야 하므로 <code>id</code>를 기준으로 묶어준다.</li>
<li><strong><code>CASE WHEN</code> (또는 <code>IF</code>)</strong>: 각 월(Month)에 해당하는 컬럼(기둥)을 세우고, 해당 월일 때만 <code>revenue</code>를 가져오고 아니면 <code>NULL</code>을 주도록 조건을 건다.</li>
<li><strong><code>SUM</code> (또는 <code>MAX</code>)</strong>: 그룹화된 여러 행 중에서 우리가 조건문으로 골라낸 특정 값을 단일 값으로 추출하기 위해 집계 함수로 감싸준다.</li>
</ol>
<hr>
<h3 id="여기서-생기는-궁금증-데이터가-아예-없는-월month은-어떻게-될까">여기서 생기는 궁금증: &quot;데이터가 아예 없는 월(Month)은 어떻게 될까?&quot;</h3>
<p>문제를 풀다 보면 한 가지 의문이 생긴다. 
*&quot;예를 들어, 원본 데이터에 <code>month</code>가 &#39;Dec&#39;인 데이터가 아예 없더라도 12월 컬럼은 존재해야 하는데, 이건 어떻게 처리되는 거지?&quot;*</p>
<p>결론부터 말하자면, <strong>데이터가 없더라도 컬럼은 정상적으로 만들어지며 값은 <code>NULL</code>로 채워진다.</strong> 그 이유는 SQL의 실행 흐름에 있다.</p>
<ol>
<li><strong>설계도는 <code>SELECT</code> 절이 결정한다:</strong> SQL은 원본 데이터 유무와 상관없이 <code>SELECT</code> 절에 작성한 컬럼 13개(id 1개 + 월 12개)를 무조건 결과 테이블의 기둥으로 세운다.</li>
<li><strong>모든 데이터가 <code>ELSE null</code> 처리됨:</strong> 12월 데이터가 원본에 단 한 줄도 없다면, <code>CASE WHEN month = &#39;Dec&#39;</code> 조건을 만족하는 데이터가 없으므로 모든 행이 <code>ELSE null</code>에 걸려 <code>null</code>을 반환한다.</li>
<li><strong>집계 함수의 결과:</strong> <code>GROUP BY</code>로 묶인 상태에서 <code>null</code>들만 모아서 <code>SUM()</code>을 하게 되므로, 최종 결과도 자연스럽게 <code>null</code>이 된다.</li>
</ol>
<hr>
<h3 id="코드-다이어트-case-when-대신-if-사용하기">코드 다이어트: <code>CASE WHEN</code> 대신 <code>IF()</code> 사용하기</h3>
<p>처음에 작성했던 정석적인 <code>CASE WHEN</code> 쿼리는 다음과 같다.</p>
<pre><code class="language-sql"># 정석적인 CASE WHEN 구문
SELECT 
    id,
    SUM(CASE WHEN month = &#39;Jan&#39; THEN revenue ELSE NULL END) AS Jan_Revenue,
    SUM(CASE WHEN month = &#39;Feb&#39; THEN revenue ELSE NULL END) AS Feb_Revenue,
    -- ... (12월까지 반복) ...
    SUM(CASE WHEN month = &#39;Dec&#39; THEN revenue ELSE NULL END) AS Dec_Revenue
FROM 
    Department
GROUP BY 
    id;</code></pre>
<p>틀린 코드는 아니지만 12달을 다 적으려니 코드가 너무 길어지고 가독성이 떨어진다. 어떻게 하면 더 줄일 수 있을까 찾아보니, MySQL 환경에서는 <code>IF()</code> 함수를 활용해 훨씬 간결하게 작성할 수 있었다.</p>
<p><code>IF(조건, 참일 때 값, 거짓일 때 값)</code> 이 구조를 적용하여 코드를 리팩토링한 최종 정답은 다음과 같다.</p>
<h3 id="최종-코드">최종 코드</h3>
<pre><code class="language-sql">SELECT 
    id,
    SUM(IF(month = &#39;Jan&#39;, revenue, null)) AS Jan_Revenue,
    SUM(IF(month = &#39;Feb&#39;, revenue, null)) AS Feb_Revenue,
    SUM(IF(month = &#39;Mar&#39;, revenue, null)) AS Mar_Revenue,
    SUM(IF(month = &#39;Apr&#39;, revenue, null)) AS Apr_Revenue,
    SUM(IF(month = &#39;May&#39;, revenue, null)) AS May_Revenue,
    SUM(IF(month = &#39;Jun&#39;, revenue, null)) AS Jun_Revenue,
    SUM(IF(month = &#39;Jul&#39;, revenue, null)) AS Jul_Revenue,
    SUM(IF(month = &#39;Aug&#39;, revenue, null)) AS Aug_Revenue,
    SUM(IF(month = &#39;Sep&#39;, revenue, null)) AS Sep_Revenue,
    SUM(IF(month = &#39;Oct&#39;, revenue, null)) AS Oct_Revenue,
    SUM(IF(month = &#39;Nov&#39;, revenue, null)) AS Nov_Revenue,
    SUM(IF(month = &#39;Dec&#39;, revenue, null)) AS Dec_Revenue
FROM 
    Department
GROUP BY 
    id;</code></pre>
<p>처음엔 반복 작업이 귀찮게 느껴졌지만, 조건부 집계를 통해 데이터베이스가 어떤 원리로 피벗 테이블을 생성하는지 정확하게 이해할 수 있는 좋은 문제였다!</p>
<h1 id="1211-queries-quality-and-percentage">1211. Queries Quality and Percentage</h1>
<h3 id="문제설명-1">문제설명</h3>
<pre><code>We define query quality as:
The average of the ratio between query rating and its position.
We also define poor query percentage as:
The percentage of all queries with rating less than 3.
Write a solution to find each query_name, the quality and poor_query_percentage.
Both quality and poor_query_percentage should be rounded to 2 decimal places.
Return the result table in any order.</code></pre><p>데이터베이스 쿼리 실행 결과가 담긴 <code>Queries</code> 테이블이 주어지고, 이 데이터를 바탕으로 각 쿼리 이름(<code>query_name</code>)별로 다음 두 가지 지표를 구해야 한다.</p>
<ol>
<li><strong>Quality (품질):</strong> 쿼리 평점(<code>rating</code>)을 위치(<code>position</code>)로 나눈 값들의 평균</li>
<li><strong>Poor Query Percentage (낮은 품질 비율):</strong> 평점이 3 미만인 쿼리의 백분율 (%)</li>
</ol>
<p>두 값 모두 <strong>소수점 둘째 자리까지 반올림</strong>해야 하며, 순서 상관없이 출력합니다.</p>
<p><strong>Input: Queries table</strong></p>
<table>
<thead>
<tr>
<th>query_name</th>
<th>result</th>
<th>position</th>
<th>rating</th>
</tr>
</thead>
<tbody><tr>
<td>Dog</td>
<td>Golden Retriever</td>
<td>1</td>
<td>5</td>
</tr>
<tr>
<td>Dog</td>
<td>German Shepherd</td>
<td>2</td>
<td>5</td>
</tr>
<tr>
<td>Dog</td>
<td>Mule</td>
<td>200</td>
<td>1</td>
</tr>
<tr>
<td>Cat</td>
<td>Shirazi</td>
<td>5</td>
<td>2</td>
</tr>
<tr>
<td>Cat</td>
<td>Siamese</td>
<td>3</td>
<td>3</td>
</tr>
<tr>
<td>Cat</td>
<td>Sphynx</td>
<td>7</td>
<td>4</td>
</tr>
</tbody></table>
<p><strong>Output:</strong></p>
<table>
<thead>
<tr>
<th>query_name</th>
<th>quality</th>
<th>poor_query_percentage</th>
</tr>
</thead>
<tbody><tr>
<td>Dog</td>
<td>2.50</td>
<td>33.33</td>
</tr>
<tr>
<td>Cat</td>
<td>0.66</td>
<td>33.33</td>
</tr>
</tbody></table>
<hr>
<h3 id="나의-첫-접근과-고민">나의 첫 접근과 고민</h3>
<p>처음 문제를 보고 핵심 로직은 바로 파악했다.</p>
<ol>
<li><code>rating / position</code>을 계산해야 한다.</li>
<li><code>query_name</code>으로 <code>GROUP BY</code>를 묶어야 한다.</li>
</ol>
<p>그래서 다음과 같이 쿼리를 짰는데, 집계 함수를 겹쳐 쓴 부분과 낮은 평점의 비율을 구하는 서브쿼리 부분에서 코드가 꼬이기 시작했다.</p>
<pre><code class="language-sql"># ❌ 수정 전 초기 접근
SELECT 
    query_name,
    AVG(SUM(rating/position)) as quality
    -- (SELECT 
    --     quality/COUNT(rating)
    --     FROM q
    --     WHERE rating &lt; quality)
    -- as poor_percentage
FROM Queries q
GROUP BY query_name;</code></pre>
<p>핵심 요구사항은 잘 파악했지만, 문법적으로 아쉬운 부분들이 있었다. 이를 어떻게 개선할 수 있을까?</p>
<h3 id="코드-리팩토링--핵심-포인트">코드 리팩토링 &amp; 핵심 포인트</h3>
<p><strong>1. quality 계산: AVG()와 SUM()은 겹쳐 쓸 필요가 없다!</strong></p>
<p>AVG() 함수는 이미 내부에 &#39;합계를 구한 뒤 개수로 나누는&#39; 기능이 포함되어 있다. 따라서 각 행의 rating / position 값을 구한 뒤 바로 AVG()로 감싸주기만 하면 된다.</p>
<p>수정 전: <code>AVG(SUM(rating / position))</code></p>
<p>수정 후: <code>AVG(rating / position)</code></p>
<p><strong>2. poor_query_percentage 계산: 서브쿼리 대신 IF() 활용하기</strong>
조건부 집계를 사용하면 서브쿼리 없이 한 줄로 끝낼 수 있다. 평점이 3 미만이면 1을, 아니면 0을 주도록 한 뒤 전체 개수로 나누면 된다.</p>
<p>수정: <code>SUM(IF(rating &lt; 3, 1, 0)) / COUNT(*) * 100</code></p>
<p><strong>3. 소수점 둘째 자리 반올림: ROUND()</strong>
문제 조건에 맞게 계산된 값들을 ROUND(값, 2)로 감싸준다.</p>
<h3 id="실무자들의-숏코딩-팁-초강력-추천">실무자들의 숏코딩 팁 (초강력 추천!)</h3>
<p>위에서 SUM과 COUNT를 조합해서 퍼센트를 구했지만, MySQL에서는 이를 AVG()와 IF()를 결합하여 엄청나게 짧게 줄일 수 있는 마법 같은 팁이 있다.</p>
<p><code>ROUND(AVG(IF(rating &lt; 3, 100, 0)), 2) AS poor_query_percentage</code></p>
<p><strong>🤔 어떻게 이게 가능할까?</strong>
<code>IF(rating &lt; 3, 100, 0)</code> 로직을 통해 3점 미만이면 100, 아니면 0이라는 값을 준다.
예를 들어 위 문제의 &#39;Dog&#39;의 경우 점수가 5, 5, 1이므로 IF문을 거치면 0, 0, 100이 된다. 이 세 숫자의 평균(AVG)을 구하면 (0 + 0 + 100) / 3 = 33.33이 되어 자연스럽게 백분율(Percentage)이 계산된다!</p>
<p>실무에서도 비율이나 퍼센트를 구할 때 정말 자주 쓰이는 세련된 패턴이니 꼭 기억해두자.</p>
<h3 id="최종-코드-1">최종 코드</h3>
<pre><code class="language-sql">SELECT 
    query_name,
    ROUND(AVG(rating / position), 2) AS quality,
    ROUND(AVG(IF(rating &lt; 3, 100, 0)), 2) AS poor_query_percentage
FROM 
    Queries
WHERE 
    query_name IS NOT NULL
GROUP BY 
    query_name;</code></pre>
<h3 id="배운-점">배운 점</h3>
<p>집계 함수(AVG)의 특성을 정확히 이해하고 중복 사용을 피하자.</p>
<p>서브쿼리가 생각날 때, 조건부 집계(IF 또는 CASE WHEN)로 더 간단하게 해결할 수 있는지 먼저 고민해 보자.</p>
<p>AVG(IF(조건, 100, 0)) 패턴은 백분율을 구하는 치트키다!</p>
<h1 id="1251-average-selling-price-mysql">1251. Average Selling Price (MySQL)</h1>
<h3 id="문제-설명-1">문제 설명</h3>
<pre><code>Write a solution to find the average selling price for each product. 
average_price should be rounded to 2 decimal places. 
If a product does not have any sold units, 
its average selling price is assumed to be 0.
Return the result table in any order.</code></pre><p>제품의 가격 변동 이력이 담긴 <code>Prices</code> 테이블과, 제품 판매 기록이 담긴 <code>UnitsSold</code> 테이블이 주어지고, 이 두 테이블을 활용하여 <strong>각 제품별 평균 판매 가격(Average Selling Price)</strong>을 구해야 한다.</p>
<ul>
<li>평균 판매 가격은 <code>(해당 제품의 총 매출액) / (해당 제품의 총 판매 개수)</code>로 계산합니다.</li>
<li>소수점 둘째 자리까지 반올림해야 합니다.</li>
<li>판매 기록이 없는 제품의 평균 가격은 <code>0</code>으로 처리해야 합니다.</li>
</ul>
<p><strong>Input: Prices table</strong></p>
<table>
<thead>
<tr>
<th>product_id</th>
<th>start_date</th>
<th>end_date</th>
<th>price</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>2019-02-17</td>
<td>2019-02-28</td>
<td>5</td>
</tr>
<tr>
<td>1</td>
<td>2019-03-01</td>
<td>2019-03-22</td>
<td>20</td>
</tr>
<tr>
<td>2</td>
<td>2019-02-01</td>
<td>2019-02-20</td>
<td>15</td>
</tr>
<tr>
<td>2</td>
<td>2019-02-21</td>
<td>2019-03-31</td>
<td>30</td>
</tr>
</tbody></table>
<p><strong>Input: UnitsSold table</strong></p>
<table>
<thead>
<tr>
<th>product_id</th>
<th>purchase_date</th>
<th>units</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>2019-02-25</td>
<td>100</td>
</tr>
<tr>
<td>1</td>
<td>2019-03-01</td>
<td>15</td>
</tr>
<tr>
<td>2</td>
<td>2019-02-10</td>
<td>200</td>
</tr>
<tr>
<td>2</td>
<td>2019-03-22</td>
<td>30</td>
</tr>
</tbody></table>
<p><strong>Output:</strong></p>
<table>
<thead>
<tr>
<th>product_id</th>
<th>average_price</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>6.96</td>
</tr>
<tr>
<td>2</td>
<td>16.96</td>
</tr>
</tbody></table>
<hr>
<h3 id="나의-첫-접근-이게-왜-안-될까">나의 첫 접근: &quot;이게 왜 안 될까?&quot;</h3>
<p>가장 먼저 떠오른 생각은 <code>product_id</code>로 두 테이블을 조인하고, 총액을 구한 뒤 개수로 나누어 평균(<code>AVG</code>)을 내는 것이었다. 그래서 아래와 같이 쿼리를 작성했다.</p>
<pre><code class="language-sql"># ❌ 초기 접근 쿼리
SELECT 
    p.product_id,
    ROUND(AVG(p.price * us.units / us.units), 2) as average_price
FROM Prices p 
JOIN UnitsSold us 
  ON p.product_id = us.product_id
GROUP BY p.product_id;</code></pre>
<p>논리적으로 그럴싸해 보였지만, 결과는 오답이었다. 이 쿼리에는 데이터베이스의 작동 방식을 오해한 3가지 결정적인 함정이 숨어있었다.</p>
<h3 id="코드-피드백--3가지-함정-탈출하기">코드 피드백 &amp; 3가지 함정 탈출하기</h3>
<p>함정 1. 평균 계산의 오류: <code>(price * units) / units</code>
가장 흔히 하는 수학적 실수다. <code>p.price * us.units / us.units</code>를 수식으로 보면, 곱한 units를 다시 units로 나누었기 때문에 결국 units는 약분되어 사라진다. 즉, 데이터베이스는 <code>AVG(p.price)</code>를 계산한 것과 완전히 똑같이 행동하게 되어 판매량(가중치)을 전혀 반영하지 못하는 단순 가격 평균을 내버린다.</p>
<p>해결책: &#39;가중 평균&#39;의 정확한 공식인 (총 매출액) / (총 판매 개수)를 SUM 함수를 이용해 명시적으로 적어주어야 한다.</p>
<p>수정: <code>SUM(p.price * us.units) / SUM(us.units)</code></p>
<p>함정 2. 기간(Date) 조건 누락: 가격은 시간에 따라 변한다!
단순히 <code>ON p.product_id = us.product_id</code>만 조건으로 주면 대참사가 일어난다. 1번 상품의 경우 2월 가격(5달러)과 3월 가격(20달러)이라는 두 개의 데이터가 존재한다. 단순히 id만으로 조인하면, 2월 25일에 팔린 물건이 3월 가격표와도 억지로 짝지어지는 크로스 조인(Cross Join) 현상이 발생한다.</p>
<p>해결책: 물건이 판매된 날짜(purchase_date)가 가격표에 명시된 기간(start_date ~ end_date) 안에 있을 때만 조인해야 한다.</p>
<p>수정: <code>ON p.product_id = us.product_id AND us.purchase_date BETWEEN p.start_date AND p.end_date</code></p>
<p>함정 3. 팔리지 않은 상품의 처리: INNER JOIN의 한계
문제 조건 중 &quot;판매 기록이 없는 상품의 평균 가격은 0으로 간주한다.&quot; 라는 문장이 있다. 기본 JOIN (INNER JOIN)을 사용하면 한 번도 안 팔린 상품은 조인 과정에서 매칭되지 않아 결과 테이블에서 아예 증발해 버린다.</p>
<p>해결책: 가격표(Prices)를 기준으로 모두 살려두고 판매 기록을 갖다 붙이는 LEFT JOIN을 사용해야 한다. 그리고 판매 기록이 없어 평균값이 NULL로 나올 경우, IFNULL() 함수를 이용해 0으로 바꿔주어야 한다.</p>
<p>✨ 최종 정답 코드
위의 3가지 문제를 모두 수정한 깔끔한 정답 코드는 다음과 같다.</p>
<pre><code class="language-sql">SELECT 
    p.product_id,
    IFNULL(ROUND(SUM(p.price * us.units) / SUM(us.units), 2), 0) AS average_price
FROM 
    Prices p
LEFT JOIN 
    UnitsSold us 
    ON p.product_id = us.product_id 
    AND us.purchase_date BETWEEN p.start_date AND p.end_date
GROUP BY 
    p.product_id;</code></pre>
<h3 id="배운-점-takeaways">배운 점 (Takeaways)</h3>
<p><strong>날짜 이력 데이터 조인 패턴</strong>: 가격이나 상태가 시간에 따라 변하는 이력(History) 테이블을 조인할 때는 <code>ON A.id = B.id AND date BETWEEN start AND end</code> 조건을 반드시 기억하자. 실무에서도 숨 쉬듯 사용하는 필수 패턴이다.</p>
<p>가중 평균의 정석: 수학적인 비례/가중 평균을 구할 때는 AVG() 함수에 의존하기보다, 분자와 분모를 각각 SUM()으로 구해서 직접 나누는 것이 데이터 오염을 막는 가장 확실하고 정확한 방법이다.</p>
<p>엣지 케이스 고려: 조인하기 전에 &#39;데이터가 없는 경우(팔리지 않은 경우)&#39;를 어떻게 처리할지 항상 고민하고 LEFT JOIN과 IFNULL을 적절히 활용하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 (#627, #1050, #1068, #1075, #1084)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-627-1050-1068-1075-1084</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-627-1050-1068-1075-1084</guid>
            <pubDate>Mon, 27 Apr 2026 05:00:41 GMT</pubDate>
            <description><![CDATA[<h1 id="627-swap-sex-of-employees">627. Swap Sex of Employees</h1>
<h3 id="문제-설명">문제 설명</h3>
<p>Write a solution to swap all &#39;f&#39; and &#39;m&#39; values (i.e., change all &#39;f&#39; values to &#39;m&#39; and vice versa) with a single update statement and no intermediate temporary tables.</p>
<p>Note that you must write a single update statement, do not write any select statement for this problem.</p>
<p>sex가 &#39;f&#39;인 값을 &#39;m&#39;으로 변경하고, &#39;m&#39;인 값은 &#39;f&#39;로 변경하는 문제입니다.
<code>SELECT</code>가 아닌 <code>UPDATE</code>를 사용해야합니다.</p>
<pre><code class="language-sql">UPDATE Salary
SET sex = CASE 
    WHEN sex = &#39;m&#39; THEN &#39;f&#39;
    ELSE &#39;m&#39;
    END;</code></pre>
<p><code>UPDATE</code>는 처음 사용해봤기 때문에 내용을 좀 찾아 정리했습니다.</p>
<p><strong>기본문법구조</strong></p>
<pre><code class="language-sql">UPDATE 테이블명
SET 컬럼명1 = 변경할값1, 컬럼명2 = 변경할값2
WHERE 조건;</code></pre>
<p><code>UPDATE</code>를 활용할 때에는 <code>WHERE</code>구문을 잘 활용하는 것이 중요합니다. 실행 전, 먼저 <code>SELECT * FROM 테이블명 WHERE 조건;</code>을 실행 후 내가 수정하려는 대상이 정확히 의도한 데이터인지 확인하는 습관이 필요할 것 같습니다.</p>
<h1 id="1050-actors-and-directors-who-cooperated-at-least-three-times">1050. Actors and Directors Who Cooperated At Least Three Times</h1>
<h3 id="문제설명">문제설명</h3>
<p>Write a solution to find all the pairs (actor_id, director_id) where the actor has cooperated with the director at least three times.</p>
<p>Return the result table in any order.</p>
<p>(actor_id, director_id) 쌍의 같이 작업한 횟수가 최소 3개 이상인 조합을 찾아내면 되는 문제입니다.</p>
<pre><code class="language-sql">SELECT actor_id, director_id

FROM ActorDirector

GROUP BY actor_id, director_id

HAVING COUNT(*) &gt;= 3</code></pre>
<p>easy하게 풀 수 있는 문제였다.</p>
<h1 id="1068-product-sales-analysis-i">1068. Product Sales Analysis I</h1>
<h3 id="문제설명-1">문제설명</h3>
<p>Write a solution to report the product_name, year, and price for each sale_id in the Sales table.</p>
<p>Return the resulting table in any order.</p>
<p>두 테이블을 <code>JOIN</code>해서 해당하는 컬럼들을 추출하는 문제입니다.</p>
<pre><code class="language-sql">SELECT 
    p.product_name,
    s.year,
    s.price
FROM Sales s
JOIN Product p
ON s.product_id = p.product_id</code></pre>
<p>이정도는 너무 easy합니다</p>
<h1 id="1075-project-employees-i">1075. Project Employees I</h1>
<h3 id="문제설명-2">문제설명</h3>
<p>Write an SQL query that reports the average experience years of all the employees for each project, rounded to 2 digits.</p>
<p>Return the result table in any order.</p>
<p>프로젝트 별로 참여한 employee들의 experience years의 평균을 소수점 둘째자리까지 나타내는 문제입니다.</p>
<pre><code class="language-sql">-- 프로젝트 별 참여한 근로자들의 평균 경험 연도 추출
SELECT 
    p.project_id,
    ROUND(AVG(experience_years), 2) as average_years
FROM Project p 
LEFT JOIN Employee e
ON p.employee_id = e.employee_id

GROUP BY p.project_id</code></pre>
<p>일단 문제에서 ~별로 이런게 나오면 <code>GROUPBY</code>를 활용한다.
처음에는 <code>SUM(e.experience_years)/COUNT(e.experience_years)</code>를 사용했지만 <code>AVG</code> 함수가 있었지.. 아직도 효율적으로, 빠르게 생각이 안나는걸 보니 SQL 연습 아직 멀었다!!!!
아직은 Easy 난이도의 문제만 풀고 있지만 Median, Hard까지 열심히 가보자. 🔥🔥</p>
<h1 id="1084-sales-analysis-iii">1084. Sales Analysis III</h1>
<h3 id="문제설명-3">문제설명</h3>
<p>Write a solution to report the products that were only sold in the first quarter of 2019. That is, between 2019-01-01 and 2019-03-31 inclusive.</p>
<p>Return the result table in any order.</p>
<p>1분기에만 팔린 product_id와 product_name을 추출하는 문제입니다.
처음에 작성한 쿼리는 다음과 같습니다.</p>
<pre><code class="language-sql">SELECT 
    p.product_id,
    p.product_name
FROM Sales s
JOIN Product p
ON s.product_id = p.product_id

WHERE DATE(s.sale_date) BETWEEN DATE(&#39;2019-01-01&#39;) AND DATE(&#39;2019-03-31&#39;)+1
GROUP BY s.product_id
HAVING COUNT(s.product_id) = 1</code></pre>
<p><strong>[내가 겪은 문제점]</strong></p>
<p>처음에는 WHERE 절을 사용해 2019년 1분기(1월~3월) 데이터만 필터링한 뒤, COUNT()를 이용해 판별하려고 했습니다. 하지만 이 방식은 치명적인 논리적 오류를 가지고 있었습니다.</p>
<p><strong>[원인 분석: SQL 실행 순서]</strong>
SQL은 FROM → WHERE → GROUP BY → HAVING 순서로 실행됩니다.
만약 어떤 상품(product_id = 2)이 2월(1분기)과 6월(2분기)에 모두 팔렸다면, WHERE 절에서 6월 판매 기록이 먼저 <strong>삭제(필터링)</strong>되어 버립니다.
그 결과, GROUP BY 단계로 넘어갈 때는 2월 판매 기록만 남게 되고, HAVING COUNT() = 1이라는 조건마저 통과해버려서 1분기에만 팔린 상품으로 잘못 분류되는 문제가 발생한 것입니다.</p>
<table>
<thead>
<tr>
<th>seller_id</th>
<th>product_id</th>
<th>buyer_id</th>
<th>sale_date</th>
<th>quantity</th>
<th>price</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
<td>1</td>
<td>2019-01-21</td>
<td>2</td>
<td>2000</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>2</td>
<td>2019-02-17</td>
<td>1</td>
<td>800</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>3</td>
<td>2019-06-02</td>
<td>1</td>
<td>800</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>4</td>
<td>2019-05-13</td>
<td>2</td>
<td>2800</td>
</tr>
</tbody></table>
<p><strong>해결 방법: HAVING 절과 MIN/MAX의 활용 (정답 접근법)</strong></p>
<p>WHERE 절에서 미리 데이터를 잘라내면 안 됩니다. 상품의 전체 판매 이력을 온전히 유지한 채로 그룹화(GROUP BY)를 진행해야 해당 상품이 언제 팔렸는지 정확히 검증할 수 있습니다.</p>
<p><strong>[MIN과 MAX를 이용한 논리]</strong></p>
<p>그룹화된 전체 데이터에서 조건 검사를 하기 위해 HAVING 절을 사용했습니다. 어떤 상품이 &#39;오직 1분기에만&#39; 팔렸다는 것을 증명하려면 다음 두 가지 조건이 모두 만족해야 합니다.</p>
<p><code>MIN(sale_date) &gt;= &#39;2019-01-01&#39;</code>: 해당 상품의 가장 첫 판매일이 1월 1일 이후여야 한다. (이전 년도 판매 방지)</p>
<p><code>MAX(sale_date) &lt;= &#39;2019-03-31&#39;</code>: 해당 상품의 가장 마지막 판매일이 3월 31일 이전이어야 한다. (2분기 이후 판매 방지)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 (#577, #595, #607, #610, #619)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-577-595-607-610-619</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-577-595-607-610-619</guid>
            <pubDate>Thu, 23 Apr 2026 08:36:38 GMT</pubDate>
            <description><![CDATA[<h1 id="577-employee-bonus">577. Employee Bonus</h1>
<h3 id="문제설명">문제설명</h3>
<p>Write a solution to report the name and bonus amount of each employee who satisfies either of the following:</p>
<ul>
<li>The employee has a bonus less than 1000.</li>
<li>The employee did not get any bonus.</li>
</ul>
<p>Return the result table in any order.</p>
<p>두가지 조건을 만족하는 employee를 추출하는 문제이다.</p>
<pre><code>1. 보너스가 1000보다 이하인 employee
2. 보너스를 받지 않은 employee </code></pre><p><code>LEFT JOIN</code>을 활용하면 쉽게 풀 수 있는 문제였다! </p>
<pre><code class="language-sql"> # Write your MySQL query statement below
SELECT e.name, b.bonus
FROM Employee e
LEFT JOIN Bonus b 
ON e.empId = b.empId
WHERE b.bonus &lt; 1000 
OR b.bonus IS NULL;</code></pre>
<h1 id="595-big-countries">595. Big Countries</h1>
<h3 id="문제설명-1">문제설명</h3>
<p>A country is big if:</p>
<p>it has an area of at least three million (i.e., 3000000 km2), or
it has a population of at least twenty-five million (i.e., 25000000).
Write a solution to find the name, population, and area of the big countries.</p>
<p>Return the result table in any order.</p>
<p>해당 조건을 만족하는 나라를 출력하는 되는 문제이다.</p>
<pre><code class="language-sql">SELECT name, population, area

FROM World

WHERE area &gt;= 3000000
    OR population &gt;= 25000000</code></pre>
<p><code>WHERE</code>절에 해당 조건을 넣어주기만 하면 되기 때문에 쉽게 풀 수 있었다.</p>
<h1 id="607-sales-person">607. Sales Person</h1>
<h3 id="문제-설명">문제 설명</h3>
<p>Write a solution to find the names of all the salespersons who did not have any orders related to the company with the name &quot;RED&quot;.</p>
<p>Return the result table in any order.</p>
<p>The result format is in the following example.</p>
<p><code>SalesPerson</code> <code>Company</code> <code>Orders</code> 3개의 테이블이 주어져서 어떻게 해결해야할까 고민을 많이 했다. 처음에는 <code>SalesPerson</code> <code>Orders</code> 두개의 테이블을 이용해서  <code>WHERE o.com_id != 1</code> 조건을 사용했지만 이건 <strong>&quot;RED 회사와 거래한 적이 있는 사람 전체를 제외해야 하는데, RED 회사와 거래한 &#39;개별 주문 건&#39;만 제외하기 때문&quot;</strong>에 원하는 방식이다.</p>
<p>RED 회사와 거래한 sales_id 목록&quot;을 먼저 구한 뒤, 그 목록에 포함되지 않은 사람만 뽑는 방식을 사용했다.</p>
<pre><code class="language-sql"># Write your MySQL query statement below
# &quot;RED 회사와 거래한 sales_id 목록&quot;을 먼저 구한 뒤, 그 목록에 포함되지 않은 사람만 뽑는 방식

SELECT name

FROM SalesPerson

WHERE sales_id NOT IN (
    SELECT o.sales_id
    FROM Company c
    JOIN Orders o
    ON c.com_id = o.com_id
    WHERE c.name = &#39;RED&#39;
)</code></pre>
<h1 id="610-triangle-judgement">610. Triangle Judgement</h1>
<h3 id="문제설명-2">문제설명</h3>
<p>Report for every three line segments whether they can form a triangle.</p>
<p>Return the result table in any order.</p>
<p>삼각형을 만들기 위한 조건이 제일 어려웠던 문제였다.. 생각 안나.. 🤦🏻‍♂️</p>
<p><strong>삼각형 결정 조건(Triangle Inequality Theorem)</strong>
이 정리에 따르면, 세 변 중 어떤 두 변의 길이를 더해도 나머지 한 변의 길이보다 항상 커야 한다.
즉, $x, y, z$가 삼각형을 이루려면 다음 세 가지 조건을 모두 만족</p>
<ul>
<li>$x + y &gt; z$</li>
<li>$x + z &gt; y$</li>
<li>$y + z &gt; x$</li>
</ul>
<pre><code class="language-sql">SELECT x, y, z,
    CASE 
        WHEN (x+y &gt; z) AND (x+z &gt; y) AND (y+z &gt; x) THEN &#39;Yes&#39;
        ELSE &#39;No&#39;    
        END AS triangle

FROM Triangle</code></pre>
<h1 id="619-biggest-single-number">619. Biggest Single Number</h1>
<h3 id="문제설명-3">문제설명</h3>
<p>A single number is a number that appeared only once in the MyNumbers table.</p>
<p>Find the largest single number. If there is no single number, report null.</p>
<pre><code class="language-sql">SELECT MAX(num) AS num
FROM (
    SELECT num
    FROM MyNumbers
    GROUP BY num
    HAVING COUNT(num) = 1
) AS single_nums;</code></pre>
<p>집계함수는 NULL을 자동으로 반환한다!!</p>
<p><strong>집계 함수의 결과값 동작</strong></p>
<table>
<thead>
<tr>
<th>함수</th>
<th>결과 집합이 비어있을 때 (대상 없음)</th>
</tr>
</thead>
<tbody><tr>
<td><code>MAX()</code></td>
<td>NULL</td>
</tr>
<tr>
<td><code>MIN()</code></td>
<td>NULL</td>
</tr>
<tr>
<td><code>SUM()</code></td>
<td>NULL</td>
</tr>
<tr>
<td><code>AVG()</code></td>
<td>NULL</td>
</tr>
<tr>
<td><code>COUNT()</code></td>
<td>0</td>
</tr>
</tbody></table>
<p>NULL이 나오는 것이 싫고, 데이터가 없으면 0이나 다른 기본값을 보여주고 싶다면
<code>COALESCE(값, 대체값)</code>을 활용한다.
<code>COALESCE(값, 대체값)</code>은 첫 번째 값이 NULL이면 두 번째 값을 반환한다.</p>
<pre><code class="language-sql">SELECT COALESCE(MAX(num), 0) AS num
FROM (
    SELECT num
    FROM MyNumbers
    GROUP BY num
    HAVING COUNT(num) = 1
) AS single_numbers;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 (#182, #183, #196, #197, #511)]]></title>
            <link>https://velog.io/@cheonroro_/LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-182-183-196-197-511</link>
            <guid>https://velog.io/@cheonroro_/LeetCode-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-182-183-196-197-511</guid>
            <pubDate>Wed, 22 Apr 2026 07:12:44 GMT</pubDate>
            <description><![CDATA[<h1 id="182-duplicate-emails">182. Duplicate Emails</h1>
<h3 id="문제설명">문제설명</h3>
<p>주어진 Person 테이블은 id와 email 두개의 컬럼을 갖고 있다. 
중복으로 갖고 있는 email을 <code>DISTINCT</code>하도록 추출하는 것이다.</p>
<pre><code class="language-sql">SELECT DISTINCT email

FROM Person

GROUP BY email

HAVING COUNT(1) &gt;= 2</code></pre>
<p>따라서 email을 <code>GROUPBY</code>를 진행하여 COUNT가 2개 이상인 것을 추출하였다.
<img src="https://velog.velcdn.com/images/cheonroro_/post/4eccba23-3676-4d93-a586-010172181597/image.jpg" alt="">
Runtime이 549ms로 어떻게 하면 더 줄일 수 있을까?</p>
<p><img src="https://velog.velcdn.com/images/cheonroro_/post/0512b61f-70c6-410b-88c4-5280296732e9/image.png" alt=""></p>
<pre><code class="language-sql">SELECT DISTINCT email

FROM Person

GROUP BY 1

HAVING COUNT(1) &gt;= 2</code></pre>
<p>다음과 같은 방법으로 Runtime을 줄일 수 있었다. 
해당 방법은 정리 후 게시할 예정이다.</p>
<h1 id="183-customers-who-never-order">183. Customers Who Never Order</h1>
<h3 id="문제설명-1">문제설명</h3>
<p>Write a solution to find all customers who never order anything.</p>
<p>Return the result table in any order.</p>
<p><code>Customers</code>와 <code>Orders</code> 두개의 테이블이 존재하고, <code>Orders</code>에 존재하는 customerId를 보유하지 않은 <code>Customers</code>테이블의 컬럼을 출력하는 것이다.</p>
<p>생각한 방법은 두가지이다.</p>
<p>첫번째 방법은 <code>NOT IN</code>을 활용하여, 주문 테이블에 있는 모든 고객 ID를 제외한 나머지 고객&quot;을 찾는 방식이다!</p>
<pre><code class="language-sql">SELECT name AS Customers
FROM Customers
WHERE id NOT IN (
    SELECT customerId 
    FROM Orders);</code></pre>
<p>두번째 방법은 <code>NOT EXISTS</code>를 활용한 방법이다.</p>
<pre><code class="language-sql">SELECT c.name AS Customers
FROM Customers c
WHERE NOT EXISTS (
    SELECT 1 
    FROM Orders o 
    WHERE o.customerId = c.id
);</code></pre>
<p>근데 생각해보니 제일 쉽고 간단한 방법이 있었다..</p>
<pre><code class="language-sql">SELECT c.name AS Customers
FROM Customers c
LEFT JOIN Orders o ON c.id = o.customerId
WHERE o.id IS NULL;</code></pre>
<p><code>LEFT JOIN</code>을 진행하면 대응되는 데이터가 없다면 <code>Orders</code>의 컬럼들은 NULL값을 갖게 된다. 이 때 WHERE절에 NULL인 것을 추출하는 조건을 넣어주면 끝. </p>
<h1 id="196-delete-duplicate-emails">196. Delete Duplicate Emails</h1>
<h3 id="문제-설명">문제 설명</h3>
<p>Write a solution to delete all duplicate emails, keeping only one unique email with the smallest id.</p>
<p>For SQL users, please note that you are supposed to write a DELETE statement and not a SELECT one.</p>
<p>For Pandas users, please note that you are supposed to modify Person in place.</p>
<p>After running your script, the answer shown is the Person table. The driver will first compile and run your piece of code and then show the Person table. The final order of the Person table does not matter.</p>
<p>중복된 email을 삭제하고, 작은 id값을 가진 데이터만 남기는 문제.
처음에 SELECT 문을 하는데 왜 안되지 ..? 했는데 역시 문제를 꼼꼼히 읽지 않는 버릇은 중학생 때부터 고쳐지지 않는다..  문제에 보면 SQL의 경우 SELECT 문이 아닌 DELETE 문을 사용하라고 적혀있다.</p>
<pre><code class="language-sql">DELETE p1
FROM Person p1
JOIN Person p2
ON p1.email = p2.email
WHERE p1.id &gt; p2.id -- -- 더 큰 ID 삭제</code></pre>
<h1 id="197-rising-temperature">197. Rising Temperature</h1>
<h3 id="문제설명-2">문제설명</h3>
<p>Write a solution to find all dates&#39; id with higher temperatures compared to its previous dates (yesterday).</p>
<p>Return the result table in any order.</p>
<p>잘못된 쿼리 </p>
<pre><code class="language-sql">SELECT w2.id as Id

FROM Weather w1
JOIN Weather w2
ON w1.id = w2.id

WHERE w2.id &gt; w1.id 
    AND DATEDIFF(w2.recordDate, w1.recordDate) = 1 
    AND w2.temperature - w1.temperature &gt; 0 </code></pre>
<p><strong>작성한 쿼리에서 문제점</strong></p>
<ol>
<li><p>조인 조건(ON)의 문제: ON w1.id = w2.id라고 작성하면, w1과 w2가 <strong>같은 행(Row)</strong>을 가리키게 된다.</p>
</li>
<li><p>WHERE 절의 모순: w2.id &gt; w1.id 조건이 붙어있는데, 위에서 이미 id가 같다고 설정했기 때문에 이 조건은 수학적으로 절대 만족할 수 없는 조건(x &gt; x)이 된다. 
그래서 결과가 나오지 않았던 것이다..</p>
</li>
</ol>
<p><strong>수정된 쿼리</strong></p>
<pre><code class="language-sql">SELECT w2.id
FROM Weather w1
JOIN Weather w2 
  ON DATEDIFF(w2.recordDate, w1.recordDate) = 1
WHERE w2.temperature &gt; w1.temperature;</code></pre>
<h1 id="511-game-play-analysis-i">511. Game Play Analysis I</h1>
<h3 id="문제설명-3">문제설명</h3>
<p>Write a solution to find the first login date for each player.</p>
<p>Return the result table in any order.</p>
<pre><code class="language-sql">SELECT player_id, MIN(event_date) AS first_login
FROM Activity
GROUP BY player_id;</code></pre>
<p>위의 문제를 풀기 위해서는 단순히 첫 로그인 날짜만을 필요로 하기 때문에 <code>MIN()</code>을 사용하는 것이 효율적인 쿼리이다. 하지만 다른 세부적인 정보가 필요할 때에는 <code>ROW_NUMBER()</code>를 활용하는 것이 더욱 효율적이다.</p>
<p>** GROUP BY + MIN(): &quot;요약&quot;이 필요할 때 **
이 방식은 ** &quot;데이터를 압축&quot; ** 하는 방식이다. 여러 개의 행을 하나의 행으로 뭉쳐서 통계 값을 뽑아낼 때 주로 사용합니다.</p>
<p>장점:  코드가 매우 짧고 직관적이다.
한계: &#39;첫 로그인 날짜&#39;는 알 수 있지만, 그날 어떤 기기(device_id)로 로그인했는지는 바로 알 수 없습니다. 만약 첫 로그인 날짜와 함께 그날의 기기 정보까지 가져오려면 다시 JOIN을 해야 하는 번거로움이 생긴다.</p>
<p>** ROW_NUMBER(): &quot;상세 내역&quot;이 필요할 때 **
이 방식은 &quot;전체 행을 유지하면서 순번만 매기는&quot; 방식입니다. 데이터의 원형을 보존하고 싶을 때 사용합니다.</p>
<p>용도: 첫 로그인 날짜뿐만 아니라, &quot;첫 로그인을 했을 때의 device_id나 games_played 정보도 함께 보고 싶을 때&quot; 사용합니다.</p>
<p>장점: 첫 번째 행을 고른 뒤, 그 행의 나머지 데이터(기기 정보 등)를 그대로 가져올 수 있다.</p>
<p>&quot;상위 3명&quot;, &quot;날짜별 순위&quot; 등 복잡한 조건(Top-N 문제)을 처리할 때 매우 강력!! </p>
<p>한계: GROUP BY보다는 문법이 조금 더 길고 복잡하다.</p>
<p><strong><code>ROW_NUMBER()</code></strong> 를 활용한 쿼리</p>
<p>```sql
SELECT player_id, event_date AS first_login
FROM (
    SELECT player_id, event_date,
           ROW_NUMBER() OVER(PARTITION BY player_id ORDER BY event_date ASC) as rn
    FROM Activity
) t
WHERE rn = 1;</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 파이프라인 심화: Batch/Streaming부터 Data Quality까지]]></title>
            <link>https://velog.io/@cheonroro_/%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8B%AC%ED%99%94-BatchStreaming%EB%B6%80%ED%84%B0-Data-Quality%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@cheonroro_/%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8B%AC%ED%99%94-BatchStreaming%EB%B6%80%ED%84%B0-Data-Quality%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 21 Apr 2026 06:33:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 글에서 ETL/ELT 기본 개념을 정리했다면, 이번엔 실무에서 반드시 마주치는 핵심 개념들을 다룹니다.</p>
</blockquote>
<hr>
<h2 id="⚡-1-batch-vs-streaming-파이프라인">⚡ 1. Batch vs Streaming 파이프라인</h2>
<h3 id="batch-processing">Batch Processing</h3>
<p>데이터를 일정 시간 동안 모아서 <strong>한꺼번에 처리</strong>하는 방식입니다. 스케줄에 따라 주기적으로 실행됩니다.</p>
<pre><code>Source → [데이터 쌓임] → Batch Job 실행 (1시간/1일마다) → Target DW 적재</code></pre><h3 id="stream-processing">Stream Processing</h3>
<p>데이터가 발생하는 즉시 <strong>연속적으로 처리</strong>하는 방식입니다.</p>
<pre><code>Source → 이벤트 발생 → Stream Processor → 실시간 적재/알림</code></pre><h3 id="비교">비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>Batch</th>
<th>Streaming</th>
</tr>
</thead>
<tbody><tr>
<td>처리 단위</td>
<td>대량 (묶음)</td>
<td>이벤트 단위</td>
</tr>
<tr>
<td>지연(Latency)</td>
<td>분~시간</td>
<td>밀리초~초</td>
</tr>
<tr>
<td>처리량(Throughput)</td>
<td>높음</td>
<td>상대적으로 낮음</td>
</tr>
<tr>
<td>복잡도</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>대표 도구</td>
<td>Spark, dbt, Airflow</td>
<td>Kafka, Flink, Spark Streaming</td>
</tr>
<tr>
<td>적합한 사례</td>
<td>일일 리포트, 정산</td>
<td>실시간 알림, 사기 탐지</td>
</tr>
</tbody></table>
<hr>
<h2 id="🏗️-2-lambda--kappa-아키텍처">🏗️ 2. Lambda / Kappa 아키텍처</h2>
<h3 id="lambda-아키텍처">Lambda 아키텍처</h3>
<p>Batch와 Streaming을 <strong>병렬로 운영</strong>해 정확성과 실시간성을 동시에 확보하는 아키텍처입니다.</p>
<pre><code>Data Source
    ├── Batch Layer    → Batch View   ─┐
    │   (Hadoop/Spark, 높은 정확도)    ├→ Serving Layer → 쿼리 응답
    └── Speed Layer   → Real-time View┘
        (Kafka/Flink, 낮은 지연)</code></pre><ul>
<li><strong>Batch Layer</strong>: 전체 데이터를 주기적으로 재처리. 정확하지만 느림.</li>
<li><strong>Speed Layer</strong>: 최근 데이터만 실시간 처리. 빠르지만 완전하지 않음.</li>
<li><strong>Serving Layer</strong>: 두 결과를 병합해 쿼리 응답.</li>
</ul>
<blockquote>
<p>⚠️ 단점: 두 레이어를 모두 유지해야 해서 복잡도가 높고, 같은 처리 로직을 두 번 작성해야 합니다.</p>
</blockquote>
<h3 id="kappa-아키텍처">Kappa 아키텍처</h3>
<p>Lambda의 복잡성을 제거하고 <strong>스트리밍 단일 경로</strong>만 사용하는 아키텍처입니다.</p>
<pre><code>Data Source
    → Append-only Event Log (Kafka)
    → Stream Processor (Flink / Spark Streaming)
    → Serving Layer</code></pre><p>모든 데이터를 이벤트 로그로 영구 보관하고, 재처리가 필요할 때는 처음부터 다시 읽습니다.</p>
<blockquote>
<p>✅ 장점: 아키텍처가 단순하고 운영이 쉬움. 단, 스트리밍 처리 기술에 대한 높은 역량이 필요합니다.</p>
</blockquote>
<hr>
<h2 id="🔁-3-멱등성-idempotency과-재실행-가능한-파이프라인">🔁 3. 멱등성 (Idempotency)과 재실행 가능한 파이프라인</h2>
<p><strong>멱등성</strong>이란 동일한 작업을 여러 번 실행해도 결과가 항상 동일한 성질입니다.</p>
<p>파이프라인 장애 후 재실행 시, 데이터 중복이나 오염 없이 안전하게 복구할 수 있어야 합니다.</p>
<h3 id="멱등성이-없는-경우-vs-있는-경우">멱등성이 없는 경우 vs 있는 경우</h3>
<pre><code>❌ 멱등성 없음:
  1회 실행 → 100행
  2회 실행 → 200행 (중복 누적!)
  3회 실행 → 300행 (집계 오류 발생)

✅ 멱등성 있음:
  1회 실행 → 100행
  2회 실행 → 100행 (동일)
  3회 실행 → 100행 (동일)</code></pre><h3 id="멱등성-구현-핵심-패턴">멱등성 구현 핵심 패턴</h3>
<table>
<thead>
<tr>
<th>패턴</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>INSERT → UPSERT</strong></td>
<td>중복 키가 있으면 UPDATE, 없으면 INSERT</td>
</tr>
<tr>
<td><strong>DELETE → INSERT</strong></td>
<td>먼저 기존 파티션 삭제 후 재삽입 (Overwrite)</td>
</tr>
<tr>
<td><strong>파티션 키 활용</strong></td>
<td>날짜/시간 기준 파티션을 통째로 교체</td>
</tr>
<tr>
<td><strong>고유 실행 ID</strong></td>
<td>job_id로 중복 실행 여부를 추적·방지</td>
</tr>
</tbody></table>
<hr>
<h2 id="📥-4-incremental-load-vs-full-refresh-vs-backfill">📥 4. Incremental Load vs Full Refresh vs Backfill</h2>
<h3 id="incremental-load-증분-적재">Incremental Load (증분 적재)</h3>
<p>마지막 실행 시점 이후 <strong>변경된 데이터만</strong> 추출해 적재합니다.</p>
<pre><code>[1월 ✓] [2월 ✓] [3월 ✓] [4월 ✓] [5월 NEW →처리]</code></pre><ul>
<li>✅ 빠른 처리 속도, 리소스 효율적</li>
<li>⚠️ 소스에서 변경 감지 필요, 삭제된 레코드 처리 어려움</li>
</ul>
<h3 id="full-refresh-전체-갱신">Full Refresh (전체 갱신)</h3>
<p>매번 전체 데이터를 삭제하고 소스에서 <strong>전부 다시 적재</strong>합니다.</p>
<pre><code>실행 1회차 → 전체 삭제 후 100% 재적재
실행 2회차 → 전체 삭제 후 100% 재적재
실행 3회차 → 전체 삭제 후 100% 재적재</code></pre><ul>
<li>✅ 구현이 단순, 데이터 일관성 보장</li>
<li>⚠️ 처리 시간·비용 증가, 대용량 테이블에 부적합</li>
</ul>
<h3 id="backfill-소급-처리">Backfill (소급 처리)</h3>
<p>파이프라인 신규 도입 또는 로직 변경 후, <strong>과거 특정 기간</strong>의 데이터를 재처리합니다.</p>
<pre><code>새 로직 적용 → [2024-01 🔄] [2024-02 🔄] [2024-03 🔄] [2024-04 🔄] [2024-05 현재]</code></pre><ul>
<li>✅ 로직 변경 시 과거 데이터 정합성 유지, 신규 컬럼 과거분 채우기</li>
<li>⚠️ 대규모 처리 비용 발생, <strong>멱등성 설계가 필수</strong></li>
</ul>
<hr>
<h2 id="✅-5-데이터-품질-data-quality-6대-원칙">✅ 5. 데이터 품질 (Data Quality) 6대 원칙</h2>
<p>데이터 품질은 파이프라인 신뢰성의 핵심입니다. 6가지 차원으로 측정하고 관리합니다.</p>
<table>
<thead>
<tr>
<th>차원</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Completeness (완전성)</strong></td>
<td>필요한 데이터가 누락 없이 존재하는가</td>
<td>email 컬럼에 NULL이 없는가</td>
</tr>
<tr>
<td><strong>Accuracy (정확성)</strong></td>
<td>데이터가 실제 값을 정확히 반영하는가</td>
<td>상품 가격이 실제 판매 가격과 일치하는가</td>
</tr>
<tr>
<td><strong>Consistency (일관성)</strong></td>
<td>여러 시스템 간 데이터가 서로 일치하는가</td>
<td>CRM과 DW의 고객 수가 동일한가</td>
</tr>
<tr>
<td><strong>Timeliness (적시성)</strong></td>
<td>데이터가 필요한 시점에 최신 상태인가</td>
<td>일일 리포트가 오전 9시 전에 갱신되는가</td>
</tr>
<tr>
<td><strong>Uniqueness (유일성)</strong></td>
<td>중복 레코드 없이 고유한 데이터인가</td>
<td>user_id가 중복 없이 유일한가</td>
</tr>
<tr>
<td><strong>Validity (유효성)</strong></td>
<td>데이터가 정해진 형식·범위를 따르는가</td>
<td>나이 컬럼이 0~120 사이 값인가</td>
</tr>
</tbody></table>
<h3 id="파이프라인에서-품질-체크-적용-위치">파이프라인에서 품질 체크 적용 위치</h3>
<pre><code>Source → [수집 시 검증] → Staging → [변환 전 검증] → Transform → [변환 후 검증] → Target</code></pre><blockquote>
<p>🛠️ 대표 도구: <strong>Great Expectations</strong>, <strong>dbt tests</strong>, <strong>Soda</strong><br>파이프라인 각 단계에서 자동 품질 체크를 실행하고, 실패 시 알림을 보냅니다.</p>
</blockquote>
<hr>
<h2 id="📝-정리">📝 정리</h2>
<ul>
<li><strong>Batch</strong>: 주기적 대량 처리 / <strong>Streaming</strong>: 실시간 이벤트 처리</li>
<li><strong>Lambda</strong>: Batch + Stream 병렬 운영 (복잡) / <strong>Kappa</strong>: Stream 단일 경로 (단순)</li>
<li><strong>멱등성</strong>: 몇 번을 실행해도 같은 결과 → 안전한 재실행의 핵심</li>
<li><strong>Incremental</strong>: 변경분만 / <strong>Full Refresh</strong>: 전체 재적재 / <strong>Backfill</strong>: 과거 소급 처리</li>
<li><strong>Data Quality</strong>: 완전성·정확성·일관성·적시성·유일성·유효성 6가지로 측정</li>
</ul>
<hr>
<blockquote>
<p>이전 글: <a href="#">데이터 파이프라인 완전 정복: ETL vs ELT, 뭐가 다를까?</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 파이프라인 완전 정복: ETL vs ELT, 뭐가 다를까?]]></title>
            <link>https://velog.io/@cheonroro_/%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-ETL-vs-ELT-%EB%AD%90%EA%B0%80-%EB%8B%A4%EB%A5%BC%EA%B9%8C</link>
            <guid>https://velog.io/@cheonroro_/%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-ETL-vs-ELT-%EB%AD%90%EA%B0%80-%EB%8B%A4%EB%A5%BC%EA%B9%8C</guid>
            <pubDate>Tue, 21 Apr 2026 05:48:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>데이터 엔지니어링을 처음 공부하다 보면 ETL, ELT라는 단어를 자주 마주치게 됩니다.<br>이 글에서는 데이터 파이프라인의 기본 구조부터 ETL/ELT의 차이와 선택 기준까지 정리해봤습니다.</p>
</blockquote>
<hr>
<h2 id="📦-데이터-파이프라인이란">📦 데이터 파이프라인이란?</h2>
<p>데이터 파이프라인은 데이터가 흘러가는 경로입니다. 크게 세 단계로 이루어집니다.</p>
<pre><code>Source → Staging → Target</code></pre><table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Source</strong></td>
<td>데이터가 발생하는 원천 시스템 (CRM, IoT, 소셜 미디어 등)</td>
</tr>
<tr>
<td><strong>Staging</strong></td>
<td>추출된 데이터를 임시로 보관하는 중간 영역 (랜딩 존)</td>
</tr>
<tr>
<td><strong>Target</strong></td>
<td>분석에 활용되는 최종 데이터 웨어하우스</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔄-etl이란">🔄 ETL이란?</h2>
<p><strong>ETL(Extract, Transform, Load)</strong> 은 다양한 소스의 데이터를 <strong>추출(Extract)</strong> 하고, <strong>변환(Transform)</strong> 한 뒤, 데이터 웨어하우스에 <strong>적재(Load)</strong> 하는 프로세스입니다.</p>
<p>단순한 데이터 이동이 아니라, 비즈니스 규칙을 적용해 데이터를 정리·구조화함으로써 분석, BI, ML에 바로 활용 가능한 상태로 만드는 것이 핵심입니다.</p>
<h3 id="etl이-필요한-이유">ETL이 필요한 이유</h3>
<p>기업 데이터는 수많은 소스에 정형·비정형으로 분산되어 있습니다.</p>
<ul>
<li>온라인 결제 및 <strong>CRM 시스템</strong>의 고객 데이터</li>
<li>공급업체 시스템의 <strong>재고 및 운영 데이터</strong></li>
<li><strong>IoT 디바이스</strong>의 센서 데이터</li>
<li>소셜 미디어의 <strong>마케팅 데이터</strong></li>
<li>내부 <strong>HR 시스템</strong>의 직원 데이터</li>
</ul>
<p>ETL은 이 데이터들을 하나의 통합된 뷰로 만들어 의미 있는 인사이트를 뽑아낼 수 있게 해줍니다.</p>
<hr>
<h2 id="⚙️-etl은-어떻게-작동하나요">⚙️ ETL은 어떻게 작동하나요?</h2>
<h3 id="1단계--추출-extract">1단계 — 추출 (Extract)</h3>
<p>소스에서 원시 데이터를 복사해 <strong>스테이징 영역(랜딩 존)</strong> 에 임시 저장합니다.</p>
<p>추출 방식은 세 가지입니다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>업데이트 알림</strong></td>
<td>소스 시스템이 변경 시 즉시 알림 → 해당 변경분만 추출</td>
</tr>
<tr>
<td><strong>증분 추출</strong></td>
<td>주기적으로 변경된 데이터만 선별해 추출 (주 1회, 월 1회 등)</td>
</tr>
<tr>
<td><strong>전체 추출</strong></td>
<td>변경 감지 불가 시 전체 데이터 재로드 (소규모 테이블에만 권장)</td>
</tr>
</tbody></table>
<h3 id="2단계--변환-transform">2단계 — 변환 (Transform)</h3>
<p>추출된 데이터를 분석에 적합한 형태로 가공합니다.</p>
<p><strong>기본 변환</strong></p>
<ul>
<li>오류 제거 및 데이터 정리</li>
<li>중복 레코드 제거</li>
<li>날짜·단위·문자셋 형식 통일</li>
</ul>
<p><strong>고급 변환 (비즈니스 규칙 적용)</strong></p>
<table>
<thead>
<tr>
<th>유형</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>파생</strong></td>
<td>수량 × 단가 → 총 구매액 계산</td>
</tr>
<tr>
<td><strong>결합</strong></td>
<td>여러 공급업체 구매가 합산</td>
</tr>
<tr>
<td><strong>분할</strong></td>
<td><code>&quot;Jane John Doe&quot;</code> → 이름 / 중간 이름 / 성 분리</td>
</tr>
<tr>
<td><strong>요약</strong></td>
<td>개별 주문 금액 → 고객 평생 가치(CLV) 집계</td>
</tr>
<tr>
<td><strong>암호화</strong></td>
<td>민감 데이터 보호 및 컴플라이언스 대응</td>
</tr>
</tbody></table>
<h3 id="3단계--적재-load">3단계 — 적재 (Load)</h3>
<p>변환된 데이터를 대상 데이터 웨어하우스에 올립니다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>전체 로드</strong></td>
<td>최초 구축 시 전체 데이터를 한 번에 적재</td>
</tr>
<tr>
<td><strong>증분 로드 (스트리밍)</strong></td>
<td>소량 데이터를 파이프라인으로 실시간 스트리밍</td>
</tr>
<tr>
<td><strong>증분 로드 (배치)</strong></td>
<td>대용량 데이터를 주기적으로 배치 단위로 적재</td>
</tr>
</tbody></table>
<hr>
<h2 id="☁️-elt란-그리고-왜-대세가-됐나">☁️ ELT란? 그리고 왜 대세가 됐나?</h2>
<p><strong>ELT(Extract, Load, Transform)</strong> 는 변환을 적재 이후에 수행하는 방식입니다.</p>
<pre><code>ETL:  Source → Extract → Staging → Transform → Load → Target
ELT:  Source → Extract → Staging → Load → Target → Transform</code></pre><p>데이터를 일단 클라우드 데이터 웨어하우스(BigQuery, Snowflake 등)에 <strong>원시 상태로 올려두고</strong>, 필요할 때 쿼리로 변환합니다.</p>
<h3 id="elt가-주류가-된-이유">ELT가 주류가 된 이유</h3>
<p>클라우드 DW의 발전 덕분입니다.</p>
<ul>
<li><strong>스토리지 비용</strong> 이 크게 낮아졌고</li>
<li><strong>스케일아웃 쿼리 엔진</strong> 이 대용량 변환을 빠르게 처리할 수 있게 됨</li>
<li>굳이 적재 전에 무거운 변환 작업을 할 필요가 없어졌습니다</li>
</ul>
<hr>
<h2 id="🤔-etl-vs-elt-언제-뭘-쓸까">🤔 ETL vs ELT, 언제 뭘 쓸까?</h2>
<table>
<thead>
<tr>
<th>고려 요소</th>
<th>ETL 적합</th>
<th>ELT 적합</th>
</tr>
</thead>
<tbody><tr>
<td><strong>데이터 크기</strong></td>
<td>소~중규모</td>
<td>대규모</td>
</tr>
<tr>
<td><strong>민감 정보</strong></td>
<td>적재 전 마스킹 필요 시</td>
<td>클라우드 보안 신뢰 시</td>
</tr>
<tr>
<td><strong>스키마 변경 빈도</strong></td>
<td>안정적·고정적</td>
<td>자주 바뀌는 경우</td>
</tr>
<tr>
<td><strong>팀 역량</strong></td>
<td>ETL 툴 전문가 보유</td>
<td>SQL / 클라우드 역량 보유</td>
</tr>
<tr>
<td><strong>인프라</strong></td>
<td>온프레미스 / 레거시</td>
<td>클라우드 네이티브</td>
</tr>
</tbody></table>
<hr>
<h2 id="📝-정리">📝 정리</h2>
<ul>
<li><strong>ETL</strong>: 변환 먼저, 적재 나중. 정제된 데이터만 웨어하우스에 들어갑니다.</li>
<li><strong>ELT</strong>: 적재 먼저, 변환 나중. 클라우드의 강력한 컴퓨팅 파워를 활용합니다.</li>
<li>현재 대부분의 현대적인 데이터 스택은 <strong>ELT</strong> 방식을 채택하고 있으며, dbt 같은 도구가 Target 내부의 Transform을 담당합니다.</li>
</ul>
<hr>
<blockquote>
<p>참고: <a href="https://aws.amazon.com/ko/what-is/etl/">AWS - ETL이란 무엇인가요?</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 Grouping & Aggregation (#1280, #1045, #608, #1341)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-Grouping-Aggregation</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-Grouping-Aggregation</guid>
            <pubDate>Thu, 16 Apr 2026 08:29:44 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-오늘의-문제-목록">📋 오늘의 문제 목록</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>문제</th>
<th>난이도</th>
<th>핵심 개념</th>
</tr>
</thead>
<tbody><tr>
<td>1280</td>
<td>Students and Examinations</td>
<td>Easy</td>
<td>CROSS JOIN, LEFT JOIN, COUNT(DISTINCT)</td>
</tr>
<tr>
<td>1045</td>
<td>Customers Who Bought All Products</td>
<td>Medium</td>
<td>HAVING COUNT(DISTINCT) = 전체 COUNT</td>
</tr>
<tr>
<td>608</td>
<td>Tree Node</td>
<td>Medium</td>
<td>CASE WHEN, IN + 서브쿼리, NULL 체크</td>
</tr>
<tr>
<td>1341</td>
<td>Movie Ratings</td>
<td>Medium</td>
<td>UNION ALL, 다중 정렬, AVG</td>
</tr>
</tbody></table>
<hr>
<h2 id="🟢-1280-students-and-examinations">🟢 1280. Students and Examinations</h2>
<h3 id="문제-요약">문제 요약</h3>
<p>모든 학생이 모든 과목 시험에 몇 번 참석했는지 반환한다. 한 번도 안 본 조합도 <strong>0으로 출력</strong>해야 한다.</p>
<h3 id="💡-핵심-개념---cross-join">💡 핵심 개념 - CROSS JOIN</h3>
<p>두 테이블의 <strong>모든 조합</strong>을 생성하는 JOIN. 조건(ON) 없이 사용한다.</p>
<pre><code class="language-sql">-- Students 2명 × Subjects 3개 = 6가지 조합 생성
SELECT s.student_id, s.student_name, sub.subject_name
FROM Students s
CROSS JOIN Subjects sub</code></pre>
<p>시험을 안 본 조합도 0으로 출력해야 하므로:</p>
<ul>
<li><strong>CROSS JOIN</strong> → 모든 학생-과목 조합 생성</li>
<li><strong>LEFT JOIN</strong> → Examinations와 연결, 없으면 NULL</li>
<li><strong>COUNT(e.student_id)</strong> → NULL은 카운트 안 함 → 자동으로 0 반환</li>
</ul>
<pre><code class="language-sql">COUNT(e.student_id)  -- ✅ NULL 제외 → 0 반환
COUNT(*)             -- ❌ NULL도 카운트 → 1 반환</code></pre>
<h3 id="✅-최종-풀이">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT
    s.student_id,
    s.student_name,
    sub.subject_name,
    COUNT(e.student_id) AS attended_exams
FROM Students s
CROSS JOIN Subjects sub
LEFT JOIN Examinations e
    ON s.student_id = e.student_id
    AND sub.subject_name = e.subject_name
GROUP BY s.student_id, s.student_name, sub.subject_name
ORDER BY s.student_id, sub.subject_name</code></pre>
<h3 id="📝-배운-점">📝 배운 점</h3>
<ul>
<li><strong>CROSS JOIN</strong> : 모든 조합 생성, 0을 포함한 집계 문제에서 활용</li>
<li>LEFT JOIN 후 COUNT 할 때 <code>COUNT(*)</code> 대신 <strong><code>COUNT(특정 컬럼)</code></strong> 사용해야 NULL 제외됨</li>
<li>&quot;모든 조합을 보여줘야 할 때&quot; → CROSS JOIN + LEFT JOIN 패턴 기억하기</li>
</ul>
<hr>
<h2 id="🟡-1045-customers-who-bought-all-products">🟡 1045. Customers Who Bought All Products</h2>
<h3 id="문제-요약-1">문제 요약</h3>
<p>Product 테이블의 <strong>모든 제품</strong>을 구매한 customer_id를 반환한다.</p>
<h3 id="💡-핵심-개념---having-countdistinct--전체-count">💡 핵심 개념 - HAVING COUNT(DISTINCT) = 전체 COUNT</h3>
<p>특정 고객이 구매한 <strong>distinct 제품 수</strong>가 전체 제품 수와 같으면 모든 제품을 산 것.</p>
<pre><code class="language-sql">HAVING COUNT(DISTINCT p.product_key) = (SELECT COUNT(*) FROM Product)</code></pre>
<h3 id="처음-접근-오답">처음 접근 (오답)</h3>
<pre><code class="language-sql">-- ❌ EXISTS 방식 - 하나라도 일치하면 통과 → &quot;모든 제품&quot; 조건 충족 불가
SELECT DISTINCT c.customer_id
FROM Customer c
WHERE EXISTS (
    SELECT 1 FROM Product p
    WHERE c.product_key = p.product_key
);</code></pre>
<p>EXISTS는 조건을 하나라도 만족하면 TRUE를 반환하므로 &quot;모든 제품&quot;을 검증할 수 없다.</p>
<h3 id="✅-최종-풀이-1">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT c.customer_id
FROM Customer c
JOIN Product p
ON c.product_key = p.product_key
GROUP BY c.customer_id
HAVING COUNT(DISTINCT p.product_key) = (SELECT COUNT(*) FROM Product)</code></pre>
<h3 id="📝-배운-점-1">📝 배운 점</h3>
<ul>
<li><strong>&quot;모든 ~를 포함하는&quot;</strong> 조건 → <code>HAVING COUNT(DISTINCT) = (SELECT COUNT(*) FROM 전체테이블)</code> 패턴</li>
<li>EXISTS는 &quot;하나라도 존재하면&quot; 조건, 전체 포함 여부 검증에는 부적합</li>
<li>중복 구매 가능성이 있으므로 <code>COUNT(DISTINCT)</code>로 중복 제거 필수</li>
</ul>
<hr>
<h2 id="🟡-608-tree-node">🟡 608. Tree Node</h2>
<h3 id="문제-요약-2">문제 요약</h3>
<p>Tree 테이블에서 각 노드의 타입을 반환한다.</p>
<ul>
<li><strong>Root</strong> : p_id가 NULL (부모 없음)</li>
<li><strong>Inner</strong> : 자식이 있는 노드 (다른 노드의 p_id로 등장)</li>
<li><strong>Leaf</strong> : 자식이 없는 노드 (나머지)</li>
</ul>
<h3 id="💡-핵심-개념---case-when--in-서브쿼리">💡 핵심 개념 - CASE WHEN + IN 서브쿼리</h3>
<p>조건 분기 로직을 CASE WHEN으로, 자식 존재 여부를 IN 서브쿼리로 확인한다.</p>
<pre><code class="language-sql">-- 자식이 있는 노드 = 다른 노드의 p_id 목록에 내 id가 있는 경우
WHEN id IN (SELECT p_id FROM Tree WHERE p_id IS NOT NULL) THEN &#39;Inner&#39;</code></pre>
<blockquote>
<p><strong>주의</strong>: <code>WHERE p_id IS NOT NULL</code>을 빠뜨리면 서브쿼리 결과에 NULL이 포함되어
<code>NOT IN</code> 사용 시 전체 결과가 빈값이 될 수 있음.</p>
</blockquote>
<h3 id="✅-최종-풀이-2">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT id,
    CASE
        WHEN p_id IS NULL
            THEN &#39;Root&#39;
        WHEN id IN (SELECT p_id FROM Tree WHERE p_id IS NOT NULL)
            THEN &#39;Inner&#39;
        ELSE &#39;Leaf&#39;
    END AS type
FROM Tree</code></pre>
<h3 id="📝-배운-점-2">📝 배운 점</h3>
<ul>
<li>CASE WHEN은 위에서 아래로 순서대로 평가 → Root 먼저 걸러야 Inner/Leaf 구분 가능</li>
<li>IN 서브쿼리에 NULL이 포함되면 <code>NOT IN</code>이 항상 false → <strong><code>WHERE p_id IS NOT NULL</code></strong> 필수</li>
<li>트리 구조 문제는 &quot;부모 여부(p_id IS NULL)&quot;, &quot;자식 여부(id IN p_id 목록)&quot; 두 가지로 분류</li>
</ul>
<hr>
<h2 id="🟡-1341-movie-ratings">🟡 1341. Movie Ratings</h2>
<h3 id="문제-요약-3">문제 요약</h3>
<p>두 가지를 반환한다.</p>
<ol>
<li>가장 많은 영화를 평가한 유저 이름 (동점이면 사전순 앞)</li>
<li>2020년 2월 평균 평점이 가장 높은 영화 제목 (동점이면 사전순 앞)</li>
</ol>
<h3 id="💡-핵심-개념---union-all--다중-정렬">💡 핵심 개념 - UNION ALL + 다중 정렬</h3>
<p>성격이 다른 두 결과를 하나로 합칠 때 UNION ALL을 사용한다.</p>
<pre><code class="language-sql">-- UNION    : 중복 제거 (느림)
-- UNION ALL : 중복 유지 (빠름) ← 두 결과가 애초에 다른 종류이므로 UNION ALL이 적합</code></pre>
<p><strong>동점 처리</strong> → <code>ORDER BY 기준1 DESC, 기준2 ASC</code>로 우선순위 지정</p>
<pre><code class="language-sql">ORDER BY COUNT(mr.movie_id) DESC, u.name ASC
-- 평가 수 많은 순 → 같으면 이름 사전순</code></pre>
<h3 id="✅-최종-풀이-3">✅ 최종 풀이</h3>
<pre><code class="language-sql">-- 가장 많은 영화를 평가한 유저
(
    SELECT u.name AS results
    FROM Users u
    JOIN MovieRating mr ON u.user_id = mr.user_id
    GROUP BY mr.user_id
    ORDER BY COUNT(mr.movie_id) DESC, u.name ASC
    LIMIT 1
)
UNION ALL
-- 2020년 2월 평균 평점이 가장 높은 영화
(
    SELECT m.title AS results
    FROM MovieRating mr
    JOIN Movies m ON mr.movie_id = m.movie_id
    WHERE YEAR(mr.created_at) = 2020
      AND MONTH(mr.created_at) = 02
    GROUP BY mr.movie_id
    ORDER BY AVG(mr.rating) DESC, m.title ASC
    LIMIT 1
)</code></pre>
<h3 id="📝-배운-점-3">📝 배운 점</h3>
<ul>
<li><strong>UNION ALL</strong> : 성격이 다른 두 결과를 합칠 때, 중복 제거 불필요하면 UNION보다 빠름</li>
<li>동점 처리는 <code>ORDER BY 주기준 DESC, 보조기준 ASC</code> 패턴</li>
<li>각 서브쿼리를 <strong>괄호로 감싸야</strong> ORDER BY + LIMIT이 각각 적용됨</li>
</ul>
<hr>
<h2 id="🔑-오늘의-핵심-정리">🔑 오늘의 핵심 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>CROSS JOIN</td>
<td>두 테이블의 모든 조합 생성, 0 포함 집계 문제에 활용</td>
</tr>
<tr>
<td>COUNT(컬럼) vs COUNT(*)</td>
<td>LEFT JOIN 후 NULL 제외 카운트는 COUNT(컬럼)</td>
</tr>
<tr>
<td>HAVING COUNT(DISTINCT) = 전체수</td>
<td>&quot;모든 ~를 포함하는&quot; 조건 처리 패턴</td>
</tr>
<tr>
<td>CASE WHEN 순서</td>
<td>위에서 아래로 평가, 좁은 조건 먼저 작성</td>
</tr>
<tr>
<td>IN 서브쿼리 + NULL</td>
<td>서브쿼리에 NULL 포함 시 NOT IN 오작동 → IS NOT NULL 필수</td>
</tr>
<tr>
<td>UNION ALL</td>
<td>성격 다른 두 결과 합치기, UNION보다 빠름</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 - Filtering & Aggregation Operation Cabin (#586, #596, #1193, #1141, #1327)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-%ED%92%80%EC%9D%B4-%EC%A0%95%EB%A6%AC-Filtering-Aggregation-Operation-Cabin</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-%ED%92%80%EC%9D%B4-%EC%A0%95%EB%A6%AC-Filtering-Aggregation-Operation-Cabin</guid>
            <pubDate>Tue, 14 Apr 2026 08:20:20 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-오늘의-문제-목록">📋 오늘의 문제 목록</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>문제</th>
<th>난이도</th>
<th>핵심 개념</th>
</tr>
</thead>
<tbody><tr>
<td>586</td>
<td>Customer Placing the Largest Number of Orders</td>
<td>Easy</td>
<td>ORDER BY + LIMIT, EXPLAIN</td>
</tr>
<tr>
<td>596</td>
<td>Classes More Than 5 Students</td>
<td>Easy</td>
<td>HAVING vs WHERE</td>
</tr>
<tr>
<td>1193</td>
<td>Monthly Transactions I</td>
<td>Medium</td>
<td>조건부 집계, 쿼리 최적화</td>
</tr>
<tr>
<td>1141</td>
<td>User Activity for the Past 30 Days I</td>
<td>Easy</td>
<td>COUNT(DISTINCT), 날짜 범위</td>
</tr>
<tr>
<td>1327</td>
<td>List the Products Ordered in a Period</td>
<td>Easy</td>
<td>SQL 절 실행 순서, WHERE vs HAVING</td>
</tr>
</tbody></table>
<hr>
<h2 id="🟡-586-customer-placing-the-largest-number-of-orders">🟡 586. Customer Placing the Largest Number of Orders</h2>
<h3 id="문제-요약">문제 요약</h3>
<p>Orders 테이블에서 주문을 가장 많이 한 <strong>customer_number</strong>를 반환한다.</p>
<h3 id="💡-핵심-개념---order-by--limit-vs-서브쿼리">💡 핵심 개념 - ORDER BY + LIMIT vs 서브쿼리</h3>
<table>
<thead>
<tr>
<th>방법</th>
<th>테이블 스캔</th>
<th>성능</th>
</tr>
</thead>
<tbody><tr>
<td>ORDER BY COUNT(*) DESC LIMIT 1</td>
<td><strong>1회</strong></td>
<td>빠름</td>
</tr>
<tr>
<td>HAVING + 서브쿼리</td>
<td><strong>2회</strong></td>
<td>느림</td>
</tr>
</tbody></table>
<p>직관과 반대로 <strong>ORDER BY + LIMIT이 더 빠르다.</strong> 이유는:</p>
<ul>
<li>정렬 대상이 전체 row가 아닌 <strong>그룹화된 결과(k개)</strong></li>
<li>옵티마이저가 LIMIT 1을 만나면 전체 정렬 없이 최솟값/최댓값 탐색으로 최적화함</li>
</ul>
<h3 id="💡-explain으로-실행-계획-확인">💡 EXPLAIN으로 실행 계획 확인</h3>
<pre><code class="language-sql">EXPLAIN SELECT ...</code></pre>
<table>
<thead>
<tr>
<th>컬럼</th>
<th>의미</th>
<th>주목할 값</th>
</tr>
</thead>
<tbody><tr>
<td><code>type</code></td>
<td>테이블 접근 방식</td>
<td>ALL(풀스캔) → 인덱스 없을 때</td>
</tr>
<tr>
<td><code>rows</code></td>
<td>예상 읽을 행 수</td>
<td>작을수록 좋음</td>
</tr>
<tr>
<td><code>Extra</code></td>
<td>Using filesort, Using temporary</td>
<td>주의 신호</td>
</tr>
</tbody></table>
<pre><code>type 성능 순서: const &gt; ref &gt; range &gt; index &gt; ALL(최악)</code></pre><h3 id="✅-최종-풀이">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT customer_number
FROM (
    SELECT customer_number, COUNT(order_number) AS order_count
    FROM Orders
    GROUP BY customer_number
) AS cnt
ORDER BY order_count DESC
LIMIT 1</code></pre>
<h3 id="📝-배운-점">📝 배운 점</h3>
<ul>
<li>쿼리 효율성을 고민할 때 <strong>&quot;테이블을 몇 번 읽는가&quot;</strong> 를 먼저 따질 것</li>
<li>ORDER BY + LIMIT 1 패턴은 옵티마이저가 최솟값/최댓값 탐색으로 최적화함</li>
<li>EXPLAIN / EXPLAIN ANALYZE로 실제 실행 계획을 확인하는 습관 들이기</li>
</ul>
<hr>
<h2 id="🟢-596-classes-more-than-5-students">🟢 596. Classes More Than 5 Students</h2>
<h3 id="문제-요약-1">문제 요약</h3>
<p>Courses 테이블에서 수강생이 <strong>5명 이상</strong>인 수업명을 반환한다.</p>
<h3 id="💡-핵심-개념---where-vs-having">💡 핵심 개념 - WHERE vs HAVING</h3>
<table>
<thead>
<tr>
<th>절</th>
<th>실행 시점</th>
<th>집계 함수</th>
<th>사용 상황</th>
</tr>
</thead>
<tbody><tr>
<td><code>WHERE</code></td>
<td>GROUP BY 이전</td>
<td>❌ 불가</td>
<td>개별 행 필터링</td>
</tr>
<tr>
<td><code>HAVING</code></td>
<td>GROUP BY 이후</td>
<td>✅ 가능</td>
<td>그룹 결과 필터링</td>
</tr>
</tbody></table>
<h3 id="두-가지-풀이-비교">두 가지 풀이 비교</h3>
<pre><code class="language-sql">-- 서브쿼리 방식 ✅
SELECT class
FROM (
    SELECT class, COUNT(*) AS student_cnt
    FROM Courses
    GROUP BY class
) AS cnt
WHERE student_cnt &gt;= 5

-- HAVING 방식 ✅ (더 간결)
SELECT class
FROM Courses
GROUP BY class
HAVING COUNT(*) &gt;= 5</code></pre>
<p><strong>서브쿼리가 유리한 경우</strong> → 집계 결과를 여러 조건에서 재사용할 때</p>
<h3 id="✅-최종-풀이-1">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT class
FROM Courses
GROUP BY class
HAVING COUNT(*) &gt;= 5</code></pre>
<h3 id="📝-배운-점-1">📝 배운 점</h3>
<ul>
<li>간단한 집계 조건은 HAVING이 더 깔끔하고 성능도 좋음 (임시 테이블 생성 없음)</li>
<li>집계 결과에 이름을 붙여 재사용해야 할 때는 서브쿼리가 유리</li>
</ul>
<hr>
<h2 id="🔴-1193-monthly-transactions-i">🔴 1193. Monthly Transactions I</h2>
<h3 id="문제-요약-2">문제 요약</h3>
<p>월별/국가별로 전체 거래 수/금액, 승인된 거래 수/금액을 반환한다.</p>
<h3 id="💡-핵심-개념---조건부-집계-conditional-aggregation">💡 핵심 개념 - 조건부 집계 (Conditional Aggregation)</h3>
<p>같은 행을 조건에 따라 다르게 집계하는 기법. JOIN 없이 한 번에 해결 가능.</p>
<pre><code class="language-sql">-- 기본 패턴
SUM(CASE WHEN state = &#39;approved&#39; THEN 1 ELSE 0 END)      -- 승인 건수
SUM(CASE WHEN state = &#39;approved&#39; THEN amount ELSE 0 END)  -- 승인 금액

-- MySQL 최적화 버전
SUM(state = &#39;approved&#39;)                     -- Boolean 평가 (0/1)
SUM(IF(state = &#39;approved&#39;, amount, 0))       -- IF 함수</code></pre>
<h3 id="쿼리-성능-최적화">쿼리 성능 최적화</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>느린 버전</th>
<th>빠른 버전</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>날짜 포맷</td>
<td><code>DATE_FORMAT(date, &#39;%Y-%m&#39;)</code></td>
<td><code>LEFT(date, 7)</code></td>
<td>단순 문자열 연산</td>
</tr>
<tr>
<td>조건부 카운트</td>
<td><code>SUM(CASE WHEN ... THEN 1 ELSE 0 END)</code></td>
<td><code>SUM(state = &#39;approved&#39;)</code></td>
<td>Boolean 평가</td>
</tr>
<tr>
<td>조건부 합계</td>
<td><code>SUM(CASE WHEN ... THEN amount ELSE 0 END)</code></td>
<td><code>SUM(IF(..., amount, 0))</code></td>
<td>IF가 CASE보다 경량</td>
</tr>
</tbody></table>
<p><strong>실제 결과</strong>: 3818ms (하위 5%) → <strong>565ms (상위 40%)</strong> 개선</p>
<blockquote>
<p>실무 주의: <code>SUM(state = &#39;approved&#39;)</code>는 MySQL 전용 문법. 타 DB 호환이 필요하면 CASE WHEN 사용.</p>
</blockquote>
<h3 id="❌-처음-접근의-실수">❌ 처음 접근의 실수</h3>
<ul>
<li>CTE + JOIN 방식으로 approved 데이터를 분리 시도 → JOIN 후 approved 건만 남아 trans_count 오류</li>
<li>조건부 집계로 한 테이블에서 한 번에 해결 가능</li>
</ul>
<h3 id="✅-최종-풀이-2">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT
    LEFT(trans_date, 7) AS month,
    country,
    COUNT(*) AS trans_count,
    SUM(amount) AS trans_total_amount,
    SUM(state = &#39;approved&#39;) AS approved_count,
    SUM(IF(state = &#39;approved&#39;, amount, 0)) AS approved_total_amount
FROM Transactions
GROUP BY month, country</code></pre>
<h3 id="📝-배운-점-2">📝 배운 점</h3>
<ul>
<li><strong>조건부 집계</strong>는 JOIN 없이 한 테이블에서 다양한 조건의 집계를 동시에 처리</li>
<li><code>LEFT(date, 7)</code>가 <code>DATE_FORMAT</code>보다 빠름 (단순 문자열 연산)</li>
<li>실무에서는 DB 호환성을 고려해 CASE WHEN을 선호하는 팀도 많음</li>
</ul>
<hr>
<h2 id="🟡-1141-user-activity-for-the-past-30-days-i">🟡 1141. User Activity for the Past 30 Days I</h2>
<h3 id="문제-요약-3">문제 요약</h3>
<p>2019-07-27 기준 30일간(포함) <strong>일별 활성 유저 수</strong>를 반환한다.</p>
<h3 id="💡-핵심-개념---countdistinct">💡 핵심 개념 - COUNT(DISTINCT)</h3>
<p>테이블에 중복 행이 있을 수 있으므로 같은 날 여러 활동을 한 유저는 1명으로 세야 한다.</p>
<pre><code class="language-sql">COUNT(user_id)          -- ❌ 활동 수를 셈 (중복 포함)
COUNT(DISTINCT user_id) -- ✅ 유저 수를 셈 (중복 제거)</code></pre>
<h3 id="30일-날짜-범위-정확히-계산하기">30일 날짜 범위 정확히 계산하기</h3>
<pre><code>2019-07-27 기준 30일 → 2019-06-28 ~ 2019-07-27 (양 끝 포함)
= INTERVAL 29 DAY (30일이 아님!)</code></pre><pre><code class="language-sql">-- ❌ INTERVAL 1 MONTH = 2019-06-27~ → 31일
WHERE activity_date &gt;= DATE_SUB(&#39;2019-07-27&#39;, INTERVAL 1 MONTH)

-- ✅ 정확히 30일
WHERE activity_date &gt;= DATE_SUB(&#39;2019-07-27&#39;, INTERVAL 29 DAY)
  AND activity_date &lt;= &#39;2019-07-27&#39;</code></pre>
<h3 id="❌-처음-접근의-실수-1">❌ 처음 접근의 실수</h3>
<ol>
<li><code>GROUP BY user_id</code> → <code>GROUP BY activity_date</code> 로 수정 (날짜별 집계 필요)</li>
<li><code>INTERVAL 1 MONTH</code> → <code>INTERVAL 29 DAY</code> (30일 포함 범위)</li>
<li>상한 조건 <code>AND activity_date &lt;= &#39;2019-07-27&#39;</code> 누락</li>
</ol>
<h3 id="✅-최종-풀이-3">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT activity_date AS day,
    COUNT(DISTINCT user_id) AS active_users
FROM Activity
WHERE activity_date &gt;= DATE_SUB(&#39;2019-07-27&#39;, INTERVAL 29 DAY)
  AND activity_date &lt;= &#39;2019-07-27&#39;
GROUP BY activity_date</code></pre>
<h3 id="📝-배운-점-3">📝 배운 점</h3>
<ul>
<li>중복 데이터가 있는 테이블에서 유일한 값을 세려면 <strong>COUNT(DISTINCT)</strong> 사용</li>
<li>&quot;N일간&quot; 날짜 범위는 <strong>INTERVAL N-1 DAY</strong> (양 끝 포함 시)</li>
<li>GROUP BY 대상은 항상 &quot;무엇을 기준으로 집계하는가&quot;를 먼저 생각하기</li>
</ul>
<hr>
<h2 id="🟡-1327-list-the-products-ordered-in-a-period">🟡 1327. List the Products Ordered in a Period</h2>
<h3 id="문제-요약-4">문제 요약</h3>
<p>2020년 2월에 100개 이상 주문된 제품의 이름과 수량을 반환한다.</p>
<h3 id="💡-핵심-개념---sql-절-실행-순서">💡 핵심 개념 - SQL 절 실행 순서</h3>
<pre><code>FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY</code></pre><p>이 순서는 반드시 지켜야 한다. WHERE가 GROUP BY 뒤에 오면 <strong>문법 오류</strong>.</p>
<h3 id="where-vs-having-최종-정리">WHERE vs HAVING 최종 정리</h3>
<pre><code class="language-sql">WHERE  → GROUP BY 이전, 개별 행 필터링, 집계 함수 ❌
HAVING → GROUP BY 이후, 그룹 필터링,   집계 함수 ✅</code></pre>
<p>이 문제에서:</p>
<ul>
<li><code>LEFT(order_date, 7) = &#39;2020-02&#39;</code> → 개별 행 조건 → <strong>WHERE</strong></li>
<li><code>SUM(unit) &gt;= 100</code> → 집계 후 조건 → <strong>HAVING</strong></li>
</ul>
<h3 id="❌-처음-접근의-실수-2">❌ 처음 접근의 실수</h3>
<pre><code class="language-sql">-- ❌ 오류 쿼리
GROUP BY product_name
WHERE LEFT(o.order_date, 7) = &#39;2020-02&#39;   -- WHERE가 GROUP BY 뒤에 위치
  AND SUM(unit) &gt;= 100                     -- 집계 함수를 WHERE에 사용</code></pre>
<h3 id="✅-최종-풀이-4">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT p.product_name, SUM(o.unit) AS unit
FROM Products p
JOIN Orders o ON p.product_id = o.product_id
WHERE LEFT(o.order_date, 7) = &#39;2020-02&#39;
GROUP BY p.product_name
HAVING SUM(o.unit) &gt;= 100</code></pre>
<h3 id="📝-배운-점-4">📝 배운 점</h3>
<ul>
<li>SQL 절 순서 <strong>FROM → WHERE → GROUP BY → HAVING → SELECT</strong> 는 반드시 암기</li>
<li>날짜 필터는 WHERE, 집계 조건은 HAVING으로 명확히 구분</li>
<li>두 테이블 모두 매칭되는 데이터만 필요할 때는 INNER JOIN이 의도를 명확하게 표현</li>
</ul>
<hr>
<h2 id="🔑-오늘의-핵심-정리">🔑 오늘의 핵심 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>ORDER BY + LIMIT</td>
<td>서브쿼리보다 빠름 - 테이블 스캔 1회, 옵티마이저 최적화</td>
</tr>
<tr>
<td>EXPLAIN</td>
<td>실행 계획 확인 - type(접근 방식), rows(예상 행 수) 주목</td>
</tr>
<tr>
<td>HAVING</td>
<td>GROUP BY 이후 집계 조건 필터링, 집계 함수 사용 가능</td>
</tr>
<tr>
<td>조건부 집계</td>
<td>CASE WHEN을 SUM 안에 넣어 JOIN 없이 다중 조건 집계</td>
</tr>
<tr>
<td>COUNT(DISTINCT)</td>
<td>중복 제거 후 카운트</td>
</tr>
<tr>
<td>SQL 절 실행 순서</td>
<td>FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] LeetCode 문제풀이 - SQL Basic Query Workstation (#175, #176, #181, #584, #1978)]]></title>
            <link>https://velog.io/@cheonroro_/SQL-LeetCode-%ED%92%80%EC%9D%B4-%EC%A0%95%EB%A6%AC-JOIN-Window-Function-NULL-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@cheonroro_/SQL-LeetCode-%ED%92%80%EC%9D%B4-%EC%A0%95%EB%A6%AC-JOIN-Window-Function-NULL-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Mon, 13 Apr 2026 05:32:34 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-오늘의-문제-목록">📋 오늘의 문제 목록</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>문제</th>
<th>난이도</th>
<th>핵심 개념</th>
</tr>
</thead>
<tbody><tr>
<td>175</td>
<td>Combine Two Tables</td>
<td>Easy</td>
<td>LEFT JOIN</td>
</tr>
<tr>
<td>176</td>
<td>Second Highest Salary</td>
<td>Medium</td>
<td>DENSE_RANK, NULL 반환</td>
</tr>
<tr>
<td>181</td>
<td>Employees Earning More Than Their Managers</td>
<td>Easy</td>
<td>Self Join</td>
</tr>
<tr>
<td>584</td>
<td>Find Customer Referee</td>
<td>Easy</td>
<td>NULL 비교 연산</td>
</tr>
<tr>
<td>1978</td>
<td>Employees Whose Manager Left the Company</td>
<td>Easy</td>
<td>NOT IN + 서브쿼리</td>
</tr>
</tbody></table>
<hr>
<h2 id="🟢-175-combine-two-tables">🟢 175. Combine Two Tables</h2>
<h3 id="문제-요약">문제 요약</h3>
<p>Person 테이블의 <strong>모든 사람</strong>에 대해 이름과 주소를 반환한다. Address 테이블에 주소가 없으면 <code>NULL</code>을 반환한다.</p>
<h3 id="💡-핵심-개념---join의-종류">💡 핵심 개념 - JOIN의 종류</h3>
<table>
<thead>
<tr>
<th>JOIN 종류</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>INNER JOIN</code></td>
<td>양쪽 테이블 모두에 매칭되는 행만 반환</td>
</tr>
<tr>
<td><code>LEFT JOIN</code></td>
<td>왼쪽 테이블은 전부 반환, 오른쪽에 없으면 NULL</td>
</tr>
<tr>
<td><code>RIGHT JOIN</code></td>
<td>오른쪽 테이블은 전부 반환, 왼쪽에 없으면 NULL</td>
</tr>
</tbody></table>
<h3 id="❌-틀렸던-이유">❌ 틀렸던 이유</h3>
<ul>
<li>INNER JOIN을 쓰면 주소가 없는 사람이 결과에서 탈락해버림</li>
<li>항상 &quot;어느 테이블의 모든 행을 보존해야 하는가&quot;를 먼저 생각할 것</li>
</ul>
<h3 id="✅-최종-풀이">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT p.firstName, p.lastName, a.city, a.state
FROM Person p
LEFT JOIN Address a ON p.personId = a.personId</code></pre>
<hr>
<h2 id="🟡-176-second-highest-salary">🟡 176. Second Highest Salary</h2>
<h3 id="문제-요약-1">문제 요약</h3>
<p>Employee 테이블에서 두 번째로 높은 distinct salary를 반환한다. 없으면 NULL 반환.</p>
<h3 id="💡-핵심-개념---rank-vs-dense_rank">💡 핵심 개념 - RANK() vs DENSE_RANK()</h3>
<table>
<thead>
<tr>
<th>salary</th>
<th>RANK()</th>
<th>DENSE_RANK()</th>
</tr>
</thead>
<tbody><tr>
<td>300</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>300</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>200</td>
<td>3</td>
<td>2</td>
</tr>
</tbody></table>
<ul>
<li>RANK() → 동점 시 다음 순위를 건너뜀 (1,1,3...)</li>
<li>DENSE_RANK() → 동점 시 순위를 연속으로 매김 (1,1,2...)</li>
</ul>
<h3 id="❌-틀렸던-이유-1">❌ 틀렸던 이유</h3>
<ol>
<li>Window 함수를 같은 SELECT 절에서 바로 사용 → 서브쿼리 분리 필요</li>
<li>RANK() 사용 → DENSE_RANK()로 변경</li>
<li>NULL 반환 처리 누락 → MAX()로 감싸기</li>
</ol>
<h3 id="✅-최종-풀이-1">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT MAX(salary) AS SecondHighestSalary
FROM (
    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM Employee
) ranked
WHERE rnk = 2</code></pre>
<hr>
<h2 id="🟡-181-employees-earning-more-than-their-managers">🟡 181. Employees Earning More Than Their Managers</h2>
<h3 id="문제-요약-2">문제 요약</h3>
<p>자신의 매니저보다 급여가 높은 직원의 이름을 반환한다.</p>
<h3 id="💡-핵심-개념---self-join">💡 핵심 개념 - Self Join</h3>
<p>같은 테이블을 두 개인 것처럼 alias를 다르게 줘서 JOIN하는 기법.</p>
<ul>
<li>ON → 두 테이블을 어떻게 연결할 것인가 (관계 정의)</li>
<li>WHERE → 연결된 결과에서 어떤 행을 필터링할 것인가 (조건 적용)</li>
</ul>
<h3 id="✅-최종-풀이-2">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT e.name AS Employee
FROM Employee e
JOIN Employee m ON e.managerId = m.id
WHERE e.salary &gt; m.salary</code></pre>
<hr>
<h2 id="🟢-584-find-customer-referee">🟢 584. Find Customer Referee</h2>
<h3 id="문제-요약-3">문제 요약</h3>
<p>referee_id가 2가 아니거나, 추천인이 없는(NULL) 고객의 이름을 반환한다.</p>
<h3 id="💡-핵심-개념---null과의-비교-연산">💡 핵심 개념 - NULL과의 비교 연산</h3>
<p>NULL과의 모든 비교 연산은 TRUE/FALSE가 아닌 NULL(unknown)을 반환한다.</p>
<ul>
<li>referee_id != 2 에서 NULL인 행 → 결과가 NULL → WHERE 조건 탈락!</li>
<li>반드시 IS NULL로 명시적으로 체크해야 한다.</li>
</ul>
<h3 id="❌-틀렸던-이유-2">❌ 틀렸던 이유</h3>
<ol>
<li>불필요한 Self Join 시도</li>
<li>NULL 처리 누락</li>
<li>IS NOT IN → 없는 문법, NOT IN이 올바름</li>
</ol>
<h3 id="✅-최종-풀이-3">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT name
FROM Customer
WHERE referee_id != 2 OR referee_id IS NULL</code></pre>
<hr>
<h2 id="🟡-1978-employees-whose-manager-left-the-company">🟡 1978. Employees Whose Manager Left the Company</h2>
<h3 id="문제-요약-4">문제 요약</h3>
<p>급여가 $30,000 미만이면서 매니저가 회사를 떠난 직원의 ID를 반환한다.</p>
<h3 id="💡-핵심-개념---not-in--서브쿼리">💡 핵심 개념 - NOT IN + 서브쿼리</h3>
<table>
<thead>
<tr>
<th>manager_id</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>NULL</td>
<td>처음부터 매니저 없음 (최상위 직원)</td>
</tr>
<tr>
<td>존재하지 않는 ID</td>
<td>매니저가 퇴사함 ← 우리가 찾는 경우</td>
</tr>
</tbody></table>
<h3 id="❌-틀렸던-이유-3">❌ 틀렸던 이유</h3>
<ol>
<li>IS NOT IN 문법 오류 → NOT IN으로 수정</li>
<li>&lt;= 30000 → strictly less than이므로 &lt; 30000</li>
<li>manager_id IS NOT NULL 누락</li>
</ol>
<h3 id="✅-최종-풀이-4">✅ 최종 풀이</h3>
<pre><code class="language-sql">SELECT employee_id
FROM Employees
WHERE salary &lt; 30000
  AND manager_id IS NOT NULL
  AND manager_id NOT IN (SELECT employee_id FROM Employees)
ORDER BY employee_id</code></pre>
<hr>
<h2 id="🔑-오늘의-핵심-정리">🔑 오늘의 핵심 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>LEFT JOIN</td>
<td>기준 테이블의 모든 행 보존, 매칭 없으면 NULL</td>
</tr>
<tr>
<td>Self Join</td>
<td>같은 테이블을 두 번 사용해 계층 관계 비교</td>
</tr>
<tr>
<td>DENSE_RANK</td>
<td>동점자가 있어도 순위를 연속으로 매김</td>
</tr>
<tr>
<td>NULL 비교</td>
<td>= NULL ❌ → IS NULL ✅</td>
</tr>
<tr>
<td>NOT IN</td>
<td>IS NOT IN ❌ → NOT IN ✅, NULL 포함 시 주의</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] Windows Functions 정리]]></title>
            <link>https://velog.io/@cheonroro_/%EC%9C%88%EB%8F%84%EC%9A%B0%ED%95%A8%EC%88%98-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@cheonroro_/%EC%9C%88%EB%8F%84%EC%9A%B0%ED%95%A8%EC%88%98-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 29 Oct 2025 10:39:08 GMT</pubDate>
            <description><![CDATA[<h2 id="1-윈도우-함수-windowns-functions">1. 윈도우 함수 (Windowns Functions)</h2>
<p>데이터를 집계하거나 순위를 매길 때 <code>GROUP BY</code>를 사용하지 않고 
개별 행의 속성을 유지하면서 계산할 수 있게 해주는 기능</p>
<p>일반적인 집계 함수(<code>SUM</code>, <code>AVG</code> 등)는 <code>GROUP BY</code> 절과 함께 쓰여 여러 행을 <em>하나의 행</em>으로 압축(Collapse)한다.</p>
<p>하지만 윈도우 함수는 <strong>행을 압축하지 않는다.</strong> 
대신, 쿼리 결과의 각 행에 대해 <strong>윈도우</strong>라고 불리는 특정 행 집합을 기준으로 계산한 값을 <strong>추가 열</strong>로 보여준다.</p>
<p><strong>비유:</strong> 반 학생들의 &#39;반 평균&#39; 점수를 알고 싶을 때,</p>
<ul>
<li><code>GROUP BY</code>: &#39;A반&#39;의 평균 점수 70점&#39;이라는 <em>한 줄의 결과</em>만 보여준다.</li>
<li><strong>윈도우 함수:</strong> &#39;철수 (A반)&#39;의 점수 80점, &#39;A반 평균&#39; 70점 / &#39;영희 (A반)&#39;의 점수 60점, &#39;A반 평균&#39; 70점... 처럼 <em>모든 학생 목록</em>을 보여주면서 각 학생 옆에 반 평균을 같이 보여준다.</li>
</ul>
<hr>
<h2 id="2-기본-문법">2. 기본 문법</h2>
<pre><code class="language-sql">SELECT 
    함수명(인자) OVER (
        [PARTITION BY 컬럼1, 컬럼2 ...]
        [ORDER BY 컬럼3, 컬럼4 ...] 
    ) AS 별칭
FROM 테이블명;</code></pre>
<ul>
<li><code>FUNCTION()</code> :사용할 윈도우 함수 (예: <code>RANK()</code>, <code>SUM()</code>)</li>
<li><code>OVER()</code>: 윈도우 함수임을 명시.</li>
<li><code>PARTITION BY</code> (선택): 윈도우를 나눌 기준.(예: <code>PARTITION BY department</code> -&gt; 부서별로 윈도우를 나눔)</li>
<li><code>ORDER BY</code> (선택): 윈도우 내에서 어떤 순서로 계산할지 정한다. (예: 급여 순, 입사일 순)</li>
</ul>
<hr>
<h2 id="3-예시-데이터">3. 예시 데이터</h2>
<table>
<thead>
<tr>
<th><strong>id</strong></th>
<th><strong>name</strong></th>
<th><strong>department</strong></th>
<th><strong>salary</strong></th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Alice</td>
<td>Sales</td>
<td>5000</td>
</tr>
<tr>
<td>2</td>
<td>Bob</td>
<td>Sales</td>
<td>6000</td>
</tr>
<tr>
<td>3</td>
<td>Charlie</td>
<td>Sales</td>
<td>5000</td>
</tr>
<tr>
<td>4</td>
<td>David</td>
<td>Engineering</td>
<td>7000</td>
</tr>
<tr>
<td>5</td>
<td>Eve</td>
<td>Engineering</td>
<td>8000</td>
</tr>
<tr>
<td>6</td>
<td>Frank</td>
<td>Marketing</td>
<td>5500</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-주요-윈도우-함수">4. 주요 윈도우 함수</h2>
<h3 id="4-1-순위-함수-ranking-functions">4-1. 순위 함수 (Ranking Functions)</h3>
<p><code>PARTITION BY</code>로 그룹을 나누고, 그 안에서 <code>ORDER BY</code>로 순위를 매긴다.</p>
<ol>
<li><code>ROW_NUMBER()</code><ul>
<li><strong>의미:</strong> 윈도우 내에서 순서를 1부터 차례대로 계산한다. (중복 값도 다른 순위 부여)</li>
<li><strong>사용법:</strong> <code>ROW_NUMBER() OVER (ORDER BY salary DESC)</code></li>
<li><strong>예시:</strong> 전체 직원 급여 순위 (동점자도 다른 순위)</li>
</ul>
</li>
</ol>
<pre><code>```sql
SELECT
    name, department, salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees;
```</code></pre><ul>
<li><strong>결과</strong>:</li>
</ul>
<pre><code>  | **name** | **department** | **salary** | **row_num** |
  | --- | --- | --- | --- |
  | Eve | Engineering | 8000 | 1 |
  | David | Engineering | 7000 | 2 |
  | Bob | Sales | 6000 | 3 |
  | Frank | Marketing | 5500 | 4 |
  | Alice | Sales | 5000 | 5 |
  | Charlie | Sales | 5000 | 6 |</code></pre><ol start="2">
<li><p><code>RANK()</code></p>
<ul>
<li><p><strong>의미:</strong> 순위를 매기되, <strong>동점자에게는 같은 순위</strong>를 부여. 단, 다음 순위는 동점자 수를 건너뛰고 계산한다.</p>
</li>
<li><p><strong>사용법:</strong> <code>RANK() OVER (PARTITION BY department ORDER BY salary DESC)</code></p>
</li>
<li><p><strong>예시:</strong> <strong>부서별</strong> 급여 순위 (동점자 같은 순위, 중간 순위 비움)</p>
<pre><code class="language-sql">SELECT
   name, department, salary,
   RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
FROM employees;</code></pre>
</li>
<li><p><strong>결과:</strong></p>
</li>
</ul>
</li>
</ol>
<pre><code>  | **name** | **department** | **salary** | **rnk** |
  | --- | --- | --- | --- |
  | Eve | Engineering | 8000 | 1 |
  | David | Engineering | 7000 | 2 |
  | Frank | Marketing | 5500 | 1 |
  | Bob | Sales | 6000 | 1 |
  | Alice | Sales | 5000 | 2 |
  | Charlie | Sales | 5000 | 2 |



  **[참고]** Sales 부서에서 1위(Bob) 다음 2위가 2명(Alice, Charlie)이므로 다음 순위인 3위는 없고 1, 2, 2가 된다. 
  만약 4위가 있었다면 4위가 된다.</code></pre><ol start="3">
<li><p><code>DENSE_RANK()</code></p>
<ul>
<li><p><strong>의미:</strong> <code>RANK()</code>와 같지만, 다음 순위를 건너뛰지 않는다. (예: 1, 2, 2, 3)</p>
</li>
<li><p><strong>사용법:</strong> <code>DENSE_RANK() OVER (ORDER BY salary DESC)</code></p>
</li>
<li><p><strong>예시:</strong> 전체 직원 급여 순위 (동점자 같은 순위, 중간 순위 비우지 않음)</p>
<pre><code class="language-sql">SELECT
   name, department, salary,
   DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;</code></pre>
</li>
<li><p>결과:</p>
</li>
</ul>
</li>
</ol>
<pre><code>  | **name** | **department** | **salary** | **dense_rnk** |
  | --- | --- | --- | --- |
  | Eve | Engineering | 8000 | 1 |
  | David | Engineering | 7000 | 2 |
  | Bob | Sales | 6000 | 3 |
  | Frank | Marketing | 5500 | 4 |
  | Alice | Sales | 5000 | 5 |
  | Charlie | Sales | 5000 | 5 |</code></pre><hr>
<h3 id="4-2-집계함수-aggregate-functions">4-2. 집계함수 (Aggregate Functions)</h3>
<p><code>SUM</code>, <code>AVG</code>, <code>COUNT</code>, <code>MAX</code>, <code>MIN</code> 등 기존 집계 함수를 <code>OVER()</code>와 함께 사용.</p>
<ol>
<li><p><code>SUM() OVER ()</code></p>
<ul>
<li><p><strong>의미:</strong> 윈도우 내의 합계를 구한다.</p>
</li>
<li><p><strong>사용법 1 (누적 합계):</strong> <code>SUM(salary) OVER (PARTITION BY department ORDER BY id)</code></p>
</li>
<li><p><strong>사용법 2 (그룹 전체 합계):</strong> <code>SUM(salary) OVER (PARTITION BY department)</code></p>
</li>
<li><p><strong>예시:</strong> 부서별 급여 합계 및 누적 급여</p>
<pre><code class="language-sql">SELECT
   name, department, salary,
   -- 부서별(PARTITION BY)로 총 급여 합계 (ORDER BY 없음)
   SUM(salary) OVER (PARTITION BY department) AS dept_total_salary,
   -- 부서별(PARTITION BY)로 id 순서 (ORDER BY)대로 누적 급여
   SUM(salary) OVER (PARTITION BY department ORDER BY id) AS running_total
   FROM employees;</code></pre>
</li>
</ul>
</li>
</ol>
<ul>
<li><strong>결과:</strong></li>
</ul>
<pre><code>  | **name** | **department** | **salary** | **dept_total_salary** | **running_total** |
  | --- | --- | --- | --- | --- |
  | David | Engineering | 7000 | 15000 | 7000 |
  | Eve | Engineering | 8000 | 15000 | 15000 |
  | Frank | Marketing | 5500 | 5500 | 5500 |
  | Alice | Sales | 5000 | 16000 | 5000 |
  | Bob | Sales | 6000 | 16000 | 11000 |
  | Charlie | Sales | 5000 | 16000 | 16000 |</code></pre><ol start="2">
<li><p><code>AVG() OVER ()</code></p>
<ul>
<li><p><strong>의미:</strong> 윈도우 내의 평균을 구합니다.</p>
</li>
<li><p><strong>사용법:</strong> <code>AVG(salary) OVER (PARTITION BY department)</code></p>
</li>
<li><p><strong>예시:</strong> 각 직원의 급여와 <strong>해당 직원이 속한 부서의 평균 급여</strong>를 함께 보기</p>
<pre><code class="language-sql">SELECT
   name, department, salary,
   AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary
FROM emplyees;</code></pre>
</li>
<li><p><strong>결과:</strong> (모든 Sales 부서 직원은 동일한 dept_avg_salary 5333.33을 가짐</p>
<table>
<thead>
<tr>
<th><strong>name</strong></th>
<th><strong>department</strong></th>
<th><strong>salary</strong></th>
<th><strong>dept_avg_salary</strong></th>
</tr>
</thead>
<tbody><tr>
<td>David</td>
<td>Engineering</td>
<td>7000</td>
<td>7500.00</td>
</tr>
<tr>
<td>Eve</td>
<td>Engineering</td>
<td>8000</td>
<td>7500.00</td>
</tr>
<tr>
<td>Frank</td>
<td>Marketing</td>
<td>5500</td>
<td>5500.00</td>
</tr>
<tr>
<td>Alice</td>
<td>Sales</td>
<td>5000</td>
<td>5333.33</td>
</tr>
<tr>
<td>Bob</td>
<td>Sales</td>
<td>6000</td>
<td>5333.33</td>
</tr>
<tr>
<td>Charlie</td>
<td>Sales</td>
<td>5000</td>
<td>5333.33</td>
</tr>
</tbody></table>
</li>
</ul>
</li>
</ol>
<hr>
<h3 id="4-3-값-함수-value-functions">4-3. 값 함수 (Value Functions)</h3>
<p>윈도우 내에서 특정 위치의 값(예: 이전 행, 다음 행)을 가져온다. <code>ORDER BY</code>가 필수.</p>
<p>1.<code>RAG()</code></p>
<ul>
<li><p><strong>의미:</strong> 윈도우 내에서 <strong>이전(Previous) 행</strong>의 값을 가져옵니다.</p>
</li>
<li><p><strong>사용법:</strong> <code>LAG(가져올 컬럼, [몇 칸 앞인지, 기본값 1], [이전 행이 없을 때 기본값]) OVER (ORDER BY ...)</code></p>
</li>
<li><p><strong>예시:</strong> 부서별로 정렬했을 때, 바로 <strong>이전 직원</strong>의 급여 가져오기</p>
<pre><code class="language-sql">SELECT
    name, department, salary,
    LAG(salary, 1, 0) OVER (PARTITION BY department ORDER BY salary)     AS prev_salary
FROM employees;</code></pre>
</li>
<li><p><strong>결과:</strong> (Engineering의 David, Marketing의 Frank, Sales의 Alice/Charlie는 부서 내 첫 번째 순서이므로 prev_salary가 0)</p>
</li>
</ul>
<pre><code>  | **name** | **department** | **salary** | **prev_salary** |
  | --- | --- | --- | --- |
  | David | Engineering | 7000 | 0 |
  | Eve | Engineering | 8000 | 7000 |
  | Frank | Marketing | 5500 | 0 |
  | Alice | Sales | 5000 | 0 |
  | Charlie | Sales | 5000 | 5000 |
  | Bob | Sales | 6000 | 5000 |</code></pre><p>2.<code>LEAD()</code></p>
<ul>
<li><p><strong>의미:</strong> 윈도우 내에서 <strong>다음(Next) 행</strong>의 값을 가져온다.</p>
</li>
<li><p><strong>사용법:</strong> <code>LEAD(가져올 컬럼, [몇 칸 뒤인지, 기본값 1], [다음 행이 없을 때 기본값]) OVER (ORDER BY ...)</code></p>
</li>
<li><p><strong>예시:</strong> 부서별로 정렬했을 때, 바로 <strong>다음 직원</strong>의 급여 가져오기</p>
<pre><code class="language-sql">SELECT
    name, department, salary,
    LEAD(salary, 1, 0) OVER (PARTITION BY department ORDER BY salary) AS next_salary
FROM employees;</code></pre>
</li>
<li><p><strong>결과:</strong> (Engineering의 Eve, Marketing의 Frank, Sales의 Bob은 부서 내 마지막 순서이므로 next_salary가 0)</p>
</li>
</ul>
<pre><code>  | **name** | **department** | **salary** | **next_salary** |
  | --- | --- | --- | --- |
  | David | Engineering | 7000 | 8000 |
  | Eve | Engineering | 8000 | 0 |
  | Frank | Marketing | 5500 | 0 |
  | Alice | Sales | 5000 | 5000 |
  | Charlie | Sales | 5000 | 6000 |
  | Bob | Sales | 6000 | 0 |</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[VDD (Vibe Driven Development) 스터디 6주차]]></title>
            <link>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 28 Oct 2025 05:55:25 GMT</pubDate>
            <description><![CDATA[<h1 id="vdd-6주차-promptory--지능형-태그-추출--대시보드-고도화-✨">VDD 6주차: Promptory – 지능형 태그 추출 &amp; 대시보드 고도화 ✨</h1>
<hr>
<h2 id="🎬-이번-주-활동-요약">🎬 이번 주 활동 요약</h2>
<ul>
<li><strong>제출 기한:</strong> 10월 25일(토) 24:00  </li>
<li><strong>프로젝트명:</strong> <code>Promptory</code>  </li>
<li><strong>주제:</strong> 생성형 AI 대화 자동 수집·요약·분류 서비스  </li>
<li><strong>개발 목표:</strong>  <ul>
<li>대시보드 기능 고도화 및 시각화 완성  </li>
<li>태그 추출 알고리즘 구현 (<code>KoNLPy</code>, <code>nltk</code>)  </li>
<li>UI/UX 개선 및 안정화  </li>
</ul>
</li>
<li><strong>GitHub:</strong> <a href="https://github.com/early-developer-club/promptory">early-developer-club/promptory</a></li>
</ul>
<hr>
<h2 id="💡-프로젝트-개요">💡 프로젝트 개요</h2>
<p><strong>Promptory</strong>는 Gemini와 ChatGPT 등 생성형 AI와의 대화를 자동으로 수집·요약·분류하는<br>AI 대화 아카이빙 서비스다.<br>6주차에는 단순한 기록을 넘어, <strong>대화 데이터의 의미를 분석하고 시각화</strong>하는 기능을 중심으로 개발이 진행되었다.  </p>
<blockquote>
<p>“AI와의 대화가 단순 로그를 넘어,<br>내가 쌓아온 학습 패턴과 인사이트로 구조화되는 순간이었다.”</p>
</blockquote>
<hr>
<h2 id="⚙️-6주차-주요-개발-내용">⚙️ 6주차 주요 개발 내용</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>작업 요약</th>
</tr>
</thead>
<tbody><tr>
<td><strong>대시보드 고도화</strong></td>
<td>AI별 대화 수, 태그 빈도 분석, 태그 클릭 시 필터링 기능 구현</td>
</tr>
<tr>
<td><strong>태그 추출 기능 구현</strong></td>
<td><code>KoNLPy</code> + <code>nltk</code> 기반 형태소 분석 및 품사 태깅으로 핵심 키워드 자동 추출</td>
</tr>
<tr>
<td><strong>질문 가중치 로직 추가</strong></td>
<td>Prompt 키워드에 5배 가중치를 부여하여 주제 중심 태그 생성</td>
</tr>
<tr>
<td><strong>불용어 관리 기능</strong></td>
<td><code>crud.py</code>에 stopwords 목록 추가 및 필터링 로직 개선</td>
</tr>
<tr>
<td><strong>UI/UX 개선</strong></td>
<td><code>shadcn/ui</code> 기반으로 UI 컴포넌트 재구성, 반응형 디자인 적용</td>
</tr>
<tr>
<td><strong>DatePicker 개선</strong></td>
<td>실제 대화가 있는 날짜만 선택 가능하도록 API 연동</td>
</tr>
<tr>
<td><strong>Chrome Extension 안정화</strong></td>
<td>기존 대화 제외, 신규 대화만 수집하도록 로직 재설계</td>
</tr>
<tr>
<td><strong>버그 수정</strong></td>
<td>AttributeError, NameError 등 다수 오류 해결 및 렌더링 최적화</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧠-지능형-태그-추출-방식">🧠 지능형 태그 추출 방식</h2>
<p>이번 주의 핵심은 <strong>“대화를 분석해 의미 있는 태그를 자동 생성하는 기능”</strong>이었다.<br>Promptory는 다음과 같은 과정을 통해 태그를 생성한다.</p>
<ol>
<li><strong>형태소 분석:</strong><br><code>KoNLPy</code>의 <code>Okt</code> 분석기를 이용해 대화(질문+응답)를 단어 단위로 분리  </li>
<li><strong>핵심 품사 추출:</strong><br>명사(Noun) 및 알파벳(Alpha) 품사만 선택  </li>
<li><strong>질문 가중치 부여:</strong><br>Prompt(질문)에 포함된 키워드에 <strong>5배 가중치</strong>를 적용  </li>
<li><strong>불용어 및 필터링:</strong><br>불필요한 단어, 한 글자 단어, stopwords 목록을 제거  </li>
<li><strong>상위 5개 태그 선정:</strong><br>가중치 + 빈도수 기반으로 최종 태그 추출  </li>
</ol>
<p>[대화 발생] → [형태소 분석] → [명사/알파벳 추출] → [질문(×5) 가중치 부여] → [불용어 필터링] → [상위 5개 태그 선정]</p>
<blockquote>
<p>단순한 문자열 분석을 넘어,<br><strong>“AI가 생성한 대화를 또 다른 AI로 분석한다”</strong>는 점에서<br>Promptory의 진정한 지능형 구조가 완성되었다.</p>
</blockquote>
<hr>
<h2 id="📊-주요-결과-화면">📊 주요 결과 화면</h2>
<p><strong>1️⃣ 대시보드 기능 고도화 – 태그 통계 시각화</strong>  </p>
<ul>
<li>백엔드 <code>/api/v1/statistics/tags</code> API 추가  </li>
<li><code>recharts</code>를 활용한 <strong>상위 10개 태그 빈도 분석 차트</strong> 구현  </li>
<li>차트 클릭 시 해당 태그가 포함된 대화 목록으로 이동  </li>
</ul>
<p><strong>2️⃣ DatePicker 개선 및 반응형 UI</strong>  </p>
<ul>
<li>실제 대화가 있는 날짜만 활성화되도록 API 연동  </li>
<li>캘린더 및 레이아웃 여백(padding) 조정으로 사용자 경험 개선  </li>
</ul>
<p><strong>3️⃣ Chrome Extension 안정화</strong>  </p>
<ul>
<li>ChatGPT: 기존 대화 제외, 신규 대화만 저장  </li>
<li>Gemini: CSP 정책을 우회하기 위해 <code>background.js</code>에서 백엔드 통신 처리  </li>
</ul>
<hr>
<h2 id="🧩-인상-깊었던-포인트">🧩 인상 깊었던 포인트</h2>
<blockquote>
<p>“태그가 단순 문자열이 아니라,<br>내가 어떤 질문을 반복하고 어떤 주제에 집중하는지를 보여주는 거울 같았다.”</p>
</blockquote>
<p>대시보드의 태그 분석 그래프를 통해<br>AI와의 대화 패턴이 시각적으로 드러나는 경험은 매우 흥미로웠다.<br><strong>‘대화를 통한 자기 분석’</strong>이라는 Promptory의 방향성이 구체화된 순간이었다.</p>
<hr>
<h2 id="⚠️-어려웠던-점">⚠️ 어려웠던 점</h2>
<ul>
<li><strong>형태소 분석 라이브러리 이슈:</strong> <code>KoNLPy</code> 설치 시 <code>JPype1</code> 버전 충돌 발생 → 버전 고정으로 해결  </li>
<li><strong>태그 중복 처리:</strong> 영어·한글 혼용 시 중복되는 키워드가 발생 → 소문자 변환 및 중복 필터링 추가  </li>
<li><strong>대시보드 렌더링 속도:</strong> 태그 데이터 fetch 시 지연 발생 → API 응답 캐싱 적용  </li>
</ul>
<hr>
<h2 id="💬-이번-주-회고">💬 이번 주 회고</h2>
<p>6주차는 Promptory의 “<strong>지능화</strong>”와 “<strong>완성도 개선</strong>”이 동시에 이뤄진 주차였다.<br>AI 대화를 단순히 저장하는 것을 넘어,<br>그 대화를 분석하고 <strong>나의 AI 사용 습관을 시각화</strong>하는 단계로 진화했다.  </p>
<p>이제 Promptory는 단순한 AI 로거를 넘어,<br>“<strong>AI와 함께 성장하는 학습 파트너</strong>”로 발전할 준비를 마쳤다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VDD (Vibe Driven Development) 스터디 5주차]]></title>
            <link>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 28 Oct 2025 05:19:22 GMT</pubDate>
            <description><![CDATA[<h1 id="vdd-5주차-promptory--ai-대화-기록-시각화-및-프론트엔드-연동-⚙️">VDD 5주차: Promptory – AI 대화 기록 시각화 및 프론트엔드 연동 ⚙️</h1>
<hr>
<h2 id="🎬-5주차-활동-요약">🎬 5주차 활동 요약</h2>
<ul>
<li><strong>제출 기한:</strong> 10월 18일(토) 24:00  </li>
<li><strong>프로젝트명:</strong> <code>Promptory</code>  </li>
<li><strong>주제:</strong> 생성형 AI 대화 자동 수집·요약·분류 서비스  </li>
<li><strong>개발 목표:</strong>  <ul>
<li>백엔드(FastAPI)와 프론트엔드(Next.js) 연동  </li>
<li>실시간 대화 데이터 시각화 구현  </li>
<li>Chrome Extension → 백엔드 → DB → Dashboard 전 흐름 완성  </li>
</ul>
</li>
<li><strong>GitHub:</strong> <a href="https://github.com/early-developer-club/promptory">early-developer-club/promptory</a></li>
</ul>
<hr>
<h2 id="💡-프로젝트-개요">💡 프로젝트 개요</h2>
<p><strong>Promptory</strong>는 ChatGPT, Gemini 등과의 대화를 자동으로 <strong>수집·요약·분류·시각화</strong>하는 서비스다.<br>이번 주에는 백엔드로 수집된 대화 데이터가 실제로 <strong>화면에 렌더링되는 전체 흐름</strong>을 구현했다.  </p>
<blockquote>
<p>“AI 대화가 단순 텍스트 로그가 아니라, <strong>지식 자산으로 시각화되는 순간</strong>을 보고 싶었다.”</p>
</blockquote>
<hr>
<h2 id="⚙️-5주차-주요-개발-내용">⚙️ 5주차 주요 개발 내용</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>작업 요약</th>
</tr>
</thead>
<tbody><tr>
<td><strong>백엔드-프론트엔드 연동</strong></td>
<td>FastAPI → Next.js 간 API 연동 완료</td>
</tr>
<tr>
<td><strong>대화 데이터 시각화</strong></td>
<td>총 대화 수 / AI별 비율 / 태그 분포를 대시보드 형태로 구현</td>
</tr>
<tr>
<td><strong>Conversation Details 페이지 구성</strong></td>
<td>각 대화의 프롬프트 및 응답 내용 세부 조회 가능</td>
</tr>
<tr>
<td><strong>Chrome Extension → DB 연계 테스트</strong></td>
<td>Gemini·ChatGPT 대화 데이터를 실시간 전송 후 저장 확인</td>
</tr>
<tr>
<td><strong>UI 개선</strong></td>
<td>Tailwind CSS를 활용한 카드형 대시보드, 상세 뷰 구성</td>
</tr>
<tr>
<td><strong>README 업데이트</strong></td>
<td>개발 계획, 시각화 스크린샷, 기능별 진행상황 반영</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧩-결과-화면">🧩 결과 화면</h2>
<p><strong>1️⃣ 프로젝트 개발 계획 정리 (README 일부)</strong><br><img src="https://velog.velcdn.com/images/cheonroro_/post/708996e1-5837-49af-b6f3-83f4c31a7ae0/image.jpg" alt="개발 계획표"></p>
<p><strong>2️⃣ Promptory Dashboard 화면</strong><br><img src="https://velog.velcdn.com/images/cheonroro_/post/62fbfaea-eba0-4e16-89ce-31f5e19b9740/image.jpg" alt=""></p>
<p><strong>3️⃣ Conversation Details 페이지</strong><br><img src="https://velog.velcdn.com/images/cheonroro_/post/9f720571-de4e-4bd5-860c-a2b0dc575a90/image.jpg" alt=""></p>
<blockquote>
<p>실제로 ChatGPT와 Gemini를 통해 수집된 대화가<br>FastAPI → DB → Next.js를 거쳐 프론트엔드 대시보드에 시각화되었다.</p>
</blockquote>
<hr>
<h2 id="⚠️-개발-중-이슈-docker-빌드-중-코드-롤백-사고">⚠️ 개발 중 이슈: “Docker 빌드 중 코드 롤백 사고”</h2>
<blockquote>
<p>“빌드 테스트 중 gemini가 git 옵션으로 파일을 돌려버렸다…”</p>
</blockquote>
<p>이번 주 가장 큰 사건이었다.<br>FastAPI와 Next.js를 각각 로컬 서버로 띄워 테스트하다가<br>Docker 이미지 빌드 과정을 자동화하려고 했는데,<br>Gemini가 git 명령어로 “이전 커밋 시점”을 복원해버려<br><strong>작성해둔 코드가 통째로 롤백되는 사고</strong>가 발생했다.  </p>
<p>결과적으로 Promptory 프로젝트를 <strong>두 번 개발</strong>하게 되었고,<br>그 과정에서 아래 두 가지 차이가 생겼다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>첫 번째 버전</th>
<th>두 번째 버전</th>
</tr>
</thead>
<tbody><tr>
<td><strong>UI 구성</strong></td>
<td>깔끔한 카드형 대시보드 &amp; 디테일 화면</td>
<td>CSS 깨짐 및 정렬 오류 발생</td>
</tr>
<tr>
<td><strong>상태</strong></td>
<td>완성도 높은 화면 유지</td>
<td>복구 중이던 테스트 상태</td>
</tr>
</tbody></table>
<hr>
<p><strong>정상 버전(초기 개발 버전)</strong></p>
<p><img src="https://velog.velcdn.com/images/cheonroro_/post/d4b048ab-032a-473e-8c6e-c82762a6ce9e/image.jpg" alt="MainUI"></p>
<p><img src="https://velog.velcdn.com/images/cheonroro_/post/522c67e4-aa87-4c80-b499-28cc742c040a/image.jpg" alt="Conversation_Detail"></p>
<p><img src="https://velog.velcdn.com/images/cheonroro_/post/dfa68665-6636-4a21-a284-4064ec88ad9e/image.jpg" alt="Dashboard"></p>
<blockquote>
<p>이 경험 덕분에 “Git 커밋은 보험이다”라는 말을 몸으로 배웠다.<br>자동화된 AI 환경에서도 결국 <strong>버전 관리의 주도권은 사람에게 있다.</strong></p>
</blockquote>
<hr>
<h2 id="💬-인상-깊었던-포인트">💬 인상 깊었던 포인트</h2>
<blockquote>
<p>“AI와의 협업은 효율적이지만, 모든 책임은 개발자에게 있다.
개발자가 최종 책임을 갖지 않으면 프로젝트는 언제든 무너질 수 있다.”</p>
</blockquote>
<p>Gemini가 빌드를 돕는 과정에서 예기치 못한 명령을 실행했지만,<br>그 상황을 되돌리고 복구해가는 과정 자체가 큰 배움이었다.<br>덕분에 Git 관리의 중요성, Docker 워크플로우의 리스크,<br>AI 협업 시 명령어 검증의 필요성을 절실히 깨달았다.</p>
<hr>
<h2 id="🧠-이번-주-회고">🧠 이번 주 회고</h2>
<p>이번 주는 <strong>기능 완성과 사고 대응이 동시에 있었던 주차</strong>였다.<br>결과적으로 Promptory는 시각화 구조를 완성했고,<br>대화 데이터가 실제 서비스 형태로 보여지는 첫 버전을 완성했다.  </p>
<p>다음 주에는 UI를 복구하고 요약(summary) 및 태그 자동화 로직을 추가하여<br>대화 데이터의 <strong>“가공 → 분석 → 가치화” 단계</strong>로 발전시킬 예정이다.  </p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[VDD (Vibe Driven Development) 스터디 4주차]]></title>
            <link>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 28 Oct 2025 04:42:04 GMT</pubDate>
            <description><![CDATA[<h1 id="vdd-4주차-promptory--ai-대화-아카이빙-서비스-백엔드-구축-🧠">VDD 4주차: Promptory – AI 대화 아카이빙 서비스 백엔드 구축 🧠</h1>
<hr>
<h2 id="🎬-이번-주-활동-요약">🎬 이번 주 활동 요약</h2>
<ul>
<li><strong>제출 기한:</strong> 10월 11일(토) 24:00  </li>
<li><strong>프로젝트명:</strong> <code>Promptory</code>  </li>
<li><strong>주제:</strong> 생성형 AI 대화 자동 수집·요약·분류 서비스  </li>
<li><strong>개발 목표:</strong> FastAPI 기반 백엔드 초기 구축 및 Chrome Extension 연동  </li>
<li><strong>작업 기간:</strong> 2025년 10월 8일 기준  </li>
<li><a href="https://github.com/early-developer-club/promptory">Promptory Repository 바로가기</a></li>
</ul>
<hr>
<h2 id="💡-프로젝트-개요">💡 프로젝트 개요</h2>
<p><strong>Promptory</strong>는 ChatGPT, Gemini 등과의 대화를 자동으로 <strong>백업·요약·태깅</strong>하여<br>프롬프트 재사용과 학습 자산화를 돕는 개발자용 서비스다.  </p>
<blockquote>
<p>“매일 쏟아지는 대화 속에서 내가 어떤 질문을 했는지조차 헷갈린다.”  </p>
<p>이 문제를 해결하기 위해, Promptory는 생성형 AI와의 모든 대화를<br><strong>하나의 데이터 자산으로 시각화</strong>하는 것을 목표로 한다.</p>
</blockquote>
<hr>
<h2 id="⚙️-4주차-주요-개발-내용">⚙️ 4주차 주요 개발 내용</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>작업 요약</th>
</tr>
</thead>
<tbody><tr>
<td><strong>프로젝트 초기 구조 생성</strong></td>
<td><code>promptory/backend</code>, <code>promptory/chrome-extension</code> 폴더 구조 설계 및 생성</td>
</tr>
<tr>
<td><strong>FastAPI 기본 설정</strong></td>
<td><code>main.py</code>, <code>requirements.txt</code>, 라우터/환경변수 설정</td>
</tr>
<tr>
<td><strong>데이터베이스 연동</strong></td>
<td><code>models.py</code>, <code>schemas.py</code>, <code>database.py</code> 작성 후 SQLAlchemy 연결</td>
</tr>
<tr>
<td><strong>OAuth2 기반 Google 로그인</strong></td>
<td><code>Flow.from_client_config()</code> 기반 OAuth2 인증 구현</td>
</tr>
<tr>
<td><strong>JWT 기반 세션 유지</strong></td>
<td>Access/Refresh Token 발급 및 검증 로직 구축</td>
</tr>
<tr>
<td><strong>Chrome Extension 초기 세팅</strong></td>
<td><code>manifest.json</code>, <code>popup.html</code>, <code>content.js</code> 작성 및 데이터 수집 구조 설계</td>
</tr>
<tr>
<td><strong>서버 실행 및 디버깅 완료</strong></td>
<td>FastAPI 서버 실행 테스트 완료 및 로그인 처리 성공</td>
</tr>
</tbody></table>
<p>✅ <strong>결과:</strong> Week 1 목표(백엔드 기본 구조 + 인증 시스템 구축) 달성!</p>
<hr>
<h2 id="💬-인상-깊었던-포인트">💬 인상 깊었던 포인트</h2>
<blockquote>
<p>“AI가 문제를 해결해주는 게 아니라, <strong>함께 디버깅하며 설계 방향을 잡는 경험</strong>이었다.”</p>
</blockquote>
<p>이번 주는 로그인 인증 관련된 오류와의 싸움이 많았지만, Gemini가 실제 코드 수정·가이드·경로 교정까지 수행하며 비교적 수월하게 진행할 수 있었다.</p>
<p>특히 Python 버전 이슈나 OAuth 환경 변수 설정 같은 세부적인 문제들을 AI가 단계적으로 해결해준 점이 인상 깊었다.</p>
<hr>
<h2 id="🧩-이번-주-소감">🧩 이번 주 소감</h2>
<p>4주차는 Promptory 서비스의 기반을 다진 주차였다.<br>AI와 협업하며 <strong>PRD → 기능 명세 → 개발 → 디버깅</strong>의 전체 사이클을 경험했고,<br>이 과정이 바로 “바이브코딩의 본질”임을 실감했다.  </p>
<p>다음 주에는 프론트엔드(Next.js) 연동과 <strong>AI 대화 요약/태깅 로직 개발</strong>에 도전할 예정이다.  </p>
<hr>
<p><a href="https://github.com/early-developer-club/promptory/blob/main/2025-10-08_Vibecoding_Log.md">바이브 코딩 로그 보러가기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VDD (Vibe Driven Development) 스터디 3주차]]></title>
            <link>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 19 Oct 2025 09:55:26 GMT</pubDate>
            <description><![CDATA[<h1 id="vdd-3주차-절대-색감-게임absolute-color-game-🎨">VDD 3주차: 절대 색감 게임(Absolute Color Game) 🎨</h1>
<hr>
<h2 id="🎬-3주차-활동-요약">🎬 3주차 활동 요약</h2>
<ul>
<li><strong>제출 기한:</strong> 10월 4일(토) 24:00  </li>
<li><strong>과제:</strong> 절대색감 게임 바이브 코딩하기 </li>
<li><strong>GitHub:</strong> <a href="https://github.com/early-developer-club/color-game-jinyoung">early-developer-club/color-game-jinyoung</a>  </li>
<li><strong>배포 주소:</strong> <a href="https://color-game-jinyoung.netlify.app/">https://color-game-jinyoung.netlify.app/</a>  </li>
</ul>
<hr>
<h2 id="🎮-프로젝트-개요">🎮 프로젝트 개요</h2>
<p><strong>절대 색감 게임(Absolute Color Game)</strong> 은 제한 시간 내에 n×n 색상 그리드에서<br>‘하나만 다른 색상’을 찾아 점수를 얻는 간단한 웹 게임이다.<br>게임이 끝나면 점수가 Firebase DB에 저장되어, 이름과 함께 랭킹보드에 표시된다.  </p>
<ul>
<li><strong>소요 시간:</strong> Gemini와 함께한 단일 페어 프로그래밍 세션  </li>
<li><strong>기술 스택:</strong>  <ul>
<li><strong>Frontend:</strong> HTML, CSS, JavaScript  </li>
<li><strong>Backend / DB:</strong> Firebase Realtime Database  </li>
<li><strong>Deploy:</strong> Netlify  </li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/cheonroro_/post/7286ded8-6610-4441-8437-4a708071e8bc/image.jpg" alt=""></p>
<hr>
<h2 id="🧠-개발-과정-프롬프트--처리-내역">🧠 개발 과정 (프롬프트 &amp; 처리 내역)</h2>
<p>Gemini CLI와의 대화 기반으로 게임이 점점 발전해갔다.<br>아래는 주요 프롬프트 흐름과 AI가 처리한 작업 요약이다.</p>
<h3 id="1️⃣-초기-설계">1️⃣ 초기 설계</h3>
<blockquote>
<p>절대 색감 게임을 만들려고 해. Netlify를 통해 간단한 배포를 진행해볼 예정이야.<br>게임 정보를 저장해서 게임이 끝난 후 진행해 본 사용자들의 데이터를 바탕으로 랭킹을 보여주는 기능도 있으면 좋을 것 같아.</p>
</blockquote>
<ul>
<li>Gemini가 <code>HTML/CSS/JS + Firebase + Netlify</code> 스택을 제안하고 전체 개발 계획을 설계.  </li>
</ul>
<h3 id="2️⃣-기본-구조-생성">2️⃣ 기본 구조 생성</h3>
<blockquote>
<p>응 동의해.</p>
</blockquote>
<ul>
<li>프로젝트 폴더 구조 및 기본 파일(<code>index.html</code>, <code>style.css</code>, <code>script.js</code>) 생성.  </li>
<li>초기 버전의 색상 찾기 로직 구현.  </li>
</ul>
<h3 id="3️⃣-데이터베이스-연동">3️⃣ 데이터베이스 연동</h3>
<blockquote>
<p>(Firebase 설정 객체 제공)</p>
</blockquote>
<ul>
<li>Firebase SDK 추가 및 <code>saveScore</code>, <code>fetchRanking</code> 함수 구현.  </li>
</ul>
<h3 id="4️⃣-readme-및-보안-강화">4️⃣ README 및 보안 강화</h3>
<blockquote>
<p>(DB 정보 마스킹 요청)</p>
</blockquote>
<ul>
<li><code>README.md</code> 생성 및 Firebase 설정 정보 마스킹 처리.  </li>
</ul>
<h3 id="5️⃣-게임-규칙-개선">5️⃣ 게임 규칙 개선</h3>
<blockquote>
<p>... n×n 형태로 시작한 뒤 1개의 다른 색상을 찾는 방식으로 진행해도 좋을 것 같아.<br>30초 안에 가장 많은 점수를 획득하는 것으로 하자.</p>
</blockquote>
<ul>
<li>타이머 기능, 난이도 증가 로직, 보너스/패널티 설계 추가.  </li>
</ul>
<h3 id="6️⃣-uiux-수정">6️⃣ UI/UX 수정</h3>
<blockquote>
<p>웅 동의해 수정해줘.</p>
</blockquote>
<ul>
<li><code>index.html</code> / <code>style.css</code> / <code>script.js</code> 전면 수정.  </li>
<li>동적 그리드 구성 및 타이머 시각화 추가.  </li>
</ul>
<h3 id="7️⃣-음악-및-몰입감-추가">7️⃣ 음악 및 몰입감 추가</h3>
<blockquote>
<p>시간 차감이 좋을 것 같아. 긴장감을 높이기 위해 배경음악도 추가할 수 있을까?</p>
</blockquote>
<ul>
<li>오답 시 2초 차감 로직 추가.  </li>
<li>배경음악 기능 구현 (<code>background_music.mp3</code>, loop 재생).  </li>
</ul>
<h3 id="8️⃣-최종-문서화">8️⃣ 최종 문서화</h3>
<blockquote>
<p>README.md 파일에 모든 프롬프트와 처리 내용을 업데이트해줘.</p>
</blockquote>
<ul>
<li>전체 개발 이력 및 규칙을 정리한 README 완성.  </li>
</ul>
<hr>
<h2 id="💡-인상-깊었던-포인트">💡 인상 깊었던 포인트</h2>
<blockquote>
<p>“AI가 만든 코드를 그대로 사용하는 것이 아니라,<br>내가 원하는 방향으로 ‘조율하고 발전시키는 과정’이 바이브코딩의 진짜 핵심이다.”</p>
</blockquote>
<p>Gemini CLI는 단순한 코드 생성의 역할이 아니라 개발의 전 과정을 함께 설계·수정·기록할 수 있는 협업 파트너로 느껴졌다. 
다른 팀원들이 진행한 프로젝트를 봤을 때, Claude.md를 통해 </p>
<hr>
<h2 id="⚙️-어려웠던-점">⚙️ 어려웠던 점</h2>
<ul>
<li>배포까지의 과정은 혼자서 해본 적이 처음이었다. 관련된 지식이 없기 때문에 시간이 조금 소요되었지만, 핵심 개념을 갖고 있는 사람이 바이브 코딩을 사용한다면 엄청난 무기가 될 수 있을 것 같다.</li>
</ul>
<hr>
<h2 id="🧩-이번-주-소감">🧩 이번 주 소감</h2>
<p>이번 주는 단순한 실습이 아니라 <strong>AI와 함께 하나의 웹서비스를 완성한 경험</strong>이었다.<br>코드를 자동으로 생성하는 데서 그치지 않고,<br><strong>요구사항 설계 → 구현 → 테스트 → 배포 → 문서화</strong>의 전 단계를<br>AI와 대화로 완성했다는 점이 인상 깊었다.  </p>
<p>AI와 함께한 이 과정은 “바이브코딩(Vibe Coding)”이라는 개념을<br>가장 실감 나게 보여준 시간이었다.<br>다음 주에는 배포된 게임의 개선 아이디어나 기능 확장을 시도해볼 예정이다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VDD (Vibe Driven Development) 스터디 2주차]]></title>
            <link>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Wed, 08 Oct 2025 15:21:53 GMT</pubDate>
            <description><![CDATA[<h1 id="vdd-2주차-바이브-코딩-실습--툴-선택--첫-프롬프트">VDD 2주차: 바이브 코딩 실습 – 툴 선택 &amp; 첫 프롬프트</h1>
<hr>
<h2 id="🎬-2주차-활동-요약">🎬 2주차 활동 요약</h2>
<ul>
<li><p><strong>과제 마감:</strong> 27일(토) 24:00  </p>
</li>
<li><p><strong>Basic 미션</strong>
1) 이후 스터디에 사용할 <strong>AI 툴 선택·설치(필요시 구독)</strong><br>2) AI에게 다음을 지시하고 결과 캡처  </p>
<blockquote>
<p>&quot;Hello, Early Developer Club!&quot;이라고 출력하는 프로그램을 작성해줘.</p>
</blockquote>
</li>
<li><p><strong>나의 선택:</strong> <code>Gemini CLI</code></p>
</li>
</ul>
<hr>
<h2 id="🧠-세션-내용-정리">🧠 세션 내용 정리</h2>
<h3 id="1️⃣-툴-선택--세팅">1️⃣ 툴 선택 &amp; 세팅</h3>
<ul>
<li>바이브 코딩 환경으로 <strong>Gemini CLI</strong>를 선택하여 설치·인증을 진행했다.  </li>
<li>로컬 환경에서 프롬프트 → 코드 생성 → 실행까지의 <strong>짧은 피드백 루프</strong>를 만드는 데 초점을 맞춤.  </li>
<li>Gemini CLI는 터미널에서 바로 AI에게 명령을 내릴 수 있어 개발 워크플로우에 자연스럽게 녹아드는 장점이 있었다.</li>
<li>또한 대학생 프로모션으로 pro 모델을 사용할 수 있는 것도 장점이 있었다.</li>
</ul>
<h3 id="2️⃣-첫-지시와-결과">2️⃣ 첫 지시와 결과</h3>
<blockquote>
<p>“Hello, Early Developer Club!”이라고 출력하는 프로그램을 작성해줘.  </p>
</blockquote>
<p>출력 화면은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/cheonroro_/post/dceca994-ff9d-4aac-a85c-ac5556ccc5f5/image.jpg" alt=""></p>
<p>세션 종료 후 Gemini CLI가 자동으로 모델 사용량과 성능 리포트를 보여주는 점이 인상적이었다.</p>
<p><img src="https://velog.velcdn.com/images/cheonroro_/post/a6dac8dd-48a9-48e1-a012-4879543cfaa8/image.jpg" alt=""></p>
<h2 id="⚙️-gemini-cli-세션-종료-리포트-분석">⚙️ Gemini CLI 세션 종료 리포트 분석</h2>
<p>Gemini CLI에서 <code>/quit</code> 명령어를 입력하면, 세션 요약(Interaction Summary)이 자동으로 출력된다.<br>이번 실습을 마친 뒤 CLI가 보여준 결과를 살펴보며 구조를 간단히 정리해봤다.</p>
<hr>
<h3 id="🧾-interaction-summary">🧾 Interaction Summary</h3>
<p>세션의 기본 정보와 수행 통계를 보여주는 구간이다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Session ID</strong></td>
<td>세션을 구분하기 위한 고유 식별자.</td>
</tr>
<tr>
<td><strong>Tool Calls</strong></td>
<td>CLI에서 실행된 툴 호출 횟수 (<code>2 (✓ 2 × 0)</code> → 2회 성공, 실패 0회).</td>
</tr>
<tr>
<td><strong>Success Rate</strong></td>
<td>툴 호출 성공률 (100%).</td>
</tr>
<tr>
<td><strong>User Agreement</strong></td>
<td>세션 중 사용자의 피드백 또는 리뷰 여부 (<code>1 reviewed</code>).</td>
</tr>
</tbody></table>
<p>👉 즉, 이번 세션은 <strong>2개의 명령을 모두 성공적으로 실행</strong>했음을 의미한다.</p>
<hr>
<h3 id="⚙️-performance">⚙️ Performance</h3>
<p>AI가 응답하는 과정에서의 시간 비율을 보여준다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Wall Time</strong></td>
<td>세션 전체가 열린 시간 (7분 9초)</td>
</tr>
<tr>
<td><strong>Agent Active</strong></td>
<td>실제 AI가 작동한 시간 (14.6초)</td>
</tr>
<tr>
<td><strong>API Time</strong></td>
<td>모델 API가 응답한 시간 (10.0초, 전체의 68.9%)</td>
</tr>
<tr>
<td><strong>Tool Time</strong></td>
<td>CLI 내부 툴 실행에 소요된 시간 (4.5초, 31.1%)</td>
</tr>
</tbody></table>
<p>💡 대부분의 시간은 <strong>AI 응답 생성(약 69%)</strong> 에 사용되었으며, 나머지는 로컬 툴 실행이었다.</p>
<hr>
<h3 id="🤖-model-usage">🤖 Model Usage</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Model</strong></td>
<td>사용된 모델 버전 (<code>gemini-2.5-pro</code>).</td>
</tr>
<tr>
<td><strong>Reqs</strong></td>
<td>세션 중 모델 호출 횟수 (3회).</td>
</tr>
<tr>
<td><strong>Input Tokens</strong></td>
<td>입력 토큰 수 (18,536) – 내가 보낸 프롬프트의 전체 길이.</td>
</tr>
<tr>
<td><strong>Output Tokens</strong></td>
<td>출력 토큰 수 (43) – 모델이 생성한 응답의 길이.</td>
</tr>
</tbody></table>
<hr>
<h3 id="💰-savings-highlight">💰 Savings Highlight</h3>
<blockquote>
<p><strong>11,356 (61.3%) of input tokens were served from the cache, reducing costs.</strong></p>
</blockquote>
<p>Gemini CLI는 <strong>캐시 기능</strong>을 통해 이전 세션의 일부 입력을 재사용했다.<br>즉, 61.3%의 입력 데이터를 새로 계산하지 않아 <strong>비용 절감 및 속도 향상 효과</strong>가 있었다.</p>
<hr>
<h3 id="💡-tip">💡 Tip</h3>
<p>CLI에서는 <code>/stats model</code> 명령어를 사용해 토큰 사용량을 더 자세히 분석할 수 있다.<br>덕분에 모델 호출 효율성과 비용 최적화를 직접 확인할 수 있었다.</p>
<hr>
<h2 id="🧩-이번-주-소감">🧩 이번 주 소감</h2>
<p>처음 Gemini CLI를 설정할 때 인증 관련 에러가 반복되어 가장 어려웠다.
Loaded cached credentials 같은 메시지가 출력되었지만, 실제로는 API Key가 올바르게 연결되지 않아 여러 번 재설정을 시도했다.
결국 환경 변수 설정과 캐시 파일 삭제를 통해 문제를 해결했지만, CLI 기반 툴의 인증 과정이 생각보다 까다롭다는 점을 체감했다.</p>
<p>하지만 문제를 해결하고 난 후, 터미널에서 곧바로 “프롬프트 → 코드 → 실행”이 이어지는 경험이 새로웠다.
명령 한 줄로 AI와 대화하며 코드를 생성하는 과정이 진짜 바이브 코딩의 시작처럼 느껴졌다.</p>
<p>세션을 종료하면서 자동으로 이런 <strong>성능 리포트와 토큰 사용량</strong>이 출력되는 점이 인상적이었다.<br>AI와의 대화가 단순 텍스트 수준을 넘어, 실제 <strong>리소스 단위로 추적 가능한 개발 활동</strong>이라는 걸 체감할 수 있었다.<br>앞으로는 이런 로그를 기반으로 프롬프트 효율성이나 캐시 전략을 비교해보는 것도 흥미로울 것 같다.</p>
<p>다음 주에는 이 환경을 기반으로 좀 더 구체적인 기능을 가진 코드를 작성해보고 싶다. 
단순 출력에서 벗어나, 프롬프트 설계와 실행 결과를 반복적으로 다듬는 과정을 연습할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VDD (Vibe Driven Development) 스터디 1주차 ]]></title>
            <link>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@cheonroro_/VDD-Vibe-Driven-Development-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Wed, 08 Oct 2025 14:31:59 GMT</pubDate>
            <description><![CDATA[<h1 id="vdd-1주차-온보딩--ai-트렌드-찍먹-특강">VDD 1주차: 온보딩 &amp; AI 트렌드 찍먹 특강</h1>
<hr>
<h2 id="🎬-온보딩--첫-만남">🎬 온보딩 &amp; 첫 만남</h2>
<p>처음 스터디를 참여하게 된 계기는 지인이 SNS를 통해 같이 스터디 참여할 인원을 모집하여 참여하게 되었다.</p>
<p>Early Developer Club으로 새로운 기술을 즐겁게 경험하고 삶에 적용하고 싶은 모임이며, 개발자가 아니더라도 참여할 수 있는 것이 큰 특징이었다.</p>
<p>EDC의 첫 스터디로 VDD(Vibe Driven Development)가 열렸다. </p>
<p>스터디 목표는 바이브 코딩을 경험하는 것이며 바이브 코딩은 다음과 같다. </p>
<p><strong>바이브 코딩(Vibe Coding)</strong></p>
<ul>
<li>개발자가 코드를 직접 설계하고 작성하는 기존 코딩 방식과 달리, AI를 파트너 삼아 자연어(사람의 언어)로 지시를 내리고 AI가 생성한 코드를 검토, 수정, 활용하는 방식입니다.</li>
</ul>
<p>VDD 첫 주는 자기소개와 앞으로의 활동 방향을 공유하는 <strong>온보딩 세션</strong>으로 시작되었다.  </p>
<p>다양한 직군의 스터디원들이 모여 다양한 방법으로 활용될 것을 생각하며 오히려 좋았다. </p>
<hr>
<h2 id="🧠-특강-ai-트렌드의-변화와-바이브코딩-툴-소개">🧠 특강: AI 트렌드의 변화와 바이브코딩 툴 소개</h2>
<p>이도하님의 세션에서는 최근 <strong>AI 트렌드의 흐름</strong>과 <strong>개발자가 알아두면 좋은 툴</strong> 중심으로 이야기가 진행되었다.  </p>
<pre><code>1️⃣ LLM의 이해
2️⃣ RAG (Retrieval Augmented Generation)
3️⃣ MCP (Model Context Protocol)</code></pre><p>다음과 같은 순서로 특강이 진행되었으며, 자세한 내용은 비밀 .. 
인상깊었던 점은 “<strong>귀멸의 칼날: 무한성편 = 거대한 맞춤형 AI 세계</strong>”라는 비유이다😄  </p>
<hr>
<h2 id="🧩-이번-주-소감">🧩 이번 주 소감</h2>
<p>첫 주차부터 AI 트렌드 전반과 Vibe Coding에 대해 폭넓게 배울 수 있었다.<br>LLM, RAG, MCP 같은 키워드를 중심으로 “AI를 어떻게 내 개발에 녹일 수 있을까?”에 대한 방향성이 잡힌 시간이었다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 SQL 고득점 Kit - 물고기 종류 별 잡은 수 구하기]]></title>
            <link>https://velog.io/@cheonroro_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-%EA%B3%A0%EB%93%9D%EC%A0%90-Kit-%EB%AC%BC%EA%B3%A0%EA%B8%B0-%EC%A2%85%EB%A5%98-%EB%B3%84-%EC%9E%A1%EC%9D%80-%EC%88%98-%EA%B5%AC%ED%95%98</link>
            <guid>https://velog.io/@cheonroro_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-%EA%B3%A0%EB%93%9D%EC%A0%90-Kit-%EB%AC%BC%EA%B3%A0%EA%B8%B0-%EC%A2%85%EB%A5%98-%EB%B3%84-%EC%9E%A1%EC%9D%80-%EC%88%98-%EA%B5%AC%ED%95%98</guid>
            <pubDate>Sun, 17 Aug 2025 10:15:04 GMT</pubDate>
            <description><![CDATA[<p>프로그래머스 SQL 고득점 Kit
<a href="https://school.programmers.co.kr/learn/challenges?tab=sql_practice_kit">https://school.programmers.co.kr/learn/challenges?tab=sql_practice_kit</a></p>
<h2 id="group-by">GROUP BY</h2>
<h3 id="problem">problem</h3>
<blockquote>
<p>FISH_NAME_INFO에서 물고기의 종류 별 물고기의 이름과 잡은 수를 출력하는 SQL문을 작성해주세요.
물고기의 이름 컬럼명은 <code>FISH_NAME</code>, 잡은 수 컬럼명은 <code>FISH_COUNT</code>로 해주세요.
결과는 잡은 수 기준으로 내림차순 정렬해주세요.</p>
</blockquote>
<h3 id="code">code</h3>
<pre><code class="language-sql">SELECT COUNT(*) AS FISH_COUNT,
       B.FISH_NAME AS FISH_NAME

FROM FISH_INFO A

JOIN FISH_NAME_INFO B

ON A.FISH_TYPE = B.FISH_TYPE

GROUP BY B.FISH_TYPE, B.FISH_NAME

ORDER BY FISH_COUNT DESC; </code></pre>
<h3 id="note">note</h3>
<p>처음에 작성한 쿼리는 다음과 같다.</p>
<pre><code class="language-sql">SELECT COUNT(A.FISH_TYPE) AS FISH_COUNT, 
       B.FISH_NAME AS FISH_NAME

FROM FISH_INFO AS A

JOIN FISH_NAME_INFO AS B

ON A.FISH_TYPE = B.FISH_TYPE

GROUP BY A.FISH_TYPE

ORDER BY FISH_COUNT;
</code></pre>
<p>안됬던 이유는 뭘까... </p>
<pre><code class="language-css">실패 (1055, &quot;Expression #3 of SELECT list is not in GROUP BY clause and contains 
nonaggregated column &#39;programmers.B.FISH_NAME&#39; which is not functionally dependent on
columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by&quot;)</code></pre>
<p>에러를 해석하면 <code>GROUP BY A.FISH_TYPE</code>만 있어서 <code>B.FISH_NAME</code>이 비집계 컬럼이 된다.
따라서 <code>B.FISH_NAME</code>을 가져올 수 없는 것이다.
이 문제를 <code>GROUP BY B.FISH_TPYE, B.FISH_NAME</code>으로 바꿔주며 해결하였다.
요즘 문제를 풀면서 느끼는 생각인데, 과연 실무에 들어갔을 때 내가 원하는 결과 값을 한번에 쿼리를 작성해서 도출할 수 있을까?? 라는 생각이 든다.
공부를 하면 할수록 부족함을 느끼고 그에 따라 불안감도 커져만 간다.
나를 믿고 하루하루 후회없이 살아보자 🔥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 SQL 고득점 Kit - 조건에 맞는 사원 정보 조회하기]]></title>
            <link>https://velog.io/@cheonroro_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-%EA%B3%A0%EB%93%9D%EC%A0%90-Kit-%EC%A1%B0%EA%B1%B4%EC%97%90-%EB%A7%9E%EB%8A%94-%EC%82%AC%EC%9B%90-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cheonroro_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-%EA%B3%A0%EB%93%9D%EC%A0%90-Kit-%EC%A1%B0%EA%B1%B4%EC%97%90-%EB%A7%9E%EB%8A%94-%EC%82%AC%EC%9B%90-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 17 Aug 2025 06:31:37 GMT</pubDate>
            <description><![CDATA[<p>프로그래머스 SQL 고득점 Kit
<a href="https://school.programmers.co.kr/learn/challenges?tab=sql_practice_kit">https://school.programmers.co.kr/learn/challenges?tab=sql_practice_kit</a></p>
<h2 id="group-by">GROUP BY</h2>
<h3 id="problem">problem</h3>
<blockquote>
<p>HR_DEPARTMENT, HR_EMPLOYEES, HR_GRADE 테이블에서 2022년도 한해 평가 점수가 가장 높은 사원 정보를 조회하려 합니다. 
2022년도 평가 점수가 가장 높은 사원들의 점수, 사번, 성명, 직책, 이메일을 조회하는 SQL문을 작성해주세요.
2022년도의 평가 점수는 상,하반기 점수의 합을 의미하고, 평가 점수를 나타내는 컬럼의 이름은 SCORE로 해주세요.</p>
</blockquote>
<h3 id="code">code</h3>
<pre><code class="language-sql">-- 코드를 작성해주세요
SELECT G.SCORE, E.EMP_NO, E.EMP_NAME, E.POSITION, E.EMAIL

FROM HR_EMPLOYEES AS E

JOIN (SELECT EMP_NO, SUM(SCORE) AS SCORE

      FROM HR_GRADE

      GROUP BY EMP_NO) AS G

ON E.EMP_NO = G.EMP_NO

ORDER BY SCORE DESC

LIMIT 1;        </code></pre>
<h3 id="note">note</h3>
<p>테이블이 3개가 나와서 당황했지만 쉽게 풀 수 있는 문제였다.
어떻게 하면 더욱 효율적이고, 문제에서 요구한 바를 정확하게 표현할 수 있을까? 고민했다. 총 3가지를 보완할 수 있었다.</p>
<pre><code>2022년 필터 빠짐 → WHERE YEAR = 2022

동점 처리 → LIMIT 1은 동점을 잘라버림. “가장 높은 사원들”이면 동점 모두 반환

불필요한 작업 최소화 → 집계 끝난 뒤에만 사원 테이블과 조인</code></pre><pre><code class="language-sql">WITH yearly AS (
  SELECT
    EMP_NO,
    SUM(SCORE) AS SCORE
  FROM HR_GRADE
  WHERE `YEAR` = 2022
  GROUP BY EMP_NO
)

SELECT
  y.SCORE,
  e.EMP_NO,
  e.EMP_NAME,
  e.`POSITION`,
  e.EMAIL

FROM (
  SELECT
    EMP_NO,
    SCORE,
    DENSE_RANK() OVER (ORDER BY SCORE DESC) AS rnk
  FROM yearly
) y

JOIN HR_EMPLOYEES e ON e.EMP_NO = y.EMP_NO

WHERE y.rnk = 1;
</code></pre>
<p>추가로 조만간 <code>WINDOW FUNCTION</code> (윈도우 함수)에 대해 정리하는 시간을 가져야겠다!</p>
]]></description>
        </item>
    </channel>
</rss>