DBA 실무/PostgreSQL

[PostgreSQL 시리즈 4편] psql 완벽 활용 + AI로 학습 가속

isony 2026. 8. 19. 07:53
반응형

[PostgreSQL 시리즈 4편] psql 완벽 활용 + AI로 학습 가속

- 테스트 환경: PostgreSQL 17, Rocky Linux 9 / Oracle 19c

Oracle DBA에게 SQL*Plus는 손에 붙은 도구입니다. DESC emp, SET LINESIZE 200, SPOOL result.log, login.sql… 눈 감고도 칩니다.

그런데 psql 앞에 앉으면 막막합니다. DESC를 치면 에러가 나고, 컬럼 20개짜리 테이블을 조회하면 화면이 무너집니다. 문법은 3편까지 익혔는데 도구가 손에 안 붙는 상태입니다.

이번 편은 그 간극을 메웁니다. 그리고 Phase 1의 마지막인 만큼, 앞으로 남은 학습을 AI로 가속하는 방법까지 함께 다룹니다. 핵심은 하나입니다. AI에게 묻고, psql로 채점한다.

이번 편에서 다룰 내용입니다.

  • ★ SQL*Plus vs psql 명령어 대응표 (이 글의 치트시트)
  • DESCRIBE보다 강력한 \d 계열 메타 명령어
  • ★ login.sql 대신 .psqlrc — 한 번 세팅하고 평생 쓰기
  • tnsnames.ora 대신 .pg_service.conf
  • WHENEVER SQLERROR 대응 — 스크립트 안전장치
  • ★ AI 활용법과 검증 루프 — Oracle 습관이 섞여 나오는 함정

 

★ SQL*Plus vs psql 명령어 대응표

먼저 치트시트부터 봅니다. 이 표 하나로 첫 주가 편해집니다.

하려는 일 SQL*Plus psql

테이블 구조 보기 DESC emp \d emp
테이블 목록 SELECT * FROM user_tables \dt
인덱스 목록 USER_INDEXES 조회 \di
DB 목록 (인스턴스 개념 다름) \l
스키마 목록 ALL_USERS 조회 \dn
사용자/롤 목록 DBA_USERS 조회 \du
권한 확인 DBA_TAB_PRIVS 조회 \dp (= \z)
함수 소스 보기 USER_SOURCE 조회 \sf 함수명
뷰 정의 보기 USER_VIEWS.TEXT \sv 뷰명
에디터로 편집 ed / EDIT \e
스크립트 실행 @script.sql \i script.sql
결과 파일 저장 SPOOL out.txt … SPOOL OFF \o out.txt … \o
실행 시간 표시 SET TIMING ON \timing on
NULL 표시 SET NULL '⌀' \pset null '⌀'
줄 길이 조정 SET LINESIZE 200 \pset format wrapped / \x auto
세로 출력 (없음, 수동 조합) \x on
접속 정보 확인 SHOW USER \conninfo
DB 전환 CONNECT user/pass@db \c dbname
변수 정의 DEFINE v_id = 42 \set v_id 42
변수 참조 &v_id :v_id
에러 시 중단 WHENEVER SQLERROR EXIT \set ON_ERROR_STOP on
셸 명령 실행 HOST ls \! ls
도움말 (거의 없음) \?, \h
설정 확인 SHOW PARAMETER work_area SHOW work_mem / \dconfig
실행계획 자동 표시 SET AUTOTRACE ON EXPLAIN (ANALYZE, BUFFERS)
대량 로딩 SQL*Loader \copy / COPY
종료 EXIT / QUIT \q
초기화 스크립트 login.sql / glogin.sql ~/.psqlrc
접속 정보 파일 tnsnames.ora ~/.pg_service.conf
비밀번호 저장 Oracle Wallet ~/.pgpass

대응이 아예 없는 것들도 있습니다. psql에는 SQL*Plus의 COLUMN ... FORMAT A20 같은 컬럼별 서식 지정이 없습니다. 대신 psql에만 있는 \gexec, \watch, \crosstabview 같은 강력한 기능들이 있습니다. 아래에서 하나씩 봅니다.

 

DESCRIBE보다 강력한 \d

Oracle의 DESC는 컬럼 목록만 보여줍니다. 인덱스를 보려면 USER_INDEXES를, 제약을 보려면 USER_CONSTRAINTS를 따로 조회해야 합니다.

