<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>8_1st0.log</title>
        <link>https://velog.io/</link>
        <description>Running on hopes and tiny skills...</description>
        <lastBuildDate>Tue, 18 Nov 2025 11:54:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>8_1st0.log</title>
            <url>https://velog.velcdn.com/images/8_1st0/profile/d50cbeb3-8906-4cfb-8db6-87029e392e32/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 8_1st0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/8_1st0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[251118] 내배캠 D+21]]></title>
            <link>https://velog.io/@8_1st0/251118-%EB%82%B4%EB%B0%B0%EC%BA%A0-D21</link>
            <guid>https://velog.io/@8_1st0/251118-%EB%82%B4%EB%B0%B0%EC%BA%A0-D21</guid>
            <pubDate>Tue, 18 Nov 2025 11:54:06 GMT</pubDate>
            <description><![CDATA[<h2 id="music-domain-sql-practice-subquery--join-skills">Music Domain SQL Practice (Subquery &amp; JOIN Skills)</h2>
<h2 id="📌-til">📌 TIL</h2>
<p>오늘은 <strong>Spotify/Melon/Youtube Music 같은 음악 스트리밍 서비스에서 쓸 법한 DB 스키마</strong>를 기반으로 다양한 SQL 실습을 하며, 그 과정 중에서 <strong>오답</strong>, <strong>실패한 시도</strong>, <strong>정답을 위한 과정</strong>, <strong>최종 해결 쿼리</strong>까지 모두 기록한 TIL이다.</p>
<h1 id="music-is-my-life🎧🎤🎹🔊🎶🎛️">music is my life<del>~</del>🎧🎤🎹🔊🎶🎛️</h1>
<p><img src="https://velog.velcdn.com/images/8_1st0/post/13eed92a-16f0-48a9-9277-be6dbdad74c3/image.gif" alt=""></p>
<hr>
<h2 id="1-사용한-스키마-정의">1. 사용한 스키마 정의</h2>
<p>오늘 실습에서는 아래와 같은 음악 도메인 스키마를 사용했다.</p>
<h3 id="🎵-music-domain-schema">🎵 Music Domain Schema</h3>
<pre><code class="language-sql">-- Artists table
CREATE TABLE artists (
    artist_id INT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    debut_year INT
);

-- Albums table
CREATE TABLE albums (
    album_id INT PRIMARY KEY,
    artist_id INT,
    title VARCHAR(255) NOT NULL,
    release_year INT,
    FOREIGN KEY (artist_id) REFERENCES artists(artist_id)
);

-- Tracks table
CREATE TABLE tracks (
    track_id INT PRIMARY KEY,
    album_id INT,
    title VARCHAR(255) NOT NULL,
    duration_sec INT,
    genre VARCHAR(50),
    FOREIGN KEY (album_id) REFERENCES albums(album_id)
);

-- Users table
CREATE TABLE users (
    user_id INT PRIMARY KEY,
    nickname VARCHAR(255),
    signup_date DATE
);

-- Play history table
CREATE TABLE plays (
    play_id INT PRIMARY KEY,
    user_id INT,
    track_id INT,
    played_at DATETIME,
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (track_id) REFERENCES tracks(track_id)
);</code></pre>
<hr>
<h2 id="2-실습-목표">2. 실습 목표</h2>
<ul>
<li>JOIN 심화 (INNER, LEFT, SELF JOIN)</li>
<li>Subquery 2~4개를 활용한 문제 해결</li>
<li>음악 스트리밍에서 발생할 법한 데이터 분석 상황 가정</li>
<li>GPT에게 문제 제시 요구, 틀리면 왜 틀렸는지 분석</li>
<li>정답으로 가기 위한 사고 과정 기록</li>
</ul>
<hr>
<h2 id="3-실습-문제--풀이-과정">3. 실습 문제 &amp; 풀이 과정</h2>
<h3 id="🎯-문제-1-가장-많이-재생된-곡-top-10-찾기">🎯 문제 1. &quot;가장 많이 재생된 곡 TOP 10 찾기&quot;</h3>
<h3 id="🐞-첫-번째-시도-틀린-쿼리">🐞 첫 번째 시도 (틀린 쿼리)</h3>
<pre><code class="language-sql">SELECT t.title, COUNT(*) AS play_count
FROM tracks t
JOIN plays p ON t.track_id = p.track_id
ORDER BY play_count DESC
LIMIT 10;</code></pre>
<h3 id="📝-왜-틀렸나">📝 왜 틀렸나?</h3>
<ul>
<li><code>GROUP BY</code> 없음 → MySQL ONLY_FULL_GROUP_BY 설정이면 에러</li>
<li>MySQL이 허용하더라도 비표준</li>
<li>사실 문제 제대로 안 봐서 틀린 것임.</li>
</ul>
<h3 id="🐞-두-번째-시도">🐞 두 번째 시도</h3>
<pre><code class="language-sql">SELECT t.title, COUNT(*) AS play_count
FROM tracks t
JOIN plays p ON t.track_id = p.track_id
GROUP BY t.title
ORDER BY play_count DESC
LIMIT 10;</code></pre>
<h3 id="❗-아티스트-정보가-필요하단-걸-놓쳤다-">❗ 아티스트 정보가 필요하단 걸 놓쳤다 !</h3>
<p>→ JOIN을 하나 더 해야 한다 (albums → artists)</p>
<h3 id="🎯-최종-쿼리">🎯 최종 쿼리</h3>
<pre><code class="language-sql">SELECT a.name AS artist_name, t.title AS track_title, COUNT(*) AS play_count
FROM plays p
JOIN tracks t ON p.track_id = t.track_id
JOIN albums al ON t.album_id = al.album_id
JOIN artists a ON al.artist_id = a.artist_id
GROUP BY a.name, t.title
ORDER BY play_count DESC
LIMIT 10;</code></pre>
<hr>
<h3 id="📝-문제-2-indie-장르에서-가장-오래된-아티스트-찾기">📝 문제 2. &quot;Indie 장르에서 가장 오래된 아티스트 찾기&quot;</h3>
<h3 id="🐞-첫-시도-틀린-쿼리">🐞 첫 시도 (틀린 쿼리)</h3>
<pre><code class="language-sql">SELECT a.name, a.debut_year
FROM artists a
JOIN albums al ON a.artist_id = al.artist_id
JOIN tracks t ON al.album_id = t.album_id
WHERE t.genre = &#39;Indie&#39;
ORDER BY a.debut_year
LIMIT 1;</code></pre>
<h3 id="❌-문제점">❌ 문제점</h3>
<ul>
<li>특정 아티스트가 Pop 장르로 여러 곡을 냈으면 <strong>중복 발생</strong></li>
<li>DISTINCT 필요 또는 서브쿼리 필요</li>
</ul>
<h3 id="🐞-중복-제거-시도">🐞 중복 제거 시도</h3>
<pre><code class="language-sql">SELECT DISTINCT a.artist_id, a.name, a.debut_year
FROM artists a
JOIN albums al ON a.artist_id = al.artist_id
JOIN tracks t ON al.album_id = t.album_id
WHERE t.genre = &#39;Pop&#39;
ORDER BY a.debut_year
LIMIT 1;</code></pre>
<p>하지만 &#39;Pop 장르 트랙을 가진 아티스트 중 데뷔년도가 가장 빠른 사람&#39;을 찾는 더 깔끔한 방식: <strong>서브쿼리 활용</strong></p>
<h3 id="🎯-최종-서브쿼리-기반-풀이">🎯 최종 서브쿼리 기반 풀이</h3>
<pre><code class="language-sql">SELECT a.name, a.debut_year
FROM artists a
WHERE a.artist_id IN (
    SELECT DISTINCT al.artist_id
    FROM albums al
    JOIN tracks t ON al.album_id = t.album_id
    WHERE t.genre = &#39;Pop&#39;
)
ORDER BY a.debut_year ASC
LIMIT 1;</code></pre>
<hr>
<h3 id="📝-문제-3-유저별-가장-많이-들은-장르-찾기-서브쿼리-3개">📝 문제 3. &quot;유저별 가장 많이 들은 장르 찾기 (서브쿼리 3개)&quot;</h3>
<h3 id="🐞-첫-시도--비효율적이고-틀린-접근">🐞 첫 시도 — 비효율적이고 틀린 접근</h3>
<pre><code class="language-sql">SELECT u.nickname, t.genre, COUNT(*) AS cnt
FROM users u
JOIN plays p ON u.user_id = p.user_id
JOIN tracks t ON p.track_id = t.track_id
GROUP BY u.user_id
ORDER BY cnt DESC;</code></pre>
<h3 id="❌-문제점-1">❌ 문제점</h3>
<ul>
<li>유저별 <strong>최다 장르</strong>를 구하지 못함 → 유저별 집계가 아님</li>
<li>GROUP BY 컬럼 잘못함</li>
</ul>
<h3 id="🐞-두-번째-시도--유저장르로-그룹화">🐞 두 번째 시도 — 유저+장르로 그룹화</h3>
<pre><code class="language-sql">SELECT u.user_id, t.genre, COUNT(*) AS cnt
FROM users u
JOIN plays p ON u.user_id = p.user_id
JOIN tracks t ON p.track_id = t.track_id
GROUP BY u.user_id, t.genre;</code></pre>
<p>이제 유저별로 장르별 재생 수가 나옴 → 그중 MAX 장르를 뽑으면 됨.</p>
<h3 id="🎯-최종--서브쿼리-3단-구성">🎯 최종 — 서브쿼리 3단 구성</h3>
<pre><code class="language-sql">SELECT final.user_id, final.genre, final.cnt
FROM (
    SELECT ug.user_id, ug.genre, ug.cnt
    FROM (
        SELECT u.user_id, t.genre, COUNT(*) AS cnt
        FROM users u
        JOIN plays p ON u.user_id = p.user_id
        JOIN tracks t ON p.track_id = t.track_id
        GROUP BY u.user_id, t.genre
    ) ug
    JOIN (
        SELECT user_id, MAX(cnt) AS max_cnt
        FROM (
            SELECT u.user_id, t.genre, COUNT(*) AS cnt
            FROM users u
            JOIN plays p ON u.user_id = p.user_id
            JOIN tracks t ON p.track_id = t.track_id
            GROUP BY u.user_id, t.genre
        ) sub
        GROUP BY user_id
    ) mx
    ON ug.user_id = mx.user_id AND ug.cnt = mx.max_cnt
) final;</code></pre>
<hr>
<h3 id="📝-문제-4-특정-사용자user_id--10가-가장-좋아하는-아티스트-찾기">📝 문제 4. &quot;특정 사용자(user_id = 10)가 가장 좋아하는 아티스트 찾기&quot;</h3>
<h3 id="🐞-틀린-시도">🐞 틀린 시도</h3>
<pre><code class="language-sql">SELECT a.name, COUNT(*)
FROM plays p
JOIN tracks t ON p.track_id = t.track_id
JOIN albums al ON t.album_id = al.album_id
JOIN artists a ON al.artist_id = a.artist_id
WHERE p.user_id = 10
GROUP BY a.artist_id;</code></pre>
<h3 id="❌-문제">❌ 문제</h3>
<ul>
<li>가장 좋아하는 = 재생수 최다 → MAX 필요</li>
<li>ORDER BY, LIMIT 추가</li>
</ul>
<h3 id="🎯-최종">🎯 최종</h3>
<pre><code class="language-sql">SELECT a.name, COUNT(*) AS play_count
FROM plays p
JOIN tracks t ON p.track_id = t.track_id
JOIN albums al ON t.album_id = al.album_id
JOIN artists a ON al.artist_id = a.artist_id
WHERE p.user_id = 10
GROUP BY a.artist_id, a.name
ORDER BY play_count DESC
LIMIT 1;</code></pre>
<hr>
<h2 id="4-join-오류-모음-및-교훈">4. JOIN 오류 모음 및 교훈</h2>
<h3 id="🤦-실수-1--join-조건-빠뜨림">🤦 실수 1 — JOIN 조건 빠뜨림</h3>
<pre><code class="language-sql">SELECT *
FROM tracks t
JOIN albums al;</code></pre>
<ul>
<li>CROSS JOIN 발생 → 예상치 못한 결과</li>
</ul>
<h3 id="🤦-실수-2--컬럼명이-동일한데-테이블-alias-안-붙이기">🤦 실수 2 — 컬럼명이 동일한데 테이블 alias 안 붙이기</h3>
<pre><code class="language-sql">SELECT title FROM tracks JOIN albums;</code></pre>
<ul>
<li>ambiguous column 에러</li>
</ul>
<h3 id="🤦-실수-3--left-join인데-where에서-null-제거">🤦 실수 3 — LEFT JOIN인데 WHERE에서 NULL 제거</h3>
<pre><code class="language-sql">SELECT *
FROM artists a
LEFT JOIN albums al ON a.artist_id = al.artist_id
WHERE al.album_id IS NOT NULL;</code></pre>
<ul>
<li>사실상 INNER JOIN이 되어버림</li>
</ul>
<hr>
<h2 id="5-subquery-실수-모음">5. Subquery 실수 모음</h2>
<h3 id="🤦-실수-1--서브쿼리에서-여러-행-반환되는-걸-where--로-비교">🤦 실수 1 — 서브쿼리에서 여러 행 반환되는 걸 WHERE = 로 비교</h3>
<pre><code class="language-sql">WHERE artist_id = (SELECT artist_id FROM albums);</code></pre>
<h3 id="🤦-실수-2--from-서브쿼리-alias-누락">🤦 실수 2 — FROM 서브쿼리 alias 누락</h3>
<pre><code class="language-sql">SELECT *
FROM (SELECT * FROM tracks);</code></pre>
<p>→ <code>ERROR: Every derived table must have its own alias</code></p>
<h3 id="🤦-실수-3--select-서브쿼리에서-limit-없이-max-기능-흉내">🤦 실수 3 — SELECT 서브쿼리에서 LIMIT 없이 MAX 기능 흉내</h3>
<pre><code class="language-sql">SELECT (SELECT debut_year FROM artists ORDER BY debut_year);</code></pre>
<p>→ 다중 행 반환 에러</p>
<hr>
<h2 id="6-오늘-til-요약">6. 오늘 TIL 요약</h2>
<p>SQL 사고 과정, JOIN 구조 분석, 서브쿼리를 왜 사용하는지, INDEX가 필요할 수 있는 지점, 실제 서비스에서 쿼리 최적화가 어떤 식으로 일어나는지 등의 내용을 자세히 서술했다. 또한 문제를 해결하며 겪은 시행착오를 실례로 다시 설명했다.</p>
<hr>
<h3 id="🔍-1-join을-실제-음악-스트리밍-서비스에서-어떻게-쓰는가">🔍 1) JOIN을 실제 음악 스트리밍 서비스에서 어떻게 쓰는가</h3>
<p>음악 스트리밍 서비스에서는 수많은 테이블이 서로 연결된다. 아티스트–앨범–트랙은 기본 구조이고, 여기에 사용자 정보(users), 재생 기록(plays), 플레이리스트(playlists), 좋아요 정보(likes), 구독 상품(subscription) 같은 데이터가 붙는다. 이런 구조에서 JOIN은 <strong>비즈니스 지표를 구축하는 핵심 도구</strong>가 된다.</p>
<p>예를 들어, 특정 유저가 가장 많이 듣는 아티스트를 구하는 쿼리는 <code>사용자 추천 모델의 기반</code>이 된다. 또 어떤 장르가 특정 연령대에서 많이 들리는지 확인하면 <code>큐레이션 알고리즘의 근거</code>가 될 수 있다. 그리고 이러한 데이터는 대개 여러 테이블을 한 번에 묶어야 나오기 때문에 <code>JOIN</code>이 필수다.</p>
<h3 id="🔍-2-서브쿼리를-사용하는-이유">🔍 2) 서브쿼리를 사용하는 이유</h3>
<p>서브쿼리는 크게 두 가지 이유로 쓰게 된다.</p>
<ol>
<li><strong>중간 결과를 만들기 위해</strong></li>
<li><strong>필터링 조건을 더 정교하게 주기 위해</strong></li>
</ol>
<p>JOIN만으로는 답을 내기 까다로운 문제도 있고, 
필터링 조건이 <code>최대 재생 수를 가진 장르</code>, <code>특정 장르를 가진 아티스트</code>, <code>재생 수 상위 5% 안에 드는 사용자</code>처럼 복잡해지면 서브쿼리가 훨씬 직관적이고 유지보수하기도 쉽다.</p>
<h3 id="🔍-3-실무에서-자주-등장하는-패턴들">🔍 3) 실무에서 자주 등장하는 패턴들</h3>
<p>오늘 실습 과정에서 자연스럽게 등장한 패턴들은 음악 서비스뿐 아니라 모든 서비스에서 자주 쓰인다.</p>
<h4 id="🧩-패턴-1-집계-후-다시-join하는-방식">🧩 패턴 1: 집계 후 다시 JOIN하는 방식</h4>
<ul>
<li>예: 유저별 가장 많이 들은 장르, 유저별 가장 많이 들은 아티스트</li>
<li>이유: <code>GROUP BY user_id, genre</code> 같은 중간 결과에서 MAX(cnt)를 구한 뒤 다시 JOIN해야 원하는 행만 뽑을 수 있기 때문</li>
</ul>
<h4 id="🧩-패턴-2-distinct-기반-필터링">🧩 패턴 2: DISTINCT 기반 필터링</h4>
<ul>
<li>예: 어떤 아티스트가 특정 장르를 보유했는지 판별</li>
<li>JOIN만으로 중복이 생길 때 DISTINCT 또는 GROUP BY로 정제하는 방식</li>
</ul>
<h4 id="🧩-패턴-3-from-서브쿼리alias-필수">🧩 패턴 3: FROM 서브쿼리(alias 필수)</h4>
<ul>
<li>복잡한 계산을 한 번에 처리하기 위해 중간 테이블을 만드는 방식</li>
<li>이 패턴을 활용하면 쿼리가 구조화되어 훨씬 읽기 좋아짐</li>
</ul>
<h3 id="🔍-5-추가적인-실습을-통해-느낀-것들">🔍 5) 추가적인 실습을 통해 느낀 것들</h3>
<ol>
<li><p><strong>쿼리를 작성하기 전에 데이터를 먼저 이미지화해보면 에러가 줄어든다.</strong></p>
<ul>
<li>예: users → plays → tracks → albums → artists</li>
<li>이런 선형 관계를 머릿속에서 먼저 그려보면 JOIN 조건을 실수할 가능성이 훨씬 줄어든다.</li>
</ul>
</li>
<li><p><strong>GROUP BY와 ORDER BY는 의도에 따라 결과가 완전히 달라진다.</strong></p>
<ul>
<li>예: GROUP BY를 잘못하면 전체 기준이 아니라 특정 기준으로 묶여버리고, ORDER BY는 중간 결과 기준인지 최종 결과 기준인지 헷갈리기 쉽다.</li>
</ul>
</li>
<li><p><strong>서브쿼리는 과용하면 느려질 수 있다.</strong></p>
<ul>
<li>하지만 오늘 같은 문제에서는 오히려 서브쿼리가 더 직관적이었다.</li>
<li>실무에서는 index, explain plan을 보고 최적화할 필요가 있다.</li>
</ul>
</li>
</ol>
<h3 id="🔍-6-오늘-실습에서-얻은-핵심-정리">🔍 6) 오늘 실습에서 얻은 핵심 정리</h3>
<ul>
<li>JOIN은 많아도 상관없지만, ON 조건이 틀리면 모든 게 틀린다.</li>
<li>서브쿼리는 <strong>정답을 뽑는 과정</strong>을 계층적으로 표현할 때 매우 강력하다.</li>
<li>실습을 하면서 틀린 쿼리를 먼저 작성하는 것도 공부에 큰 도움이 된다.</li>
<li>음악 스트리밍 구조는 테이블 간 관계가 명확해 SQL 연습하기 좋은 도메인이다.</li>
<li>중첩된 집계는 한 번의 GROUP BY로 절대 해결되지 않는다.</li>
<li>DISTINCT와 GROUP BY는 비슷해 보이지만 목적이 다르다. 상황에 따라 적절히 선택해야 한다.</li>
</ul>
<h3 id="🔍-7-마무리-sql은-결국-사고방식의-문제">🔍 7) 마무리: SQL은 결국 사고방식의 문제</h3>
<p>SQL은 &quot;문법&quot;보다 &quot;사고 과정&quot;이 훨씬 더 중요하다.</p>
<ul>
<li>어떤 중간 구조를 만들 것인지</li>
<li>어떤 조건이 데이터의 범위를 결정하는지</li>
<li>어떤 기준으로 집계할지</li>
<li>어떤 순서로 JOIN해야 하는지</li>
</ul>
<p>이 네 가지를 명확히 하면 서브쿼리든 JOIN이든 자연스럽게 흘러간다.</p>
<p>지금까지 작성한 문제들은 음악 도메인 SQL의 좋은 예시였고, 앞으로도 조금 더 난도 높은 문제(예: 윈도우 함수 포함, 재생 패턴 분석, retention 기반 지표 계산 등)를 이어서 작성해볼 수 있을 것 같다. 재밌었다 오늘<del>!#@!</del>@!~</p>
<hr>
<h1 id="끝">끝<del>#@</del>!#<del>@!!#</del>!@~!#</h1>
]]></description>
        </item>
    </channel>
</rss>