# 6주차 실습 절차서: Parquet 저장과 조회

## 1. 실습 폴더 준비

Docker Desktop 실행. PowerShell 실행 후 다음 명령으로 Docker Engine 응답 확인.

```powershell
docker version
```

`Client`, `Server` 정보가 모두 출력되는지 확인. `Server` 연결 오류가 있으면 Docker Desktop 시작 완료 후 재실행.

이전 주차 Spark 클러스터가 실행 중이면 이전 실습 폴더의 PowerShell에서 종료.

```powershell
docker compose down
```

배포 파일 `week06-lab.zip`을 다운로드 폴더에 둔 뒤 다음 명령 실행. `C:\bigdata\week06-lab`이 이미 있으면 기존 작성 파일을 별도 보관한 후 새 폴더에 압축 해제. 기존 폴더 위에 덮어쓰지 않음.

```powershell
New-Item -ItemType Directory -Force C:\bigdata
Expand-Archive -LiteralPath "$HOME\Downloads\week06-lab.zip" -DestinationPath C:\bigdata
Set-Location C:\bigdata\week06-lab
Get-ChildItem
New-Item -ItemType Directory -Force .\output
```

`compose.yaml`, `apps`, `data`, `실습절차서.md`, `EXERCISES.md` 존재 확인. 이후 모든 명령은 이 폴더의 PowerShell에서 실행.

## 2. 클러스터 시작

```powershell
docker compose config --quiet
$LASTEXITCODE
docker compose config --services
docker compose pull --policy missing
docker compose up -d --scale spark-worker=2
docker compose ps
```

첫 번째 `$LASTEXITCODE`는 `0`. 서비스 이름은 `spark-master`, `spark-worker`, `spark-client`. 컨테이너는 Master 1개, Worker 2개, Client 1개.

`pull --policy missing`은 지정된 이미지가 PC에 없을 때 다운로드. 이미 설치된 같은 태그의 이미지는 재사용.

브라우저에서 `http://localhost:18080` 접속. Master UI에서 `ALIVE` Worker 2개 확인. 시작 직후 2개가 아니면 잠시 후 새로 고침.

## 3. 입력과 저장 경로 확인

```powershell
Get-Content .\data\orders.csv
docker compose exec spark-client ls /opt/spark/apps
docker compose exec spark-client ls /opt/spark/data
docker compose exec spark-client bash -lc 'test -w /opt/spark/output && echo CLIENT_OUTPUT_WRITABLE'
docker compose exec --index 1 spark-worker bash -lc 'test -w /opt/spark/output && echo WORKER_1_OUTPUT_WRITABLE'
docker compose exec --index 2 spark-worker bash -lc 'test -w /opt/spark/output && echo WORKER_2_OUTPUT_WRITABLE'
```

CSV는 헤더를 제외하고 12행. 마지막 세 명령에서 각각 `CLIENT_OUTPUT_WRITABLE`, `WORKER_1_OUTPUT_WRITABLE`, `WORKER_2_OUTPUT_WRITABLE` 확인.

| Windows 경로 | 컨테이너 경로 | 용도 |
|---|---|---|
| `apps` | `/opt/spark/apps` | 프로그램. 읽기 전용 연결 |
| `data` | `/opt/spark/data` | CSV 입력. 읽기 전용 연결 |
| `output` | `/opt/spark/output` | 저장 결과. Client와 모든 Worker가 같은 호스트 폴더에 접근 |

이 절차서는 단계별 프로그램 `week06_lab.py` 사용. 결과는 `output\week06-lab`에 저장. 강의노트의 전체 예제 `week06_parquet.py`는 `output\week06`에 저장하므로 두 프로그램의 결과는 별도 폴더에서 확인.