psql의 \d는 한 번에 다 보여줍니다.

app_db=# \d employees
                    Table "public.employees"
   Column   |           Type           | Nullable |      Default
------------+--------------------------+----------+-------------------
 emp_id     | integer                  | not null | nextval('...')
 emp_name   | text                     | not null |
 dept_id    | integer                  |          |
 salary     | numeric(10,2)            |          |
Indexes:
    "employees_pkey" PRIMARY KEY, btree (emp_id)
    "employees_dept_idx" btree (dept_id)
Foreign-key constraints:
    "fk_dept" FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
Triggers:
    trg_audit AFTER UPDATE ON employees ...

컬럼 + 인덱스 + 제약 + 외래키 + 트리거가 전부 한 화면입니다. Oracle에서 네 번 조회할 것을 한 번에 끝냅니다.

+를 붙이면 더 나옵니다.

\d+ employees     -- 위 내용 + 테이블 크기, 스토리지 설정, 파티션 정보
\dt+              -- 테이블 목록 + 각 테이블 크기

패턴 매칭 - 이름을 정확히 몰라도 된다

\dt public.*          -- public 스키마 전체
\dt *emp*             -- 이름에 emp가 들어간 테이블
\df *json*            -- json 관련 함수 전부
\dtS pg_catalog.*     -- 시스템 테이블 (대문자 S = System 포함)

Oracle에서 SELECT ... FROM user_tables WHERE table_name LIKE '%EMP%'를 치던 것이 \dt *emp* 한 줄입니다.

도움말 - Oracle DBA가 가장 놀라는 기능

\?                    -- psql 메타 명령어 전체 목록
\h                    -- SQL 명령어 전체 목록
\h ALTER TABLE        -- ALTER TABLE의 정확한 문법
\h CREATE INDEX

\h가 보여주는 문법은 지금 접속한 서버 버전의 진짜 문법입니다. SQL*Plus에는 없던 기능이고, 이 사실은 2부 AI 검증에서 결정적으로 중요해집니다.

 

출력 제어 - SET LINESIZE의 PostgreSQL 버전

컬럼 30개짜리 테이블을 조회하면 터미널이 줄바꿈 지옥이 됩니다. Oracle에서 SET LINESIZE 300 PAGESIZE 50으로 버티던 상황입니다.

psql의 답은 다릅니다. 세로로 눕힙니다.

\x on        -- 한 행을 세로로 펼쳐서 출력
\x auto      -- 화면에 안 들어갈 때만 자동 전환 ← 사실상 정답
\x           -- 토글
app_db=# SELECT * FROM employees WHERE emp_id = 1 \gx
-[ RECORD 1 ]-------------------
emp_id   | 1
emp_name | 홍길동
dept_id  | 10
salary   | 3000.00

세미콜론 대신 \gx로 끝내면 그 쿼리만 세로로 봅니다.

★ \pset null - 반드시 설정할 것

\pset null '⌀'

PostgreSQL 기본 설정에서는 NULL과 ''(빈 문자열)이 화면에서 똑같이 공백으로 보입니다. Oracle의 SET NULL '(null)'과 같은 역할인데, Oracle DBA가 이걸 모르고 넘어가면 데이터 문제를 몇 시간 헤맵니다.

특히 Oracle은 ''를 NULL로 취급하지만 PostgreSQL은 둘을 명확히 구분합니다(2편 참고). 그래서 이 설정이 Oracle 출신에게 더 중요합니다.

\pset format aligned         -- aligned | unaligned | csv | wrapped
\pset border 2
\pset linestyle unicode
\pset pager off
\timing on

페이저는 less -SFX 조합이 편합니다(-S 가로 스크롤, -F 한 화면이면 그냥 출력, -X 화면 안 지움). 넓은 테이블을 자주 본다면 pspg를 권합니다. 헤더 고정과 셀 단위 이동이 됩니다.

export PAGER='less -SFXi'
# 또는
export PAGER='pspg -s 6'

 

★ login.sql 대신 .psqlrc

Oracle DBA는 login.sql에 SET LINESIZE, SET PAGESIZE, 자주 쓰는 별칭을 넣어두고 씁니다. psql의 대응물이 ~/.psqlrc입니다.

