<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>no-glass-otacku.log</title>
        <link>https://velog.io/</link>
        <description>이제 개발해야지...</description>
        <lastBuildDate>Wed, 22 Jul 2026 05:00:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>no-glass-otacku.log</title>
            <url>https://velog.velcdn.com/images/no-glass-otacku/profile/baf194e2-1f83-4314-a853-1d5b5a30163c/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. no-glass-otacku.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/no-glass-otacku" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Azure SQL Database CPU 사용률 알림]]></title>
            <link>https://velog.io/@no-glass-otacku/Azure-SQL-Database-CPU-%EC%82%AC%EC%9A%A9%EB%A5%A0-%EC%95%8C%EB%A6%BC-vi08n5sr</link>
            <guid>https://velog.io/@no-glass-otacku/Azure-SQL-Database-CPU-%EC%82%AC%EC%9A%A9%EB%A5%A0-%EC%95%8C%EB%A6%BC-vi08n5sr</guid>
            <pubDate>Wed, 22 Jul 2026 05:00:37 GMT</pubDate>
            <description><![CDATA[<p>구현 시에는 86. Lab CPU 사용량식별 및 경고 v2.0 문서를 참고.</p>
<h2 id="1-한-줄-정의">1. 한 줄 정의</h2>
<p>Azure SQL Database의 CPU 사용률이 일정 임계값(예: 평균 80%)을 넘으면 Azure Monitor를 통해 자동으로 이메일 알림이 오도록 Alert Rule을 구성하는 실습을 진행했다.</p>
<h2 id="2-이걸-왜-하는지-핵심-개념">2. 이걸 왜 하는지 (핵심 개념)</h2>
<ul>
<li><strong>Signal / Threshold / Action Group</strong> 구조가 핵심이다. Signal은 감시할 지표(CPU %), Threshold는 위험 판단 기준(평균 80% 초과), Action Group은 위험할 때 실행할 조치(이메일 알림)를 의미한다.</li>
<li>문제가 생긴 <strong>후에</strong> 로그를 뒤져서 원인을 찾는 것과, 문제가 생기기 <strong>전에</strong> 미리 알림을 받아 대응하는 것은 다르다. 알림 체계는 후자를 가능하게 해준다.</li>
<li>Aggregation Type을 <strong>Average</strong>로 설정하는 이유는, 순간적인 CPU 스파이크(쿼리 하나가 잠깐 튀는 것)와 지속적인 부하(계속 높은 상태가 유지되는 것)를 구분하기 위함이다. 실제로 위험한 건 후자이기 때문에 평균값 기준으로 판단한다.</li>
<li>Elastic Pool을 쓰는 환경에서는 여러 DB가 CPU/메모리를 공유 자원 풀로 나눠 쓰기 때문에, DB 하나의 부하가 다른 DB의 성능에도 영향을 줄 수 있다. 이런 구조에서는 어느 DB가 원인인지 특정하기 위해 DB별 CPU 알림이 더 중요해진다.</li>
</ul>
<h2 id="3-내-프로젝트와-연결">3. 내 프로젝트와 연결</h2>
<p>방역로 프로젝트에서 Azure Functions(<code>fn_weather</code>, <code>fn_kahis</code>, <code>fn_disinfection</code>, <code>fn_migratory</code>)로 매일 자동으로 데이터를 수집해 적재하는 파이프라인을 구축했다. 이런 정기 배치 구조를 실제 운영 환경으로 확장한다면, 데이터 양이 늘어나거나 특정 배치 작업이 무거워질 때 DB에 지속적인 부하가 걸릴 수 있다. 이번 실습에서 배운 CPU 알림을 적용하면 이런 상황을 사후에 로그로 확인하는 게 아니라 사전에 감지하고 대응할 수 있다고 생각했다.</p>
<h2 id="4-예상-꼬리질문-대비">4. 예상 꼬리질문 대비</h2>
<p><strong>Q. Static threshold 말고 Dynamic threshold는 언제 써요?</strong>
A. 트래픽 패턴이 일정하지 않고 가변적인 경우, 고정된 임계값(예: 80%)은 false positive를 자주 발생시킬 수 있다. Dynamic threshold는 ML 기반으로 정상 패턴을 학습해 임계값을 자동 조정하기 때문에, 트래픽이 시간대별로 크게 변하는 서비스에 적합하다. 다만 정확도를 위해 14일 이상의 메트릭 히스토리가 필요하다.</p>
<p><strong>Q. Elastic Pool에서는 이 알림이 왜 더 중요해요?</strong>
A. Pool 밖에서는 각 DB가 자원을 독점하기 때문에 한 DB의 부하가 다른 DB에 영향을 주지 않지만, Pool 안에서는 자원을 공유하기 때문에 한 DB의 부하 급증이 다른 DB들의 성능 저하로 이어질 수 있다. 이런 연쇄 영향을 조기에 감지하고 원인 DB를 특정하기 위해 DB별 알림이 더 중요하다.</p>
<p><strong>Q. 알림을 받으면 그다음엔 뭘 해요?</strong>
A. 원인 쿼리를 확인해 최적화하거나, 지속적으로 부하가 높다면 더 높은 컴퓨팅 티어로 스케일업을 검토한다. 자동화까지 고려한다면 Action Group에 Webhook이나 Logic App을 연결해 임계값 초과 시 자동 스케일업 같은 대응도 구성할 수 있다.</p>
<h2 id="5-헷갈렸던-부분-복습용-메모">5. 헷갈렸던 부분 (복습용 메모)</h2>
<ul>
<li>CPU 80%는 물리적 CPU 성능의 80%가 아니라, <strong>내가 할당받은 컴퓨팅 티어(DTU 또는 vCore) 대비 사용 비율</strong>이다.</li>
<li>논리 서버(Logical Server)는 실제 컴퓨터가 아니라 로그인/방화벽 등을 관리하는 껍데기이고, CPU 같은 실제 자원은 서버가 아니라 그 안의 개별 DB(또는 Elastic Pool) 단위로 할당된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[PySpark 6 - Query Optimization]]></title>
            <link>https://velog.io/@no-glass-otacku/PySpark-6-Query-Optimization</link>
            <guid>https://velog.io/@no-glass-otacku/PySpark-6-Query-Optimization</guid>
            <pubDate>Fri, 19 Jun 2026 08:21:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>13 Query Optimization</p>
</blockquote>
<hr>
<h2 id="📌-핵심-개념-catalyst-옵티마이저">📌 핵심 개념: Catalyst 옵티마이저</h2>
<p>Spark는 코드를 실행하기 전에 <strong>Catalyst</strong>라는 최적화 엔진이 쿼리 계획을 자동으로 다듬어줌.</p>
<blockquote>
<p><strong>비유</strong>: 내가 요리 순서를 비효율적으로 짜도, 주방장(Catalyst)이 알아서 더 효율적인 순서로 바꿔서 실행하는 것</p>
</blockquote>
<hr>
<h2 id="1-explain--쿼리-계획-확인">1. explain() — 쿼리 계획 확인</h2>
<pre><code class="language-python">df.explain(True)</code></pre>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>True</code></td>
<td>논리적 계획 + 물리적 계획 모두 출력</td>
</tr>
<tr>
<td><code>False</code> / 생략</td>
<td>물리적 계획만 출력</td>
</tr>
</tbody></table>
<p>출력 순서:</p>
<pre><code>== Parsed Logical Plan ==      ← 내가 쓴 코드 그대로
== Analyzed Logical Plan ==    ← 컬럼/타입 검증 후
== Optimized Logical Plan ==   ← Catalyst가 최적화한 버전  ← 여기가 핵심
== Physical Plan ==            ← 실제 실행될 계획</code></pre><blockquote>
<p>실무에서는 <strong>Optimized Logical Plan</strong>과 <strong>Physical Plan</strong>을 주로 봄.<br>&quot;내가 짠 코드가 실제로 어떻게 실행되나?&quot; 확인할 때 사용.</p>
</blockquote>
<hr>
<h2 id="2-논리적-최적화-logical-optimization">2. 논리적 최적화 (Logical Optimization)</h2>
<h3 id="2-1-filter-자동-병합">2-1. filter 자동 병합</h3>
<p>filter를 여러 번 체이닝해도 Catalyst가 <strong>하나로 합쳐서</strong> 실행함.</p>
<pre><code class="language-python"># 내가 쓴 코드 (filter 8번)
limit_events_df = (df
    .filter(col(&quot;event_name&quot;) != &quot;reviews&quot;)
    .filter(col(&quot;event_name&quot;) != &quot;checkout&quot;)
    .filter(col(&quot;event_name&quot;) != &quot;register&quot;)
    ...8개...
)

# Catalyst가 실제로 실행하는 것 (filter 1번으로 합쳐짐)
# → Optimized Plan에서 확인 가능</code></pre>
<p>그래서 이렇게 써도 성능 차이 없음:</p>
<pre><code class="language-python"># 한 번에 쓴 버전 — 가독성은 더 좋음
better_df = (df
    .filter(
        (col(&quot;event_name&quot;) != &quot;reviews&quot;) &amp;
        (col(&quot;event_name&quot;) != &quot;checkout&quot;) &amp;
        (col(&quot;event_name&quot;) != &quot;register&quot;) &amp;
        ...
    )
)</code></pre>
<blockquote>
<p><strong>실무 팁</strong>: 성능은 같으니까 <strong>가독성 기준으로 선택</strong>하면 됨.<br>조건이 많으면 한 번에 쓰는 게 더 보기 좋음.</p>
</blockquote>
<hr>
<h3 id="2-2-중복-filter-자동-제거">2-2. 중복 filter 자동 제거</h3>
<p>같은 조건을 실수로 여러 번 써도 Catalyst가 <strong>중복을 제거</strong>하고 한 번만 실행함.</p>
<pre><code class="language-python"># 이렇게 실수로 써도
stupid_df = (df
    .filter(col(&quot;event_name&quot;) != &quot;finalize&quot;)
    .filter(col(&quot;event_name&quot;) != &quot;finalize&quot;)
    .filter(col(&quot;event_name&quot;) != &quot;finalize&quot;)
    .filter(col(&quot;event_name&quot;) != &quot;finalize&quot;)
    .filter(col(&quot;event_name&quot;) != &quot;finalize&quot;)
)

# Catalyst가 filter 1번으로 줄여서 실행</code></pre>
<blockquote>
<p><strong>비유</strong>: &quot;문 잠갔어? 잠갔어? 잠갔어?&quot; 라고 5번 물어봐도 실제로 문 확인은 한 번만 하는 것</p>
</blockquote>
<hr>
<h2 id="3-조건자-푸시다운-predicate-pushdown">3. 조건자 푸시다운 (Predicate Pushdown)</h2>
<blockquote>
<p><strong>조건자(Predicate)</strong> = filter 조건<br><strong>푸시다운(Pushdown)</strong> = 가능한 한 데이터 소스에 가까운 쪽으로 밀어 내리는 것</p>
</blockquote>
<h3 id="개념">개념</h3>
<pre><code>일반적 실행 순서:
파일에서 전체 데이터 읽기 → 메모리에 올림 → filter 적용

푸시다운 후:
파일을 읽을 때 filter를 같이 적용 → 처음부터 필요한 데이터만 읽음</code></pre><blockquote>
<p><strong>비유</strong>: 도서관에서 책 1000권 다 꺼낸 후 골라내기 vs. 처음부터 원하는 책만 꺼내기</p>
</blockquote>
<h3 id="언제-작동하나">언제 작동하나?</h3>
<p>데이터 소스가 푸시다운을 지원할 때 자동으로 작동:</p>
<table>
<thead>
<tr>
<th>데이터 소스</th>
<th>푸시다운 지원</th>
</tr>
</thead>
<tbody><tr>
<td>Parquet</td>
<td>✅</td>
</tr>
<tr>
<td>Delta Lake</td>
<td>✅</td>
</tr>
<tr>
<td>JDBC (DB 연결)</td>
<td>✅ (DB가 직접 필터링)</td>
</tr>
<tr>
<td>CSV</td>
<td>❌ (전체 읽은 후 필터링)</td>
</tr>
<tr>
<td>JSON</td>
<td>❌</td>
</tr>
</tbody></table>
<h3 id="explain으로-확인하는-법">explain()으로 확인하는 법</h3>
<pre><code class="language-python">df.filter(col(&quot;event_name&quot;) == &quot;purchase&quot;).explain(True)</code></pre>
<p>Physical Plan에서 <code>PushedFilters</code>가 보이면 푸시다운 작동 중:</p>
<pre><code>== Physical Plan ==
*(1) Filter (isnotnull(event_name) AND (event_name = purchase))
+- *(1) ColumnarToRow
   +- FileScan parquet [...]
      PushedFilters: [IsNotNull(event_name), EqualTo(event_name, purchase)]  ← 이게 보이면 OK</code></pre><h3 id="푸시다운이-작동-안-하는-경우">푸시다운이 작동 안 하는 경우</h3>
<p>UDF(사용자 정의 함수)를 filter에 쓰면 푸시다운 불가:</p>
<pre><code class="language-python">from pyspark.sql.functions import udf

@udf(&quot;boolean&quot;)
def is_purchase(name):
    return name == &quot;purchase&quot;

# 이렇게 쓰면 푸시다운 안 됨 — Spark가 UDF 내부를 모르기 때문
df.filter(is_purchase(col(&quot;event_name&quot;)))

# 이렇게 써야 푸시다운 됨
df.filter(col(&quot;event_name&quot;) == &quot;purchase&quot;)</code></pre>
<blockquote>
<p><strong>실무 팁</strong>: filter 조건에는 가능하면 <strong>내장 함수</strong>를 쓰고, UDF는 최후의 수단으로.</p>
</blockquote>
<hr>
<h2 id="4-실무에서-explain-활용하는-패턴">4. 실무에서 explain() 활용하는 패턴</h2>
<h3 id="쿼리가-느릴-때-체크리스트">쿼리가 느릴 때 체크리스트</h3>
<pre><code class="language-python"># 1. 쿼리 계획 확인
df.filter(...).join(...).groupBy(...).explain(True)

# 체크 포인트:
# - Physical Plan에 PushedFilters 있는가? (푸시다운 작동 여부)
# - Filter가 Join보다 앞에 있는가? (작은 데이터로 join해야 빠름)
# - 불필요한 컬럼을 끝까지 들고 가고 있지 않은가?</code></pre>
<h3 id="filter는-최대한-앞에">filter는 최대한 앞에</h3>
<pre><code class="language-python"># 느린 패턴: join 후 filter
df1.join(df2, &quot;user_id&quot;).filter(col(&quot;event_name&quot;) == &quot;purchase&quot;)

# 빠른 패턴: filter 후 join (Catalyst가 자동으로 해주기도 하지만 명시적으로 쓰는 게 안전)
df1.filter(col(&quot;event_name&quot;) == &quot;purchase&quot;).join(df2, &quot;user_id&quot;)</code></pre>
<h3 id="select로-필요한-컬럼만-미리-추리기">select로 필요한 컬럼만 미리 추리기</h3>
<pre><code class="language-python"># 컬럼 20개짜리 DataFrame을 끝까지 들고 다니지 말고
df.select(&quot;user_id&quot;, &quot;event_name&quot;, &quot;event_timestamp&quot;)  # 필요한 것만 먼저 추리기
  .filter(...)
  .join(...)</code></pre>
<hr>
<h2 id="5-빠른-참조">5. 빠른 참조</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
<th>실무 포인트</th>
</tr>
</thead>
<tbody><tr>
<td>Catalyst</td>
<td>Spark 내장 쿼리 최적화 엔진</td>
<td>자동으로 작동, 의식할 필요 없음</td>
</tr>
<tr>
<td><code>explain(True)</code></td>
<td>논리/물리 쿼리 계획 출력</td>
<td>느린 쿼리 디버깅 시 사용</td>
</tr>
<tr>
<td>filter 병합</td>
<td>여러 filter → 자동으로 1개로 합침</td>
<td>가독성 기준으로 코드 작성해도 됨</td>
</tr>
<tr>
<td>중복 filter 제거</td>
<td>같은 조건 여러 번 → 자동으로 1번으로 줄임</td>
<td>복잡한 쿼리에서 실수해도 성능 영향 없음</td>
</tr>
<tr>
<td>Predicate Pushdown</td>
<td>filter를 데이터 읽는 시점으로 밀어 내림</td>
<td>Parquet/Delta에서 자동. UDF 쓰면 비활성화</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[PySpark 5 - 날짜·배열·null처리·Join]]></title>
            <link>https://velog.io/@no-glass-otacku/PySpark-%EB%82%A0%EC%A7%9C%EB%B3%B5%ED%95%A9%ED%83%80%EC%9E%85%EC%B6%94%EA%B0%80%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@no-glass-otacku/PySpark-%EB%82%A0%EC%A7%9C%EB%B3%B5%ED%95%A9%ED%83%80%EC%9E%85%EC%B6%94%EA%B0%80%ED%95%A8%EC%88%98</guid>
            <pubDate>Fri, 19 Jun 2026 06:36:47 GMT</pubDate>
            <description><![CDATA[<table>
<thead>
<tr>
<th>노트북</th>
<th>핵심 주제</th>
</tr>
</thead>
<tbody><tr>
<td>9 Datetimes</td>
<td>타임스탬프 변환, 날짜 포맷, 날짜 추출·조작</td>
</tr>
<tr>
<td>10 Complex Types</td>
<td>문자열 함수, 배열 컬렉션 함수, union</td>
</tr>
<tr>
<td>11 Additional Functions</td>
<td>col/lit, Null 처리 (na), DataFrame join</td>
</tr>
</tbody></table>
<hr>
<h2 id="1-날짜시간-함수-9-datetimes">1. 날짜/시간 함수 (9 Datetimes)</h2>
<h3 id="1-1-타임스탬프로-변환--cast">1-1. 타임스탬프로 변환 — <code>cast()</code></h3>
<blockquote>
<p><strong>비유</strong>: cast는 &quot;형변환 캐스팅&quot; — 배우(데이터)를 다른 역할(타입)로 바꾸는 것</p>
</blockquote>
<p>원시 데이터의 타임스탬프는 보통 <strong>마이크로초(μs)</strong> 단위 Long 숫자로 저장됨.<br>초 단위로 바꾸려면 <code>1e6</code>(= 1,000,000)으로 나눈 뒤 cast.</p>
<pre><code class="language-python"># 방법 A: 문자열로 타입 지정
timestamp_df = df.withColumn(&quot;timestamp&quot;, (col(&quot;timestamp&quot;) / 1e6).cast(&quot;timestamp&quot;))

# 방법 B: TimestampType 객체로 지정 (동일한 결과)
from pyspark.sql.types import TimestampType
timestamp_df = df.withColumn(&quot;timestamp&quot;, (col(&quot;timestamp&quot;) / 1e6).cast(TimestampType()))</code></pre>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>&quot;timestamp&quot;</code></td>
<td>변환할 타입 (문자열)</td>
</tr>
<tr>
<td><code>TimestampType()</code></td>
<td>변환할 타입 (타입 객체)</td>
</tr>
</tbody></table>
<hr>
<h3 id="1-2-날짜-포맷-지정--date_format">1-2. 날짜 포맷 지정 — <code>date_format()</code></h3>
<pre><code class="language-python">from pyspark.sql.functions import date_format

df.withColumn(&quot;date string&quot;, date_format(&quot;timestamp&quot;, &quot;MMMM dd, yyyy&quot;))
  .withColumn(&quot;time string&quot;, date_format(&quot;timestamp&quot;, &quot;HH:mm:ss.SSSSSS&quot;))</code></pre>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1번째</td>
<td>변환할 컬럼 (컬럼명 문자열 또는 col 객체)</td>
</tr>
<tr>
<td>2번째</td>
<td>날짜 패턴 문자열</td>
</tr>
</tbody></table>
<h4 id="주요-날짜-패턴-문자">주요 날짜 패턴 문자</h4>
<table>
<thead>
<tr>
<th>패턴</th>
<th>의미</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>yyyy</code></td>
<td>4자리 연도</td>
<td><code>2024</code></td>
</tr>
<tr>
<td><code>MM</code></td>
<td>2자리 월</td>
<td><code>07</code></td>
</tr>
<tr>
<td><code>MMMM</code></td>
<td>월 전체 이름</td>
<td><code>July</code></td>
</tr>
<tr>
<td><code>dd</code></td>
<td>2자리 일</td>
<td><code>28</code></td>
</tr>
<tr>
<td><code>HH</code></td>
<td>24시간 시</td>
<td><code>14</code></td>
</tr>
<tr>
<td><code>mm</code></td>
<td>분</td>
<td><code>30</code></td>
</tr>
<tr>
<td><code>ss</code></td>
<td>초</td>
<td><code>05</code></td>
</tr>
<tr>
<td><code>SSSSSS</code></td>
<td>마이크로초</td>
<td><code>123456</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="1-3-날짜시간-속성-추출">1-3. 날짜/시간 속성 추출</h3>
<blockquote>
<p>타임스탬프에서 연/월/일/분/초를 <strong>숫자 컬럼</strong>으로 뽑아냄</p>
</blockquote>
<pre><code class="language-python">from pyspark.sql.functions import year, month, dayofweek, minute, second

df.withColumn(&quot;year&quot;,      year(col(&quot;timestamp&quot;)))
  .withColumn(&quot;month&quot;,     month(col(&quot;timestamp&quot;)))
  .withColumn(&quot;dayofweek&quot;, dayofweek(col(&quot;timestamp&quot;)))  # 1=일요일, 7=토요일
  .withColumn(&quot;minute&quot;,    minute(col(&quot;timestamp&quot;)))
  .withColumn(&quot;second&quot;,    second(col(&quot;timestamp&quot;)))</code></pre>
<p>모두 동일한 형태: <code>함수명(컬럼)</code> → 정수 반환</p>
<hr>
<h3 id="1-4-날짜-타입으로-변환--to_date">1-4. 날짜 타입으로 변환 — <code>to_date()</code></h3>
<blockquote>
<p>타임스탬프(날짜+시간) → 날짜만 남김 (시간 정보 제거)</p>
</blockquote>
<pre><code class="language-python">from pyspark.sql.functions import to_date

date_df = timestamp_df.withColumn(&quot;date&quot;, to_date(col(&quot;timestamp&quot;)))
# 결과: &quot;2023-07-15 14:30:00&quot; → &quot;2023-07-15&quot;</code></pre>
<hr>
<h3 id="1-5-날짜-더하기--date_add">1-5. 날짜 더하기 — <code>date_add()</code></h3>
<pre><code class="language-python">from pyspark.sql.functions import date_add

df.withColumn(&quot;plus_two_days&quot;, date_add(col(&quot;timestamp&quot;), 2))</code></pre>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1번째</td>
<td>기준 날짜 컬럼</td>
</tr>
<tr>
<td>2번째</td>
<td>더할 일수 (정수)</td>
</tr>
</tbody></table>
<hr>
<h3 id="1-6-기타-유용한-날짜-함수-참고용">1-6. 기타 유용한 날짜 함수 (참고용)</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>current_timestamp()</code></td>
<td>현재 시각 반환</td>
</tr>
<tr>
<td><code>add_months(col, n)</code></td>
<td>n달 후 날짜</td>
</tr>
<tr>
<td><code>from_unixtime(col)</code></td>
<td>Unix 초 → 날짜 문자열</td>
</tr>
<tr>
<td><code>unix_timestamp(col)</code></td>
<td>날짜 문자열 → Unix 초</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-복합-타입-함수-10-complex-types">2. 복합 타입 함수 (10 Complex Types)</h2>
<h3 id="2-1-문자열-함수">2-1. 문자열 함수</h3>
<pre><code class="language-python">from pyspark.sql.functions import split, lower, regexp_replace  # 필요한 것만 import</code></pre>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>split(col, 패턴, limit)</code></td>
<td>패턴 기준으로 문자열 분리 → 배열 반환</td>
<td><code>split(col(&quot;email&quot;), &quot;@&quot;, 0)</code></td>
</tr>
<tr>
<td><code>lower(col)</code></td>
<td>소문자 변환</td>
<td><code>lower(col(&quot;name&quot;))</code></td>
</tr>
<tr>
<td><code>ltrim(col)</code></td>
<td>왼쪽 공백 제거</td>
<td><code>ltrim(col(&quot;text&quot;))</code></td>
</tr>
<tr>
<td><code>regexp_replace(col, 패턴, 치환)</code></td>
<td>정규식 패턴 치환</td>
<td><code>regexp_replace(col(&quot;s&quot;), &quot;[0-9]&quot;, &quot;&quot;)</code></td>
</tr>
<tr>
<td><code>regexp_extract(col, 패턴, idx)</code></td>
<td>정규식으로 일부 추출</td>
<td></td>
</tr>
<tr>
<td><code>translate(col, src, rep)</code></td>
<td>문자 단위 치환</td>
<td></td>
</tr>
</tbody></table>
<h4 id="split-예시">split() 예시</h4>
<pre><code class="language-python"># email에서 @ 기준으로 분리
df.select(split(df.email, &#39;@&#39;, 0).alias(&#39;email_handle&#39;))
# 결과: [&quot;user&quot;, &quot;gmail.com&quot;] 형태의 배열 컬럼</code></pre>
<table>
<thead>
<tr>
<th>split 인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1번째</td>
<td>분리할 컬럼</td>
</tr>
<tr>
<td>2번째</td>
<td>구분 패턴 (정규식)</td>
</tr>
<tr>
<td>3번째</td>
<td>최대 분리 수 (0 = 제한 없음)</td>
</tr>
</tbody></table>
<hr>
<h3 id="2-2-컬렉션-함수-배열-조작">2-2. 컬렉션 함수 (배열 조작)</h3>
<blockquote>
<p><strong>비유</strong>: 배열이 담긴 컬럼을 다루는 함수들. 마치 리스트를 다루는 파이썬 메서드처럼.</p>
</blockquote>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>explode(col)</code></td>
<td>배열의 각 요소를 <strong>별도 행</strong>으로 분리</td>
<td>아래 참고</td>
</tr>
<tr>
<td><code>array_contains(col, 값)</code></td>
<td>배열에 값이 있으면 True</td>
<td><code>array_contains(col(&quot;details&quot;), &quot;Mattress&quot;)</code></td>
</tr>
<tr>
<td><code>element_at(col, 인덱스)</code></td>
<td>배열의 n번째 요소 반환 (<strong>1부터 시작</strong>)</td>
<td><code>element_at(col(&quot;details&quot;), 2)</code></td>
</tr>
</tbody></table>
<h4 id="explode-핵심">explode() 핵심</h4>
<pre><code class="language-python"># items 컬럼이 배열이면, 각 원소가 별도 행으로 &quot;터져 나옴&quot;
sales_exploded_df = df.withColumn(&quot;items&quot;, explode(&quot;items&quot;))</code></pre>
<blockquote>
<p><strong>비유</strong>: 달걀 한 판(배열)을 하나씩 꺼내서 각각 새 줄에 올려놓는 것</p>
</blockquote>
<h4 id="실전-패턴--explode-→-select-→-split">실전 패턴 — explode → select → split</h4>
<pre><code class="language-python">details_df = (df
              .withColumn(&quot;items&quot;, explode(&quot;items&quot;))          # 배열 → 행 분리
              .select(&quot;email&quot;, &quot;items.item_name&quot;)             # 필요 컬럼만
              .withColumn(&quot;details&quot;, split(col(&quot;item_name&quot;), &quot; &quot;))  # 문자열 → 배열
             )</code></pre>
<hr>
<h3 id="2-3-집계-함수-배열-생성">2-3. 집계 함수 (배열 생성)</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>collect_set(col)</code></td>
<td>그룹 내 <strong>고유값</strong>만 모아서 배열로 반환 (중복 제거)</td>
</tr>
<tr>
<td><code>collect_list(col)</code></td>
<td>그룹 내 <strong>모든 값</strong>을 배열로 반환 (중복 포함)</td>
</tr>
</tbody></table>
<pre><code class="language-python"># 이메일별로 주문한 매트리스 크기 종류 수집
size_df = mattress_df.groupBy(&quot;email&quot;).agg(collect_set(&quot;size&quot;).alias(&quot;size options&quot;))</code></pre>
<hr>
<h3 id="2-4-dataframe-합치기--union-vs-unionbyname">2-4. DataFrame 합치기 — union vs unionByName</h3>
<blockquote>
<p><strong>비유</strong>:</p>
<ul>
<li><code>union</code> = 위치 기준 합치기 (컬럼 순서가 같아야 안전)</li>
<li><code>unionByName</code> = 이름 기준 합치기 (컬럼 순서 달라도 OK)</li>
</ul>
</blockquote>
<pre><code class="language-python"># union: 컬럼 순서가 완전히 같을 때
df1.union(df2)

# unionByName: 컬럼명이 같으면 순서 달라도 됨 (더 안전)
df1.unionByName(df2)</code></pre>
<p>⚠️ <strong>둘 다 중복 행을 제거하지 않음</strong> (SQL의 UNION ALL과 같음)</p>
<hr>
<h2 id="3-추가-함수-11-additional-functions">3. 추가 함수 (11 Additional Functions)</h2>
<h3 id="3-1-col--컬럼-객체-참조">3-1. col() — 컬럼 객체 참조</h3>
<pre><code class="language-python">from pyspark.sql.functions import col

# 문자열 컬럼명 대신 col 객체를 쓰면 메서드 체이닝 가능
df.filter(col(&quot;email&quot;).endswith(&quot;gmail.com&quot;))</code></pre>
<blockquote>
<p><strong>왜 쓰나?</strong>: <code>&quot;email&quot;</code> 문자열은 그냥 이름이지만, <code>col(&quot;email&quot;)</code>은 컬럼 <strong>객체</strong>라서 <code>.endswith()</code>, <code>.isNull()</code> 같은 메서드를 붙일 수 있음</p>
</blockquote>
<hr>
<h3 id="3-2-lit--고정값으로-컬럼-만들기">3-2. lit() — 고정값으로 컬럼 만들기</h3>
<pre><code class="language-python">from pyspark.sql.functions import lit

# 모든 행에 True 값을 가진 &quot;gmail user&quot; 컬럼 추가
df.select(&quot;email&quot;, lit(True).alias(&quot;gmail user&quot;))</code></pre>
<table>
<thead>
<tr>
<th>lit 인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>아무 Python 값</td>
<td>문자열, 숫자, 불리언 모두 가능</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>비유</strong>: 모든 행에 도장 찍기. &quot;이 데이터는 전부 gmail 유저다&quot; 같은 플래그 컬럼 만들 때 유용</p>
</blockquote>
<hr>
<h3 id="3-3-null-처리--dfna">3-3. Null 처리 — df.na</h3>
<blockquote>
<p><strong>접근 방법</strong>: <code>df.na.함수명()</code> 형태로 사용</p>
</blockquote>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>df.na.drop()</code></td>
<td>null 있는 행 전체 삭제</td>
</tr>
<tr>
<td><code>df.na.fill(값)</code></td>
<td>null을 지정값으로 채움</td>
</tr>
<tr>
<td><code>df.na.replace(찾을값, 바꿀값)</code></td>
<td>특정 값을 다른 값으로 교체</td>
</tr>
</tbody></table>
<pre><code class="language-python"># null 있는 행 제거
df.na.drop()

# null을 &quot;NO COUPON&quot;으로 채우기
df.na.fill(&quot;NO COUPON&quot;)

# 행 수 비교로 null 존재 확인
print(df.count())          # 전체
print(df.na.drop().count()) # null 제거 후 → 숫자 다르면 null 있음</code></pre>
<p>⚠️ <strong>주의</strong>: 배열 컬럼(items 같은) 안에 null이 있으면 <code>na.drop()</code>으로 안 잡힘.<br><code>explode()</code> 먼저 해서 펼친 뒤 확인해야 함.</p>
<hr>
<h3 id="3-4-dataframe-조인--join">3-4. DataFrame 조인 — join()</h3>
<pre><code class="language-python">df1.join(other=df2, on=조인조건, how=조인방식)</code></pre>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>other</code></td>
<td>조인할 두 번째 DataFrame</td>
</tr>
<tr>
<td><code>on</code></td>
<td>조인 기준 컬럼 (문자열, 리스트, 또는 조건식)</td>
</tr>
<tr>
<td><code>how</code></td>
<td>조인 방식 (아래 표 참고)</td>
</tr>
</tbody></table>
<h4 id="조인-방식-how">조인 방식 (how)</h4>
<table>
<thead>
<tr>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>&quot;inner&quot;</code></td>
<td>양쪽 모두 있는 행만 (기본값)</td>
</tr>
<tr>
<td><code>&quot;left&quot;</code> / <code>&quot;left_outer&quot;</code></td>
<td>왼쪽 기준, 오른쪽에 없으면 null</td>
</tr>
<tr>
<td><code>&quot;right&quot;</code> / <code>&quot;right_outer&quot;</code></td>
<td>오른쪽 기준</td>
</tr>
<tr>
<td><code>&quot;outer&quot;</code> / <code>&quot;full&quot;</code></td>
<td>양쪽 모두 포함</td>
</tr>
</tbody></table>
<h4 id="예시-패턴">예시 패턴</h4>
<pre><code class="language-python"># 같은 이름의 컬럼으로 inner join
joined_df = df1.join(other=df2, on=&quot;email&quot;, how=&quot;inner&quot;)

# 여러 컬럼으로 join
df1.join(df2, [&quot;name&quot;, &quot;age&quot;])

# 컬럼명이 다를 때
df1.join(df2, df1[&quot;customer_name&quot;] == df2[&quot;account_name&quot;], &quot;left_outer&quot;)</code></pre>
<hr>
<h2 id="4-전체-함수-빠른-참조표">4. 전체 함수 빠른 참조표</h2>
<h3 id="날짜시간">날짜/시간</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>인자</th>
<th>반환</th>
</tr>
</thead>
<tbody><tr>
<td><code>.cast(&quot;timestamp&quot;)</code></td>
<td>타입명 문자열</td>
<td>변환된 컬럼</td>
</tr>
<tr>
<td><code>date_format(col, 패턴)</code></td>
<td>컬럼, 패턴 문자열</td>
<td>문자열 컬럼</td>
</tr>
<tr>
<td><code>to_date(col)</code></td>
<td>컬럼</td>
<td>DateType 컬럼</td>
</tr>
<tr>
<td><code>date_add(col, n)</code></td>
<td>컬럼, 정수</td>
<td>DateType 컬럼</td>
</tr>
<tr>
<td><code>year/month/dayofweek/minute/second(col)</code></td>
<td>컬럼</td>
<td>정수 컬럼</td>
</tr>
</tbody></table>
<h3 id="문자열">문자열</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>인자</th>
<th>반환</th>
</tr>
</thead>
<tbody><tr>
<td><code>split(col, 패턴, limit)</code></td>
<td>컬럼, 패턴, 최대분리수</td>
<td>ArrayType 컬럼</td>
</tr>
<tr>
<td><code>lower(col)</code></td>
<td>컬럼</td>
<td>StringType</td>
</tr>
<tr>
<td><code>regexp_replace(col, 패턴, 치환)</code></td>
<td>컬럼, 정규식, 치환문자</td>
<td>StringType</td>
</tr>
</tbody></table>
<h3 id="컬렉션배열">컬렉션/배열</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>인자</th>
<th>반환</th>
</tr>
</thead>
<tbody><tr>
<td><code>explode(col)</code></td>
<td>배열 컬럼</td>
<td>행 분리된 DataFrame</td>
</tr>
<tr>
<td><code>array_contains(col, 값)</code></td>
<td>배열 컬럼, 찾을 값</td>
<td>BooleanType</td>
</tr>
<tr>
<td><code>element_at(col, n)</code></td>
<td>배열 컬럼, 인덱스(<strong>1부터</strong>)</td>
<td>원소 타입</td>
</tr>
<tr>
<td><code>collect_set(col)</code></td>
<td>컬럼</td>
<td>ArrayType (중복 제거)</td>
</tr>
<tr>
<td><code>collect_list(col)</code></td>
<td>컬럼</td>
<td>ArrayType (중복 포함)</td>
</tr>
</tbody></table>
<h3 id="기타">기타</h3>
<table>
<thead>
<tr>
<th>함수/메서드</th>
<th>인자</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>col(&quot;컬럼명&quot;)</code></td>
<td>문자열</td>
<td>컬럼 객체 반환</td>
</tr>
<tr>
<td><code>lit(값)</code></td>
<td>Python 값</td>
<td>고정값 컬럼 생성</td>
</tr>
<tr>
<td><code>df.na.drop()</code></td>
<td>없음</td>
<td>null 행 제거</td>
</tr>
<tr>
<td><code>df.na.fill(값)</code></td>
<td>채울 값</td>
<td>null 채우기</td>
</tr>
<tr>
<td><code>df1.join(df2, on, how)</code></td>
<td>DF, 조건, 방식</td>
<td>두 DF 결합</td>
</tr>
<tr>
<td><code>df1.union(df2)</code></td>
<td>DF</td>
<td>위치 기준 합치기</td>
</tr>
<tr>
<td><code>df1.unionByName(df2)</code></td>
<td>DF</td>
<td>이름 기준 합치기</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[PySpark 4 - Reader & Writer]]></title>
            <link>https://velog.io/@no-glass-otacku/PySpark-4-Reader-Writer</link>
            <guid>https://velog.io/@no-glass-otacku/PySpark-4-Reader-Writer</guid>
            <pubDate>Fri, 19 Jun 2026 05:54:17 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-핵심-개념-한눈에-보기">📌 핵심 개념 한눈에 보기</h2>
<table>
<thead>
<tr>
<th>방향</th>
<th>인터페이스</th>
<th>접근 방법</th>
</tr>
</thead>
<tbody><tr>
<td>파일 → DataFrame</td>
<td><strong>DataFrameReader</strong></td>
<td><code>spark.read</code></td>
</tr>
<tr>
<td>DataFrame → 파일/테이블</td>
<td><strong>DataFrameWriter</strong></td>
<td><code>df.write</code></td>
</tr>
</tbody></table>
<blockquote>
<p><strong>비유</strong>: Reader는 &quot;데이터를 가져오는 택배 수령&quot;, Writer는 &quot;데이터를 보내는 택배 발송&quot;</p>
</blockquote>
<hr>
<h2 id="1-dataframereader--데이터-읽기">1. DataFrameReader — 데이터 읽기</h2>
<h3 id="기본-패턴">기본 패턴</h3>
<pre><code class="language-python">df = (spark
      .read
      .option(&quot;옵션이름&quot;, 값)   # 옵션 추가 (체이닝 가능)
      .schema(스키마)           # 스키마 직접 지정 (선택)
      .csv(&quot;파일경로&quot;)          # 파일 형식 메서드로 마무리
     )</code></pre>
<hr>
<h3 id="1-1-csv-읽기">1-1. CSV 읽기</h3>
<h4 id="방법-a-option-체이닝">방법 A: <code>.option()</code> 체이닝</h4>
<pre><code class="language-python">users_df = (spark
            .read
            .option(&quot;sep&quot;, &quot;\t&quot;)        # 구분자 (기본값은 쉼표 &quot;,&quot;)
            .option(&quot;header&quot;, True)     # 첫 줄을 컬럼명으로 사용
            .option(&quot;inferSchema&quot;, True) # 타입 자동 추론
            .csv(&quot;파일경로&quot;)
           )</code></pre>
<h4 id="방법-b-csv-메서드에-직접-인자-전달">방법 B: csv() 메서드에 직접 인자 전달</h4>
<pre><code class="language-python">users_df = (spark
            .read
            .csv(&quot;파일경로&quot;, sep=&quot;\t&quot;, header=True, inferSchema=True)
           )</code></pre>
<blockquote>
<p>두 방법은 동일한 결과. 취향에 따라 선택</p>
</blockquote>
<h4 id="주요-option-인자-정리">주요 option 인자 정리</h4>
<table>
<thead>
<tr>
<th>옵션 키</th>
<th>설명</th>
<th>예시 값</th>
</tr>
</thead>
<tbody><tr>
<td><code>&quot;sep&quot;</code></td>
<td>구분자</td>
<td><code>&quot;,&quot;</code>, <code>&quot;\t&quot;</code>, `&quot;</td>
</tr>
<tr>
<td><code>&quot;header&quot;</code></td>
<td>첫 줄 = 컬럼명 여부</td>
<td><code>True</code> / <code>False</code></td>
</tr>
<tr>
<td><code>&quot;inferSchema&quot;</code></td>
<td>데이터 타입 자동 추론</td>
<td><code>True</code> / <code>False</code></td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ <code>inferSchema=True</code>는 전체 데이터를 한 번 스캔하므로 <strong>느림</strong>. 대용량이면 스키마 직접 지정 권장</p>
</blockquote>
<hr>
<h3 id="1-2-json-읽기">1-2. JSON 읽기</h3>
<pre><code class="language-python">events_df = (spark
             .read
             .option(&quot;inferSchema&quot;, True)  # 타입 자동 추론
             .json(&quot;파일경로&quot;)
            )</code></pre>
<hr>
<h3 id="1-3-스키마-직접-정의--structtype-방식">1-3. 스키마 직접 정의 — StructType 방식</h3>
<blockquote>
<p><strong>비유</strong>: 테이블의 설계도를 미리 그려서 &quot;이 건물은 이렇게 지어야 해&quot;라고 Spark에게 알려주는 것</p>
</blockquote>
<h4 id="import-먼저">Import 먼저</h4>
<pre><code class="language-python">from pyspark.sql.types import (
    LongType, StringType, DoubleType, IntegerType,
    ArrayType, StructType, StructField
)</code></pre>
<h4 id="기본-구조">기본 구조</h4>
<pre><code class="language-python">스키마 = StructType([
    StructField(&quot;컬럼명&quot;, 데이터타입(), nullable여부),
    StructField(&quot;컬럼명&quot;, 데이터타입(), nullable여부),
    ...
])</code></pre>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>첫 번째</td>
<td>컬럼 이름 (문자열)</td>
</tr>
<tr>
<td>두 번째</td>
<td>데이터 타입 (아래 타입 표 참고)</td>
</tr>
<tr>
<td>세 번째</td>
<td><code>True</code> = null 허용, <code>False</code> = null 불허</td>
</tr>
</tbody></table>
<h4 id="주요-데이터-타입">주요 데이터 타입</h4>
<table>
<thead>
<tr>
<th>타입</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>StringType()</code></td>
<td>문자열</td>
<td><code>&quot;hello&quot;</code></td>
</tr>
<tr>
<td><code>LongType()</code></td>
<td>큰 정수 (64bit)</td>
<td><code>123456789</code></td>
</tr>
<tr>
<td><code>IntegerType()</code></td>
<td>일반 정수 (32bit)</td>
<td><code>42</code></td>
</tr>
<tr>
<td><code>DoubleType()</code></td>
<td>소수 (64bit)</td>
<td><code>3.14</code></td>
</tr>
<tr>
<td><code>StructType([...])</code></td>
<td>중첩 구조 (object 안에 object)</td>
<td><code>{&quot;city&quot;: &quot;Seoul&quot;}</code></td>
</tr>
<tr>
<td><code>ArrayType(타입)</code></td>
<td>배열</td>
<td><code>[&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]</code></td>
</tr>
</tbody></table>
<h4 id="사용-예시-단순-csv용">사용 예시 (단순 CSV용)</h4>
<pre><code class="language-python">schema = StructType([
    StructField(&quot;user_id&quot;, StringType(), True),
    StructField(&quot;user_first_touch_timestamp&quot;, LongType(), True),
    StructField(&quot;email&quot;, StringType(), True)
])

df = spark.read.option(&quot;sep&quot;, &quot;\t&quot;).option(&quot;header&quot;, True).schema(schema).csv(&quot;경로&quot;)</code></pre>
<h4 id="중첩-구조-예시-json용--structtype-안에-structtype">중첩 구조 예시 (JSON용 — StructType 안에 StructType)</h4>
<pre><code class="language-python">schema = StructType([
    StructField(&quot;device&quot;, StringType(), True),
    StructField(&quot;geo&quot;, StructType([           # ← 중첩 struct
        StructField(&quot;city&quot;, StringType(), True),
        StructField(&quot;state&quot;, StringType(), True)
    ]), True),
    StructField(&quot;items&quot;, ArrayType(           # ← 배열 안에 struct
        StructType([
            StructField(&quot;item_id&quot;, StringType(), True),
            StructField(&quot;quantity&quot;, LongType(), True)
        ])
    ), True)
])</code></pre>
<hr>
<h3 id="1-4-스키마-직접-정의--ddl-문자열-방식">1-4. 스키마 직접 정의 — DDL 문자열 방식</h3>
<blockquote>
<p>더 짧고 간단한 대안. SQL의 CREATE TABLE 문법과 유사</p>
</blockquote>
<pre><code class="language-python">ddl_schema = &quot;user_id string, user_first_touch_timestamp long, email string&quot;

df = spark.read.option(&quot;sep&quot;, &quot;\t&quot;).option(&quot;header&quot;, True).schema(ddl_schema).csv(&quot;경로&quot;)</code></pre>
<hr>
<h3 id="1-5-toddl-트릭--스키마-자동-생성-개발용">1-5. toDDL 트릭 — 스키마 자동 생성 (개발용)</h3>
<blockquote>
<p><strong>목적</strong>: 복잡한 JSON의 스키마를 처음부터 손으로 쓰기 어려울 때, Spark가 자동 추론한 스키마를 DDL 문자열로 뽑아주는 편법</p>
</blockquote>
<blockquote>
<p>⚠️ <strong>개발/탐색 단계에서만 사용. 운영 환경에서는 금지</strong> (inferSchema는 전체 데이터를 읽어서 느림)</p>
</blockquote>
<pre><code class="language-python"># Step 1: Python에서 경로를 Spark config로 공유
spark.conf.set(&quot;com.scope.events_path&quot;, &quot;파일경로&quot;)</code></pre>
<pre><code class="language-scala">// Step 2: Scala 셀에서 실행 (%scala 마법 명령어 필요)
val path = spark.conf.get(&quot;com.scope.events_path&quot;)
val schema = spark.read.option(&quot;inferSchema&quot;, true).json(path).schema.toDDL
println(schema)</code></pre>
<pre><code class="language-python"># Step 3: 출력된 DDL 문자열을 Python 변수에 붙여넣기
events_schema = &quot;`device` STRING, `event_name` STRING, ...&quot;  # 복사한 값

df = spark.read.schema(events_schema).json(&quot;경로&quot;)</code></pre>
<hr>
<h2 id="2-dataframewriter--데이터-쓰기">2. DataFrameWriter — 데이터 쓰기</h2>
<h3 id="기본-패턴-1">기본 패턴</h3>
<pre><code class="language-python">(df
 .write
 .option(&quot;옵션이름&quot;, 값)   # 선택
 .mode(&quot;쓰기 모드&quot;)        # 중요!
 .저장메서드(&quot;경로 또는 테이블명&quot;)
)</code></pre>
<hr>
<h3 id="2-1-파일로-저장--parquet">2-1. 파일로 저장 — Parquet</h3>
<pre><code class="language-python">(users_df
 .write
 .option(&quot;compression&quot;, &quot;snappy&quot;)  # 압축 방식
 .mode(&quot;overwrite&quot;)                # 기존 파일 덮어쓰기
 .parquet(&quot;저장경로&quot;)
)

# 또는 메서드에 직접 인자 전달
(users_df
 .write
 .parquet(&quot;저장경로&quot;, compression=&quot;snappy&quot;, mode=&quot;overwrite&quot;)
)</code></pre>
<h4 id="mode-옵션-정리">mode() 옵션 정리</h4>
<table>
<thead>
<tr>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>&quot;overwrite&quot;</code></td>
<td>기존 데이터 삭제 후 새로 씀</td>
</tr>
<tr>
<td><code>&quot;append&quot;</code></td>
<td>기존 데이터에 추가</td>
</tr>
<tr>
<td><code>&quot;ignore&quot;</code></td>
<td>이미 있으면 아무것도 안 함</td>
</tr>
<tr>
<td><code>&quot;error&quot;</code></td>
<td>이미 있으면 오류 발생 (기본값)</td>
</tr>
</tbody></table>
<hr>
<h3 id="2-2-테이블로-저장--saveastable">2-2. 테이블로 저장 — saveAsTable</h3>
<pre><code class="language-python">events_df.write.mode(&quot;overwrite&quot;).saveAsTable(&quot;테이블명&quot;)</code></pre>
<blockquote>
<p><strong>createOrReplaceTempView vs saveAsTable 차이</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th>createOrReplaceTempView</th>
<th>saveAsTable</th>
</tr>
</thead>
<tbody><tr>
<td>범위</td>
<td>현재 세션만 (로컬)</td>
<td>전체 작업공간 (전역)</td>
</tr>
<tr>
<td>영속성</td>
<td>세션 종료 시 사라짐</td>
<td>영구 저장</td>
</tr>
</tbody></table>
<hr>
<h3 id="2-3-delta-테이블로-저장">2-3. Delta 테이블로 저장</h3>
<pre><code class="language-python">(events_df
 .write
 .format(&quot;delta&quot;)     # 저장 형식을 Delta로 지정
 .mode(&quot;overwrite&quot;)
 .save(&quot;저장경로&quot;)
)</code></pre>
<hr>
<h2 id="3-delta-lake--왜-써야-하나">3. Delta Lake — 왜 써야 하나?</h2>
<blockquote>
<p><strong>비유</strong>: 일반 Parquet은 그냥 파일 보관함, Delta Lake는 &quot;버전 관리되는 구글 드라이브&quot;</p>
</blockquote>
<table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>ACID 트랜잭션</td>
<td>중간에 실패해도 데이터 깨지지 않음</td>
</tr>
<tr>
<td>Time Travel</td>
<td>과거 버전 데이터 조회 가능</td>
</tr>
<tr>
<td>스키마 적용</td>
<td>잘못된 형식의 데이터 자동 차단</td>
</tr>
<tr>
<td>스트리밍 + 배치 통합</td>
<td>실시간/일괄 처리 모두 지원</td>
</tr>
<tr>
<td>Parquet 기반</td>
<td>Spark API와 호환</td>
</tr>
</tbody></table>
<blockquote>
<p>Databricks에서는 <strong>거의 항상 Delta Lake 사용 권장</strong></p>
</blockquote>
<hr>
<h2 id="4-결과-확인-방법">4. 결과 확인 방법</h2>
<pre><code class="language-python"># 스키마 확인
df.printSchema()

# 데이터 미리보기
display(df)         # Databricks 전용 (테이블 형태)
df.show(5)          # 일반 Spark (텍스트 형태)

# 저장된 파일 목록 확인
display(dbutils.fs.ls(&quot;저장경로&quot;))</code></pre>
<hr>
<h2 id="5-전체-흐름-요약">5. 전체 흐름 요약</h2>
<pre><code>CSV/JSON 파일
    ↓  spark.read.option().schema().csv() / .json()
DataFrame (메모리 내 처리)
    ↓  df.write.mode().parquet() / .saveAsTable() / .format(&quot;delta&quot;).save()
Parquet 파일 / 테이블 / Delta 테이블</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[PySpark 3 -집계]]></title>
            <link>https://velog.io/@no-glass-otacku/PySpark-3-%EC%A7%91%EA%B3%84</link>
            <guid>https://velog.io/@no-glass-otacku/PySpark-3-%EC%A7%91%EA%B3%84</guid>
            <pubDate>Fri, 19 Jun 2026 03:19:22 GMT</pubDate>
            <description><![CDATA[<h1 id="databricks-pyspark-집계aggregation--groupby-agg-내장-함수-총정리">[Databricks] PySpark 집계(Aggregation) — groupBy, agg, 내장 함수 총정리</h1>
<blockquote>
<p>🗓️ 학습일: 2026.06<br>📚 출처: Databricks 수업 자료 (5_Aggregation.ipynb)</p>
</blockquote>
<hr>
<h2 id="🎯-오늘-배운-것-한-줄-요약">🎯 오늘 배운 것 한 줄 요약</h2>
<blockquote>
<p><strong>&quot;데이터를 그룹으로 묶고(groupBy), 그 그룹에 집계를 적용한다(agg / count / sum / avg …)&quot;</strong></p>
</blockquote>
<hr>
<h2 id="1-집계aggregation란">1. 집계(Aggregation)란?</h2>
<p>집계는 <strong>여러 행(row)을 하나의 요약 값으로 압축</strong>하는 작업이다.</p>
<p><strong>비유 🧾</strong><br>엑셀에서 피벗 테이블을 만들 때를 떠올려보자.<br>&quot;지역별 매출 합계&quot;를 구할 때, 수백 개의 행을 지역으로 묶고, 매출을 더하지 않나?<br>PySpark의 <code>groupBy</code> + 집계 함수가 바로 그 역할이다.</p>
<hr>
<h2 id="2-핵심-흐름">2. 핵심 흐름</h2>
<pre><code>DataFrame
  └─ .groupBy(&quot;열이름&quot;)       ← 그룹화 (GroupedData 객체 생성)
       └─ .count()            ← 집계 적용
       └─ .avg(&quot;다른열&quot;)
       └─ .sum(&quot;다른열&quot;)
       └─ .agg(함수1, 함수2)  ← 여러 집계를 한 번에</code></pre><hr>
<h2 id="3-groupby--그룹화하기">3. groupBy — 그룹화하기</h2>
<p><code>groupBy</code>는 DataFrame을 특정 열 기준으로 묶어 <strong>GroupedData</strong> 객체를 만든다.<br>이 시점에서는 아직 계산이 일어나지 않는다. 집계 메서드를 붙여야 실제 결과가 나온다.</p>
<pre><code class="language-python"># 단일 열 기준
df.groupBy(&quot;event_name&quot;)

# 여러 열 기준 (조합)
df.groupBy(&quot;geo.state&quot;, &quot;geo.city&quot;)</code></pre>
<blockquote>
<p><strong>💡 중첩 열(nested column) 접근</strong><br><code>&quot;geo.state&quot;</code> 처럼 점(<code>.</code>)으로 구분해서 중첩된 구조의 필드에 접근할 수 있다.</p>
</blockquote>
<hr>
<h2 id="4-groupeddata-집계-메서드">4. GroupedData 집계 메서드</h2>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>count()</code></td>
<td>각 그룹의 행 수</td>
</tr>
<tr>
<td><code>avg(&quot;열&quot;)</code></td>
<td>각 그룹의 평균</td>
</tr>
<tr>
<td><code>sum(&quot;열&quot;)</code></td>
<td>각 그룹의 합계</td>
</tr>
<tr>
<td><code>max(&quot;열&quot;)</code></td>
<td>각 그룹의 최댓값</td>
</tr>
<tr>
<td><code>min(&quot;열&quot;)</code></td>
<td>각 그룹의 최솟값</td>
</tr>
<tr>
<td><code>agg(...)</code></td>
<td>여러 집계를 한 번에 적용</td>
</tr>
</tbody></table>
<h3 id="코드-예시">코드 예시</h3>
<pre><code class="language-python"># 이벤트 이름별 행 수
event_counts_df = df.groupBy(&quot;event_name&quot;).count()
display(event_counts_df)

# 주(state)별 평균 구매 수익
avg_state_purchases_df = df.groupBy(&quot;geo.state&quot;).avg(&quot;ecommerce.purchase_revenue_in_usd&quot;)
display(avg_state_purchases_df)

# 주 + 도시 조합별 총 수량 &amp; 수익 합계
city_purchase_quantities_df = df.groupBy(&quot;geo.state&quot;, &quot;geo.city&quot;).sum(
    &quot;ecommerce.total_item_quantity&quot;,
    &quot;ecommerce.purchase_revenue_in_usd&quot;
)
display(city_purchase_quantities_df)</code></pre>
<hr>
<h2 id="5-내장-함수built-in-functions와-agg">5. 내장 함수(Built-in Functions)와 agg</h2>
<p><code>pyspark.sql.functions</code> 모듈에는 더 다양한 집계 함수가 있다.<br>이 함수들은 <code>agg()</code>와 함께 사용하며, <strong>결과 열에 별칭(alias)</strong> 도 붙일 수 있다.</p>
<h3 id="주요-집계-내장-함수">주요 집계 내장 함수</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>sum(&quot;열&quot;)</code></td>
<td>합계</td>
</tr>
<tr>
<td><code>avg(&quot;열&quot;)</code></td>
<td>평균</td>
</tr>
<tr>
<td><code>approx_count_distinct(&quot;열&quot;)</code></td>
<td>고유값 개수 (근사치, 빠름)</td>
</tr>
<tr>
<td><code>collect_list(&quot;열&quot;)</code></td>
<td>그룹 내 값을 리스트로 묶기</td>
</tr>
<tr>
<td><code>stddev_samp(&quot;열&quot;)</code></td>
<td>표본 표준편차</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>❓ approx_count_distinct vs count(distinct …)</strong><br>정확한 고유값 수를 세려면 전체 데이터를 다 봐야 해서 느리다.<br><code>approx_count_distinct</code>는 HyperLogLog 알고리즘으로 <strong>빠르게 근사치</strong>를 구한다.<br>대용량 데이터에서는 이 함수가 훨씬 실용적이다.</p>
</blockquote>
<h3 id="코드-예시-1">코드 예시</h3>
<pre><code class="language-python">from pyspark.sql.functions import sum

# 단일 집계 + alias
state_purchases_df = df.groupBy(&quot;geo.state&quot;).agg(
    sum(&quot;ecommerce.total_item_quantity&quot;).alias(&quot;total_purchases&quot;)
)
display(state_purchases_df)</code></pre>
<pre><code class="language-python">from pyspark.sql.functions import avg, approx_count_distinct

# 여러 집계를 한 번에
state_aggregates_df = (df
    .groupBy(&quot;geo.state&quot;)
    .agg(
        avg(&quot;ecommerce.total_item_quantity&quot;).alias(&quot;avg_quantity&quot;),
        approx_count_distinct(&quot;user_id&quot;).alias(&quot;distinct_users&quot;)
    )
)
display(state_aggregates_df)</code></pre>
<hr>
<h2 id="6-수학-내장-함수-보너스">6. 수학 내장 함수 (보너스)</h2>
<p>집계 외에도 열(column) 단위 수학 연산 함수도 있다.</p>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>sqrt(&quot;열&quot;)</code></td>
<td>제곱근</td>
</tr>
<tr>
<td><code>cos(&quot;열&quot;)</code></td>
<td>코사인</td>
</tr>
<tr>
<td><code>ceil(&quot;열&quot;)</code></td>
<td>올림</td>
</tr>
<tr>
<td><code>round(&quot;열&quot;)</code></td>
<td>반올림</td>
</tr>
<tr>
<td><code>log(&quot;열&quot;)</code></td>
<td>자연 로그</td>
</tr>
</tbody></table>
<pre><code class="language-python">from pyspark.sql.functions import cos, sqrt

display(
    spark.range(10)          # 0~9 범위의 id 열을 가진 DataFrame 생성
         .withColumn(&quot;sqrt&quot;, sqrt(&quot;id&quot;))
         .withColumn(&quot;cos&quot;, cos(&quot;id&quot;))
)</code></pre>
<hr>
<h2 id="7-헷갈리기-쉬운-포인트-정리">7. 헷갈리기 쉬운 포인트 정리</h2>
<h3 id="q-groupby-메서드의-집계avg-sum와-내장-함수의-집계avg-sum의-차이는">Q. <code>groupBy</code> 메서드의 집계(<code>avg</code>, <code>sum</code>)와 내장 함수의 집계(<code>avg</code>, <code>sum</code>)의 차이는?</h3>
<p><strong>A.</strong><br>둘 다 같은 결과를 내지만, 내장 함수 방식(<code>agg</code> + 내장 함수)이 더 강력하다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>방법</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>GroupedData 메서드</td>
<td><code>.avg(&quot;열&quot;)</code></td>
<td>간단하지만, alias 불가 / 한 번에 하나의 집계만 쉽게 가능</td>
</tr>
<tr>
<td>내장 함수 + agg</td>
<td><code>.agg(avg(&quot;열&quot;).alias(&quot;별칭&quot;))</code></td>
<td>alias 가능, 여러 집계 동시에, 더 다양한 함수 사용 가능</td>
</tr>
</tbody></table>
<h3 id="q-alias는-왜-쓰는가">Q. <code>alias</code>는 왜 쓰는가?</h3>
<p>집계를 하면 열 이름이 <code>avg(ecommerce.total_item_quantity)</code> 처럼 길고 못생기게 된다.<br><code>alias(&quot;avg_quantity&quot;)</code> 로 읽기 좋은 이름을 붙여주는 것이다.</p>
<hr>
<h2 id="8-오늘의-qa">8. 오늘의 Q&amp;A</h2>
<blockquote>
<p><em>(질문을 하면 이 섹션에 추가됩니다)</em></p>
</blockquote>
<hr>
<h2 id="9-전체-구조-한눈에-보기">9. 전체 구조 한눈에 보기</h2>
<pre><code>[데이터 집계 전략]

① 단순 집계
   df.groupBy(&quot;A&quot;).count()
   df.groupBy(&quot;A&quot;).avg(&quot;B&quot;)
   df.groupBy(&quot;A&quot;, &quot;B&quot;).sum(&quot;C&quot;, &quot;D&quot;)

② agg + 내장 함수 (추천 ⭐)
   from pyspark.sql.functions import avg, approx_count_distinct
   df.groupBy(&quot;A&quot;).agg(
       avg(&quot;B&quot;).alias(&quot;평균B&quot;),
       approx_count_distinct(&quot;C&quot;).alias(&quot;고유C수&quot;)
   )

③ 열 단위 수학 함수
   from pyspark.sql.functions import sqrt, cos
   df.withColumn(&quot;sqrt_id&quot;, sqrt(&quot;id&quot;))</code></pre><hr>
<blockquote>
<p>✏️ <strong>초안 상태입니다.</strong> 질문 후 Q&amp;A 섹션과 설명이 보강될 예정입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[PySpark 2 - DataFrame & Column 조작
]]></title>
            <link>https://velog.io/@no-glass-otacku/PySpark-2</link>
            <guid>https://velog.io/@no-glass-otacku/PySpark-2</guid>
            <pubDate>Fri, 19 Jun 2026 01:40:11 GMT</pubDate>
            <description><![CDATA[<h1 id="dataframe--column-조작">DataFrame &amp; Column 조작</h1>
<blockquote>
<p>컬럼 선택, 추가, 수정, 행 필터링, 정렬까지 DataFrame을 다루는 핵심 문법 정리</p>
</blockquote>
<hr>
<h2 id="읽기-전에--컬럼을-참조하는-3가지-방법">읽기 전에 — 컬럼을 참조하는 3가지 방법</h2>
<p>DataFrame에서 컬럼을 가리킬 때 세 가지 방법이 있다. 결과는 모두 동일하다.</p>
<pre><code class="language-python">from pyspark.sql.functions import col

events_df.device        # 점(.) 표기법
events_df[&quot;device&quot;]     # 딕셔너리 표기법
col(&quot;device&quot;)           # col() 함수 (가장 많이 씀)</code></pre>
<p><strong>언제 <code>col()</code>을 써야 하냐면:</strong></p>
<pre><code class="language-python"># 단순 컬럼 이름만 쓸 땐 문자열도 됨
events_df.select(&quot;user_id&quot;, &quot;device&quot;)

# 연산, 메서드 체이닝, 중첩 구조 접근할 땐 col() 필요
col(&quot;ecommerce.purchase_revenue_in_usd&quot;) * 100   # 연산
col(&quot;event_timestamp&quot;).desc()                     # 메서드
col(&quot;geo.city&quot;).alias(&quot;city&quot;)                     # 중첩 구조</code></pre>
<hr>
<h2 id="1-컬럼-연산자-및-메서드">1. 컬럼 연산자 및 메서드</h2>
<p><code>col()</code>로 가져온 컬럼에 다양한 연산과 메서드를 붙일 수 있다.</p>
<pre><code class="language-python"># 수학 연산
col(&quot;price&quot;) * 100
col(&quot;quantity&quot;) + col(&quot;extra_quantity&quot;)

# 비교 연산 (Python에서 == 는 ===와 다름에 주의)
col(&quot;device&quot;) == &quot;Android&quot;
col(&quot;price&quot;) &gt;= 200

# 메서드
col(&quot;event_timestamp&quot;).desc()                          # 내림차순 정렬용
col(&quot;price&quot;).cast(&quot;int&quot;)                               # 타입 변환
col(&quot;email&quot;).isNotNull()                               # NULL 아닌 것
col(&quot;email&quot;).isNull()                                  # NULL인 것
col(&quot;device&quot;).isin(&quot;iOS&quot;, &quot;Android&quot;)                   # 특정 값 포함 여부
col(&quot;ecommerce.purchase_revenue_in_usd&quot;) * 100         # 중첩 구조 접근</code></pre>
<p>실제 사용 예시:</p>
<pre><code class="language-python">rev_df = (events_df
  .filter(col(&quot;ecommerce.purchase_revenue_in_usd&quot;).isNotNull())
  .withColumn(&quot;purchase_revenue&quot;,
      (col(&quot;ecommerce.purchase_revenue_in_usd&quot;) * 100).cast(&quot;int&quot;))
  .withColumn(&quot;avg_purchase_revenue&quot;,
      col(&quot;ecommerce.purchase_revenue_in_usd&quot;) / col(&quot;ecommerce.total_item_quantity&quot;))
  .sort(col(&quot;avg_purchase_revenue&quot;).desc())
)
display(rev_df)</code></pre>
<hr>
<h2 id="2-컬럼-선택-열-부분-집합">2. 컬럼 선택 (열 부분 집합)</h2>
<h3 id="select--원하는-컬럼만-고르기">select() — 원하는 컬럼만 고르기</h3>
<pre><code class="language-python"># 문자열로 컬럼 이름만 지정
devices_df = events_df.select(&quot;user_id&quot;, &quot;device&quot;)</code></pre>
<pre><code class="language-python"># col()로 중첩 구조 접근 + alias로 이름 변경
from pyspark.sql.functions import col

locations_df = events_df.select(
    &quot;user_id&quot;,
    col(&quot;geo.city&quot;).alias(&quot;city&quot;),    # geo 구조체 안의 city를 꺼내서 &quot;city&quot;로 이름 변경
    col(&quot;geo.state&quot;).alias(&quot;state&quot;)
)</code></pre>
<h3 id="selectexpr--sql-표현식으로-컬럼-선택">selectExpr() — SQL 표현식으로 컬럼 선택</h3>
<pre><code class="language-python"># SQL 문법을 문자열로 그대로 쓸 수 있음
apple_df = events_df.selectExpr(
    &quot;user_id&quot;,
    &quot;device in (&#39;macOS&#39;, &#39;iOS&#39;) as apple_user&quot;  # SQL IN 절 → true/false 컬럼 생성
)
display(apple_df)</code></pre>
<p>select()와 차이:</p>
<pre><code>select()      → col() 객체나 컬럼 이름 문자열 사용
selectExpr()  → SQL 표현식 문자열 그대로 사용 (더 직관적인 경우 있음)</code></pre><h3 id="drop--특정-컬럼-제거">drop() — 특정 컬럼 제거</h3>
<pre><code class="language-python"># 문자열로 여러 컬럼 한번에 제거
anonymous_df = events_df.drop(&quot;user_id&quot;, &quot;geo&quot;, &quot;device&quot;)

# col()로도 가능
no_sales_df = events_df.drop(col(&quot;ecommerce&quot;))</code></pre>
<hr>
<h2 id="3-컬럼-추가--수정">3. 컬럼 추가 / 수정</h2>
<h3 id="withcolumn--새-컬럼-추가-or-기존-컬럼-덮어쓰기">withColumn() — 새 컬럼 추가 or 기존 컬럼 덮어쓰기</h3>
<pre><code class="language-python"># 새 컬럼 추가 (mobile 컬럼이 없으면 추가)
mobile_df = events_df.withColumn(
    &quot;mobile&quot;,
    col(&quot;device&quot;).isin(&quot;iOS&quot;, &quot;Android&quot;)   # iOS나 Android면 true, 아니면 false
)</code></pre>
<pre><code class="language-python"># 기존 컬럼 덮어쓰기 (같은 이름으로 지정하면 교체됨)
purchase_quantity_df = events_df.withColumn(
    &quot;purchase_quantity&quot;,
    col(&quot;ecommerce.total_item_quantity&quot;).cast(&quot;int&quot;)  # double → int로 타입 변환
)
purchase_quantity_df.printSchema()</code></pre>
<p><strong>withColumn 규칙:</strong></p>
<pre><code>같은 이름 지정  →  기존 컬럼 덮어씀
다른 이름 지정  →  새 컬럼 추가됨</code></pre><h3 id="withcolumnrenamed--컬럼-이름만-바꾸기">withColumnRenamed() — 컬럼 이름만 바꾸기</h3>
<pre><code class="language-python"># &quot;geo&quot; 컬럼 이름을 &quot;location&quot;으로 변경
location_df = events_df.withColumnRenamed(&quot;geo&quot;, &quot;location&quot;)</code></pre>
<hr>
<h2 id="4-행-필터링-행-부분-집합">4. 행 필터링 (행 부분 집합)</h2>
<h3 id="filter--where--조건에-맞는-행만-남기기">filter() / where() — 조건에 맞는 행만 남기기</h3>
<p><code>filter()</code>와 <code>where()</code>는 완전히 동일하다.</p>
<pre><code class="language-python"># 문자열 SQL 표현식으로 필터링
purchases_df = events_df.filter(&quot;ecommerce.total_item_quantity &gt; 0&quot;)</code></pre>
<pre><code class="language-python"># col()로 필터링
revenue_df = events_df.filter(col(&quot;ecommerce.purchase_revenue_in_usd&quot;).isNotNull())</code></pre>
<pre><code class="language-python"># 여러 조건 동시에 (AND는 &amp;, OR는 |, 각 조건은 괄호로 묶어야 함)
android_df = events_df.filter(
    (col(&quot;traffic_source&quot;) != &quot;direct&quot;) &amp; (col(&quot;device&quot;) == &quot;Android&quot;)
)</code></pre>
<p><strong>주의: Python에서 AND/OR 쓸 때</strong></p>
<pre><code class="language-python"># 틀린 방법 (and, or 쓰면 에러)
.filter(col(&quot;a&quot;) == 1 and col(&quot;b&quot;) == 2)   # ❌

# 맞는 방법 (&amp;, | 사용 + 각 조건 괄호 필수)
.filter((col(&quot;a&quot;) == 1) &amp; (col(&quot;b&quot;) == 2))  # ✅ AND
.filter((col(&quot;a&quot;) == 1) | (col(&quot;b&quot;) == 2))  # ✅ OR</code></pre>
<h3 id="dropduplicates--distinct--중복-행-제거">dropDuplicates() / distinct() — 중복 행 제거</h3>
<pre><code class="language-python"># 모든 컬럼 기준으로 완전히 중복된 행 제거
display(events_df.distinct())

# 특정 컬럼 기준으로 중복 제거 (dropDuplicates만 가능)
distinct_users_df = events_df.dropDuplicates([&quot;user_id&quot;])
#  → user_id가 같은 행이 여러 개면 첫 번째만 남김</code></pre>
<p><code>distinct()</code>와 <code>dropDuplicates()</code> 차이:</p>
<pre><code>distinct()                 → 모든 컬럼 기준, 완전히 같은 행만 제거
dropDuplicates([&quot;컬럼&quot;])   → 지정한 컬럼 기준으로 중복 제거 (다른 컬럼 값은 달라도 됨)</code></pre><h3 id="limit--상위-n개-행만-남기기">limit() — 상위 n개 행만 남기기</h3>
<pre><code class="language-python">limit_df = events_df.limit(100)</code></pre>
<hr>
<h2 id="5-행-정렬">5. 행 정렬</h2>
<h3 id="sort--orderby--정렬-둘은-완전히-동일">sort() / orderBy() — 정렬 (둘은 완전히 동일)</h3>
<pre><code class="language-python"># 오름차순 (기본값)
increase_df = events_df.sort(&quot;event_timestamp&quot;)

# 내림차순 (.desc() 붙이기)
decrease_df = events_df.sort(col(&quot;event_timestamp&quot;).desc())

# 여러 컬럼 기준 정렬 (리스트로)
multi_df = events_df.orderBy([&quot;user_first_touch_timestamp&quot;, &quot;event_timestamp&quot;])

# 여러 컬럼, 각각 다른 방향으로
mixed_df = events_df.sort(
    col(&quot;user_first_touch_timestamp&quot;).desc(),
    col(&quot;event_timestamp&quot;)             # 이건 오름차순
)</code></pre>
<hr>
<h2 id="전체-메서드-한눈에-보기">전체 메서드 한눈에 보기</h2>
<h3 id="컬럼-관련">컬럼 관련</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>select(&quot;a&quot;, &quot;b&quot;)</code></td>
<td>컬럼 선택</td>
<td><code>.select(&quot;user_id&quot;, &quot;device&quot;)</code></td>
</tr>
<tr>
<td><code>select(col(&quot;a&quot;).alias(&quot;b&quot;))</code></td>
<td>컬럼 선택 + 이름 변경</td>
<td><code>col(&quot;geo.city&quot;).alias(&quot;city&quot;)</code></td>
</tr>
<tr>
<td><code>selectExpr(&quot;a in (...) as b&quot;)</code></td>
<td>SQL 표현식으로 선택</td>
<td><code>&quot;device in (&#39;iOS&#39;) as apple&quot;</code></td>
</tr>
<tr>
<td><code>drop(&quot;a&quot;, &quot;b&quot;)</code></td>
<td>컬럼 제거</td>
<td><code>.drop(&quot;user_id&quot;, &quot;geo&quot;)</code></td>
</tr>
<tr>
<td><code>withColumn(&quot;새이름&quot;, 식)</code></td>
<td>컬럼 추가/교체</td>
<td><code>.withColumn(&quot;mobile&quot;, col(&quot;device&quot;).isin(...))</code></td>
</tr>
<tr>
<td><code>withColumnRenamed(&quot;기존&quot;, &quot;새이름&quot;)</code></td>
<td>컬럼 이름 변경</td>
<td><code>.withColumnRenamed(&quot;geo&quot;, &quot;location&quot;)</code></td>
</tr>
</tbody></table>
<h3 id="행-관련">행 관련</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>filter(&quot;조건&quot;)</code></td>
<td>문자열 조건 필터링</td>
<td><code>.filter(&quot;price &gt; 0&quot;)</code></td>
</tr>
<tr>
<td><code>filter(col(&quot;a&quot;).isNotNull())</code></td>
<td>col 조건 필터링</td>
<td><code>.filter(col(&quot;email&quot;).isNotNull())</code></td>
</tr>
<tr>
<td><code>filter((조건1) &amp; (조건2))</code></td>
<td>AND 조건</td>
<td><code>(col(&quot;a&quot;)==1) &amp; (col(&quot;b&quot;)==2)</code></td>
</tr>
<tr>
<td><code>filter((조건1) | (조건2))</code></td>
<td>OR 조건</td>
<td><code>(col(&quot;a&quot;)==1) | (col(&quot;b&quot;)==2)</code></td>
</tr>
<tr>
<td><code>distinct()</code></td>
<td>전체 중복 제거</td>
<td><code>.distinct()</code></td>
</tr>
<tr>
<td><code>dropDuplicates([&quot;컬럼&quot;])</code></td>
<td>특정 컬럼 기준 중복 제거</td>
<td><code>.dropDuplicates([&quot;user_id&quot;])</code></td>
</tr>
<tr>
<td><code>limit(n)</code></td>
<td>상위 n개만</td>
<td><code>.limit(100)</code></td>
</tr>
<tr>
<td><code>sort(&quot;컬럼&quot;)</code></td>
<td>오름차순 정렬</td>
<td><code>.sort(&quot;price&quot;)</code></td>
</tr>
<tr>
<td><code>sort(col(&quot;컬럼&quot;).desc())</code></td>
<td>내림차순 정렬</td>
<td><code>.sort(col(&quot;price&quot;).desc())</code></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[PySpark 1 - DataFrame 생성]]></title>
            <link>https://velog.io/@no-glass-otacku/PySpark</link>
            <guid>https://velog.io/@no-glass-otacku/PySpark</guid>
            <pubDate>Fri, 19 Jun 2026 00:33:20 GMT</pubDate>
            <description><![CDATA[<h1 id="spark-sql과-dataframe-api">Spark SQL과 DataFrame API</h1>
<blockquote>
<p>SQL과 Python DataFrame API, 두 가지 방법으로 같은 쿼리를 표현하는 방법 정리</p>
</blockquote>
<hr>
<h2 id="읽기-전에--핵심-개념-정리">읽기 전에 — 핵심 개념 정리</h2>
<blockquote>
<p>Q. 내용은 PySpark인데 왜 제목이 Spark SQL이야?</p>
</blockquote>
<p>Spark SQL은 &quot;SQL만 쓰는 것&quot;이 아니라 <strong>구조화된 데이터를 처리하는 Spark 모듈 전체의 이름</strong>이다.</p>
<pre><code>Spark SQL 모듈
├── SQL 방식          (%sql 셀에서 SELECT * FROM ...)
└── DataFrame API    (Python으로 spark.table(...).select(...))</code></pre><p>둘 다 같은 Spark SQL 엔진을 쓰고, 결과도 동일하다. PySpark는 Python으로 Spark를 쓰는 것이고, DataFrame API가 PySpark의 핵심이다.</p>
<blockquote>
<p>Q. SQL에서는 Spark를 쓰는지 어떻게 분간해?</p>
</blockquote>
<p>Databricks에서 <code>%sql</code> 셀에 쓰는 SQL은 <strong>전부 Spark SQL</strong>이다.</p>
<pre><code>Databricks 노트북
  %sql 셀         → 무조건 Spark SQL
  %python 셀      → spark. 붙으면 Spark 사용 중

Databricks 밖
  MySQL에서 SELECT ...      → 일반 SQL (Spark 아님)
  spark.sql(&quot;SELECT ...&quot;)   → Spark SQL (명시적으로 표시됨)</code></pre><p>Databricks 안에서는 &quot;항상 Spark SQL이다&quot;라고 이해하면 된다.</p>
<hr>
<h2 id="1-두-가지-방법으로-같은-쿼리-쓰기">1. 두 가지 방법으로 같은 쿼리 쓰기</h2>
<p>Spark SQL은 SQL과 DataFrame API, 두 가지 방법으로 동일한 쿼리를 표현할 수 있다.</p>
<p><strong>방법 1: SQL</strong></p>
<pre><code class="language-sql">%sql
SELECT name, price
FROM products
WHERE price &lt; 200
ORDER BY price</code></pre>
<p><strong>방법 2: DataFrame API (Python)</strong></p>
<pre><code class="language-python">display(spark
        .table(&quot;products&quot;)
        .select(&quot;name&quot;, &quot;price&quot;)
        .where(&quot;price &lt; 200&quot;)
        .orderBy(&quot;price&quot;)
       )</code></pre>
<p>둘 다 완전히 동일한 결과를 반환한다. 내부적으로 같은 쿼리 계획으로 변환되어 실행된다.</p>
<hr>
<h2 id="2-sparksession">2. SparkSession</h2>
<p><code>SparkSession</code>은 <strong>DataFrame API를 사용하는 Spark의 모든 기능에 대한 입구</strong>다.</p>
<p>Databricks 노트북에서는 자동으로 생성되어 <code>spark</code> 변수에 저장된다.</p>
<pre><code class="language-python">spark  # 이미 만들어져 있음</code></pre>
<p>주요 메서드:</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>spark.sql(&quot;쿼리&quot;)</code></td>
<td>SQL 실행 → DataFrame 반환</td>
</tr>
<tr>
<td><code>spark.table(&quot;테이블명&quot;)</code></td>
<td>테이블을 DataFrame으로 가져오기</td>
</tr>
<tr>
<td><code>spark.read</code></td>
<td>파일에서 DataFrame 읽기</td>
</tr>
<tr>
<td><code>spark.createDataFrame(...)</code></td>
<td>테스트용 DataFrame 직접 생성</td>
</tr>
</tbody></table>
<pre><code class="language-python"># SQL 실행 결과를 DataFrame으로 받기
result_df = spark.sql(&quot;&quot;&quot;
SELECT name, price
FROM products
WHERE price &lt; 200
ORDER BY price
&quot;&quot;&quot;)
display(result_df)</code></pre>
<hr>
<h2 id="3-dataframe">3. DataFrame</h2>
<blockquote>
<p>Q. DataFrame은 왜 만들어? 테이블이랑 같은 거야?</p>
</blockquote>
<p>비슷하지만 다르다.</p>
<pre><code>테이블                          DataFrame
──────────────────              ──────────────────
메타스토어에 등록됨               Python 변수에 저장됨
세션 끝나도 유지됨                세션 끝나면 사라짐
SQL에서 이름으로 조회 가능         Python 코드에서만 사용</code></pre><p>DataFrame을 만드는 이유는 <strong>Python 코드 안에서 데이터를 변수처럼 다루기 위해서</strong>다.</p>
<h3 id="dataframe-생성-방법">DataFrame 생성 방법</h3>
<p><strong>① spark.table() — 테이블을 DataFrame으로 가져오기</strong></p>
<pre><code class="language-python">products_df = spark.table(&quot;products&quot;)</code></pre>
<p><strong>② spark.sql() — SQL 실행 결과를 DataFrame으로 받기</strong></p>
<pre><code class="language-python">result_df = spark.sql(&quot;&quot;&quot;
  SELECT name, price
  FROM products
  WHERE price &lt; 200
  ORDER BY price
&quot;&quot;&quot;)
display(result_df)</code></pre>
<p><strong>③ spark.read — 파일에서 DataFrame 읽기</strong></p>
<pre><code class="language-python"># CSV 파일에서 읽기
df = spark.read.csv(&quot;/path/to/file.csv&quot;, header=True)

# Parquet 파일에서 읽기
df = spark.read.parquet(&quot;/path/to/file.parquet&quot;)

# JSON 파일에서 읽기
df = spark.read.json(&quot;/path/to/file.json&quot;)</code></pre>
<p><strong>④ spark.createDataFrame() — 테스트용 DataFrame 직접 만들기</strong></p>
<pre><code class="language-python"># 튜플 리스트로 직접 생성
data = [(&quot;Alice&quot;, 25), (&quot;Bob&quot;, 30), (&quot;Carol&quot;, 22)]
df = spark.createDataFrame(data, [&quot;name&quot;, &quot;age&quot;])
display(df)</code></pre>
<p><strong>⑤ spark.range() — 숫자 범위로 DataFrame 만들기</strong></p>
<pre><code class="language-python"># 0부터 9까지 숫자 DataFrame
df = spark.range(10)
display(df)</code></pre>
<h3 id="dataframe-변수처럼-다루기">DataFrame 변수처럼 다루기</h3>
<pre><code class="language-python"># 테이블에서 가져와서 변환 후 변수에 저장
budget_df = (spark
             .table(&quot;products&quot;)
             .select(&quot;name&quot;, &quot;price&quot;)
             .where(&quot;price &lt; 200&quot;)
             .orderBy(&quot;price&quot;)
            )

display(budget_df)  # 결과 출력</code></pre>
<h3 id="스키마-확인">스키마 확인</h3>
<pre><code class="language-python"># 스키마 속성으로 보기
budget_df.schema

# printSchema()로 보기 (더 읽기 쉬움)
budget_df.printSchema()

# 출력 예시:
# root
#  |-- name: string (nullable = true)
#  |-- price: double (nullable = true)</code></pre>
<hr>
<h2 id="4-변환transformation-vs-액션action">4. 변환(Transformation) vs 액션(Action)</h2>
<p>DataFrame 메서드는 두 종류로 나뉜다.</p>
<h3 id="변환--실행되지-않고-계획만-세움-지연-평가">변환 — 실행되지 않고 계획만 세움 (지연 평가)</h3>
<pre><code class="language-python"># 이 코드는 실행해도 계산이 안 됨!
(products_df
  .select(&quot;name&quot;, &quot;price&quot;)
  .where(&quot;price &lt; 200&quot;)
  .orderBy(&quot;price&quot;))
# → &quot;이렇게 할 거야&quot; 라는 계획만 만들어둠</code></pre>
<table>
<thead>
<tr>
<th>변환 메서드</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>.select(&quot;컬럼1&quot;, &quot;컬럼2&quot;)</code></td>
<td>컬럼 선택</td>
<td><code>.select(&quot;name&quot;, &quot;price&quot;)</code></td>
</tr>
<tr>
<td><code>.where(&quot;조건&quot;)</code></td>
<td>행 필터링</td>
<td><code>.where(&quot;device = &#39;macOS&#39;&quot;)</code></td>
</tr>
<tr>
<td><code>.filter(&quot;조건&quot;)</code></td>
<td>where와 동일</td>
<td><code>.filter(&quot;price &lt; 200&quot;)</code></td>
</tr>
<tr>
<td><code>.orderBy(&quot;컬럼&quot;)</code></td>
<td>오름차순 정렬</td>
<td><code>.orderBy(&quot;price&quot;)</code></td>
</tr>
<tr>
<td><code>.drop(&quot;컬럼&quot;)</code></td>
<td>컬럼 제거</td>
<td><code>.drop(&quot;price&quot;)</code></td>
</tr>
<tr>
<td><code>.withColumn(&quot;새컬럼&quot;, 식)</code></td>
<td>새 컬럼 추가/수정</td>
<td><code>.withColumn(&quot;tax&quot;, col(&quot;price&quot;)*0.1)</code></td>
</tr>
<tr>
<td><code>.withColumnRenamed(&quot;기존&quot;, &quot;새이름&quot;)</code></td>
<td>컬럼 이름 변경</td>
<td><code>.withColumnRenamed(&quot;name&quot;, &quot;item_name&quot;)</code></td>
</tr>
<tr>
<td><code>.limit(n)</code></td>
<td>상위 n개 행만</td>
<td><code>.limit(10)</code></td>
</tr>
<tr>
<td><code>.distinct()</code></td>
<td>중복 행 제거</td>
<td><code>.distinct()</code></td>
</tr>
<tr>
<td><code>.groupBy(&quot;컬럼&quot;)</code></td>
<td>그룹화</td>
<td><code>.groupBy(&quot;category&quot;).count()</code></td>
</tr>
</tbody></table>
<h3 id="액션--실제-계산을-실행시키는-메서드">액션 — 실제 계산을 실행시키는 메서드</h3>
<pre><code class="language-python"># .show()가 붙어야 비로소 실행됨
(products_df
  .select(&quot;name&quot;, &quot;price&quot;)
  .where(&quot;price &lt; 200&quot;)
  .orderBy(&quot;price&quot;)
  .show())           # ← 여기서 실제 계산 시작</code></pre>
<table>
<thead>
<tr>
<th>액션 메서드</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>.show()</code></td>
<td>상위 20개 행 출력</td>
<td><code>df.show()</code></td>
</tr>
<tr>
<td><code>.show(n)</code></td>
<td>상위 n개 행 출력</td>
<td><code>df.show(5)</code></td>
</tr>
<tr>
<td><code>display(df)</code></td>
<td>Databricks에서 표 형식으로 출력</td>
<td><code>display(budget_df)</code></td>
</tr>
<tr>
<td><code>.count()</code></td>
<td>행 수 반환 (숫자)</td>
<td><code>budget_df.count()</code></td>
</tr>
<tr>
<td><code>.collect()</code></td>
<td>모든 행을 Python 배열로 반환</td>
<td><code>budget_df.collect()</code></td>
</tr>
<tr>
<td><code>.take(n)</code></td>
<td>처음 n개 행을 배열로 반환</td>
<td><code>budget_df.take(3)</code></td>
</tr>
<tr>
<td><code>.first()</code></td>
<td>첫 번째 행 반환</td>
<td><code>budget_df.first()</code></td>
</tr>
<tr>
<td><code>.describe()</code></td>
<td>숫자/문자열 컬럼 기본 통계</td>
<td><code>df.describe().show()</code></td>
</tr>
</tbody></table>
<p><strong>왜 지연 평가를 쓰냐면:</strong></p>
<pre><code>변환 10개를 연결해도 Spark는 실행 안 함
        ↓
액션 호출 시 변환 10개를 한꺼번에 최적화해서 실행
        ↓
불필요한 중간 계산 없이 한 번에 처리 → 빠름</code></pre><hr>
<h2 id="5-dataframe-↔-sql-변환">5. DataFrame ↔ SQL 변환</h2>
<p>DataFrame을 SQL에서 쓰고 싶으면 임시 뷰로 등록하면 된다.</p>
<pre><code class="language-python"># DataFrame → 임시 뷰 등록
budget_df.createOrReplaceTempView(&quot;budget&quot;)</code></pre>
<pre><code class="language-sql">-- 이제 SQL에서 budget 테이블처럼 쓸 수 있음
SELECT * FROM budget</code></pre>
<pre><code class="language-python"># Python에서도 SQL처럼 쓰고 싶으면
display(spark.sql(&quot;SELECT * FROM budget&quot;))</code></pre>
<hr>
<h2 id="전체-흐름-정리">전체 흐름 정리</h2>
<pre><code>같은 쿼리를 두 가지로 표현 가능
  SQL (%sql 셀)                    DataFrame API (Python)
  SELECT * FROM products    ↔     spark.table(&quot;products&quot;)
  WHERE price &lt; 200         ↔     .where(&quot;price &lt; 200&quot;)
  ORDER BY price            ↔     .orderBy(&quot;price&quot;)

DataFrame 쓸 때 흐름
  spark.table() or spark.sql()     ← 생성
    ↓
  .select() .where() .orderBy()    ← 변환 (지연 평가, 실행 안 됨)
    ↓
  .show() .count() .collect()      ← 액션 (여기서 실제 실행)

DataFrame ↔ SQL 오가기
  df.createOrReplaceTempView(&quot;이름&quot;)  → SQL에서 테이블처럼 사용 가능
  spark.sql(&quot;SELECT * FROM 이름&quot;)     → SQL 결과를 DataFrame으로</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks - Spark SQL 2]]></title>
            <link>https://velog.io/@no-glass-otacku/Databricks-Spark-SQL-2</link>
            <guid>https://velog.io/@no-glass-otacku/Databricks-Spark-SQL-2</guid>
            <pubDate>Thu, 18 Jun 2026 07:06:34 GMT</pubDate>
            <description><![CDATA[<pre><code>6강: 데이터 정리     → NULL 처리, 중복 제거, 날짜/문자열 변환
7강: 고급 SQL 변환   → JSON 파싱, 배열 다루기, JOIN, PIVOT, 고차 함수
8강: SQL UDF        → 내 함수 만들어서 등록하고 재사용하기</code></pre><hr>
<h1 id="databricks-데이터-정리-고급-sql-변환-sql-udf">Databricks 데이터 정리, 고급 SQL 변환, SQL UDF</h1>
<blockquote>
<p>지저분한 데이터를 정리하고, 복잡한 구조를 다루고, 재사용 가능한 함수를 만드는 방법 정리</p>
</blockquote>
<hr>
<h2 id="1-데이터-정리">1. 데이터 정리</h2>
<h3 id="null-값-파악하기">NULL 값 파악하기</h3>
<p><code>count(컬럼)</code>은 NULL을 건너뜀. <code>count(*)</code>은 NULL이 있는 행도 포함.</p>
<pre><code class="language-sql">-- 각 컬럼에 NULL이 몇 개인지 확인
SELECT
  count_if(user_id IS NULL) AS missing_user_ids,
  count_if(email IS NULL) AS missing_emails,
  count_if(updated IS NULL) AS missing_updates
FROM users_dirty</code></pre>
<pre><code class="language-sql">-- 총 행 수 vs NULL 제외 행 수 비교
SELECT count(*), count(user_id) FROM users_dirty
--      ↑ NULL 포함    ↑ NULL 제외</code></pre>
<h3 id="중복-파악하기">중복 파악하기</h3>
<pre><code class="language-sql">-- 전체 행 수
SELECT count(*) FROM users_dirty

-- 고유 행 수
SELECT count(DISTINCT(*)) FROM users_dirty

-- 특정 컬럼 기준 고유 값 수
SELECT count(DISTINCT user_id) FROM users_dirty</code></pre>
<p><strong>주의:</strong> <code>DISTINCT</code>는 NULL이 있는 행을 제외하지 않는다. 모든 값이 NULL인 행도 &quot;하나의 고유한 행&quot;으로 처리된다.</p>
<h3 id="특정-컬럼-기준으로-중복-제거">특정 컬럼 기준으로 중복 제거</h3>
<pre><code class="language-sql">-- user_id + user_first_touch_timestamp 기준으로 중복 제거
-- email은 여러 행 중 NULL이 아닌 값을 가져오기 위해 max() 사용
CREATE OR REPLACE TEMP VIEW deduped_users AS
SELECT user_id, user_first_touch_timestamp,
       max(email) AS email,
       max(updated) AS updated
FROM users_dirty
WHERE user_id IS NOT NULL          -- NULL 행 제거
GROUP BY user_id, user_first_touch_timestamp;</code></pre>
<p><strong>왜 <code>max(email)</code>을 쓰냐면:</strong>
같은 user_id가 여러 행이면 GROUP BY 후 email이 여러 개 존재. 그 중 NULL이 아닌 값을 가져오기 위해 max() 사용.</p>
<h3 id="데이터-검증">데이터 검증</h3>
<pre><code class="language-sql">-- user_id가 중복 없는지 확인 (true면 중복 없음)
SELECT max(row_count) &lt;= 1 AS no_duplicate_ids FROM (
  SELECT user_id, count(*) AS row_count
  FROM deduped_users
  GROUP BY user_id
)</code></pre>
<h3 id="날짜-형식-변환-및-정규-표현식">날짜 형식 변환 및 정규 표현식</h3>
<pre><code class="language-sql">SELECT *,
  date_format(first_touch, &quot;MMM d, yyyy&quot;) AS first_touch_date,   -- 사람이 읽기 좋은 날짜
  date_format(first_touch, &quot;HH:mm:ss&quot;) AS first_touch_time,      -- 시간
  regexp_extract(email, &quot;(?&lt;=@).+&quot;, 0) AS email_domain           -- 이메일에서 도메인 추출
FROM (
  SELECT *,
    CAST(user_first_touch_timestamp / 1e6 AS timestamp) AS first_touch  -- Unix 타임스탬프 변환
  FROM deduped_users
)</code></pre>
<hr>
<h2 id="2-고급-sql-변환">2. 고급 SQL 변환</h2>
<h3 id="json-데이터-다루기">JSON 데이터 다루기</h3>
<p>Kafka 같은 시스템에서 오는 데이터는 JSON이 바이너리로 인코딩된 경우가 많다.</p>
<pre><code class="language-sql">-- 바이너리를 문자열로 변환해서 읽기
CREATE OR REPLACE TEMP VIEW events_strings AS
  SELECT string(key), string(value)
  FROM events_raw;</code></pre>
<p><strong>JSON 안의 중첩 데이터 접근하기:</strong></p>
<pre><code class="language-sql">-- : 구문으로 JSON 필드 접근
SELECT value:device, value:geo:city
FROM events_strings
--    ↑ 최상위 필드  ↑ 중첩 필드 (geo 안의 city)</code></pre>
<p><strong>JSON 문자열을 구조체로 파싱하기:</strong></p>
<pre><code class="language-sql">-- schema_of_json으로 스키마 자동 추론 → from_json으로 파싱
CREATE OR REPLACE TEMP VIEW parsed_events AS
  SELECT from_json(value, schema_of_json(&#39;{...샘플JSON...}&#39;)) AS json
  FROM events_strings;

-- 구조체를 개별 컬럼으로 평면화
CREATE OR REPLACE TEMP VIEW new_events_final AS
  SELECT json.*       -- ← * 로 펼치기
  FROM parsed_events;</code></pre>
<h3 id="중첩-데이터-구조-접근하기">중첩 데이터 구조 접근하기</h3>
<p>구조체(struct) 타입은 <code>.</code> 으로 접근:</p>
<pre><code class="language-sql">-- ecommerce 구조체 안의 purchase_revenue_in_usd 접근
SELECT ecommerce.purchase_revenue_in_usd
FROM events
WHERE ecommerce.purchase_revenue_in_usd IS NOT NULL</code></pre>
<h3 id="배열-다루기">배열 다루기</h3>
<pre><code class="language-sql">-- 배열 크기 필터링 (items가 2개 초과인 행만)
SELECT user_id, items
FROM events
WHERE size(items) &gt; 2</code></pre>
<p><strong>explode — 배열을 행으로 펼치기:</strong></p>
<pre><code class="language-sql">-- items 배열의 각 요소를 별도 행으로 분리
SELECT user_id, explode(items) AS item
FROM events</code></pre>
<pre><code>Before: user_1 | [item_A, item_B, item_C]
After:  user_1 | item_A
        user_1 | item_B
        user_1 | item_C</code></pre><p><strong>collect_set, flatten, array_distinct — 배열 수집 및 정리:</strong></p>
<blockquote>
<p>Q. 이 세 함수가 뭔지 코드만 보고는 이해가 안 됐다.</p>
</blockquote>
<p>단계별로 뭘 하는지 데이터로 보면 이해가 된다.</p>
<p>예시 데이터:</p>
<pre><code>user_id | event_name | items
user_1  | click      | [A, B]
user_1  | purchase   | [B, C]
user_1  | click      | [A, D]</code></pre><pre><code>① collect_set(event_name)
   → 그룹 안의 값들을 배열로 모으기 (중복 제거)
   → user_1: [&quot;click&quot;, &quot;purchase&quot;]  ← 중복 &quot;click&quot; 하나로 합쳐짐

② collect_set(items.item_id)
   → 배열 안의 값을 모으면 배열들의 배열이 됨
   → user_1: [ [A,B], [B,C], [A,D] ]

③ flatten([ [A,B], [B,C], [A,D] ])
   → 배열들의 배열을 하나의 배열로 펼치기
   → [A, B, B, C, A, D]  ← 하나로 합쳐졌지만 중복 있음

④ array_distinct([A, B, B, C, A, D])
   → 배열에서 중복 제거
   → [A, B, C, D]</code></pre><p>세 개를 합치면:</p>
<pre><code class="language-sql">SELECT user_id,
  collect_set(event_name) AS event_history,
  array_distinct(flatten(collect_set(items.item_id))) AS cart_history
  -- → 이 유저가 장바구니에 담았던 모든 상품 ID (중복 없이)
FROM events
GROUP BY user_id</code></pre>
<h3 id="join--조건에-맞는-행끼리-컬럼-합치기">JOIN — 조건에 맞는 행끼리 컬럼 합치기</h3>
<blockquote>
<p>Q. JOIN은 두 테이블의 컬럼을 합치는 거야?</p>
</blockquote>
<p>거의 맞다. 정확히는 <strong>&quot;조건에 맞는 행끼리 연결해서 컬럼을 합치는 것&quot;</strong>.</p>
<pre><code>테이블 A (sales)       테이블 B (item_lookup)
order_id | item_id     item_id  | item_name
1        | M_STAN_K    M_STAN_K | Standard King Mattress
2        | P_FOAM_S    P_FOAM_S | Small Foam Pillow

JOIN ON a.item_id = b.item_id  ← item_id가 같은 행끼리 연결

결과:
order_id | item_id  | item_name
1        | M_STAN_K | Standard King Mattress
2        | P_FOAM_S | Small Foam Pillow</code></pre><blockquote>
<p>Q. ON에 사용한 컬럼명이 다르면 합쳐지지 않고 둘 다 컬럼으로 적히는 거 맞지?</p>
</blockquote>
<p>맞다.</p>
<pre><code>컬럼명 같음 (item_id = item_id)  →  하나로 합쳐져서 컬럼 1개
컬럼명 다름 (order_item_id = product_id)  →  둘 다 컬럼으로 남아서 컬럼 2개</code></pre><p>이름이 달라도 값은 같기 때문에, 컬럼명이 다를 때 SELECT * 하면 거의 똑같은 값의 컬럼 두 개가 나란히 생겨서 지저분해 보인다.</p>
<pre><code class="language-sql">-- explode 후 조회 테이블과 JOIN
CREATE OR REPLACE VIEW sales_enriched AS
SELECT *
FROM (
  SELECT *, explode(items) AS item
  FROM sales) a
INNER JOIN item_lookup b
ON a.item.item_id = b.item_id;</code></pre>
<p>JOIN 종류:</p>
<pre><code>INNER JOIN  → 양쪽 다 있는 행만 (가장 많이 씀)
LEFT JOIN   → 왼쪽 테이블 기준, 오른쪽에 없으면 NULL
RIGHT JOIN  → 오른쪽 테이블 기준, 왼쪽에 없으면 NULL

A: [1, 2, 3]  B: [2, 3, 4]
INNER → [2, 3]      공통인 것만
LEFT  → [1, 2, 3]   A 기준
RIGHT → [2, 3, 4]   B 기준</code></pre><h3 id="집합-연산자">집합 연산자</h3>
<pre><code class="language-sql">-- UNION: 두 쿼리 결과 합치기
SELECT * FROM events
UNION
SELECT * FROM new_events_final

-- INTERSECT: 두 쿼리에 공통으로 있는 행만
SELECT * FROM events
INTERSECT
SELECT * FROM new_events_final

-- MINUS: 첫 번째에는 있고 두 번째에는 없는 행
SELECT * FROM events
MINUS
SELECT * FROM new_events_final</code></pre>
<h3 id="pivot--행을-컬럼으로-변환">PIVOT — 행을 컬럼으로 변환</h3>
<blockquote>
<p>Q. PIVOT 사용법을 몰라서 문제를 못 풀었다.</p>
</blockquote>
<p>PIVOT이 하는 일:</p>
<pre><code>Before (행으로):               After (열로):
user_id | event_name           user_id | click | purchase | view
user_1  | click            →   user_1  | 3     | 1        | 2
user_1  | click
user_1  | purchase
user_1  | view
user_1  | view</code></pre><p>&quot;event_name 값들을 각각 컬럼으로 만들고, 그 안에 집계값을 넣어라&quot;</p>
<p><strong>PIVOT 문법 구조:</strong></p>
<pre><code class="language-sql">SELECT * FROM (
  SELECT 필요한_컬럼들        -- ① 재료 준비 (PIVOT에 필요한 컬럼만)
  FROM 테이블
) PIVOT (
  집계함수(집계할_컬럼)        -- ② 각 칸에 뭘 넣을지 (count, sum 등)
  FOR 피벗할_컬럼 IN (        -- ③ 어떤 컬럼을 기준으로 펼칠지
    &#39;값1&#39;, &#39;값2&#39;, &#39;값3&#39;       -- ④ 열로 만들 값들 (미리 알고 있어야 함)
  )
)</code></pre>
<p><strong>IN 안의 값을 어떻게 아냐면:</strong></p>
<pre><code class="language-sql">-- 먼저 이걸 실행해서 어떤 값들이 있는지 확인
SELECT DISTINCT event_name FROM events</code></pre>
<p><strong>문제 풀이 — &quot;user_id로 그룹화하고 event_name을 피벗하여 각 이벤트 횟수를 열로&quot;:</strong></p>
<pre><code>&quot;각 이벤트 유형의 개수&quot;  →  집계함수 = count(event_name)
&quot;event_name을 피벗&quot;     →  FOR event_name IN (...)
&quot;user_id로 그룹화&quot;      →  재료에 user_id만 남기면 자동으로 그룹화</code></pre><pre><code class="language-sql">SELECT * FROM (
  SELECT user_id, event_name    -- user_id랑 event_name만 남김
  FROM events
) PIVOT (
  count(event_name)             -- 각 칸에 이벤트 개수
  FOR event_name IN (
    &#39;cart&#39;, &#39;login&#39;, &#39;main&#39;,
    &#39;finalize&#39;, &#39;payment_review&#39;,
    -- SELECT DISTINCT event_name FROM events 로 확인한 값들
    &#39;logout&#39;, &#39;home&#39;, &#39;warehouse&#39;
  )
)</code></pre>
<h3 id="고차-함수--배열을-직접-조작">고차 함수 — 배열을 직접 조작</h3>
<p>일반 함수는 배열에 못 씀. 고차 함수를 쓰면 배열 안을 직접 조작 가능.</p>
<p><strong>FILTER — 조건에 맞는 요소만 남기기:</strong></p>
<pre><code class="language-sql">SELECT
  order_id,
  FILTER(items, i -&gt; i.item_id LIKE &quot;%K&quot;) AS king_items
--        ↑배열  ↑반복변수  ↑ 조건 (K로 끝나는 것만 남김)
FROM sales</code></pre>
<p><strong>EXISTS — 배열 안에 조건을 만족하는 요소가 있는지 확인 (true/false 반환):</strong></p>
<blockquote>
<p>실습에서 직접 풀었던 문제: items 배열에서 item_name이 &quot;Mattress&quot;로 끝나면 true, &quot;Pillow&quot;로 끝나면 true인 부울 열 생성</p>
</blockquote>
<pre><code class="language-sql">CREATE OR REPLACE TABLE sales_product_flags AS
SELECT
  items,
  EXISTS(items, x -&gt; x.item_name LIKE &quot;%Mattress&quot;) AS mattress,
  EXISTS(items, x -&gt; x.item_name LIKE &quot;%Pillow&quot;) AS pillow
FROM sales;</code></pre>
<p>결과:</p>
<pre><code>items                                      | mattress | pillow
[{item_id: &quot;M_PREM_K&quot;, item_name: &quot;Premium King Mattress&quot;, ...}] | true  | false
[{item_id: &quot;P_FOAM_S&quot;, item_name: &quot;Standard Foam Pillow&quot;, ...}]  | false | true
[{item_id: &quot;M_STAN_F&quot;, item_name: &quot;Standard Full Mattress&quot;,...}] | true  | false</code></pre><p>FILTER vs EXISTS 차이:</p>
<pre><code>FILTER  → 조건에 맞는 요소만 남긴 배열 반환
          [item_A, item_B, item_C] → [item_B]

EXISTS  → 조건을 만족하는 요소가 하나라도 있으면 true, 없으면 false 반환
          [item_A, item_B, item_C] → true / false</code></pre><p><strong>TRANSFORM — 배열의 각 요소 변환:</strong></p>
<pre><code class="language-sql">SELECT
  order_id,
  TRANSFORM(king_items, k -&gt; CAST(k.item_revenue_in_usd * 100 AS INT)) AS item_revenues
--           ↑배열       ↑반복변수  ↑ 각 요소에 적용할 변환
FROM king_size_sales</code></pre>
<hr>
<h2 id="3-sql-udf-사용자-정의-함수">3. SQL UDF (사용자 정의 함수)</h2>
<h3 id="udf가-뭔데">UDF가 뭔데?</h3>
<p>자주 쓰는 SQL 로직을 <strong>함수로 등록</strong>해서 이름만으로 재사용하는 것.</p>
<pre><code>엑셀의 사용자 정의 함수랑 같은 개념
내가 자주 쓰는 수식을 함수로 저장 → 필요할 때 함수 이름만 호출</code></pre><h3 id="udf-만들기">UDF 만들기</h3>
<pre><code class="language-sql">CREATE OR REPLACE FUNCTION yelling(text STRING)
RETURNS STRING                        -- ← &quot;STRING 타입을 반환할 거야&quot; 라는 선언 (약속)
RETURN concat(upper(text), &quot;!!!&quot;)     -- ← 실제로 반환하는 코드 (실행)</code></pre>
<blockquote>
<p>Q. RETURNS랑 RETURN이 둘 다 있던데 무슨 차이야?</p>
</blockquote>
<pre><code>RETURNS = 메뉴판에 &quot;아메리카노 - 커피입니다&quot; 라고 적어둔 것 (반환 타입 선언)
RETURN  = 실제로 커피를 내어주는 것 (실제 반환 코드)</code></pre><p><code>RETURNS</code>는 어떤 타입을 반환할지 미리 선언, <code>RETURN</code>은 실제로 반환하는 코드.</p>
<pre><code class="language-sql">-- 사용하기
SELECT yelling(food) FROM foods
-- 결과: BEEF!!!, BEANS!!!, POTATOES!!!</code></pre>
<h3 id="udf-정보-확인">UDF 정보 확인</h3>
<pre><code class="language-sql">DESCRIBE FUNCTION yelling           -- 기본 정보
DESCRIBE FUNCTION EXTENDED yelling  -- Body 필드에서 실제 SQL 로직 확인 가능</code></pre>
<h3 id="udf의-특징">UDF의 특징</h3>
<ul>
<li><strong>메타스토어에 저장됨</strong> → 노트북, DBSQL, 작업 등 어디서든 재사용 가능</li>
<li><strong>세션이 끝나도 유지됨</strong> → TEMP VIEW와 달리 영구적</li>
<li><strong>권한 관리 가능</strong> → USAGE, SELECT 권한 부여 필요</li>
</ul>
<h3 id="casewhen--조건-분기">CASE/WHEN — 조건 분기</h3>
<pre><code class="language-sql">SELECT *,
  CASE
    WHEN food = &quot;beans&quot; THEN &quot;I love beans&quot;
    WHEN food = &quot;potatoes&quot; THEN &quot;My favorite vegetable is potatoes&quot;
    WHEN food &lt;&gt; &quot;beef&quot; THEN concat(&quot;Do you have any good recipes for &quot;, food, &quot;?&quot;)
    ELSE concat(&quot;I don&#39;t eat &quot;, food)
  END
FROM foods</code></pre>
<h3 id="casewhen을-udf-안에-넣기">CASE/WHEN을 UDF 안에 넣기</h3>
<pre><code class="language-sql">CREATE FUNCTION foods_i_like(food STRING)
RETURNS STRING
RETURN CASE
  WHEN food = &quot;beans&quot; THEN &quot;I love beans&quot;
  WHEN food = &quot;potatoes&quot; THEN &quot;My favorite vegetable is potatoes&quot;
  WHEN food &lt;&gt; &quot;beef&quot; THEN concat(&quot;Do you have any good recipes for &quot;, food, &quot;?&quot;)
  ELSE concat(&quot;I don&#39;t eat &quot;, food)
END;

SELECT foods_i_like(food) FROM foods</code></pre>
<hr>
<h2 id="전체-흐름-정리">전체 흐름 정리</h2>
<pre><code>6강 데이터 정리
  → NULL 파악 (count_if) → 중복 제거 (GROUP BY + max) → 검증 → 날짜/문자열 변환

7강 고급 SQL 변환
  → JSON 파싱 (from_json, schema_of_json)
  → 배열 조작 (explode, collect_set, flatten, array_distinct)
  → 고차 함수 (FILTER, EXISTS, TRANSFORM)
  → 테이블 결합 (JOIN, UNION, INTERSECT)
  → 형태 변경 (PIVOT)

8강 SQL UDF
  → 반복 로직을 함수로 등록 (CREATE FUNCTION)
  → RETURNS: 반환 타입 선언 / RETURN: 실제 반환 코드
  → CASE/WHEN과 결합해서 제어 흐름 함수 만들기
  → 메타스토어에 저장 → 어디서든 재사용</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks - Spark SQL 1]]></title>
            <link>https://velog.io/@no-glass-otacku/Databricks-Spark-SQL</link>
            <guid>https://velog.io/@no-glass-otacku/Databricks-Spark-SQL</guid>
            <pubDate>Thu, 18 Jun 2026 05:28:26 GMT</pubDate>
            <description><![CDATA[<h1 id="databricks-데이터-수집과-테이블-다루기">Databricks 데이터 수집과 테이블 다루기</h1>
<blockquote>
<p>Spark SQL로 파일 직접 쿼리하는 것부터 Delta 테이블 생성, 외부 소스 연결, 쓰기 방법까지 정리</p>
</blockquote>
<hr>
<h2 id="읽기-전에--가장-중요한-개념-구분">읽기 전에 — 가장 중요한 개념 구분</h2>
<p>이걸 모르면 수업 내내 헷갈린다.</p>
<pre><code>Databricks의 테이블 종류

① 외부 테이블 (= 외부 소스 테이블)
   → 데이터는 밖(CSV, JSON, 외부 DB 등)에 그대로
   → Databricks는 &quot;저기 가면 데이터 있어&quot;라는 정보만 저장
   → USING + LOCATION 키워드가 있으면 외부 테이블

② Delta 테이블
   → 데이터가 Databricks 안으로 들어온 상태
   → 버전 관리, 빠른 쿼리, 자동 최신화 등 모든 기능 사용 가능
   → CTAS (CREATE TABLE AS SELECT) 로 만드는 게 일반적</code></pre><p><strong>&quot;JSON 파일에서 외부 테이블을 생성하라&quot;</strong> = 데이터는 JSON 파일에 그대로 두고, 그 파일을 테이블처럼 읽을 수 있도록 Databricks에 등록하라는 뜻. Delta 테이블 아님.</p>
<hr>
<h3 id="delta-테이블-vs-외부-테이블-vs-jdbc">Delta 테이블 vs 외부 테이블 vs JDBC</h3>
<p><strong>코드로 구분하기</strong></p>
<pre><code class="language-sql">-- 관리형 Delta 테이블
-- USING도 없고 LOCATION도 없음
CREATE TABLE item_lookup AS
SELECT * FROM parquet.`/path`

-- 외부 테이블 (파일)
-- USING + LOCATION 있음
CREATE TABLE sales_csv
USING CSV
OPTIONS (header=&quot;true&quot;, delimiter=&quot;|&quot;)
LOCATION &quot;/path/to/file&quot;

-- JDBC (외부 DB)
-- USING JDBC + url + dbtable
CREATE TABLE users_jdbc
USING JDBC
OPTIONS (
  url = &quot;jdbc:mysql://서버주소:3306&quot;,
  dbtable = &quot;users&quot;
)</code></pre>
<h3 id="한눈에-비교">한눈에 비교</h3>
<table>
<thead>
<tr>
<th></th>
<th>관리형 Delta</th>
<th>외부 테이블</th>
<th>JDBC</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 위치</td>
<td>Databricks 안</td>
<td>파일 (S3 등)</td>
<td>외부 DB 서버</td>
</tr>
<tr>
<td>코드 특징</td>
<td>USING/LOCATION 없음</td>
<td>USING+LOCATION</td>
<td>USING JDBC+url</td>
</tr>
<tr>
<td>최신 데이터</td>
<td>자동</td>
<td>REFRESH 필요</td>
<td>자동</td>
</tr>
<tr>
<td>성능</td>
<td>빠름</td>
<td>보통</td>
<td>DB 크기에 따라 다름</td>
</tr>
</tbody></table>
<h3 id="문제-풀-때-판단하는-순서">문제 풀 때 판단하는 순서</h3>
<pre><code>&quot;어떤 테이블 만들어라&quot; 문제가 나오면

1. &quot;관리형&quot; or &quot;Delta&quot; 언급 → USING/LOCATION 없는 CTAS
2. &quot;외부&quot; or 파일 경로 + 옵션 필요 → USING + LOCATION
3. DB 연결 (MySQL, PostgreSQL 등) → USING JDBC + OPTIONS</code></pre><hr>
<h2 id="1-파일-직접-쿼리하기-테이블-없이-그냥-읽기">1. 파일 직접 쿼리하기 (테이블 없이 그냥 읽기)</h2>
<p>Databricks에서는 테이블을 만들지 않아도 파일을 바로 쿼리할 수 있다.</p>
<pre><code class="language-sql">-- 단일 파일 쿼리
SELECT * FROM json.`/path/to/001.json`

-- 디렉터리 전체 쿼리 (같은 형식/스키마인 경우)
SELECT * FROM json.`/path/to/directory`

-- Parquet도 동일하게
SELECT * FROM parquet.`/path/to/file`</code></pre>
<p>경로는 반드시 <strong>백틱(`)</strong> 으로 감싸야 한다. 작은따옴표(&#39;)가 아님에 주의.</p>
<p>문법 구조: <code>파일형식.경로</code></p>
<pre><code class="language-sql">json.`/path`      -- JSON 파일
parquet.`/path`   -- Parquet 파일
csv.`/path`       -- CSV 파일</code></pre>
<h3 id="뷰와-cte로-더-편리하게-참조하기">뷰와 CTE로 더 편리하게 참조하기</h3>
<pre><code class="language-sql">-- 임시 뷰로 등록 (세션 끝나면 사라짐)
CREATE OR REPLACE TEMP VIEW kafka_events AS
SELECT * FROM json.`${DA.paths.kafka_events}`

-- CTE로 사용
WITH events AS (
  SELECT * FROM json.`/path/events`
)
SELECT * FROM events WHERE topic = &#39;clickstream&#39;</code></pre>
<h3 id="raw-파일-내용-확인-디버깅용">raw 파일 내용 확인 (디버깅용)</h3>
<pre><code class="language-sql">-- 텍스트로 읽기
SELECT * FROM text.`/path/to/file`

-- 바이너리로 읽기 (파일 메타데이터 포함)
SELECT * FROM binaryFile.`/path/to/file`</code></pre>
<hr>
<h2 id="2-외부-소스-테이블--외부-테이블">2. 외부 소스 테이블 (= 외부 테이블)</h2>
<h3 id="외부-테이블이-뭔데-≠delta-테이블">외부 테이블이 뭔데? (≠Delta 테이블)</h3>
<p>비유로 먼저 이해하자.</p>
<pre><code>도서관 책 대출 시스템

책(데이터)은 도서관 서가(외부 경로)에 그대로 있음
대출 카드(외부 테이블 정의)만 시스템에 등록
&quot;이 책은 3층 A구역 12번 선반에 있어&quot; 라는 정보만 저장

→ 읽을 때마다 서가에서 꺼내서 읽음
→ 책 자체를 복사해서 들고오지 않음</code></pre><h3 id="외부-테이블이라는-걸-코드에서-어떻게-알아보나">&quot;외부 테이블&quot;이라는 걸 코드에서 어떻게 알아보나?</h3>
<p>딱 두 가지 키워드가 있으면 외부 테이블이다.</p>
<pre><code class="language-sql">CREATE TABLE sales_csv
  (order_id LONG, email STRING, ...)
USING CSV          -- ① Delta가 아닌 형식 (CSV, JSON, JDBC 등)
OPTIONS (
  header = &quot;true&quot;,
  delimiter = &quot;|&quot;
)
LOCATION &quot;/path&quot;   -- ② 데이터가 외부 경로에 있음</code></pre>
<p>반대로 Delta 테이블은 이렇게 생겼다:</p>
<pre><code class="language-sql">-- USING도 없고 LOCATION도 없음
CREATE TABLE sales AS
SELECT * FROM parquet.`/path`
-- 데이터가 Databricks 안으로 들어옴</code></pre>
<h3 id="csv-파일로-외부-테이블-만들기">CSV 파일로 외부 테이블 만들기</h3>
<p>CSV 파일을 직접 쿼리하면 이런 문제가 생긴다:</p>
<pre><code>헤더가 데이터 행으로 들어옴
모든 컬럼이 하나로 합쳐짐  ← 구분자가 | 인데 Spark가 모름
마지막 컬럼이 잘림</code></pre><p>그래서 옵션을 줘서 외부 테이블로 등록한다:</p>
<pre><code class="language-sql">CREATE TABLE sales_csv
  (order_id LONG, email STRING, transactions_timestamp LONG,
   total_item_quantity INTEGER, purchase_revenue_in_usd DOUBLE,
   unique_items INTEGER, items STRING)
USING CSV
OPTIONS (
  header = &quot;true&quot;,     -- 첫 행은 헤더야
  delimiter = &quot;|&quot;      -- | 로 구분돼 있어
)
LOCATION &quot;${DA.paths.sales_csv}&quot;</code></pre>
<p>이렇게 하면 <code>SELECT * FROM sales_csv</code> 로 깔끔하게 읽을 수 있다.</p>
<h3 id="왜-외부-소스를-써-그냥-데이터-가져오면-되는-거-아니야">왜 외부 소스를 써? 그냥 데이터 가져오면 되는 거 아니야?</h3>
<p>현실에서 &quot;그냥 가져오는 것&quot;이 안 되는 경우가 많다:</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>실시간으로 계속 바뀌는 데이터</td>
<td>복사하는 순간 이미 낡은 데이터</td>
</tr>
<tr>
<td>10TB짜리 대용량 데이터</td>
<td>복사 자체가 시간/비용 낭비</td>
</tr>
<tr>
<td>다른 팀 소유 데이터</td>
<td>복사 권한이 없음</td>
</tr>
<tr>
<td>매일 새 파일이 쌓이는 경우</td>
<td>LOCATION만 걸면 자동으로 읽힘</td>
</tr>
</tbody></table>
<p>실무에서는 <strong>외부 소스로 원본 연결 → 가공 후 Delta로 저장</strong> 흐름이 일반적. 이게 메달리온 아키텍처의 Bronze 단계가 하는 일이다.</p>
<h3 id="외부-테이블의-치명적-한계--캐시-문제">외부 테이블의 치명적 한계 — 캐시 문제</h3>
<pre><code class="language-sql">-- 외부에서 파일에 데이터 추가됨
SELECT COUNT(*) FROM sales_csv  -- → 숫자 그대로 (캐시 때문에 못 봄)

REFRESH TABLE sales_csv  -- 수동으로 캐시 갱신 필요

SELECT COUNT(*) FROM sales_csv  -- → 이제 최신 숫자</code></pre>
<p>Delta 테이블이면 자동으로 최신 데이터를 읽지만, 외부 테이블은 수동 갱신이 필요하다.</p>
<hr>
<h2 id="3-jdbc--외부-db-연결">3. JDBC — 외부 DB 연결</h2>
<h3 id="jdbc가-뭔데">JDBC가 뭔데?</h3>
<pre><code>우리 집(Databricks)에서
이웃 집(MySQL, PostgreSQL, SQLite 등 외부 DB)에 있는 데이터를
전화선(JDBC 연결)으로 당겨와서 읽는 것</code></pre><p>쉽게 말해 <strong>Databricks 밖에 있는 데이터베이스 서버에 연결하는 방법</strong>이다.</p>
<h3 id="외부-db가-왜-따로-있냐">외부 DB가 왜 따로 있냐?</h3>
<p>회사에서 이런 상황이 흔하다:</p>
<pre><code>영업팀이 오래전부터 쓰던 Oracle DB
  → 주문 데이터, 고객 데이터가 거기 다 있음
  → Databricks로 이사오기엔 너무 크고 복잡함
  → 그냥 거기 두고 Databricks에서 연결해서 읽음</code></pre><h3 id="jdbc로-외부-db-연결하기">JDBC로 외부 DB 연결하기</h3>
<pre><code class="language-sql">CREATE TABLE users_jdbc
USING JDBC                          -- JDBC 방식으로 연결
OPTIONS (
  url = &quot;jdbc:sqlite:/path/to/db&quot;,  -- DB 서버 주소
  dbtable = &quot;users&quot;                 -- 읽어올 테이블 이름
)</code></pre>
<p>주소 형식: <code>jdbc:DB종류://서버주소:포트</code></p>
<pre><code>jdbc:mysql://192.168.0.1:3306       -- MySQL 연결
jdbc:postgresql://myserver:5432     -- PostgreSQL 연결
jdbc:sqlite:/path/to/file.db        -- SQLite 연결 (파일 기반)</code></pre><p>만들고 나면 그냥 일반 테이블처럼 쓸 수 있다:</p>
<pre><code class="language-sql">SELECT * FROM users_jdbc  -- 외부 DB에서 데이터 가져옴</code></pre>
<p>단, 데이터가 Databricks 안으로 복사되는 게 아니라 <strong>쿼리할 때마다 외부 DB에서 가져오는 것</strong>이라 대용량이면 느릴 수 있다.</p>
<hr>
<h2 id="4-delta-테이블-생성">4. Delta 테이블 생성</h2>
<h3 id="ctas-create-table-as-select">CTAS (Create Table As Select)</h3>
<p>&quot;SELECT 결과물로 Delta 테이블 만들기&quot;</p>
<pre><code class="language-sql">CREATE OR REPLACE TABLE sales AS
SELECT * FROM parquet.`/path/sales-historical`</code></pre>
<p>스키마(컬럼 구조)를 직접 안 써도 SELECT 결과에서 자동으로 추론한다.</p>
<p>Parquet, JSON처럼 스키마가 명확한 파일에서 특히 유용하다.</p>
<p>CSV처럼 옵션이 필요한 파일은 CTAS 전에 임시 뷰를 먼저 만들어야 한다:</p>
<pre><code class="language-sql">-- 1단계: CSV 읽기 옵션을 담은 임시 뷰 생성
CREATE OR REPLACE TEMP VIEW sales_tmp
  (order_id LONG, email STRING, ...)
USING CSV
OPTIONS (path = &quot;/path&quot;, header = &quot;true&quot;, delimiter = &quot;|&quot;);

-- 2단계: 임시 뷰로 Delta 테이블 생성
CREATE TABLE sales_delta AS
  SELECT * FROM sales_tmp;</code></pre>
<h3 id="cras-vs-insert-overwrite">CRAS vs INSERT OVERWRITE</h3>
<pre><code class="language-sql">-- CRAS: 스키마도 바꿀 수 있는 강력한 덮어쓰기
CREATE OR REPLACE TABLE events AS SELECT ...

-- INSERT OVERWRITE: 스키마 변경 불가, 더 안전한 덮어쓰기
INSERT OVERWRITE sales SELECT * FROM parquet.`/path`</code></pre>
<h3 id="생성된-열-generated-column">생성된 열 (Generated Column)</h3>
<pre><code class="language-sql">CREATE OR REPLACE TABLE purchase_dates (
  id STRING,
  transaction_timestamp LONG,
  price STRING,
  date DATE GENERATED ALWAYS AS (
    cast(cast(transaction_timestamp/1e6 AS TIMESTAMP) AS DATE))
    COMMENT &quot;generated based on transactions_timestamp column&quot;
)</code></pre>
<p>다른 열의 값을 기반으로 자동 계산되는 열. 삽입 시 직접 값을 안 써도 Delta가 알아서 채워준다.</p>
<h3 id="테이블-제약-조건">테이블 제약 조건</h3>
<pre><code class="language-sql">-- 조건을 위반하는 데이터가 들어오면 쓰기 에러 발생
ALTER TABLE purchase_dates ADD CONSTRAINT valid_date CHECK (date &gt; &#39;2020-01-01&#39;);</code></pre>
<h3 id="clone--테이블-복제">Clone — 테이블 복제</h3>
<h4 id="shallow-clone-얕은-복제">Shallow Clone (얕은 복제)</h4>
<pre><code class="language-sql">CREATE OR REPLACE TABLE purchases_shallow SHALLOW CLONE purchases</code></pre>
<p>비유:</p>
<pre><code>도서관 책은 그대로 두고
&quot;이 책은 3층 A구역 12번에 있어&quot; 라는 메모(Delta Log)만 복사

읽을 때 → 메모 보고 원본 위치 찾아가서 읽음
수정할 때 → 원본은 건드리지 않고, 내 공간에 새 파일 생성
원본 삭제되면 → 메모는 있는데 책이 없으니 에러</code></pre><p><strong>수정해도 원본이 안 바뀌는 이유:</strong>
Spark는 파일을 절대 덮어쓰지 않는다. 수정 = 기존 파일 건드리는 게 아니라 새 파일을 만드는 것. 그 새 파일이 원본 경로가 아닌 Clone 자체 경로에 생기기 때문에 원본은 그대로다.</p>
<h4 id="deep-clone-깊은-복제">Deep Clone (깊은 복제)</h4>
<pre><code class="language-sql">CREATE OR REPLACE TABLE purchases_clone DEEP CLONE purchases</code></pre>
<p>비유:</p>
<pre><code>책을 통째로 복사해서 내 책상에 올려두기

원본이 어떻게 되든 내 복사본은 독립적으로 존재
원본 삭제돼도 내 복사본은 멀쩡함
단, 복사하는 데 시간이 오래 걸림</code></pre><h4 id="한눈에-비교-1">한눈에 비교</h4>
<table>
<thead>
<tr>
<th></th>
<th>Shallow Clone</th>
<th>Deep Clone</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 복사</td>
<td>❌ 원본 참조</td>
<td>✅ 완전 복사</td>
</tr>
<tr>
<td>속도</td>
<td>빠름 (로그만 복사)</td>
<td>느림 (데이터 전체 복사)</td>
</tr>
<tr>
<td>원본 삭제되면</td>
<td>망가짐</td>
<td>독립 유지</td>
</tr>
<tr>
<td>원본이 바뀌면</td>
<td>영향 받을 수 있음</td>
<td>영향 없음</td>
</tr>
<tr>
<td>용도</td>
<td>테스트, 임시 실험</td>
<td>완전한 백업, 마이그레이션</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-테이블에-쓰기--4가지-방법">5. 테이블에 쓰기 — 4가지 방법</h2>
<h3 id="insert-overwrite--전체-덮어쓰기">INSERT OVERWRITE — 전체 덮어쓰기</h3>
<pre><code class="language-sql">INSERT OVERWRITE sales
SELECT * FROM parquet.`/path`</code></pre>
<p>기존 데이터를 통째로 교체. 스키마(컬럼 구조)가 바뀌면 에러남. 기존 테이블 구조를 지키면서 데이터만 갈아끼울 때.</p>
<h3 id="insert-into--행-추가">INSERT INTO — 행 추가</h3>
<pre><code class="language-sql">INSERT INTO sales
SELECT * FROM parquet.`/path`</code></pre>
<p>기존 데이터 유지하고 새 행만 추가. <strong>중복 방지 기능 없음.</strong> 같은 셀 두 번 실행하면 데이터 두 번 들어감.</p>
<h3 id="merge-into--똑똑한-병합">MERGE INTO — 똑똑한 병합</h3>
<pre><code class="language-sql">MERGE INTO users a
USING users_update b
ON a.user_id = b.user_id
WHEN MATCHED AND a.email IS NULL AND b.email IS NOT NULL THEN
  UPDATE SET email = b.email, updated = b.updated
WHEN NOT MATCHED THEN INSERT *</code></pre>
<p>조건에 따라 UPDATE / INSERT / DELETE를 한 트랜잭션으로 처리.</p>
<pre><code>같은 user_id가 있고 email이 비어있으면  → UPDATE
같은 user_id가 없으면                   → INSERT</code></pre><p>INSERT INTO의 중복 문제를 해결할 수 있다. <code>WHEN NOT MATCHED</code>만 쓰면 중복 없는 삽입만 수행:</p>
<pre><code class="language-sql">MERGE INTO events a
USING events_update b
ON a.user_id = b.user_id AND a.event_timestamp = b.event_timestamp
WHEN NOT MATCHED AND b.traffic_source = &#39;email&#39; THEN
  INSERT *</code></pre>
<h3 id="copy-into--증분-로드">COPY INTO — 증분 로드</h3>
<pre><code class="language-sql">COPY INTO sales
FROM &quot;/path/sales-30m&quot;
FILEFORMAT = PARQUET</code></pre>
<p>이미 가져온 파일은 건너뛰고 새 파일만 가져온다. 매일 새 파일이 쌓이는 폴더에서 반복 실행할 때 유용.</p>
<h3 id="한눈에-비교-2">한눈에 비교</h3>
<table>
<thead>
<tr>
<th>방법</th>
<th>기존 데이터</th>
<th>중복 방지</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>INSERT OVERWRITE</code></td>
<td>전체 교체</td>
<td>-</td>
<td>정기적으로 전체 갱신</td>
</tr>
<tr>
<td><code>INSERT INTO</code></td>
<td>유지+추가</td>
<td>❌</td>
<td>단순 추가</td>
</tr>
<tr>
<td><code>MERGE INTO</code></td>
<td>조건부 처리</td>
<td>✅</td>
<td>업서트, 정교한 동기화</td>
</tr>
<tr>
<td><code>COPY INTO</code></td>
<td>유지+추가</td>
<td>✅ (파일 단위)</td>
<td>외부 파일 증분 수집</td>
</tr>
</tbody></table>
<hr>
<h2 id="전체-흐름-정리">전체 흐름 정리</h2>
<pre><code>외부 데이터 원본                외부 테이블 등록              Delta 테이블 생성
──────────────                 ────────────────              ────────────────
CSV 파일                        USING CSV                     CTAS로 변환
JSON 파일          →            USING JSON          →         Delta 최적화 적용
외부 DB (MySQL 등)              USING JDBC                    ↓
                                LOCATION 지정                 쓰기
                                (데이터는 밖에 그대로)         (MERGE / COPY INTO)

  [Bronze: 원본 연결]             [Bronze 단계]                [Silver/Gold 단계]</code></pre><p>실무에서는 <strong>외부 테이블(Bronze) → CTAS로 Delta 변환(Silver) → 정제/집계(Gold)</strong> 순서로 메달리온 아키텍처를 구성한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks - Delta Lake]]></title>
            <link>https://velog.io/@no-glass-otacku/Databricks-Delta-Lake</link>
            <guid>https://velog.io/@no-glass-otacku/Databricks-Delta-Lake</guid>
            <pubDate>Thu, 18 Jun 2026 01:10:56 GMT</pubDate>
            <description><![CDATA[<h1 id="delta-lake">Delta Lake</h1>
<p>오픈소스!</p>
<p><strong>스키마 진화</strong></p>
<ul>
<li>데이터 변경에 따라 테이블 스키마가 자동으로 조정!</li>
</ul>
<p><strong>스키마 강제</strong></p>
<ul>
<li>입력되는 데이터가 정의된 스키마와 일치하는지 확인</li>
</ul>
<p><strong>🌟시간 여행</strong> : 아래에서 자세히 다룸</p>
<h2 id="delta-lake-1">Delta Lake</h2>
<p>버전 관리, 최적화, VACUUM 핵심 정리</p>
<blockquote>
<p>Databricks 실습 중 헷갈렸던 개념들을 직접 질문하며 정리한 노트</p>
</blockquote>
<hr>
<h3 id="1-describe-history--트랜잭션-로그-조회">1. DESCRIBE HISTORY — 트랜잭션 로그 조회</h3>
<pre><code class="language-sql">DESCRIBE HISTORY beans</code></pre>
<p>Delta Lake는 테이블을 수정하는 <strong>모든 작업을 버전으로 기록</strong>한다.</p>
<table>
<thead>
<tr>
<th>version</th>
<th>operation</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>CREATE TABLE</td>
</tr>
<tr>
<td>1</td>
<td>WRITE</td>
</tr>
<tr>
<td>2</td>
<td>WRITE</td>
</tr>
<tr>
<td>3</td>
<td>UPDATE</td>
</tr>
<tr>
<td>4</td>
<td>UPDATE</td>
</tr>
<tr>
<td>5</td>
<td>DELETE</td>
</tr>
<tr>
<td>6</td>
<td>MERGE</td>
</tr>
</tbody></table>
<ul>
<li><code>operationParameters</code> : WHERE 조건 등 상세 파라미터 확인 가능</li>
<li><code>operationMetrics</code> : 추가/삭제된 행, 파일 수 확인 가능</li>
</ul>
<hr>
<h3 id="2-time-travel--이전-버전-조회-및-복원">2. Time Travel — 이전 버전 조회 및 복원</h3>
<pre><code class="language-sql">-- 버전으로 조회
SELECT * FROM beans VERSION AS OF 1
SELECT * FROM beans@v1   -- 단축 문법

-- 타임스탬프로 조회
SELECT * FROM beans TIMESTAMP AS OF &#39;2024-01-01&#39;

-- 임시 뷰로 등록
CREATE OR REPLACE TEMP VIEW pre_delete_vw AS
SELECT * FROM beans VERSION AS OF 4

-- 특정 버전으로 복원
RESTORE TABLE beans TO VERSION AS OF 5</code></pre>
<p><strong>📌 질문했던 것: <code>CREATE OR REPLACE TEMP VIEW</code>는 왜 트랜잭션 로그에 안 남을까?</strong></p>
<p>VIEW는 실제 데이터 파일을 건드리지 않는다. &quot;어떻게 볼지&quot;에 대한 정의만 저장하기 때문에 Delta 트랜잭션 로그에 기록되지 않는다. TEMP 여부와 무관하게, 일반 VIEW도 마찬가지.</p>
<hr>
<h3 id="3-optimize--z-order--파일-압축-및-데이터-재배치">3. OPTIMIZE + Z-Order — 파일 압축 및 데이터 재배치</h3>
<pre><code class="language-sql">OPTIMIZE beans ZORDER BY (name)</code></pre>
<p>이 명령 하나가 두 가지를 동시에 한다:</p>
<p><strong>① Compaction (파일 압축)</strong>
작은 파일 수백 개 → 큰 파일 몇 개로 병합</p>
<p><strong>② Z-Ordering (데이터 재배치)</strong>
<code>name</code> 기준으로 비슷한 값을 같은 파일에 모아둠</p>
<pre><code>Before: 파일1 [Alice, Zoe, Bob]  파일2 [Carol, Alice, Zoe]
After:  파일1 [Alice, Alice]     파일2 [Bob, Bob]     파일3 [Carol, Carol]

→ WHERE name = &#39;Alice&#39; 쿼리 시 파일 1개만 읽으면 됨 (Data Skipping)</code></pre><p><strong>📌 질문했던 것: 파일 개수는 어떻게 확인해?</strong></p>
<pre><code class="language-sql">DESCRIBE DETAIL beans
-- numFiles, sizeInBytes 컬럼 확인</code></pre>
<p>OPTIMIZE 결과 출력에서 <code>numFilesAdded</code>, <code>numFilesRemoved</code>로 변화를 확인할 수 있다. 현재 실습의 beans 테이블은 <code>numFiles: 1</code>, <code>sizeInBytes: 1685</code> (~1.6KB)로 매우 작아서 Z-Order 효과가 거의 없다. 데이터가 클수록 효과가 극적으로 나타남.</p>
<hr>
<h3 id="4-vacuum--오래된-파일-영구-삭제">4. VACUUM — 오래된 파일 영구 삭제</h3>
<pre><code class="language-sql">-- 삭제될 파일 미리 보기 (실제 삭제 안 함)
VACUUM beans RETAIN 0 HOURS DRY RUN

-- 실제 삭제
VACUUM beans RETAIN 0 HOURS</code></pre>
<p><strong>📌 질문했던 것: DRY RUN이 정확히 뭐야?</strong></p>
<p>&quot;실제로 실행하지 말고, 뭘 할지만 보여줘&quot;라는 뜻. 삭제 대상 파일 목록만 출력한다. <code>RETAIN 0 HOURS</code>는 현재 버전 외 모든 파일을 삭제하는 위험한 옵션이라 DRY RUN으로 먼저 확인하는 것이 중요하다.</p>
<p>기본 보존 기간은 <strong>7일</strong>이며, 아래 설정으로 우회 가능 (프로덕션에서는 주의):</p>
<pre><code class="language-sql">SET spark.databricks.delta.retentionDurationCheck.enabled = false;
SET spark.databricks.delta.vacuum.logging.enabled = true;</code></pre>
<hr>
<h3 id="5-vacuum-후-time-travel이-불가능한-이유">5. VACUUM 후 Time Travel이 불가능한 이유</h3>
<pre><code class="language-sql">SELECT * FROM beans@v1
-- FileReadException: 파일 없음 → 에러 발생</code></pre>
<p><strong>📌 질문했던 것: VACUUM 해도 DESCRIBE HISTORY는 보이는 이유?</strong></p>
<p>VACUUM은 <strong>파일</strong>을 지우는 것이지, <strong>트랜잭션 로그</strong>를 지우는 게 아니다.</p>
<pre><code>beans/
├── _delta_log/          ← 로그 파일 (VACUUM이 건드리지 않음)
│   ├── 00000.json       ← &quot;v0: 테이블 생성&quot;
│   └── 00001.json       ← &quot;v1: 데이터 추가&quot;
│
└── part-0001.parquet    ← 실제 데이터 파일 (VACUUM이 삭제)</code></pre><ul>
<li><code>DESCRIBE HISTORY</code> → 로그를 읽는 것 → VACUUM 후에도 보임</li>
<li><code>beans@v1</code> 조회 → 실제 파일을 찾는 것 → VACUUM 후 FileReadException</li>
</ul>
<p>즉, <strong>VACUUM 후 <code>beans@v1</code>에서 에러가 나는 것이 정상</strong>이다. 에러가 나야 VACUUM이 제대로 된 것.</p>
<hr>
<h3 id="정리-각-명령어-역할-한눈에-보기">정리: 각 명령어 역할 한눈에 보기</h3>
<table>
<thead>
<tr>
<th>명령어</th>
<th>역할</th>
<th>로그 기록</th>
</tr>
</thead>
<tbody><tr>
<td><code>DESCRIBE HISTORY</code></td>
<td>트랜잭션 기록 조회</td>
<td>-</td>
</tr>
<tr>
<td><code>SELECT ... VERSION AS OF</code></td>
<td>이전 버전 조회</td>
<td>-</td>
</tr>
<tr>
<td><code>RESTORE TABLE</code></td>
<td>이전 버전으로 복원</td>
<td>✅</td>
</tr>
<tr>
<td><code>OPTIMIZE ZORDER BY</code></td>
<td>파일 압축 + 데이터 재배치</td>
<td>✅</td>
</tr>
<tr>
<td><code>VACUUM DRY RUN</code></td>
<td>삭제 대상 미리보기</td>
<td>-</td>
</tr>
<tr>
<td><code>VACUUM</code></td>
<td>오래된 파일 영구 삭제</td>
<td>✅ (설정 시)</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks - MLflow, SDP(Spark 선언적 파이프라인)]]></title>
            <link>https://velog.io/@no-glass-otacku/Databricks-MLflow</link>
            <guid>https://velog.io/@no-glass-otacku/Databricks-MLflow</guid>
            <pubDate>Wed, 17 Jun 2026 02:37:14 GMT</pubDate>
            <description><![CDATA[<h1 id="mlflow--실험-기록장">MLflow — &quot;실험 기록장&quot;</h1>
<p>18가지 조합을 다 돌려봤는데, 어떤 조합이 얼마나 좋았는지 기억할 방법이 없으면 곤란하잖아요.</p>
<blockquote>
<p>MLflow = 매 실험마다 파라미터, 결과, 모델을 자동으로 저장해주는 도구</p>
</blockquote>
<hr>
<h3 id="코드에서-mlflow가-쓰이는-곳">코드에서 MLflow가 쓰이는 곳</h3>
<p>크게 세 군데예요.</p>
<p><strong>① 실험 공간 설정</strong></p>
<pre><code class="language-python">mlflow.set_experiment(EXPERIMENT_NAME)
# &quot;이 노트북의 실험들을 여기다 모아서 저장할게&quot;</code></pre>
<p><strong>② 그리드 서치 중 매 조합마다 기록 (숫자 결과만)</strong></p>
<pre><code class="language-python">with mlflow.start_run(run_name=&quot;ALS_GridSearch&quot;) as parent_run:      # 전체 실험
    for i, params in enumerate(param_grid):                           # 18번 반복
        with mlflow.start_run(run_name=run_name, nested=True):        # 각 조합
            mlflow.log_params({&quot;rank&quot;: rank, &quot;regParam&quot;: reg_param})  # 파라미터 저장
            mlflow.log_metric(&quot;rmse&quot;, rmse)                           # 결과 저장</code></pre>
<p>폴더 구조로 보면 이렇게 쌓여요.</p>
<pre><code>ALS_GridSearch (parent)
├── rank10_reg0.01_iter10 → rmse: 1.23
├── rank10_reg0.01_iter20 → rmse: 1.19
├── rank20_reg0.1_iter10  → rmse: 0.98  ← best
└── ... (18개)</code></pre><p><strong>③ 최종 모델 등록 (모델 파일)</strong></p>
<pre><code class="language-python">with mlflow.start_run(run_name=&quot;Final_ALS_Model&quot;):
    mlflow.log_params(best_params)       # 최적 파라미터
    mlflow.log_metric(&quot;test_rmse&quot;, best_rmse)  # 최종 성능
    mlflow.spark.log_model(             # 모델 파일 자체를 저장
        final_model,
        registered_model_name=f&quot;{CATALOG}.{SCHEMA}.movie_recommender_als&quot;
    )</code></pre>
<hr>
<h3 id="mlflow가-없으면">MLflow가 없으면?</h3>
<pre><code class="language-python"># 18번 돌릴 때마다 직접 print로 기록하고
print(f&quot;rank=10, reg=0.01 → RMSE: 1.23&quot;)
print(f&quot;rank=10, reg=0.1  → RMSE: 1.19&quot;)
# ... 나중에 어떤 게 최고였는지 찾아야 하고
# 모델 파일도 직접 저장해야 하고
# 다음 주에 &quot;그때 어떤 파라미터 썼지?&quot; 하면 답 없음</code></pre>
<p>MLflow는 이걸 자동화해주고, Databricks UI에서 표로 한눈에 비교할 수 있게 해줘요.</p>
<hr>
<h1 id="lakeflow-sdpspark-declarative-pipeline">Lakeflow SDP(Spark Declarative Pipeline)</h1>
<h3 id="명령적-vs-선언적">명령적 vs 선언적</h3>
<p><strong>명령적 (Imperative)</strong> — 요리 과정을 직접 지시</p>
<pre><code>&quot;냉장고에서 닭 꺼내, 180도로 예열해, 30분 구워, 소스 발라, 접시에 담아&quot;</code></pre><p><strong>선언적 (Declarative)</strong> — 결과만 말하기</p>
<pre><code>&quot;치킨 한 마리 주세요&quot;</code></pre><p>어떻게 만드는지는 관심 없고, <strong>뭘 원하는지</strong>만 말하는 거예요.</p>
<pre><code class="language-python">from pyspark import pipelines as dp</code></pre>
<h2 id="1-데이터셋-유형">1. 데이터셋 유형</h2>
<h3 id="streaming-table-vs-materialized-view">Streaming Table vs Materialized View</h3>
<table>
<thead>
<tr>
<th></th>
<th>Streaming Table</th>
<th>Materialized View</th>
</tr>
</thead>
<tbody><tr>
<td>처리 방식</td>
<td>새로 들어온 데이터만 증분 처리</td>
<td>전체 데이터 재계산</td>
</tr>
<tr>
<td>적합한 작업</td>
<td>데이터 수집, 정제</td>
<td>집계, 조인</td>
</tr>
<tr>
<td>적합한 레이어</td>
<td>Bronze, Silver</td>
<td>Gold</td>
</tr>
<tr>
<td>Python 데코레이터</td>
<td><code>@dp.table</code> + <code>readStream</code></td>
<td><code>@dp.materialized_view</code></td>
</tr>
<tr>
<td>SQL</td>
<td><code>CREATE OR REFRESH STREAMING TABLE</code></td>
<td><code>CREATE OR REFRESH MATERIALIZED VIEW</code></td>
</tr>
</tbody></table>
<h3 id="dptable-vs-dptemporary_view">@dp.table vs @dp.temporary_view</h3>
<table>
<thead>
<tr>
<th></th>
<th><code>@dp.table</code></th>
<th><code>@dp.temporary_view</code></th>
</tr>
</thead>
<tbody><tr>
<td>저장 여부</td>
<td>Delta 테이블로 물리적 저장</td>
<td>저장 안 됨</td>
</tr>
<tr>
<td>용도</td>
<td>Bronze / Silver / Gold 최종 결과</td>
<td>중간 계산용 임시 뷰</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-데이터-품질-관리-expectations">2. 데이터 품질 관리 (Expectations)</h2>
<h3 id="세-가지-유형">세 가지 유형</h3>
<table>
<thead>
<tr>
<th>유형</th>
<th>위반 시 동작</th>
<th>사용 레이어</th>
<th>전략</th>
</tr>
</thead>
<tbody><tr>
<td><code>@dp.expect()</code></td>
<td>경고만, 레코드 유지</td>
<td>Bronze</td>
<td>일단 다 쌓고 모니터링</td>
</tr>
<tr>
<td><code>@dp.expect_or_drop()</code></td>
<td>위반 레코드 제거</td>
<td>Silver</td>
<td>불량 데이터 걸러내기</td>
</tr>
<tr>
<td><code>@dp.expect_or_fail()</code></td>
<td>파이프라인 전체 중단</td>
<td>Gold</td>
<td>서비스 품질 보장</td>
</tr>
</tbody></table>
<h3 id="자주-쓰는-검증-조건">자주 쓰는 검증 조건</h3>
<pre><code class="language-sql">-- NULL 체크
&quot;column IS NOT NULL&quot;

-- 범위 검증
&quot;amount &gt; 0 AND amount &lt; 1000000&quot;

-- 값 목록
&quot;status IN (&#39;active&#39;, &#39;pending&#39;, &#39;completed&#39;)&quot;

-- 패턴 매칭
&quot;email LIKE &#39;%@%&#39;&quot;</code></pre>
<table>
<thead>
<tr>
<th>Python</th>
<th>SQL</th>
</tr>
</thead>
<tbody><tr>
<td><code>@dp.table</code></td>
<td><code>CREATE OR REFRESH STREAMING TABLE</code></td>
</tr>
<tr>
<td><code>@dp.materialized_view</code></td>
<td><code>CREATE OR REFRESH MATERIALIZED VIEW</code></td>
</tr>
<tr>
<td><code>@dp.expect_or_drop(...)</code></td>
<td><code>CONSTRAINT 이름 EXPECT (...) ON VIOLATION DROP ROW</code></td>
</tr>
</tbody></table>
<p>Python은 데코레이터로 선언하고, SQL은 키워드로 선언하는 차이만 있을 뿐 SDP가 내부적으로 처리하는 방식은 똑같아요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks - 추천시스템]]></title>
            <link>https://velog.io/@no-glass-otacku/Databricks-%EC%B6%94%EC%B2%9C%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@no-glass-otacku/Databricks-%EC%B6%94%EC%B2%9C%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Wed, 17 Jun 2026 01:50:13 GMT</pubDate>
            <description><![CDATA[<h1 id="추천-시스템">추천 시스템</h1>
<p>협업 필터링 VS 콘텐츠 기반 필터링
비슷한 취향을 가진 사용자 집단 행동 패턴을 분석
아이템 자체의 특성과 메타데이터 분석</p>
<h2 id="협업-필터링">협업 필터링</h2>
<h3 id="a유저-기반">A.유저 기반</h3>
<p><strong>01 유사 사용자 탐색</strong>
코사인 유사도, 피어슨 상관계수 등으로 비슷한 평점 패턴을 가진 유저 그룹 발견</p>
<p><strong>02 이웃 선정</strong>
유사도 상위 K명의 이웃 사용자를 선택하여 추천 기반 구축</p>
<p><strong>03 평점 예측</strong>
이웃들의 평점을 가중 평균하여 대상 아이템에 대한 예측 점수 계산</p>
<p>주의: 대규모 유저 대상 실시간 계산 비용이 급증하며, 희소 데이터 문제로 정확도가 떨어질 수 있습니다.</p>
<h3 id="b아이템-기반">B.아이템 기반</h3>
<p>좋아한 아이템과 유사한 것 추천. 유사도는 코사인 유사도로 측정</p>
<h4 id="협업-필터링의-한계-콜드-스타트">협업 필터링의 한계: 콜드 스타트</h4>
<h3 id="콜드스타드-문제-해결-전략">콜드스타드 문제 해결 전략</h3>
<ol>
<li>하이브리드 추천 시스템
알고리즘을 여러개 사용<blockquote>
<p>Multi-armed bandit (밴딧 알고리즘)
여러 슬롯머신을 당겨보고 어떤걸 얼마나 당겨야 가장 높은 수익을 얻을 수 있는지 확인하는 것.</p>
</blockquote>
</li>
<li>프로필 완성
시작할 때, 스포티파이에서 3명의 아티스트를 선택하도록 하는 것</li>
</ol>
<h3 id="c-고도화--모델-기반">C. 고도화- 모델 기반</h3>
<p>행렬 분해(Matrix Factorization)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[공부 방향 조언]]></title>
            <link>https://velog.io/@no-glass-otacku/%EA%B3%B5%EB%B6%80-%EB%B0%A9%ED%96%A5-%EC%A1%B0%EC%96%B8</link>
            <guid>https://velog.io/@no-glass-otacku/%EA%B3%B5%EB%B6%80-%EB%B0%A9%ED%96%A5-%EC%A1%B0%EC%96%B8</guid>
            <pubDate>Tue, 16 Jun 2026 03:03:46 GMT</pubDate>
            <description><![CDATA[<p>11-12시 영상 보고 다시 정리</p>
<p>DE에게 요구되는 능력치
의외로 RAG를 할 줄 알면 플러스</p>
<p>자격증 나열보다 &#39;증거&#39;를 만들어라
<strong>수치가 들어간</strong> 엔드투엔드 프로젝트 1-2개 &gt; 자격증10개</p>
<p>자격증 공부할때 외우는건 맞지만 시간이 된다면 기초를 이해하고 가져가는게 자격증이 더 의미있다.</p>
<p>깃허브를 기록하기 시작해라.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks - 이미지 분류, Auto Loader]]></title>
            <link>https://velog.io/@no-glass-otacku/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B6%84%EB%A5%98-on-Databricks</link>
            <guid>https://velog.io/@no-glass-otacku/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B6%84%EB%A5%98-on-Databricks</guid>
            <pubDate>Tue, 16 Jun 2026 00:50:17 GMT</pubDate>
            <description><![CDATA[<h1 id="이미지-분류-on-databricks">이미지 분류 on Databricks</h1>
<h2 id="computer-vision">computer vision</h2>
<h3 id="segmentation-vs-캡셔닝">Segmentation vs 캡셔닝</h3>
<p><strong>segmentation</strong>이 &quot;어디에 뭐가 있냐&quot; 를 찾는 거라면, 
<strong>캡셔닝</strong>은 &quot;이 장면을 문장으로 설명&quot; 하는 것</p>
<h3 id="semantic-vs-instance-segmentation">Semantic vs Instance Segmentation</h3>
<p>핵심 차이는 &quot;개별 객체를 구분하느냐&quot; <img src="https://velog.velcdn.com/images/no-glass-otacku/post/e1bf7563-8ce9-46ab-9201-7ac9af2e9eaa/image.png" alt=""></p>
<p><strong>Semantic Segmentation</strong>은 픽셀마다 &quot;클래스&quot;를 붙이는 것. 사람이 두 명 있어도 둘 다 <code>person</code> 레이블이 붙기 때문에 &quot;어디에 사람이 있나&quot;는 알지만 &quot;몇 명인지, 어디서 어디까지가 한 명인지&quot;는 모름.</p>
<p><strong>Instance Segmentation</strong>은 거기서 한 발 더 나아가서, 같은 클래스 안에서도 개별 객체를 구분. <code>person_1</code>, <code>person_2</code>처럼 각각 다른 마스크를 가짐. 사람이 겹쳐 있어도 분리 가능.</p>
<hr>
<h4 id="어디에-쓰이냐">어디에 쓰이냐</h4>
<table>
<thead>
<tr>
<th></th>
<th>Semantic</th>
<th>Instance</th>
</tr>
</thead>
<tbody><tr>
<td>자율주행</td>
<td>*<em>도로/보도 구분 *</em></td>
<td>보행자 각각 추적</td>
</tr>
<tr>
<td>의료 영상</td>
<td>장기 영역 분리</td>
<td>세포 개수 세기</td>
</tr>
<tr>
<td>사진 편집</td>
<td>배경 교체</td>
<td>특정 사람만 블러</td>
</tr>
</tbody></table>
<hr>
<h2 id="auto-loader">[Auto Loader]</h2>
<p>클라우드 스토리지(Azure Blob Storage, S3 등)에 <strong>새로 업로드된 파일을 자동으로 감지해서 읽어오는 Databricks 기능</strong>이다.</p>
<h3 id="어떻게-작동하냐">어떻게 작동하냐</h3>
<pre><code>클라우드 스토리지에 새 파일 업로드
        ↓
Auto Loader가 자동 감지
        ↓
Spark로 증분 처리 (새 파일만)</code></pre><p>일반 <code>spark.read</code>는 실행할 때 존재하는 파일만 읽고 끝나지만, Auto Loader는 새 파일이 생길 때마다 자동으로 처리한다.</p>
<hr>
<h3 id="cloudfiles">[cloudFiles]</h3>
<p>Auto Loader를 사용하기 위한 <strong>Databricks 전용 Spark 포맷 이름</strong>이다.</p>
<pre><code class="language-python"># 일반 포맷 (파일 형식)
spark.read.format(&quot;csv&quot;)
spark.read.format(&quot;parquet&quot;)

# Auto Loader 전용 포맷
spark.readStream.format(&quot;cloudFiles&quot;)  # ← 이게 Auto Loader 켜는 스위치</code></pre>
<p><code>format()</code> 안에 넣는다는 점은 같지만, <code>cloudFiles</code>는 파일 형식이 아니라 <strong>Auto Loader 기능을 활성화하는 키워드</strong>다. Databricks 외부 환경에서는 사용 불가.</p>
<h4 id="기본-사용법">기본 사용법</h4>
<pre><code class="language-python">df = (spark.readStream
    .format(&quot;cloudFiles&quot;)
    .option(&quot;cloudFiles.format&quot;, &quot;json&quot;)       # 실제 파일 형식은 여기서 지정
    .option(&quot;cloudFiles.schemaLocation&quot;, &quot;/checkpoint/schema&quot;)
    .load(&quot;/mnt/storage/incoming/&quot;)
)</code></pre>
<h4 id="실전-활용">실전 활용</h4>
<ul>
<li>Azure Blob Storage, AWS S3, GCS 등 어떤 클라우드 스토리지든 동일한 코드로 연결 가능</li>
<li><code>cloudFiles.format</code>만 바꿔서 CSV, JSON, Parquet, 이미지 등 다양한 형식 처리</li>
</ul>
<hr>
<h3 id="checkpoint">[Checkpoint]</h3>
<p>스트리밍 처리에서 <strong>&quot;어디까지 처리했는지&quot;를 기록해두는 위치</strong>다.</p>
<h4 id="왜-필요하냐">왜 필요하냐</h4>
<pre><code>파일 100개 처리 중 → 클러스터 갑자기 재시작
        ↓
Checkpoint 없으면: 처음부터 다시 (중복 처리)
Checkpoint 있으면: 87번까지 했으니 88번부터 이어서</code></pre><h3 id="사용법">사용법</h3>
<pre><code class="language-python">query = (df.writeStream
    .format(&quot;delta&quot;)
    .option(&quot;checkpointLocation&quot;, &quot;/checkpoint/my_pipeline&quot;)  # ← 여기 지정
    .start(&quot;/mnt/output/&quot;)
)</code></pre>
<h3 id="실전-활용-1">실전 활용</h3>
<ul>
<li>체크포인트 경로는 <strong>DBFS나 클라우드 스토리지</strong>에 저장해야 클러스터 재시작 후에도 유지됨</li>
<li><code>/tmp</code>에 저장하면 클러스터 재시작 시 날아가므로 주의</li>
<li>파이프라인마다 <strong>고유한 체크포인트 경로</strong> 사용 (공유하면 충돌)</li>
</ul>
<pre><code># 좋은 예
/checkpoint/user_logs_pipeline
/checkpoint/image_caption_pipeline

# 나쁜 예
/checkpoint/temp   ← 여러 파이프라인이 공유하면 충돌</code></pre><hr>
<h2 id="schema-evolution">[Schema Evolution]</h2>
<p>데이터 구조가 바뀌어도 (새 컬럼 추가 등) <strong>자동으로 스키마를 업데이트해주는 기능</strong>이다.</p>
<h3 id="왜-필요하냐-1">왜 필요하냐</h3>
<pre><code>1월 데이터: { &quot;name&quot;: &quot;철수&quot;, &quot;age&quot;: 25 }
2월 데이터: { &quot;name&quot;: &quot;영희&quot;, &quot;age&quot;: 23, &quot;email&quot;: &quot;y@test.com&quot; }  ← 새 컬럼 추가</code></pre><p>Schema Evolution 없으면 2월 데이터 읽을 때 에러 발생. 있으면 자동으로 <code>email</code> 컬럼 추가해서 처리.</p>
<h3 id="auto-loader에서-활성화">Auto Loader에서 활성화</h3>
<pre><code class="language-python">df = (spark.readStream
    .format(&quot;cloudFiles&quot;)
    .option(&quot;cloudFiles.format&quot;, &quot;json&quot;)
    .option(&quot;cloudFiles.schemaLocation&quot;, &quot;/checkpoint/schema&quot;)  # 스키마 저장 위치
    .option(&quot;cloudFiles.inferColumnTypes&quot;, &quot;true&quot;)              # 컬럼 타입 자동 추론
    .load(&quot;/mnt/storage/incoming/&quot;)
)</code></pre>
<h3 id="실전-활용-2">실전 활용</h3>
<ul>
<li>외부 API 응답 구조가 자주 바뀌는 경우</li>
<li>서비스 초기 단계에서 로그 포맷이 계속 변경될 때</li>
<li>여러 팀이 데이터를 올리는데 컬럼 구성이 조금씩 다를 때</li>
</ul>
<blockquote>
<p><strong>주의</strong>: Schema Evolution은 컬럼 <strong>추가</strong>는 자동 처리하지만, 컬럼 <strong>삭제나 타입 변경</strong>은 수동으로 처리해야 할 수 있다.</p>
</blockquote>
<hr>
<h2 id="전체-파이프라인-예시">전체 파이프라인 예시</h2>
<p>실제 프로젝트에서 네 가지를 합쳐서 쓰면 이렇게 된다:</p>
<pre><code class="language-python"># 이미지 캡셔닝 파이프라인 예시
df = (spark.readStream
    .format(&quot;cloudFiles&quot;)                                         # Auto Loader ON
    .option(&quot;cloudFiles.format&quot;, &quot;binaryFile&quot;)                    # 이미지 파일
    .option(&quot;cloudFiles.schemaLocation&quot;, &quot;/checkpoint/schema&quot;)    # Schema Evolution
    .load(&quot;/mnt/storage/images/&quot;)                                 # 새 이미지 감지
)

query = (df.writeStream
    .format(&quot;delta&quot;)
    .option(&quot;checkpointLocation&quot;, &quot;/checkpoint/image_pipeline&quot;)   # Checkpoint
    .start(&quot;/mnt/output/captions/&quot;)
)</code></pre>
<pre><code>새 이미지 업로드
      ↓
cloudFiles 감지 (Auto Loader)
      ↓
Schema Evolution으로 구조 변화 자동 대응
      ↓
처리 결과 저장 + Checkpoint 기록
      ↓
클러스터 재시작해도 이어서 처리 가능</code></pre><hr>
<h2 id="column-store-열-기반-저장">[Column Store (열 기반 저장)]</h2>
<blockquote>
<p>Auto Loader로 읽어온 데이터를 Delta Lake에 저장하는데, Delta Lake가 빠른 이유가 바로 Column Store 방식이기 때문이다.</p>
</blockquote>
<p>데이터를 <strong>행(Row) 단위</strong>로 저장하느냐, <strong>열(Column) 단위</strong>로 저장하느냐에 따라 성능이 크게 달라진다.</p>
<h3 id="row-store-vs-column-store">Row Store vs Column Store</h3>
<p>예시 테이블:</p>
<table>
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
<th>salary</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>철수</td>
<td>25</td>
<td>3000</td>
</tr>
<tr>
<td>2</td>
<td>영희</td>
<td>23</td>
<td>4000</td>
</tr>
<tr>
<td>3</td>
<td>민수</td>
<td>30</td>
<td>5000</td>
</tr>
</tbody></table>
<p><strong>Row Store</strong> (MySQL 등 일반 DB)</p>
<pre><code>디스크: [1, 철수, 25, 3000] [2, 영희, 23, 4000] [3, 민수, 30, 5000]</code></pre><p><strong>Column Store</strong> (Parquet, Delta Lake)</p>
<pre><code>디스크: [1, 2, 3] [철수, 영희, 민수] [25, 23, 30] [3000, 4000, 5000]</code></pre><h3 id="왜-분석용으로-빠르냐">왜 분석용으로 빠르냐</h3>
<pre><code class="language-sql">SELECT AVG(salary) FROM employees</code></pre>
<ul>
<li>Row Store → 모든 행 다 읽고 salary만 추출</li>
<li>Column Store → <strong>salary 컬럼만 딱 읽으면 끝</strong></li>
</ul>
<p>필요한 컬럼만 골라 읽으니 I/O가 압도적으로 줄어든다.</p>
<h3 id="왜-압축률이-좋냐">왜 압축률이 좋냐</h3>
<p>같은 컬럼끼리 모여 있으면 비슷한 값들이 연속으로 나열되어 압축이 잘 된다.</p>
<pre><code>age 컬럼:    [25, 23, 30, 28, 25, 24...]  ← 비슷한 숫자 → 압축 잘 됨
salary 컬럼: [3000, 3000, 4000, 3000...]  ← 중복 많음  → 압축 잘 됨</code></pre><h3 id="databricks에서는">Databricks에서는</h3>
<p>Parquet, Delta Lake 모두 Column Store 방식이다. 대용량 데이터를 다뤄도 빠른 이유가 여기에 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Row Store</th>
<th>Column Store</th>
</tr>
</thead>
<tbody><tr>
<td>대표 DB</td>
<td>MySQL, PostgreSQL</td>
<td>Parquet, Delta Lake</td>
</tr>
<tr>
<td>적합한 용도</td>
<td>단건 조회, 트랜잭션</td>
<td>대용량 분석, 집계</td>
</tr>
<tr>
<td>특정 컬럼 조회</td>
<td>느림</td>
<td>빠름</td>
</tr>
<tr>
<td>압축률</td>
<td>낮음</td>
<td>높음</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks - Feature Store]]></title>
            <link>https://velog.io/@no-glass-otacku/Databricks-feature-store</link>
            <guid>https://velog.io/@no-glass-otacku/Databricks-feature-store</guid>
            <pubDate>Mon, 15 Jun 2026 04:29:28 GMT</pubDate>
            <description><![CDATA[<h2 id="feature-store">Feature Store</h2>
<p>&lt;6월 15일 9-11시 학습한거 다시 보기&gt;</p>
<p>카탈로그 &gt; 스키마 &gt; 테이블에서 &#39;계보(Lineage)&#39; 탭에서 업스트림(원본), 다운스트림(활용처)</p>
<h2 id="diamond-data를-활용한-etl-예제">Diamond Data를 활용한 ETL 예제</h2>
<h3 id="꼭-알아야하는-개념">꼭 알아야하는 개념</h3>
<ul>
<li><p><strong>Data lake</strong>: 온갖 다양한 _원시 데이터_를 저장하는 호수와 같은 공간
&lt;-&gt; <strong>Data warehouse</strong>: 정형 데이터 보관<img src="https://velog.velcdn.com/images/no-glass-otacku/post/60beff83-2465-4d6b-89b3-f14d3b61f07b/image.svg" alt=""> </p>
</li>
<li><p>정제된 데이터 저장할 때 (silver) <strong>파티셔닝 전략</strong> :
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/a1968a42-dced-4277-bcfd-8d7ffe71729e/image.svg" alt=""></p>
</li>
</ul>
<h2 id="증분-처리-패턴">증분 처리 패턴</h2>
<p>전체 덮어쓰기(<code>overwrite</code>)는 간단하지만 비효율적. 실무에서는 <strong>새로운 데이터만 처리</strong>하는 증분 방식이 일반적.</p>
<h3 id="append-모드">Append 모드</h3>
<p>새 레코드를 기존 테이블에 추가. <code>processing_date</code> 같은 타임스탬프 컬럼으로 새 데이터를 식별.</p>
<pre><code class="language-python">df_new.write.format(&quot;delta&quot;) \
    .mode(&quot;append&quot;) \
    .saveAsTable(f&quot;{CATALOG}.{SCHEMA}.bronze_diamonds&quot;)</code></pre>
<h3 id="merge-upsert-패턴">Merge (Upsert) 패턴</h3>
<p>존재하는 레코드는 업데이트, 새 레코드는 삽입. <strong>CDC(Change Data Capture) 시나리오에 이상적.</strong></p>
<pre><code class="language-sql">MERGE INTO target_table t
USING source_table s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *</code></pre>
<h3 id="모드-비교">모드 비교</h3>
<table>
<thead>
<tr>
<th>모드</th>
<th>언제 쓰나</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><code>overwrite</code></td>
<td>초기 적재, 실습</td>
<td>전체 재작성, 단순함</td>
</tr>
<tr>
<td><code>append</code></td>
<td>로그, 이벤트 데이터</td>
<td>기존 유지, 중복 주의</td>
</tr>
<tr>
<td><code>merge</code></td>
<td>CDC, 마스터 데이터</td>
<td>가장 정교함</td>
</tr>
</tbody></table>
<hr>
<h2 id="성능-최적화-팁-5가지">성능 최적화 팁 5가지</h2>
<h3 id="1-적절한-파티셔닝">1. 적절한 파티셔닝</h3>
<p>자주 필터링되는 컬럼을 파티션 키로 선택하여 불필요한 데이터 스캔을 줄임.</p>
<h3 id="2-캐싱-활용">2. 캐싱 활용</h3>
<p>여러 번 사용되는 DataFrame은 <code>cache()</code> 또는 <code>persist()</code>로 메모리에 저장.</p>
<pre><code class="language-python">df.cache()      # 메모리
df.persist()    # 메모리 + 디스크 (대용량 적합)
df.unpersist()  # 사용 후 해제</code></pre>
<h3 id="3-브로드캐스트-조인">3. 브로드캐스트 조인</h3>
<p>작은 테이블은 <code>broadcast()</code>를 사용하여 모든 노드에 복제하면 셔플을 피할 수 있음.</p>
<pre><code class="language-python">from pyspark.sql.functions import broadcast
df_result = df_large.join(broadcast(df_small), &quot;key&quot;)</code></pre>
<p>여기서 JOIN을 하려면 같은 key를 가진 데이터가 같은 노드에 있어야 합니다. 근데 지금은 흩어져 있으니까 네트워크로 데이터를 주고받아야 합니다. 이 과정이 셔플입니다.</p>
<pre><code>노드 1 → 노드 2로 데이터 전송  ← 느림 😓
노드 3 → 노드 1로 데이터 전송  ← 느림 😓</code></pre><p>브로드캐스트 조인은 어떻게 다르냐면
작은 테이블을 모든 노드에 통째로 복사해버립니다.</p>
<pre><code>브로드캐스트 전:
  노드 1: 주문 데이터만 있음
  노드 2: 주문 데이터만 있음
  노드 3: 주문 데이터만 있음

브로드캐스트 후:
  노드 1: 주문 데이터 + 고객 테이블(복사본) ← 로컬에서 바로 JOIN
  노드 2: 주문 데이터 + 고객 테이블(복사본) ← 로컬에서 바로 JOIN
  노드 3: 주문 데이터 + 고객 테이블(복사본) ← 로컬에서 바로 JOIN</code></pre><p>각 노드가 자기 데이터랑 로컬에 있는 복사본이랑 바로 JOIN하면 되니까 노드 간 데이터 전송(셔플)이 없어집니다.</p>
<h3 id="4-컬럼-프루닝">4. 컬럼 프루닝</h3>
<p>필요한 컬럼만 <code>select</code>하여 메모리 사용량과 I/O를 최소화.</p>
<pre><code class="language-python">df = spark.read.table(&quot;diamonds&quot;).select(&quot;cut&quot;, &quot;price&quot;, &quot;carat&quot;)</code></pre>
<h3 id="5-필터-푸시다운">5. 필터 푸시다운</h3>
<p>가능한 한 일찍 <code>filter</code>를 적용하여 처리할 데이터 양을 줄임.</p>
<pre><code class="language-python">df = spark.read.table(&quot;diamonds&quot;).filter(&quot;cut = &#39;Ideal&#39;&quot;).select(&quot;price&quot;)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Azure Databricks - 기초, AutoML]]></title>
            <link>https://velog.io/@no-glass-otacku/Databricks</link>
            <guid>https://velog.io/@no-glass-otacku/Databricks</guid>
            <pubDate>Thu, 11 Jun 2026 03:40:18 GMT</pubDate>
            <description><![CDATA[<p>비용이 비싸서 사람들이 잘 못씀.</p>
<p>(면접 때 사용하면 좋은 용어)
First party처럼 한 구독제에서 작동하는....</p>
<h2 id="databricks-기초">Databricks 기초</h2>
<h3 id="카탈로그">카탈로그</h3>
<p>프로젝트(카탈로그)마다 스키마와 테이블을 생성하는 곳.<br>명시하지 않으면 기본은 delta 테이블이 생성.</p>
<h3 id="notebook">Notebook</h3>
<p>주피터 노트북 상위호환. 여러명 동시 편집 가능.
sql, python, md 언어 동시에 한 문서에서 사용가능 (ex. %sql)
html로 export 가능.</p>
<pre><code>카탈로그.스키마.테이블 순으로 접근하거나

USE CATALOG `카탈로그 이름`;
USE SCHEMA 스키마이름; 
-&gt; 실행하면 테이블 이름만으로 사용 가능


DESCRIBE HISTORY delta_students;
-&gt; 버전 확인 후 특정 시점으로 되돌아가기 가능!
RESTORE TABLE delta_students VERSION AS OF 6;</code></pre><h4 id="run">%run</h4>
<p>명령어를 사용하여 다른 노트북에서 노트북을 실행할 수 있습니다.
실행할 노트북은 상대 경로로 지정됩니다.</p>
<p>참조된 노트북은 현재 노트북의 일부인 것처럼 실행되므로 호출하는 노트북에서 임시 뷰 및 기타 로컬 선언을 사용할 수 있습니다.</p>
<h4 id="sql-문법">SQL 문법</h4>
<blockquote>
<p>하나의 트렌젝션으로 처리하고 싶을때</p>
</blockquote>
<pre><code>MERGE INTO beans t
USING new_beans s
ON t.name = s.name and t.color = s.color
WHEN MATCHED THEN 
    update set grams=t.grams+s.grams
WHEN NOT MATCHED and s.delicious= true THEN 
  insert *</code></pre><p>기본문법</p>
<pre><code>CREATE TABLE beans (
  name STRING,
  ~~
) USING DELTA;
--
INSERT INTO beans VALUES
(&#39;pinto&#39;, &#39;brown&#39;, 1.5, true)
--
UPDATE beans
SET delicious = true
--
DELETE FROM beans
WHERE delicious = false;
--
DROP TABLE beans;</code></pre><h3 id="대시보드">대시보드</h3>
<p>powerBI처럼 시각화 가능. 게시해서 공유 가능.</p>
<h3 id="genie-space">Genie Space</h3>
<p>도메인 특화 자연어 채팅 인터페이스. 
사용자가 데이터에 질문을 하면 SQL 쿼리, 결과 테이블, 시각화를 돌려받습니다. </p>
<h3 id="compute">Compute</h3>
<p>Photon 가속-&gt; 더 빠르게 계산해주는 대신 비용 1.5배
auto scaling -&gt;
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/ea7318b5-09c9-441b-b4fc-6ad125a7d0f0/image.png" alt=""></p>
<h2 id="machine-learning">Machine Learning</h2>
<p>compute 만들때 <img src="https://velog.velcdn.com/images/no-glass-otacku/post/01da5361-dc79-4970-adc5-f86a23f0b5c2/image.png" alt=""></p>
<h3 id="automl">AutoML</h3>
<p>사람이 직접 모델을 고르고 튜닝하는 대신, 
Databricks가 알아서 여러 모델을 시도해보고 가장 좋은 걸 찾아줌!</p>
<pre><code>databricks.automl.classify(...)  # 분류 문제 (예: 좋은 와인 vs 나쁜 와인)
databricks.automl.regress(...)   # 회귀 문제 (예: 와인 점수가 몇 점?)
databricks.automl.forecast(...)  # 시계열 예측 (예: 내일 판매량?)

# 💡 timeout_minutes와 max_trials로 비용을 제어합니다

summary = databricks.automl.classify(

    dataset=f&quot;{CATALOG}.{SCHEMA}.wine_quality_lab&quot;,

    target_col=&quot;is_good_quality&quot;,

    primary_metric=&quot;f1&quot;, # 어떤 기준으로 모델을 평가할지

    timeout_minutes=20,  # 최대 20분 안에 끝내라. 시간 다 되면 그때까지 결과 중 최고를 반환

    exclude_cols=[&quot;quality&quot;],  # 원본 quality 컬럼 제외 (정보 누출 방지!)
)</code></pre><blockquote>
<p>정말 대박인 자동화</p>
</blockquote>
<ol>
<li><strong>데이터 전처리</strong>: 결측치, 인코딩, 스케일링 자동으로 해줌</li>
<li>알고리즘 선택: <strong>여러 모델 자동 학습</strong> 후 비교</li>
<li>하이퍼 튜닝: <strong>최적 조합</strong>을 자동 탐색</li>
</ol>
<ul>
<li>하이퍼 파라미터 탐색 전략: 베이지안 최적화 기반-&gt; 이전 결과로 다음 후보를 확률적으로 추정하기에 적은 시도로 최적값에 근접</li>
</ul>
<ol start="4">
<li>평가 &amp; 재현: <strong>소스 노트북 자동 생성</strong>...!</li>
</ol>
<h3 id="experiments">Experiments</h3>
<p>AutoML의 결과를 Experiments에서 전부 확인가능!
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/61384bd1-dc05-44a0-9ff3-8a0d17cee72e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Azure AI 서비스 모음]]></title>
            <link>https://velog.io/@no-glass-otacku/Azure-AI-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%AA%A8%EC%9D%8C</link>
            <guid>https://velog.io/@no-glass-otacku/Azure-AI-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%AA%A8%EC%9D%8C</guid>
            <pubDate>Wed, 10 Jun 2026 01:55:56 GMT</pubDate>
            <description><![CDATA[<h3 id="document-intelligence-studio">Document Intelligence Studio <img src="https://velog.velcdn.com/images/no-glass-otacku/post/ce3fc7b3-e93f-4d3a-9e7d-3d75707202f2/image.png" alt=""></h3>
<p>단일 서비스 리소스로 독립 생성이 가능.
최근에는 Azure AI Foundry (Foundry Tools) 산하로 편입되는 방향으로 가고 있음.</p>
<h4 id="ocr--문서처리">OCR / 문서처리</h4>
<ul>
<li><p>OCR/Read
손글씨도 인식하는데 단어 순서를 반대로 추출하기로 함... 가장 기본적인 text 추출</p>
</li>
<li><p>Layout
테이블, figure(그래프), 체크박스도 추출해줌.</p>
</li>
<li><p>General documents
신청서 같은 서류에서 key-value pair와 그냥 문장 덩어리를 추출함.</p>
<h5 id="prebuilt-model">prebuilt model</h5>
</li>
<li><p>Invoices
Query fields로 field 추가하면 더 잘 인식함.</p>
<p>+a</p>
</li>
</ul>
<h3 id="azure-ai-language-studio">Azure AI Language Studio <img src="https://velog.velcdn.com/images/no-glass-otacku/post/cfb90bce-3930-423d-9e4e-d33f15f3d192/image.png" alt=""></h3>
<p> <a href="https://language.cognitive.azure.com/home">https://language.cognitive.azure.com/home</a>
 지역별로 제공 여부가 다르고 East US의 경우 모든 서비스를 제공</p>
<ul>
<li><p>정보 추출: 비구조화된 텍스트에서 범주 그룹에 속하는 개체를 추출해줌.
  NER(Named Entity Recognition)</p>
</li>
<li><p>텍스트 분류: 텍스트의 언어를 감지하거나 감정 분류</p>
<ul>
<li>고객 감정 분석</li>
<li>자동으로 텍스트 분류: 사용자 지정 텍스트 분류, 언어 감지</li>
</ul>
</li>
<li><p>질문과 대화형 언어 이해</p>
<ul>
<li>고객 쿼리에 응답 QnA: 사용자 지정 질문 답변<ul>
<li>대화형 환경 구축: 대화 언어 이해</li>
</ul>
</li>
</ul>
</li>
<li><p>텍스트 요약</p>
</li>
<li><p>번역</p>
</li>
</ul>
<h3 id="speech-studio">Speech Studio <img src="https://velog.velcdn.com/images/no-glass-otacku/post/d85259f5-bca7-45a4-9bf3-82ad0941a58a/image.png" alt=""></h3>
<h4 id="음성--텍스트">음성 &gt; 텍스트</h4>
<p><a href="https://learn.microsoft.com/ko-kr/azure/ai-services/speech-service/get-started-speech-to-text?tabs=new-foundry%2Cwindows&amp;pivots=programming-language-python">음성 텍스트 변환 빠른 시작 - Foundry Tools | Microsoft Learn
</a></p>
<ul>
<li><p>실시간 음성 텍스트 변환</p>
</li>
<li><p>Whisper 모델 이용한 실시간 음성 텍스트 변환</p>
</li>
<li><p>일괄 처리 음성 텍스트 변환
대용량 파일을 비동기적으로 처리</p>
</li>
<li><p>custom speech
음성 모델 고르고 말하기 스타일 적용</p>
</li>
<li><p>발음 평가 👍</p>
</li>
<li><p>음성 번역
다른 언어로 번역</p>
</li>
</ul>
<h4 id="텍스트--음성">텍스트 &gt; 음성</h4>
<p><a href="https://learn.microsoft.com/ko-kr/azure/ai-services/speech-service/get-started-text-to-speech?tabs=new-foundry%2Cwindows&amp;pivots=programming-language-python">텍스트 음성 변환 빠른 시작 - 음성 서비스 - Foundry Tools | Microsoft Learn
</a></p>
<ul>
<li><p>전문적인 음성 미세 조정
고유한 음성 생성</p>
</li>
<li><p>개인 음성 -&gt; 사용 불가....
인간의 음성 샘플에서 개인 음성 tts</p>
</li>
<li><p>오디오 콘텐츠 만들기
음성 말하기 스타일, 속도, 발음 조정</p>
</li>
<li><p>텍스트 음성 변환 아바타
음성에 매치하는 아바타 영상 생성</p>
</li>
</ul>
<h3 id="azure-ai-vision">Azure AI Vision</h3>
<p>Foundry에서 사용.</p>
<ul>
<li>Image analysis 
이미지 감지, 분류, 주석, 이해</li>
<li>video analysis
공간 분석(영상의 움직임 분석), 비디오 검색(자연어로 검색가능한 비디오 인덱스 생성)</li>
<li>OCR</li>
<li>Facial recognition 
얼굴 인식. 신원확인이나 얼굴 흐림 처리 등 다양한 활용.</li>
</ul>
<h4 id="사용위치-접속법">사용위치 접속법</h4>
<p>Foundry와 Azure AI를 생성하고 (East US 지역 가능 아마도) 아래 이미지와 같이 프로젝트를 생성.
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/018c4404-4196-410e-befa-da62563aec5f/image.png" alt="">
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/c78c8760-1c6e-457b-878a-8319e5e435d1/image.png" alt="">
아래와 같이 접속하면 사용 가능.
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/3a950a30-2695-4eed-bac1-bcac43c893f4/image.png" alt="">
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/d4fc4231-0bbe-4cea-bc30-9ff286fbd6a1/image.png" alt=""></p>
<h4 id="활용처">활용처</h4>
<p><img src="https://velog.velcdn.com/images/no-glass-otacku/post/c072a366-1b6f-4e45-add9-93825a6ebc7c/image.png" alt="">
Common object detection: 물체의 이름 + 좌표
Image captioning: 사진 전체를 문장으로 설명
Dense captioning: 각 영역을 문장으로 설명
Image search: 비슷한 이미지 검색 기능
Common tag extraction: 관련 키워드를 신뢰도 점수와 함께 추출
Optical character recognition
Other image analysis capabilities</p>
<h3 id="custom-vision">Custom Vision <img src="https://velog.velcdn.com/images/no-glass-otacku/post/38cfe283-e032-43aa-b8f3-aa9360a18aac/image.png" alt=""></h3>
<p>개별 리소스로 생성해서 별도의 포털(Custom Vision portal)에서 사용가능.</p>
<p>두개가 한번에 생성되는데 코드로 실습할 때는 train과 predict로 둘 다 필요함. <a href="https://learn.microsoft.com/ko-kr/azure/ai-services/custom-vision-service/quickstarts/object-detection?tabs=windows%2Cvisual-studio&amp;pivots=programming-language-python">코드 실습 참고 문서</a>
<img src="https://velog.velcdn.com/images/no-glass-otacku/post/bd45df43-31ec-48b6-8b33-9a39a8c7495d/image.png" alt=""> <img src="https://velog.velcdn.com/images/no-glass-otacku/post/52f2c84c-a637-42f3-9e33-b445aab0cd0d/image.png" alt=""> </p>
<p>가위와 포크를 구분하는 모델 학습 실습을 진행함. 코드로 진행한 것도 게시하면 포털에서 확인가능.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Azure Data Factory (미완)]]></title>
            <link>https://velog.io/@no-glass-otacku/Azure-Data-Factory</link>
            <guid>https://velog.io/@no-glass-otacku/Azure-Data-Factory</guid>
            <pubDate>Tue, 12 May 2026 05:11:03 GMT</pubDate>
            <description><![CDATA[<pre><code>{
    &quot;runStatus&quot;: {
    ------중략-----
        &quot;metrics&quot;: {
            &quot;sinkTotalCount&quot;: {
    ------중략-----
                &quot;sources&quot;: {
                    &quot;employeeBatchData&quot;: {
                        &quot;rowsRead&quot;: 200, &lt;-값을 가져오는 곳
                        &quot;store&quot;: &quot;blob&quot;,

    ------중략-----

}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[시각화] PowerBI에 Stream Analytics에 연결하는 법]]></title>
            <link>https://velog.io/@no-glass-otacku/%EC%8B%9C%EA%B0%81%ED%99%94-PowerBI%EC%97%90-Stream-Analytics%EC%97%90-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@no-glass-otacku/%EC%8B%9C%EA%B0%81%ED%99%94-PowerBI%EC%97%90-Stream-Analytics%EC%97%90-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Sun, 10 May 2026 12:41:29 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Stream Analytics에 연결하는 법<ol>
<li>PowerBI 웹사이트에서 &#39;작업 영역&#39; &gt; &#39;+새 작업 영역&#39;</li>
<li>작업 영역 생성</li>
<li>URL 중 박스 쳐진 부분(작업 영역 ID) 저장해두기</li>
</ol>
</li>
</ul>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/5bcaf07f-22f6-4eba-bb55-fe6d2f3674f5/image.png" width="300"></th>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/e5211287-d456-4bf0-8eb6-27b6ea8674cb/image.png" width="280"></th>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/7db8d75f-e760-4a9f-bf33-e71ad81531df/image.png" width="800"></th>
</tr>
</thead>
</table>
<ol start="4">
<li>Stream Analytics에서 Power BI를 출력으로 추가할 때, &#39;그룹 작업 영역&#39;에 아까 저장해둔 작업 영역 ID를 입력.</li>
<li>&#39;권한 부여&#39;를 누르고, 로그인 화면이 뜨면 작업영역이랑 동일한 powerBI 계정으로 로그인</li>
<li>powerBI로 돌아와서 &#39;데이터 가져오기&#39; &gt; &#39;powerBI 의미 체계 모델&#39; &gt; &#39;연결&#39; &gt; 내가 사용하려는 데이터셋 선택하면 완료!</li>
</ol>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/bd9b0573-b421-4105-a5bb-af2e1369dcd5/image.png" width="350"></th>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/951c8cc0-cb5c-4081-8648-c605893e72d0/image.png" width="250"></th>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/06db90f6-9313-456a-a119-34f0d8a2219e/image.png" width="280"></th>
</tr>
</thead>
</table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2026 Hult prize national] 심사위원 피드백]]></title>
            <link>https://velog.io/@no-glass-otacku/2026-Hult-prize-national-%EC%8B%AC%EC%82%AC%EC%9C%84%EC%9B%90-%ED%94%BC%EB%93%9C%EB%B0%B1</link>
            <guid>https://velog.io/@no-glass-otacku/2026-Hult-prize-national-%EC%8B%AC%EC%82%AC%EC%9C%84%EC%9B%90-%ED%94%BC%EB%93%9C%EB%B0%B1</guid>
            <pubDate>Thu, 07 May 2026 02:11:04 GMT</pubDate>
            <description><![CDATA[<h3 id="404팀---aequalis-시각장애인을-위한-점역-통합-플랫폼-솔루션">&lt;404팀 - Aequalis 시각장애인을 위한 점역 통합 플랫폼 솔루션&gt;</h3>
<p><strong>피드백 정리</strong></p>
<hr>
<p><strong>✅ 잘 된 부분</strong></p>
<p>발표 앞부분의 논리 흐름이 좋았어. 문제 제기와 Why Now까지의 흐름이 설득력 있었고 첫인상이 좋았다는 평가를 받았어. 특히 규제가 존재해서 우리 서비스가 필요하다는 부분이 가장 좋게 평가됐어.</p>
<pre><code>**문제의 원인**
1. 점역이 비싸고 수작업이라 오래 걸린다
2. 접근성 표준은 있지만 그걸 준수할 기술이 없다
3. 결과적으로 7.6%의 자료만 접근성이 보장된다

**지금 해결해야 하는 이유**
1. **규제가 존재**해서 우리 서비스가 법적 필수재가 된다
2. 예산은 이미 배정되어 있다

**기존 솔루션의 문제**
1. 충분한 자동화가 없다
2. 서비스들이 분산되어 있다

**우리 솔루션**
1. 충분한 자동화
2. 분산된 서비스를 통합한 플랫폼</code></pre><p><em>다른 팀 적용: 발표 초반 문제 정의와 감정적 훅은 심사위원의 첫인상을 결정해. 앞부분에 가장 많은 공을 들여.</em></p>
<hr>
<p><strong>❌ 부족했던 부분</strong></p>
<p><strong>1. 솔루션 설명이 불명확했어</strong></p>
<p>심사위원이 &quot;뭘 하려는 건지&quot;, &quot;결과물이 뭔지&quot;를 계속 물었어. 흩어진 기술들을 모아서 빈 공간을 채운다는 핵심 개념이 발표에서 전달되지 않았어. 특히 Pedagogical Masking 같은 독자적 기술이 충분히 설명되지 않고 슬라이드에만 적혀서 넘어갔어.</p>
<p><em>다른 팀 적용: 솔루션의 핵심을 한 문장으로 말할 수 없다면 청중도 이해하지 못해. &quot;우리는 X를 Y방식으로 해결한다&quot;를 한 문장으로 압축해서 반드시 발표에 포함시켜.</em></p>
<p><strong>2. 교과서인지 학습지인지 명확하지 않았어</strong></p>
<p>타깃 제품이 무엇인지 흐지부지 넘어갔어. 심사위원 입장에서는 범위가 불명확하면 신뢰도가 떨어져.</p>
<p><em>다른 팀 적용: 타깃을 좁게 정의할수록 설득력이 높아져. &quot;우리는 모든 것을 한다&quot;보다 &quot;우리는 이것부터 한다&quot;가 더 강해.</em></p>
<p><strong>3. 비즈니스 모델 설명이 작동 방식 없이 숫자만 있었어</strong></p>
<p>돈 버는 구조는 설명됐지만 실제로 어떻게 운영되는지가 빠졌어. 심사위원은 비즈니스 모델에서 두 가지를 봐. 어떻게 작동하는지, 그리고 어떻게 돈을 버는지. 둘 다 한눈에 보여야 해.</p>
<p><em>다른 팀 적용: 비즈니스 모델 슬라이드는 화려하게 만들 필요 없어. &quot;A가 B를 하면 C가 발생하고 우리는 D를 가져간다&quot;는 흐름이 한눈에 보이면 충분해.</em></p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/1beedb34-d285-4fcb-9a89-174300e30a3c/image.png" alt="이미지1" width="200"> X</th>
<th><img src="https://velog.velcdn.com/images/no-glass-otacku/post/a3303e9a-00af-4e33-983b-fdbf361adae2/image.png" alt="이미지2" width="300"> ?</th>
</tr>
</thead>
</table>
<p><strong>4. Callback 답변이 너무 길었어</strong></p>
<p>질문의 의도를 파악하고 요점만 말하는 게 필요했어. 심사위원이 원한 건 &quot;있는 걸 모아서 빈 공간을 채운다&quot; 한 문장이었는데 너무 길게 답했어.</p>
<p><em>다른 팀 적용: Q&amp;A에서 답변은 두 문장 이내로 시작해. 핵심을 먼저 말하고, 필요하면 한 문장을 덧붙여. 길게 말할수록 핵심이 묻혀.</em></p>
<hr>
<p><strong>💡 핵심 인사이트 — 심사위원이 가장 강조한 것</strong></p>
<blockquote>
<p>&quot;결과물은 똑같아. How to가 다른 거야. 오퍼레이션이 다른 거야.&quot;</p>
</blockquote>
<p>기존 점자 교재와 Aequalis의 결과물은 같아. 차이는 만드는 방식이야. 
이걸 한 문장으로 압축해서 발표에 넣었어야 했어.</p>
<p>심사위원이 제안한 한 줄:</p>
<blockquote>
<p>&quot;흩어져 있는 점역 기술들을 모아서 큐레이션하고, 빈 공간은 우리만의 기술로 채운다. 결과물은 같지만 훨씬 빠르고 정확하게.&quot;</p>
</blockquote>
<p><em>다른 팀 적용: 발표에서 &quot;<strong>우리만의 한 줄</strong>&quot;이 없으면 심사위원은 발표가 끝난 후 아무것도 기억하지 못해. 발표 준비의 마지막 단계는 항상 &quot;이 발표를 한 문장으로 요약하면?&quot;에 답하는 거야.</em></p>
<hr>
<p><strong>🔍 추가 피드백 인사이트</strong></p>
<p><strong>플랫폼 비즈니스는 차별화가 핵심이야</strong></p>
<p>플랫폼 비즈니스는 비슷한 게 너무 많아. &quot;찾으면 나온다&quot;는 말이 나올 수 있어. 그래서 왜 우리가, 왜 지금, 왜 이 시장에서 이 생각을 하게 됐는지를 보여줘야 해. 
차별점은 기능이 아니라 문제 정의에서 나와. 문제 정의가 정확할수록 &quot;이 팀이 들어가면 해결된다&quot;는 확신을 줄 수 있어.</p>
<p><em>다른 팀 적용: 제일 좋은 비즈니스는 벤치마킹이라고 했어. 이미 검증된 모델을 가져오되, 우리가 이 문제를 발견한 맥락과 이유를 보여주는 게 차별점이야. &quot;왜 우리가 이걸 만들게 됐는가&quot;가 설득력의 핵심이야.</em></p>
<p><strong>4단계 구조를 지켜</strong></p>
<p>심사위원이 직접 제시한 발표 구조야.</p>
<ol>
<li>현상 — 이런 일이 일어나고 있다</li>
<li>문제 정의 — 이게 왜 문제인가</li>
<li>솔루션 — 우리는 이렇게 해결한다</li>
<li>임팩트 — 그 결과 이렇게 달라진다</li>
</ol>
<p><em>다른 팀 적용: 4분 발표든 10분 발표든 이 구조를 벗어나지 마. 솔루션을 먼저 말하고 싶은 충동을 참아. 문제가 먼저 공감돼야 솔루션이 설득돼.</em></p>
]]></description>
        </item>
    </channel>
</rss>