각 단계의 실행 직후 `$LASTEXITCODE`가 `0`인지 확인. 출력 끝의 `CHECK=PASS`, `APP_STOPPED` 확인. WARN 로그만으로 실패로 판단하지 않음. `Traceback`, `AssertionError`, `CHECK=PASS` 누락 시 다음 단계로 진행하지 않음.

## 4. CSV 정제 후 Parquet 저장

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step clean
$LASTEXITCODE
Get-ChildItem .\output\week06-lab\orders_clean
```

확인할 출력:

```text
RAW_ROWS=12
VALID_ROWS=9
SAVED_ROWS=9
OUTPUT=/opt/spark/output/week06-lab/orders_clean
STEP_CHECK=clean,PASS
CHECK=PASS
UI observation: 0 seconds
APP_STOPPED
```

수량·단가가 NULL인 행, 수량이 0 이하인 행, 단가가 음수인 행 제외. `amount = quantity * price` 계산 후 Snappy Parquet 저장. `orders.csv`는 변경되지 않음.

`part-...snappy.parquet`와 `_SUCCESS` 확인. 파일 이름의 임의 문자열과 파일 수는 실행에 따라 달라질 수 있음. 이 단계는 기존 `orders_clean` 결과를 덮어쓰기.

## 5. 저장된 Schema와 값 확인

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step inspect
$LASTEXITCODE
```

입력 Schema를 다시 지정하지 않아도 `amount: decimal(23,2)` 출력. 저장된 주문 번호는 `1, 2, 3, 5, 7, 8, 10, 11, 12`. `SCHEMA_AND_VALUES=PASS`, `CHECK=PASS` 확인.

## 6. 필요한 컬럼만 조회

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step projection
$LASTEXITCODE
```

실행계획의 Parquet Scan에서 `ReadSchema` 확인. `user_id`, `amount`만 포함. 결과 표는 9행, 두 컬럼. `SELECTED_COLUMNS=user_id,amount` 확인.

`select`는 새 DataFrame의 연산을 지정. `explain`은 실행계획 출력. 이 단계의 `show`, `count`가 결과 계산 요청. DataFrame을 만들었다는 이유만으로 9행이 메모리에 저장되는 것은 아님.

## 7. 필터 조건과 Predicate Pushdown 확인

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step pushdown
$LASTEXITCODE
```

실행계획에서 `PushedFilters`의 `GreaterThanOrEqual(amount,30000.00)` 확인. 버전에 따라 표현 형식의 차이는 있을 수 있음. 결과 주문 번호 `2, 5, 8`, `HIGH_ORDER_IDS=2,5,8` 확인.

필터가 Parquet 읽기 단계에 전달되었는지 확인하는 실습. 이 작은 파일에서 읽기 속도 향상이나 Row Group 생략을 보장하는 실험은 아님.

## 8. 날짜별 디렉터리 저장과 조회

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step partition
$LASTEXITCODE
Get-ChildItem .\output\week06-lab\partition_demo -Recurse -Filter *.parquet
```

이 단계는 앞의 주문 9행이 아닌 별도 주문 4행 사용.

| order_id | user_id | amount | event_date |
|---|---|---|---|
| 101 | U001 | 24000.00 | 2026-09-01 |
| 102 | U002 | 35000.00 | 2026-09-01 |
| 103 | U003 | 20000.00 | 2026-09-02 |
| 104 | U001 | 35000.00 | 2026-09-02 |

확인할 출력:

```text
SINGLE_FILE_COLUMNS=order_id,user_id,amount
DATASET_COLUMNS=order_id,user_id,amount,event_date
DATE_DIRECTORIES=2
PARTITION_ROWS=4
DAILY_HIGH_ORDER_IDS=102
```

`partition_demo` 아래 `event_date=2026-09-01`, `event_date=2026-09-02` 디렉터리 확인. 개별 파일 안에는 세 컬럼만 존재. 상위 `partition_demo`를 읽으면 디렉터리 이름에서 `event_date`를 읽어 네 컬럼으로 조회.

계획의 `PartitionFilters`와 `PushedFilters` 구분. 전자는 날짜 디렉터리 선택, 후자는 파일 읽기 단계에 전달한 금액 조건. 결과는 9월 1일 중 금액 30,000 이상인 주문 `102` 한 행.

## 9. append와 중복 확인

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step append
$LASTEXITCODE
```