\set QUIET 1

-- ── 기본 UX (SQL*Plus의 SET 명령 대응) ──
\timing on
\x auto
\pset null '⌀'
\pset linestyle unicode
\pset border 2
\setenv PAGER 'less -SFXi'

-- ── 사고 방지 (WHENEVER SQLERROR 대응) ──
\set ON_ERROR_STOP on
\set VERBOSITY verbose
\set COMP_KEYWORD_CASE upper

-- ── DB별 히스토리 분리 + 중복 제거 ──
\set HISTFILE ~/.psql_history- :DBNAME
\set HISTCONTROL ignoredups
\set HISTSIZE 5000

-- ── 프롬프트: 유저@DB + 트랜잭션 상태(%x) ──
\set PROMPT1 '%n@%/%R%x%# '
\set PROMPT2 '%n@%/%R%x%# '

-- ── 자주 쓰는 것에 이름 붙이기 (DEFINE 대응) ──
\set e 'EXPLAIN (ANALYZE, BUFFERS, VERBOSE) '

-- V$SESSION 대응
\set activity 'SELECT pid, now() - query_start AS duration, state, left(query, 60) AS query FROM pg_stat_activity WHERE state <> \'idle\' ORDER BY duration DESC;'

-- V$LOCK 대응 (블로킹 체인)
\set locks 'SELECT blocked.pid AS blocked_pid, blocking.pid AS blocking_pid, left(blocked.query, 50) AS blocked_query FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));'

-- ★ 3편의 dead tuple 확인 (Oracle엔 대응물 없음)
\set dead 'SELECT relname, n_live_tup, n_dead_tup, round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_pct, last_autovacuum FROM pg_stat_user_tables WHERE n_dead_tup > 0 ORDER BY n_dead_tup DESC LIMIT 20;'

-- 안 쓰이는 인덱스
\set unused_idx 'SELECT schemaname, relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size FROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC;'

\unset QUIET

이제 psql 안에서 이렇게 씁니다.

:activity        -- 실행 중인 쿼리 (V$SESSION 느낌)
:locks           -- 락 대기 체인
:dead            -- ★ dead tuple 현황 (3편의 그 문제)
:unused_idx      -- 한 번도 안 쓰인 인덱스
:e SELECT * FROM employees WHERE dept_id = 10;

:dead 별칭을 만들어 두는 것을 특히 권합니다. 3편에서 본 것처럼 "UNDO 관리에서 VACUUM 관리로" 사고를 전환하려면, dead tuple을 수시로 들여다보는 습관이 필요합니다. 별칭 하나로 그 습관이 만들어집니다.

프롬프트의 %x - 3편과 이어지는 부분

app_db=#      ← 트랜잭션 밖
app_db=#*     ← 트랜잭션 안 (미커밋)
app_db=#!     ← 실패한 트랜잭션 (ROLLBACK 필요)

3편에서 PostgreSQL은 DDL도 롤백된다고 했습니다. 강력한 기능이지만, 그만큼 "커밋 안 한 상태"를 놓치기 쉽습니다. %x 하나로 화면에서 바로 보입니다.

스크립트에서는 .psqlrc가 오히려 방해가 됩니다. psql -X로 무시하세요. CI에서 이걸 빼먹으면 로컬에서는 되는데 파이프라인에서 깨지는 재미있는 일이 생깁니다.

 

접속 관리 - tnsnames.ora 대신

~/.pgpass (Oracle Wallet 대응)

형식은 호스트:포트:DB:유저:비밀번호이고 * 와일드카드를 씁니다.

cat >> ~/.pgpass <<'EOF'
db.example.com:5432:app_db:app_user:s3cret
localhost:5432:*:postgres:localpass
EOF
chmod 600 ~/.pgpass   # ★ 권한이 느슨하면 psql이 파일을 조용히 무시합니다

~/.pg_service.conf (tnsnames.ora 대응)

여러 환경을 오가는 DBA에게 진짜 물건입니다.

[prod]
host=db.example.com
port=5432
dbname=app_db
user=app_readonly

[dev]
host=dev-db.example.com
port=5432
dbname=app_db
user=app_user
psql service=prod
PGSERVICE=dev psql

tnsnames.ora에서 sqlplus user@PROD 하던 감각 그대로입니다.

