Spark SQL로 파일 직접 쿼리하는 것부터 Delta 테이블 생성, 외부 소스 연결, 쓰기 방법까지 정리
이걸 모르면 수업 내내 헷갈린다.
Databricks의 테이블 종류
① 외부 테이블 (= 외부 소스 테이블)
→ 데이터는 밖(CSV, JSON, 외부 DB 등)에 그대로
→ Databricks는 "저기 가면 데이터 있어"라는 정보만 저장
→ USING + LOCATION 키워드가 있으면 외부 테이블
② Delta 테이블
→ 데이터가 Databricks 안으로 들어온 상태
→ 버전 관리, 빠른 쿼리, 자동 최신화 등 모든 기능 사용 가능
→ CTAS (CREATE TABLE AS SELECT) 로 만드는 게 일반적
"JSON 파일에서 외부 테이블을 생성하라" = 데이터는 JSON 파일에 그대로 두고, 그 파일을 테이블처럼 읽을 수 있도록 Databricks에 등록하라는 뜻. Delta 테이블 아님.
코드로 구분하기
-- 관리형 Delta 테이블
-- USING도 없고 LOCATION도 없음
CREATE TABLE item_lookup AS
SELECT * FROM parquet.`/path`
-- 외부 테이블 (파일)
-- USING + LOCATION 있음
CREATE TABLE sales_csv
USING CSV
OPTIONS (header="true", delimiter="|")
LOCATION "/path/to/file"
-- JDBC (외부 DB)
-- USING JDBC + url + dbtable
CREATE TABLE users_jdbc
USING JDBC
OPTIONS (
url = "jdbc:mysql://서버주소:3306",
dbtable = "users"
)
| 관리형 Delta | 외부 테이블 | JDBC | |
|---|---|---|---|
| 데이터 위치 | Databricks 안 | 파일 (S3 등) | 외부 DB 서버 |
| 코드 특징 | USING/LOCATION 없음 | USING+LOCATION | USING JDBC+url |
| 최신 데이터 | 자동 | REFRESH 필요 | 자동 |
| 성능 | 빠름 | 보통 | DB 크기에 따라 다름 |
"어떤 테이블 만들어라" 문제가 나오면
1. "관리형" or "Delta" 언급 → USING/LOCATION 없는 CTAS
2. "외부" or 파일 경로 + 옵션 필요 → USING + LOCATION
3. DB 연결 (MySQL, PostgreSQL 등) → USING JDBC + OPTIONS
Databricks에서는 테이블을 만들지 않아도 파일을 바로 쿼리할 수 있다.
-- 단일 파일 쿼리
SELECT * FROM json.`/path/to/001.json`
-- 디렉터리 전체 쿼리 (같은 형식/스키마인 경우)
SELECT * FROM json.`/path/to/directory`
-- Parquet도 동일하게
SELECT * FROM parquet.`/path/to/file`
경로는 반드시 백틱(`) 으로 감싸야 한다. 작은따옴표(')가 아님에 주의.
문법 구조: 파일형식.경로
json.`/path` -- JSON 파일
parquet.`/path` -- Parquet 파일
csv.`/path` -- CSV 파일
-- 임시 뷰로 등록 (세션 끝나면 사라짐)
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 = 'clickstream'
-- 텍스트로 읽기
SELECT * FROM text.`/path/to/file`
-- 바이너리로 읽기 (파일 메타데이터 포함)
SELECT * FROM binaryFile.`/path/to/file`
비유로 먼저 이해하자.
도서관 책 대출 시스템
책(데이터)은 도서관 서가(외부 경로)에 그대로 있음
대출 카드(외부 테이블 정의)만 시스템에 등록
"이 책은 3층 A구역 12번 선반에 있어" 라는 정보만 저장
→ 읽을 때마다 서가에서 꺼내서 읽음
→ 책 자체를 복사해서 들고오지 않음
딱 두 가지 키워드가 있으면 외부 테이블이다.
CREATE TABLE sales_csv
(order_id LONG, email STRING, ...)
USING CSV -- ① Delta가 아닌 형식 (CSV, JSON, JDBC 등)
OPTIONS (
header = "true",
delimiter = "|"
)
LOCATION "/path" -- ② 데이터가 외부 경로에 있음
반대로 Delta 테이블은 이렇게 생겼다:
-- USING도 없고 LOCATION도 없음
CREATE TABLE sales AS
SELECT * FROM parquet.`/path`
-- 데이터가 Databricks 안으로 들어옴
CSV 파일을 직접 쿼리하면 이런 문제가 생긴다:
헤더가 데이터 행으로 들어옴
모든 컬럼이 하나로 합쳐짐 ← 구분자가 | 인데 Spark가 모름
마지막 컬럼이 잘림
그래서 옵션을 줘서 외부 테이블로 등록한다:
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 = "true", -- 첫 행은 헤더야
delimiter = "|" -- | 로 구분돼 있어
)
LOCATION "${DA.paths.sales_csv}"
이렇게 하면 SELECT * FROM sales_csv 로 깔끔하게 읽을 수 있다.
현실에서 "그냥 가져오는 것"이 안 되는 경우가 많다:
| 상황 | 이유 |
|---|---|
| 실시간으로 계속 바뀌는 데이터 | 복사하는 순간 이미 낡은 데이터 |
| 10TB짜리 대용량 데이터 | 복사 자체가 시간/비용 낭비 |
| 다른 팀 소유 데이터 | 복사 권한이 없음 |
| 매일 새 파일이 쌓이는 경우 | LOCATION만 걸면 자동으로 읽힘 |
실무에서는 외부 소스로 원본 연결 → 가공 후 Delta로 저장 흐름이 일반적. 이게 메달리온 아키텍처의 Bronze 단계가 하는 일이다.
-- 외부에서 파일에 데이터 추가됨
SELECT COUNT(*) FROM sales_csv -- → 숫자 그대로 (캐시 때문에 못 봄)
REFRESH TABLE sales_csv -- 수동으로 캐시 갱신 필요
SELECT COUNT(*) FROM sales_csv -- → 이제 최신 숫자
Delta 테이블이면 자동으로 최신 데이터를 읽지만, 외부 테이블은 수동 갱신이 필요하다.
우리 집(Databricks)에서
이웃 집(MySQL, PostgreSQL, SQLite 등 외부 DB)에 있는 데이터를
전화선(JDBC 연결)으로 당겨와서 읽는 것
쉽게 말해 Databricks 밖에 있는 데이터베이스 서버에 연결하는 방법이다.
회사에서 이런 상황이 흔하다:
영업팀이 오래전부터 쓰던 Oracle DB
→ 주문 데이터, 고객 데이터가 거기 다 있음
→ Databricks로 이사오기엔 너무 크고 복잡함
→ 그냥 거기 두고 Databricks에서 연결해서 읽음
CREATE TABLE users_jdbc
USING JDBC -- JDBC 방식으로 연결
OPTIONS (
url = "jdbc:sqlite:/path/to/db", -- DB 서버 주소
dbtable = "users" -- 읽어올 테이블 이름
)
주소 형식: jdbc:DB종류://서버주소:포트
jdbc:mysql://192.168.0.1:3306 -- MySQL 연결
jdbc:postgresql://myserver:5432 -- PostgreSQL 연결
jdbc:sqlite:/path/to/file.db -- SQLite 연결 (파일 기반)
만들고 나면 그냥 일반 테이블처럼 쓸 수 있다:
SELECT * FROM users_jdbc -- 외부 DB에서 데이터 가져옴
단, 데이터가 Databricks 안으로 복사되는 게 아니라 쿼리할 때마다 외부 DB에서 가져오는 것이라 대용량이면 느릴 수 있다.
"SELECT 결과물로 Delta 테이블 만들기"
CREATE OR REPLACE TABLE sales AS
SELECT * FROM parquet.`/path/sales-historical`
스키마(컬럼 구조)를 직접 안 써도 SELECT 결과에서 자동으로 추론한다.
Parquet, JSON처럼 스키마가 명확한 파일에서 특히 유용하다.
CSV처럼 옵션이 필요한 파일은 CTAS 전에 임시 뷰를 먼저 만들어야 한다:
-- 1단계: CSV 읽기 옵션을 담은 임시 뷰 생성
CREATE OR REPLACE TEMP VIEW sales_tmp
(order_id LONG, email STRING, ...)
USING CSV
OPTIONS (path = "/path", header = "true", delimiter = "|");
-- 2단계: 임시 뷰로 Delta 테이블 생성
CREATE TABLE sales_delta AS
SELECT * FROM sales_tmp;
-- CRAS: 스키마도 바꿀 수 있는 강력한 덮어쓰기
CREATE OR REPLACE TABLE events AS SELECT ...
-- INSERT OVERWRITE: 스키마 변경 불가, 더 안전한 덮어쓰기
INSERT OVERWRITE sales SELECT * FROM parquet.`/path`
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 "generated based on transactions_timestamp column"
)
다른 열의 값을 기반으로 자동 계산되는 열. 삽입 시 직접 값을 안 써도 Delta가 알아서 채워준다.
-- 조건을 위반하는 데이터가 들어오면 쓰기 에러 발생
ALTER TABLE purchase_dates ADD CONSTRAINT valid_date CHECK (date > '2020-01-01');
CREATE OR REPLACE TABLE purchases_shallow SHALLOW CLONE purchases
비유:
도서관 책은 그대로 두고
"이 책은 3층 A구역 12번에 있어" 라는 메모(Delta Log)만 복사
읽을 때 → 메모 보고 원본 위치 찾아가서 읽음
수정할 때 → 원본은 건드리지 않고, 내 공간에 새 파일 생성
원본 삭제되면 → 메모는 있는데 책이 없으니 에러
수정해도 원본이 안 바뀌는 이유:
Spark는 파일을 절대 덮어쓰지 않는다. 수정 = 기존 파일 건드리는 게 아니라 새 파일을 만드는 것. 그 새 파일이 원본 경로가 아닌 Clone 자체 경로에 생기기 때문에 원본은 그대로다.
CREATE OR REPLACE TABLE purchases_clone DEEP CLONE purchases
비유:
책을 통째로 복사해서 내 책상에 올려두기
원본이 어떻게 되든 내 복사본은 독립적으로 존재
원본 삭제돼도 내 복사본은 멀쩡함
단, 복사하는 데 시간이 오래 걸림
| Shallow Clone | Deep Clone | |
|---|---|---|
| 데이터 복사 | ❌ 원본 참조 | ✅ 완전 복사 |
| 속도 | 빠름 (로그만 복사) | 느림 (데이터 전체 복사) |
| 원본 삭제되면 | 망가짐 | 독립 유지 |
| 원본이 바뀌면 | 영향 받을 수 있음 | 영향 없음 |
| 용도 | 테스트, 임시 실험 | 완전한 백업, 마이그레이션 |
INSERT OVERWRITE sales
SELECT * FROM parquet.`/path`
기존 데이터를 통째로 교체. 스키마(컬럼 구조)가 바뀌면 에러남. 기존 테이블 구조를 지키면서 데이터만 갈아끼울 때.
INSERT INTO sales
SELECT * FROM parquet.`/path`
기존 데이터 유지하고 새 행만 추가. 중복 방지 기능 없음. 같은 셀 두 번 실행하면 데이터 두 번 들어감.
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 *
조건에 따라 UPDATE / INSERT / DELETE를 한 트랜잭션으로 처리.
같은 user_id가 있고 email이 비어있으면 → UPDATE
같은 user_id가 없으면 → INSERT
INSERT INTO의 중복 문제를 해결할 수 있다. WHEN NOT MATCHED만 쓰면 중복 없는 삽입만 수행:
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 = 'email' THEN
INSERT *
COPY INTO sales
FROM "/path/sales-30m"
FILEFORMAT = PARQUET
이미 가져온 파일은 건너뛰고 새 파일만 가져온다. 매일 새 파일이 쌓이는 폴더에서 반복 실행할 때 유용.
| 방법 | 기존 데이터 | 중복 방지 | 용도 |
|---|---|---|---|
INSERT OVERWRITE | 전체 교체 | - | 정기적으로 전체 갱신 |
INSERT INTO | 유지+추가 | ❌ | 단순 추가 |
MERGE INTO | 조건부 처리 | ✅ | 업서트, 정교한 동기화 |
COPY INTO | 유지+추가 | ✅ (파일 단위) | 외부 파일 증분 수집 |
외부 데이터 원본 외부 테이블 등록 Delta 테이블 생성
────────────── ──────────────── ────────────────
CSV 파일 USING CSV CTAS로 변환
JSON 파일 → USING JSON → Delta 최적화 적용
외부 DB (MySQL 등) USING JDBC ↓
LOCATION 지정 쓰기
(데이터는 밖에 그대로) (MERGE / COPY INTO)
[Bronze: 원본 연결] [Bronze 단계] [Silver/Gold 단계]
실무에서는 외부 테이블(Bronze) → CTAS로 Delta 변환(Silver) → 정제/집계(Gold) 순서로 메달리온 아키텍처를 구성한다.