`BEFORE_ROWS=9`, `AFTER_ROWS=10`, `ORDER_1_COUNT=2`, `ORIGINAL_FILES_UNCHANGED=True` 확인. `AFTER_FILES`가 `BEFORE_FILES`보다 큰지 비교.

기존 9행에 주문 1번을 한 번 더 저장한 결과. `append`는 기존 파일을 유지하고 파일을 추가. 같은 주문 번호를 자동으로 갱신하거나 중복 제거하지 않음.

위 명령 한 번 더 실행. 다시 `BEFORE_ROWS=9`, `AFTER_ROWS=10` 확인. 이 실습 함수는 실행할 때마다 `append_demo`를 9행으로 덮어쓴 뒤 한 행 추가. `append` 자체가 중복 누적을 방지하는 것은 아님.

## 10. 파일별 최대 행 수와 작은 파일 확인

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step options
$LASTEXITCODE
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step small-files
$LASTEXITCODE
Get-ChildItem .\output\week06-lab\tiny_files_demo -Recurse -Filter *.parquet
```

첫 실행: `ROWS_PER_FILE`의 각 값이 4 이하이고 합계가 9인지 확인. `TOTAL_ROWS=9` 확인. `maxRecordsPerFile=4`는 파일당 최대 행 수이며 파일 수를 정확히 지정하는 옵션이 아님. 압축 방식은 Gzip.

두 번째 실행: `ORDER_DIRECTORIES=9`, `PARQUET_FILES=9` 확인. 주문 번호마다 디렉터리를 구분하여 한 행짜리 파일 9개 생성. 모든 Worker의 수를 늘려 해결할 문제가 아니라 저장 시 구분할 컬럼의 선택 문제.

## 11. 실행 중 Spark UI 확인

```powershell
docker compose exec spark-client bash /opt/spark/apps/run_week06.sh --step projection --wait-seconds 120
```

`UI observation: 120 seconds` 출력 후 대기하는 동안 다음 순서로 확인.

1. 브라우저에서 `http://localhost:24040` 접속.
2. `SQL / DataFrame` 탭의 `Completed Queries`에서 `showString at ...` 설명 링크 클릭.
3. 쿼리 상세 화면에서 `Plan Details` 클릭. `Scan parquet` 아래 `ReadSchema`에 `user_id`, `amount`만 있는지 확인.
4. `Executors` 탭에서 Driver와 Executor 확인.

120초 후 프로그램이 종료되면 이 UI도 접속 불가. Job·Stage 수나 실행 시간은 고정값과 비교하지 않음.

```powershell
$LASTEXITCODE
```

`0`, `APP_STOPPED` 확인. 프로그램이 종료되기 전에 다른 실습 프로그램을 동시에 실행하지 않음.

## 12. 연습문제 작성

[EXERCISES.md](EXERCISES.md)의 두 문제 수행. 작성 파일은 `apps\exercise01_store_sales.py`, `apps\exercise02_inventory_append.py`. 입력과 결과 검사는 제공 코드 사용. `exercise_support.py`와 CSV는 수정하지 않음.

## 13. 종료와 재시작

```powershell
docker compose down
```

컨테이너와 실습 네트워크 종료. Windows의 `output` 파일은 유지. 다음 실습 시 같은 폴더에서 재시작.

```powershell
docker compose up -d --scale spark-worker=2
```

4단계를 다시 실행하면 정제 결과를 덮어쓰기. 각 단계는 자신이 사용하는 결과 디렉터리만 변경. 연습문제 결과는 `output\exercises`에 별도 저장.