PGPASSWORD=...를 명령줄 앞에 붙이는 방식은 셸 히스토리와 ps 출력에 그대로 노출됩니다. 로컬 실습이 아니면 쓰지 마세요.

 

변수와 동적 SQL - &변수 대신 :변수

SQL*Plus의 DEFINE/&에 대응합니다. 콜론 뒤 따옴표 종류가 핵심입니다.

\set uid 42
SELECT * FROM employees WHERE emp_id = :uid;         -- 그대로 치환 → 42

\set uname '홍길동'
SELECT * FROM employees WHERE emp_name = :'uname';   -- 문자열 리터럴 → '홍길동'

\set tbl employees
SELECT count(*) FROM :"tbl";                         -- 식별자 → "employees"

명령줄에서 주입할 수도 있습니다.

psql -X -v ON_ERROR_STOP=1 -v target_date="'2026-08-01'" -f report.sql

쿼리 결과를 변수로 받기:

SELECT count(*) AS total, max(emp_id) AS last_id FROM employees \gset
\echo 사원 수: :total / 마지막 ID: :last_id

★ \gexec - SQL*Plus에 없는 강력한 기능

Oracle DBA는 "SQL을 생성하는 SQL"을 짜서 SPOOL로 받아 다시 실행하는 패턴에 익숙할 겁니다. psql은 그걸 한 단계로 끝냅니다.

-- Oracle 방식:
--   SELECT 'ANALYZE ' || table_name || ';' FROM user_tables;
--   → SPOOL로 저장 → 파일 열어서 → @파일 실행

-- psql 방식: 한 번에
SELECT format('ANALYZE %I.%I;', schemaname, tablename)
FROM pg_tables WHERE schemaname = 'public'
\gexec
-- 3편 연계: dead tuple 많은 테이블만 골라 VACUUM
SELECT format('VACUUM ANALYZE %I.%I;', schemaname, relname)
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
\gexec

강력한 만큼 위험합니다. 습관을 이렇게 들이세요. 먼저 \gexec 없이 실행해서 생성된 SQL을 눈으로 읽고, 확인한 뒤에 \gexec를 붙입니다.

 

파일 입출력 - SPOOL과 SQL*Loader 대응

\i schema.sql          -- @script.sql 대응
\ir ./tables.sql       -- 현재 스크립트 기준 상대 경로
\o result.txt          -- SPOOL 시작
\o                     -- SPOOL OFF
\! ls -la              -- HOST 명령 대응
\e                     -- ed 대응: 직전 쿼리를 $EDITOR로 열어 편집 후 실행

\e는 저평가된 기능입니다. 긴 쿼리를 터미널에서 화살표 키로 고치는 대신 에디터로 열어 편집하고, 저장하고 닫으면 바로 실행됩니다.

★ COPY vs \copy - 반드시 구분할 것

SQL*Loader / UTL_FILE에 대응하는 기능인데, 백슬래시 하나 차이로 완전히 다릅니다.

실행 위치 필요 권한

COPY ... TO '/path' DB 서버의 파일 시스템 슈퍼유저 또는 pg_write_server_files
\copy ... TO '/path' 내 노트북의 파일 시스템 불필요

원격 DB를 다룬다면 십중팔구 \copy가 정답입니다. 이걸 몰라서 권한 에러로 30분 날리는 게 Oracle DBA의 흔한 첫 관문입니다.

\copy (SELECT emp_id, emp_name, salary FROM employees WHERE dept_id = 10) TO 'emp.csv' WITH (FORMAT csv, HEADER)

\copy staging_emp FROM 'emp.csv' WITH (FORMAT csv, HEADER)

 

스크립트 안전장치 - WHENEVER SQLERROR 대응

Oracle에서는 WHENEVER SQLERROR EXIT ROLLBACK을 스크립트 맨 위에 붙였습니다. psql에서는 이렇게 합니다.

psql -X -v ON_ERROR_STOP=1 --single-transaction -f migration.sql
  • ON_ERROR_STOP=1 — 없으면 psql은 에러가 나도 다음 문장을 계속 실행합니다. 마이그레이션이 반쯤 적용된 상태로 끝나는, 가장 피하고 싶은 결말입니다.
  • --single-transaction(= -1) — 전체를 하나의 트랜잭션으로 묶어 실패 시 통째로 롤백. 3편에서 본 DDL 롤백 가능 특성 덕분에 Oracle에서는 불가능했던 수준의 안전장치가 됩니다.

위험한 DML은 무조건 감싼다

BEGIN;
DELETE FROM employees WHERE dept_id = 99;
-- DELETE 12  ← 예상한 숫자인가?
ROLLBACK;   -- 아니면 되돌리고, 맞으면 COMMIT

psql은 기본이 autocommit입니다. WHERE를 빼먹은 DELETE 한 방이면 끝입니다. 프롬프트의 %x에 *가 떠 있는지 확인하는 습관을 들이세요.

운영 DB 접속 시 안전벨트 세 줄

SET statement_timeout = '30s';
SET lock_timeout = '5s';
SET idle_in_transaction_session_timeout = '60s';

세 번째가 특히 중요합니다. 3편에서 배운 대로, 열린 채 방치된 트랜잭션은 VACUUM이 dead tuple을 회수하지 못하게 막습니다. Oracle의 UNDO와 달리 PostgreSQL에서는 이게 곧 bloat으로 직결됩니다.

 

\watch - SQL*Plus에 없는 모니터링

직전 쿼리를 N초마다 재실행합니다. 별도 툴 없이 상황을 지켜볼 수 있습니다.

-- 세션 상태 실시간 감시
SELECT state, count(*) FROM pg_stat_activity GROUP BY state \watch 2

-- 인덱스 생성 진행률
SELECT phase, blocks_done, blocks_total,
       round(100.0 * blocks_done / NULLIF(blocks_total,0), 1) AS pct
FROM pg_stat_progress_create_index \watch 5

-- ★ VACUUM 진행률 (3편의 그 VACUUM)
SELECT relid::regclass, phase, heap_blks_scanned, heap_blks_total
FROM pg_stat_progress_vacuum \watch 5

대용량 테이블에 VACUUM을 걸어놓고 이 화면을 띄워두면, 3편에서 개념으로만 봤던 dead tuple 회수 과정이 눈앞에서 진행됩니다.

 

셸 스크립팅

Oracle의 sqlplus -s에 대응하는 조합입니다.

# 값 하나만 깔끔하게: -A(정렬 안 함) -t(헤더/푸터 제거)
COUNT=$(psql -X -A -t -c "SELECT count(*) FROM employees" app_db)

# CSV로 뽑기
psql -X --csv -c "SELECT emp_id, emp_name FROM employees" app_db > emp.csv

# 헬스체크
psql -X -q -c "SELECT 1" app_db >/dev/null && echo OK || echo DOWN

디버깅에는 -E가 좋습니다. \d 같은 메타 명령어가 내부적으로 던지는 카탈로그 쿼리를 보여줍니다. USER_TABLES에 해당하는 PostgreSQL 시스템 카탈로그를 배우는 가장 빠른 길이기도 합니다.

psql -E -c '\d employees' app_db

 

★ 2부. AI로 학습 가속

여기서부터가 이 글의 두 번째 축입니다. Oracle DBA에게 AI는 특히 유용합니다. 이미 아는 개념을 새 용어로 번역하는 작업이 학습의 대부분이기 때문입니다. "Oracle의 X는 PostgreSQL의 무엇인가"는 AI가 가장 잘하는 질문 유형입니다.

동시에, 그럴듯하게 틀린 답으로 며칠을 태우게 만들 수도 있습니다. 차이는 도구가 아니라 루프 설계에서 갈립니다.

AI가 잘하는 것 / 못하는 것

잘하는 것

  • Oracle ↔ PostgreSQL 개념 번역 — "UNDO를 아는 사람에게 MVCC 설명해줘"
  • 에러 메시지 해석 — ERROR: deadlock detected가 무슨 상황인지 3초 만에
  • 실행계획 읽어주기 — EXPLAIN 출력에서 뭘 봐야 하는지
  • 문법 초안 — 재귀 CTE, 윈도우 함수, LATERAL 조인의 뼈대
  • 학습 순서 설계 — "인덱스를 이해하려면 뭘 어떤 순서로"

구조적으로 못하는 것

  • 버전 특정 사실 — 학습 데이터에 여러 버전이 섞여 뭉개짐
  • 당신의 데이터 분포 — 카디널리티, 편향, 실제 통계를 모름
  • 당신의 설정 — work_mem, shared_buffers, autovacuum 파라미터를 모름
  • 없는 것을 만들어내지 않기 — 없는 함수, 없는 옵션, 다른 DB 문법을 자신 있게 제시

★ Oracle DBA가 특히 조심할 함정: 문법 혼입

AI는 종종 Oracle 문법을 PostgreSQL 답변에 그대로 섞어 냅니다. 질문자가 Oracle 맥락을 언급했다면 더 그렇습니다.

Oracle PostgreSQL 정답 AI의 흔한 오답

NVL(a, b) COALESCE(a, b) NVL 그대로 사용
DECODE(...) CASE WHEN ... DECODE 그대로
SYSDATE now(), CURRENT_DATE SYSDATE 그대로
TRUNC(SYSDATE) date_trunc('day', now()) TRUNC(now()) ← PostgreSQL의 trunc()는 숫자 전용
FROM DUAL FROM 절 생략 FROM dual 붙임
ROWNUM <= 10 LIMIT 10 ROWNUM 그대로
CONNECT BY WITH RECURSIVE CONNECT BY 그대로
MINUS EXCEPT MINUS 그대로
INSTR(...) strpos() / position() INSTR 그대로
/*+ INDEX(...) */ 힌트 없음 (pg_hint_plan 확장 별도) 힌트 제안
SET AUTOTRACE ON EXPLAIN (ANALYZE, BUFFERS) AUTOTRACE 제안

특히 마지막 두 개가 중요합니다. PostgreSQL에는 옵티마이저 힌트가 없습니다. 그런데 AI는 성능 질문에 종종 힌트를 제안합니다. Oracle 습관이 있으면 이게 이상해 보이지 않아서 그대로 받아들이기 쉽습니다.

참고: orafce 확장을 설치하면 NVL, DECODE, dual 등 일부는 실제로 동작합니다. 마이그레이션 과도기에는 유용하지만, 기본 PostgreSQL에는 없다는 사실을 먼저 알고 선택해야 합니다.

버전 함정

기능 실제 도입 버전 우리 환경(PG17)

MERGE 문 15+ ✅ 사용 가능
JSON_TABLE 등 SQL/JSON 17+ ✅ 사용 가능
pg_stat_io 뷰 16+ ✅ 사용 가능
\dconfig 15+ ✅ 사용 가능
pg_stat_statements.total_exec_time 13에서 total_time이 개명 ✅ 새 이름 사용
REINDEX CONCURRENTLY 12+ ✅ 사용 가능
DDL 논리 복제 미지원 ❌ AI가 된다고 답하기도 함

우리는 PG17이라 대부분 쓸 수 있지만, AI가 "이 기능은 PostgreSQL에 없습니다"라고 답하는 경우(학습 데이터가 낡은 경우)도 반대로 생깁니다.

★ 검증 루프: psql이 곧 채점기다

이게 이 글 2부의 핵심 문장입니다. AI에게 묻고, psql로 채점한다.

   ┌──────────────┐
   │  질문 / 가설  │
   └──────┬───────┘
          ↓
   ┌──────────────────┐
   │  AI에게 질문      │  ← 빠른 초안, Oracle 대응 개념 설명
   └──────┬───────────┘
          ↓
   ┌────────────────────────────────────┐
   │  psql에서 채점                      │
   │  SELECT version();                  │  ← 버전 확인
   │  \h 명령어  \df 함수명               │  ← 문법·존재 여부
   │  EXPLAIN (ANALYZE, BUFFERS)         │  ← 계획 주장 검증
   │  BEGIN; ... ROLLBACK;               │  ← 실제 동작 검증
   └──────┬─────────────────────────────┘
          ↓
   ┌──────────────┐
   │  공식 문서    │  ← 최종 판정, "왜"에 대한 답
   └──────────────┘

구체적인 채점 도구:

-- 이 서버 버전이 뭐지?
SELECT version();

-- AI가 말한 그 함수, 진짜 있나?
\df *nvl*             -- (기본 설치라면 결과 없음)
\df *coalesce*

-- AI가 말한 그 문법, 이 버전에서 맞나?
\h MERGE

-- AI가 말한 그 확장, 설치되어 있나?
\dx
SELECT * FROM pg_available_extensions WHERE name IN ('orafce', 'pg_hint_plan');

-- AI가 말한 그 설정, 현재 값이 뭔가?
SHOW work_mem;
\dconfig *vacuum*

-- AI가 "인덱스를 탈 겁니다"라고 했는데, 진짜?
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

\h와 \df는 지금 이 서버의 진실을 말합니다. AI 답변과 충돌하면 이긴 쪽은 항상 서버입니다.

프롬프트 패턴 - Oracle DBA 버전

나쁜 질문

이 쿼리 느린데 어떻게 최적화해?

좋은 질문 — 컨텍스트 4종 세트

PostgreSQL 17입니다. Oracle 19c에서 마이그레이션 중이고,
저는 Oracle DBA 배경입니다.

[1. 스키마 — \d employees 출력 붙여넣기]
[2. 데이터 규모 — 8천만 행, status는 5종류이고 'completed'가 92%]
[3. EXPLAIN (ANALYZE, BUFFERS) 전문 붙여넣기]
[4. 목표 — 현재 4.2초, 200ms 이하 필요]

다음을 지켜주세요:
- Oracle 문법(NVL, DECODE, ROWNUM, 힌트)은 쓰지 말 것
- PostgreSQL 17에서 실제로 동작하는 것만 제시할 것
- 접근을 3가지 제시하고 각각의 트레이드오프(쓰기 비용, 인덱스 크기,
  CONCURRENTLY 필요 여부)를 비교할 것
- Oracle의 어떤 개념에 대응하는지 함께 설명할 것

"Oracle 문법은 쓰지 말 것"을 명시하는 한 줄이 앞의 문법 혼입 함정을 상당 부분 막아줍니다.

그 외 잘 먹히는 요청

  • "이 실행계획을 한 줄씩 해석하고, 가장 비싼 노드가 어디인지 짚어줘"
  • "이 답변에서 PostgreSQL 15 이후에만 되는 부분을 표시해줘"
  • "Oracle의 AWR 리포트에 해당하는 PostgreSQL 도구는? 차이점도 알려줘"
  • "내가 이 개념을 제대로 이해했는지 확인할 psql 실험을 설계해줘" ← 최고

★ 실험 설계를 시키는 학습법

마지막 요청이 특히 좋습니다. 답 대신 실험 설계를 받으면, 검증이 자동으로 따라옵니다.

3편에서 배운 MVCC로 예를 들어봅니다.

PostgreSQL의 MVCC에서 UPDATE가 dead tuple을 만드는 과정을 직접 눈으로 확인하고 싶습니다. psql에서 재현할 실험을 설계해주세요.

받은 실험을 psql에서 직접 돌립니다.

BEGIN;

CREATE TABLE mvcc_test AS
SELECT g AS id, 'data' AS val FROM generate_series(1, 100000) g;

-- 물리적 위치(ctid)를 보면 MVCC가 눈에 보인다
SELECT ctid, id, val FROM mvcc_test WHERE id = 1;
--  (0,1) | 1 | data

UPDATE mvcc_test SET val = 'updated' WHERE id = 1;

SELECT ctid, id, val FROM mvcc_test WHERE id = 1;
--  (0,101) ← ★ 위치가 바뀌었다! 제자리 갱신이 아니라 새 버전 생성
--  Oracle이었다면 같은 블록에서 값만 바뀌었을 것

ROLLBACK;

ctid(물리적 행 위치)를 UPDATE 전후로 비교하는 이 실험 하나가, 3편에서 글로 읽은 **"덮어쓰기 vs 새 버전 추가"**를 손으로 확인시켜 줍니다.

그리고 이 루프의 진짜 장점은 이것입니다. AI가 실험 설계를 틀리게 해도 psql이 즉시 알려줍니다. 잘못된 정보가 몸에 배기 전에 걸러집니다.

하면 안 되는 것

  • 운영 데이터를 그대로 붙여넣지 않기. 스키마 예시에 실제 사원명·주민번호·결제정보가 딸려 들어가기 쉽습니다. 컬럼 구조만 남기고 값은 지웁니다.
  • 연결 문자열·비밀번호를 붙여넣지 않기. .pgpass 내용이나 로그 스니펫에 섞이는 경우가 많습니다.
  • AI가 준 DDL을 검토 없이 운영에 실행하지 않기. 특히 DROP, ALTER TABLE ... TYPE(테이블 전체 재작성 유발), CREATE INDEX(CONCURRENTLY 없으면 쓰기 락).
  • 검증 안 한 내용을 팀 문서에 복사하지 않기. 확인한 것만 적고, 나머지는 "확인 필요"라고 적습니다.

균형 잡기 - 3분 규칙

AI를 쓰면서 실력이 안 느는 전형적 패턴이 있습니다. 에러 나면 즉시 붙여넣고, 답 복사해서 붙이고, 돌아가면 넘어갑니다. 빠르지만 아무것도 안 남습니다.

간단한 규칙을 권합니다. 에러를 만나면 먼저 3분 혼자 봅니다. 메시지를 끝까지 읽고, \d로 스키마를 확인하고, \h로 문법을 확인합니다. PostgreSQL의 에러 메시지는 Oracle의 ORA-번호보다 훨씬 친절합니다. HINT: 줄에 답이 그대로 있는 경우가 정말 많습니다.

ERROR:  column "emp_nam" does not exist
LINE 1: SELECT emp_nam FROM employees;
               ^
HINT:  Perhaps you meant to reference the column "employees.emp_name".

3분 안에 안 풀리면 그때 AI로 갑니다. 답을 받으면 왜 그런지를 한 번 더 묻습니다. 동작하는 코드보다 그 설명이 남습니다.

 

실습 과제

  1. ~/.psqlrc를 만들고 위 설정 적용 후, 자기 워크플로에 맞는 \set 별칭 3개 추가하기
  2. ~/.pg_service.conf로 dev/prod 프로필 구성하기 (tnsnames.ora 옮기는 감각으로)
  3. \gexec로 스키마 전체 테이블 크기 리포트를 만들고 \copy로 CSV 저장하기
  4. 위 ctid 실험을 직접 돌린 뒤, VACUUM 실행 전후로 :dead 별칭 결과가 어떻게 바뀌는지 확인하기
  5. AI에게 "Oracle의 AWR에 대응하는 PostgreSQL 방법"을 물어본 뒤, psql로 검증하고 틀린 부분을 기록하기

5번이 진짜 과제입니다. AI가 어디서 어떻게 틀리는지 한 번 직접 잡아보면, 그 감각이 남습니다.

 

다음 글 예고

여기까지가 **Phase 1(입문)**입니다. 다음 글부터 **Phase 2(활용)**로 넘어갑니다.

[PostgreSQL 시리즈 5편] 인덱스와 실행계획 - Oracle과 무엇이 다른가에서는 B-tree 구조부터 PostgreSQL 고유의 GIN/GiST/BRIN, 그리고 SET AUTOTRACE 없이 EXPLAIN으로 계획을 읽는 법을 다룹니다. 이번 편에서 만든 psql 환경이 그대로 실험실이 됩니다.

 

마무리

Phase 1의 마지막 편을 정리하면:

  1. \d는 DESC의 상위 호환 — 컬럼·인덱스·제약·트리거를 한 번에
  2. .psqlrc는 login.sql의 대응물 — 한 번 세팅하면 평생 쓴다
  3. \pset null 필수 — PostgreSQL은 NULL과 빈 문자열을 구분한다
  4. ON_ERROR_STOP + --single-transaction — DDL 롤백이 되는 PostgreSQL만의 안전장치
  5. \gexec, \watch — SQL*Plus에 없던 무기
  6. AI는 묻고, psql로 채점한다 — \h, \df, EXPLAIN이 판정관
  7. Oracle 문법 혼입 주의 — AI가 NVL, ROWNUM, 힌트를 섞어 낸다

Phase 1 전체를 돌아보면, 1편에서 아키텍처 차이를, 2편에서 자료형과 DDL을, 3편에서 MVCC라는 가장 큰 산을, 그리고 이번 편에서 손에 쥘 도구를 익혔습니다. 이제 실제로 만지면서 배울 준비가 끝났습니다.

여러분의 .psqlrc에는 어떤 별칭이 들어 있나요? 또는 AI가 Oracle 문법을 섞어서 낸 황당한 답변이 있었다면 댓글로 공유해주세요. 같은 전환을 겪는 Oracle DBA들에게 큰 도움이 됩니다.

 

반응형