🗄️

회사 업무화면의 비밀
SQL JOIN이 모든 걸 만든다

사고번호 하나 입력했을 뿐인데 계약정보·피보험자·지급이력이 한 화면에 쏟아진다
그 뒤에서 무슨 일이 벌어지고 있는가

SQL JOIN ER 모델 업무시스템 구조 DB 역추적 실전 쿼리

화면은 그냥 껍데기다
진짜 정보는 DB에 있다

회사 업무 시스템에 처음 들어가면 이런 생각을 한다. "이 화면에 정보가 저장되어 있겠구나." 틀렸다. 화면은 단지 View(뷰)일 뿐이다. 정보는 전혀 다른 곳, 데이터베이스(DB) 안에 테이블로 쌓여 있다.

보험사 직원이 사고 접수 화면을 열고 사고번호를 입력한 뒤 조회 버튼을 누른다. 그 순간 어떤 일이 일어날까?

 보험금 청구 처리 시스템 v3.2
▶ 계약 정보
증권번호상품명계약일보험료상태
POL-20210831-002무배당 실손의료비보험2021-08-3172,300원유효
▶ 피보험자 정보
성명생년월일관계연락처
김수현1988-04-12피보험자 본인010-****-3847
▶ 기존 보험금 지급 이력
지급일지급사유지급금액담당자
2023-02-14입원의료비 (맹장수술)1,240,000원이담당
2024-01-07통원의료비48,500원박사원

직원 눈에는 하나의 화면이지만, 그 뒤에서는 최소 3~5개 테이블을 동시에 끌어오는 SQL이 돌아간다. 계약 테이블, 피보험자 테이블, 지급 이력 테이블... 각각 따로 존재하는 데이터를 사고번호 하나로 엮어서 한 화면에 뿌려주는 것이다.

핵심 개념: 업무화면 = 여러 DB 테이블 → SQL JOIN → 화면 렌더링
화면이 바뀐다고 데이터 구조가 바뀌는 게 아니다. 구조는 DB에 고정되어 있고, 화면은 그걸 어떻게 보여줄지 결정할 뿐이다.

DB 안에서 테이블들은
어떻게 연결되어 있나

아까 그 한 화면을 만들기 위해 DB 안에는 이런 테이블들이 따로 존재한다. 각 테이블은 자기 담당 데이터만 가지고 있다.

📋 ACCIDENT (사고 테이블)
PK ACC_NO (사고번호)
FK POL_NO (증권번호)
ACC_DATE (사고일자)
ACC_TYPE (사고유형)
STATUS (처리상태)
📄 POLICY (계약 테이블)
PK POL_NO (증권번호)
FK CUST_ID (고객ID)
PROD_CODE (상품코드)
CONT_DATE (계약일)
PREMIUM (보험료)
POL_STATUS (계약상태)
👤 CUSTOMER (고객 테이블)
PK CUST_ID (고객ID)
CUST_NM (성명)
BIRTH_DT (생년월일)
PHONE (연락처)
REL_TYPE (관계코드)
💰 CLAIM_PAY (지급 이력 테이블)
PK PAY_SEQ (지급일련번호)
FK ACC_NO (사고번호)
PAY_DT (지급일)
PAY_RSN (지급사유)
PAY_AMT (지급금액)
HANDLER_ID (담당자ID)

PK (Primary Key)는 그 테이블에서 행을 구분하는 고유 식별자다. 주민번호처럼 절대 중복되지 않는다. FK (Foreign Key)는 다른 테이블의 PK를 참조하는 연결고리다. 이 FK들이 바로 JOIN의 재료가 된다.

왜 이렇게 쪼개나?
고객 정보를 계약 테이블에 다 때려 넣으면 어떻게 될까? 한 고객이 보험을 10개 들면 이름, 전화번호, 생년월일이 10번 중복 저장된다. 전화번호가 바뀌면 10개 행을 전부 수정해야 한다. 테이블을 쪼개서 FK로 연결하면 고객 정보는 딱 1행만 수정하면 끝난다. 이걸 정규화(Normalization)라고 한다.

SQL JOIN — 쪼개진 테이블을
하나로 합치는 마법

쪼개진 테이블을 화면에 하나로 보여주려면 다시 합쳐야 한다. 이때 쓰는 게 JOIN이다. 공통 키(PK ↔ FK)를 기준으로 여러 테이블의 행을 옆으로 붙이는 연산이다.

아까 그 업무화면을 만드는 SQL은 실제로 이런 모양이다:

SQL
-- 사고번호 하나로 화면 전체 데이터를 끌어오는 쿼리
SELECT
    -- 계약 정보
    P.POL_NO,
    P.PROD_CODE,
    P.CONT_DATE,
    P.PREMIUM,
    P.POL_STATUS,
    -- 피보험자 정보
    C.CUST_NM,
    C.BIRTH_DT,
    C.PHONE,
    -- 사고 정보
    A.ACC_DATE,
    A.ACC_TYPE

FROM   ACCIDENT  A
JOIN   POLICY    P  ON A.POL_NO  = P.POL_NO
JOIN   CUSTOMER  C  ON P.CUST_ID = C.CUST_ID

WHERE  A.ACC_NO = 'ACC-2024-003847'

지급 이력은 한 사고에 여러 건이 붙기 때문에 별도로 조회한다:

SQL
-- 지급 이력 목록 조회
SELECT
    CP.PAY_DT,
    CP.PAY_RSN,
    CP.PAY_AMT,
    E.EMP_NM   AS HANDLER_NM

FROM   CLAIM_PAY  CP
JOIN   EMPLOYEE   E   ON CP.HANDLER_ID = E.EMP_ID

WHERE  CP.ACC_NO = 'ACC-2024-003847'
ORDER BY CP.PAY_DT DESC
JOIN의 종류도 중요하다
INNER JOIN — 양쪽 모두 일치하는 행만. 지급 이력 없으면 안 나옴
LEFT JOIN — 왼쪽 테이블은 전부, 오른쪽은 일치하면 붙이고 없으면 NULL. 지급 이력 없어도 계약은 나옴
실무에서 LEFT JOIN을 더 자주 쓰는 이유가 여기에 있다. 데이터가 없어도 화면에 행이 나와야 하니까.

ER 모델 — DB 설계도를
읽는 법

ER 모델(Entity-Relationship Model)은 테이블들이 어떻게 연결되어 있는지 그림으로 보여주는 설계도다. 건물의 도면처럼, DB를 처음 설계할 때 또는 기존 구조를 파악할 때 쓴다.

CUSTOMER
🔑 CUST_ID
CUST_NM
BIRTH_DT
PHONE
POLICY
🔑 POL_NO
🔗 CUST_ID
PROD_CODE
PREMIUM
ACCIDENT
🔑 ACC_NO
🔗 POL_NO
ACC_DATE
STATUS
CLAIM_PAY
🔑 PAY_SEQ
🔗 ACC_NO
PAY_AMT
PAY_DT
CUSTOMER 1
N POLICY    POLICY 1
N ACCIDENT    ACCIDENT 1
N CLAIM_PAY

ER 다이어그램에서 가장 중요한 건 1:N 관계다. 고객 1명이 계약 N개를 가질 수 있다. 계약 1개에 사고 N건이 생길 수 있다. 사고 1건에 지급이 N번 이루어질 수 있다. 이 관계가 FK가 어느 테이블에 들어가는지를 결정한다.

실전 팁: ER 다이어그램이 없을 때는?
현실에서는 ER 다이어그램이 없거나 최신화가 안 된 경우가 많다. 그럴 때는 직접 테이블 구조를 뜯어보는 수밖에 없다. 다음 챕터가 바로 그 방법이다.

화면에서 테이블을 역추적하는
실전 기술

ER 모델도 없고, 문서도 없다. 그래도 원하는 데이터를 뽑아야 한다. 그럴 때 쓰는 게 화면 → SQL 역추적이다. 업무 화면에 이미 뿌려지고 있는 SQL을 찾아서, 거기서 테이블 구조를 파악하는 방법이다.

1

기존 SQL 파일을 찾아라

회사 시스템은 어딘가에 SQL을 저장해 둔다. 매퍼 파일(MyBatis의 .xml), 프로시저(Stored Procedure), DAO 클래스, 또는 쿼리 관리 파일. 화면 이름으로 검색하면 대부분 찾을 수 있다.

2

FROM 절과 JOIN 절에서 테이블 목록 뽑기

FROM 뒤, JOIN 뒤에 나오는 단어들이 모두 테이블명이다. 별칭(Alias)이 붙어 있으면 AS 앞의 단어가 진짜 테이블명이다. 이것만 모아도 해당 화면의 데이터 구조가 절반은 보인다.

3

테이블 하나씩 SELECT *로 찍어보기

SELECT * FROM 테이블명 WHERE ROWNUM <= 10 으로 10행만 뽑아보자. 컬럼명과 데이터 형태를 보면 이 테이블이 무엇을 담당하는지 바로 보인다. 날짜 컬럼이 많으면 이력 테이블, 코드 컬럼이 많으면 코드 테이블이다.

4

ON 절에서 관계(FK 연결)를 파악하기

JOIN A ON B.COL = A.COL 패턴에서 등호(=) 양쪽을 보면 어떤 컬럼이 두 테이블을 연결하는지 나온다. 이게 FK다. 이 관계들을 메모해두면 나만의 미니 ER 다이어그램이 완성된다.

5

WHERE 절로 원하는 조건을 넣어 실험하기

구조를 파악했으면 WHERE 절에 조건을 하나씩 바꿔가며 원하는 데이터가 나오는지 확인한다. 화면에서 보이는 특정 값을 넣어보면 어떤 컬럼에 그 값이 들어 있는지 바로 확인된다.

화면 필드 → 테이블 컬럼 매핑 예시:
📺 화면: 계약일
POLICY.CONT_DATE
📺 화면: 피보험자 성명
CUSTOMER.CUST_NM
📺 화면: 지급금액
CLAIM_PAY.PAY_AMT
📺 화면: 담당자
EMPLOYEE.EMP_NM

실전: 이걸 알면 뭘 할 수 있나

화면에는 보이지 않지만 DB에서는 뽑을 수 있는 정보들이 있다. 업무화면은 담당자 한 명의 데이터를 보여주지만, SQL을 직접 쓰면 전체 집계, 기간별 추이, 이상 패턴 등을 한 번에 뽑을 수 있다.

실전 예시 SQL
-- 2024년 지급금액이 500만원 이상인 사고 현황
SELECT
    A.ACC_NO,
    C.CUST_NM,
    P.PROD_CODE,
    SUM(CP.PAY_AMT)  AS TOTAL_AMT,
    COUNT(CP.PAY_SEQ) AS PAY_CNT

FROM       ACCIDENT   A
JOIN       POLICY     P   ON A.POL_NO   = P.POL_NO
JOIN       CUSTOMER   C   ON P.CUST_ID  = C.CUST_ID
LEFT JOIN CLAIM_PAY  CP  ON A.ACC_NO   = CP.ACC_NO

WHERE  CP.PAY_DT BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY
    A.ACC_NO, C.CUST_NM, P.PROD_CODE
HAVING
    SUM(CP.PAY_AMT) >= 5000000
ORDER BY
    TOTAL_AMT DESC

업무화면에서는 사고번호 하나를 직접 입력해야 정보가 나온다. 하지만 SQL을 알면 "2024년에 500만 원 넘게 지급된 사고를 전부 뽑아라" 같은 분석 쿼리를 직접 만들 수 있다. 화면이 허용하는 것 이상을 볼 수 있는 것이다.

현업에서 이게 진짜 중요한 순간:
• 기획자가 "이런 데이터 통계 좀 뽑아줘" 할 때
• 화면 개발 전에 데이터가 실제로 있는지 확인할 때
• 기존 화면 SQL을 재활용해서 새 기능 빠르게 개발할 때
• 장애 발생 시 원인 데이터를 직접 추적할 때

화면은 비즈니스의 얼굴이고, SQL은 비즈니스의 언어다.
화면만 보던 사람이 SQL을 읽기 시작하는 순간,
시스템 전체가 투명하게 보이기 시작한다.

서브쿼리 — 쿼리 안의 쿼리
조건이 복잡해질 때 쓰는 기술

서브쿼리는 SQL 안에 또 다른 SQL을 넣는 것이다. 한 번의 쿼리로 표현하기 어려운 복잡한 조건을 단계적으로 풀어낼 수 있다.

① WHERE 절 서브쿼리 — "평균보다 많이 낸 사람만"

서브쿼리 예시
-- 평균 보험료보다 높은 계약자 목록
SELECT C.CUST_NM, P.PREMIUM
FROM   POLICY P
JOIN   CUSTOMER C ON P.CUST_ID = C.CUST_ID
WHERE  P.PREMIUM > (
    SELECT AVG(PREMIUM)
    FROM   POLICY
    WHERE  POL_STATUS = '유효'
)

② IN 서브쿼리 — "이 목록에 해당하는 것만"

-- 2024년에 한 번이라도 사고가 난 계약의 계약자 조회
SELECT CUST_NM, PHONE
FROM   CUSTOMER
WHERE  CUST_ID IN (
    SELECT DISTINCT P.CUST_ID
    FROM   POLICY P
    JOIN   ACCIDENT A ON P.POL_NO = A.POL_NO
    WHERE  A.ACC_DATE BETWEEN '2024-01-01' AND '2024-12-31'
)

③ FROM 절 서브쿼리(인라인 뷰) — 임시 테이블처럼 쓰기

-- 고객별 총 지급액을 먼저 구한 뒤, 상위 10명만 뽑기
SELECT *
FROM (
    SELECT
        C.CUST_NM,
        SUM(CP.PAY_AMT) AS TOTAL_AMT
    FROM      CUSTOMER  C
    JOIN      POLICY    P  ON C.CUST_ID = P.CUST_ID
    JOIN      ACCIDENT  A  ON P.POL_NO  = A.POL_NO
    JOIN      CLAIM_PAY CP ON A.ACC_NO  = CP.ACC_NO
    GROUP BY C.CUST_NM
) SUMMARY
WHERE  TOTAL_AMT >= 10000000
ORDER BY TOTAL_AMT DESC
FETCH FIRST 10 ROWS ONLY  -- Oracle / LIMIT 10 (MySQL)
WITH 절(CTE) — 서브쿼리를 이름 붙여서 재사용
인라인 뷰가 길어지면 가독성이 떨어진다. WITH 절로 이름을 붙이면 여러 번 참조할 수 있고 읽기도 쉬워진다.
WITH CUST_SUMMARY AS (
    SELECT
        C.CUST_ID,
        C.CUST_NM,
        SUM(CP.PAY_AMT) AS TOTAL_AMT,
        COUNT(A.ACC_NO)  AS ACC_CNT
    FROM CUSTOMER C
    JOIN POLICY   P  ON C.CUST_ID = P.CUST_ID
    JOIN ACCIDENT A  ON P.POL_NO  = A.POL_NO
    JOIN CLAIM_PAYCP  ON A.ACC_NO  = CP.ACC_NO
    GROUP BY C.CUST_ID, C.CUST_NM
)
-- CTE를 두 번 참조
SELECT CUST_NM, TOTAL_AMT, ACC_CNT,
       TOTAL_AMT / ACC_CNT AS AVG_PER_ACC
FROM   CUST_SUMMARY
WHERE  ACC_CNT >= 2
ORDER BY TOTAL_AMT DESC

CASE WHEN — SQL 안의 IF문
데이터를 분류하고 레이블 붙이기

데이터를 뽑을 때 단순 조회에서 끝나지 않고, 조건에 따라 값을 다르게 표시하거나 분류해야 할 때가 많다. CASE WHEN이 그 역할을 한다.

CASE WHEN 기본형
-- 지급금액에 따라 등급 부여
SELECT
    A.ACC_NO,
    SUM(CP.PAY_AMT) AS TOTAL_PAY,
    CASE
        WHEN SUM(CP.PAY_AMT) >= 10000000 THEN '대형사고'
        WHEN SUM(CP.PAY_AMT) >=  3000000 THEN '중형사고'
        WHEN SUM(CP.PAY_AMT) >         0 THEN '소형사고'
        ELSE '지급없음'
    END AS ACC_GRADE
FROM      ACCIDENT  A
LEFT JOIN CLAIM_PAY CP ON A.ACC_NO = CP.ACC_NO
GROUP BY A.ACC_NO
CASE WHEN 피벗 활용 — 행을 열로 바꾸기
-- 월별 사고건수를 가로로 펼치기 (피벗)
SELECT
    TO_CHAR(ACC_DATE, 'YYYY') AS YEAR,
    SUM(CASE WHEN TO_CHAR(ACC_DATE,'MM')='01' THEN 1 ELSE 0 END) AS JAN,
    SUM(CASE WHEN TO_CHAR(ACC_DATE,'MM')='02' THEN 1 ELSE 0 END) AS FEB,
    SUM(CASE WHEN TO_CHAR(ACC_DATE,'MM')='03' THEN 1 ELSE 0 END) AS MAR,
    -- ... 12월까지 반복
    COUNT(*) AS TOTAL
FROM   ACCIDENT
GROUP BY TO_CHAR(ACC_DATE, 'YYYY')
ORDER BY YEAR
CASE WHEN을 ORDER BY에도 쓸 수 있다
ORDER BY CASE WHEN STATUS='처리중' THEN 1 WHEN STATUS='완료' THEN 2 ELSE 3 END
코드값이 아닌 의미 순서대로 정렬할 때 유용하다.

윈도우 함수 — 집계하면서도
개별 행을 유지하는 마법

GROUP BY로 집계하면 행이 줄어든다. 하지만 윈도우 함수는 집계 결과를 원래 행에 붙인 채로 유지한다. 순위, 누적합, 전월 대비 등 분석에서 가장 많이 쓰이는 기법이다.

윈도우 함수 기본 문법:
함수명() OVER (PARTITION BY 그룹컬럼 ORDER BY 정렬컬럼)
PARTITION BY = GROUP BY처럼 그룹 나누기 (없으면 전체가 하나의 그룹)
ORDER BY = 그룹 안에서 순서 기준
ROW_NUMBER / RANK — 순위 매기기
-- 고객별로 사고 순서 번호 붙이기 (가장 최근 사고가 1번)
SELECT
    C.CUST_NM,
    A.ACC_NO,
    A.ACC_DATE,
    ROW_NUMBER() OVER (
        PARTITION BY P.CUST_ID
        ORDER BY     A.ACC_DATE DESC
    ) AS ACC_RANK,
    COUNT(*) OVER (
        PARTITION BY P.CUST_ID
    ) AS TOTAL_ACC_CNT   -- 이 고객의 총 사고 건수
FROM  ACCIDENT A
JOIN  POLICY   P ON A.POL_NO  = P.POL_NO
JOIN  CUSTOMER C ON P.CUST_ID = C.CUST_ID
LAG / LEAD — 전월 대비 비교
-- 월별 지급액과 전월 대비 증감
WITH MONTHLY AS (
    SELECT
        TO_CHAR(PAY_DT, 'YYYY-MM') AS MON,
        SUM(PAY_AMT)              AS MON_AMT
    FROM   CLAIM_PAY
    GROUP BY TO_CHAR(PAY_DT, 'YYYY-MM')
)
SELECT
    MON,
    MON_AMT,
    LAG(MON_AMT) OVER (ORDER BY MON) AS PREV_MON_AMT,
    MON_AMT - LAG(MON_AMT) OVER (ORDER BY MON) AS DIFF
FROM   MONTHLY
ORDER BY MON
SUM OVER — 누적합
-- 사고별 지급액의 누적합 (시간 순서대로)
SELECT
    PAY_DT,
    PAY_AMT,
    SUM(PAY_AMT) OVER (
        PARTITION BY ACC_NO
        ORDER BY     PAY_DT
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS CUMUL_AMT    -- 이 사고의 누적 지급액
FROM CLAIM_PAY
WHERE ACC_NO = 'ACC-2024-003847'
ROW_NUMBER vs RANK vs DENSE_RANK
동점 처리 방식이 다르다. 점수가 90, 90, 80인 경우:
ROW_NUMBER: 1, 2, 3 (무조건 다른 번호)
RANK: 1, 1, 3 (동점이면 같은 순위, 다음은 건너뜀)
DENSE_RANK: 1, 1, 2 (동점이면 같은 순위, 다음은 안 건너뜀)

아무도 알려주지 않는 기술
DB 스키마를 SQL로 스스로 파악하기

입사하면 문서도 없고 아는 사람도 없는데 DB는 눈앞에 있다. 이때 쓰는 게 데이터 사전(Data Dictionary) 쿼리다. DB가 스스로 자신의 구조를 알고 있는 테이블들이 있다. 그걸 조회하면 된다.

Oracle — 내가 볼 수 있는 테이블 목록
-- 접근 가능한 테이블 전체 목록
SELECT TABLE_NAME, OWNER, NUM_ROWS
FROM   ALL_TABLES
WHERE  TABLE_NAME LIKE '%CLAIM%'  -- 이름에 CLAIM 포함
ORDER BY TABLE_NAME

-- 특정 테이블의 컬럼 구조 조회
SELECT
    COLUMN_NAME,
    DATA_TYPE,
    DATA_LENGTH,
    NULLABLE,
    COMMENTS   -- 컬럼 설명이 있으면 여기 나옴
FROM   ALL_TAB_COLUMNS  C
LEFT JOIN ALL_COL_COMMENTS CM
    ON C.TABLE_NAME  = CM.TABLE_NAME
    AND C.COLUMN_NAME = CM.COLUMN_NAME
WHERE  C.TABLE_NAME = 'CLAIM_PAY'
ORDER BY COLUMN_ID
MySQL / MariaDB
-- 테이블 목록
SHOW TABLES LIKE '%claim%';

-- 컬럼 구조
DESCRIBE claim_pay;

-- 또는 더 자세하게
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT
FROM   INFORMATION_SCHEMA.COLUMNS
WHERE  TABLE_NAME = 'claim_pay'
ORDER BY ORDINAL_POSITION
FK 관계 파악 — 어떤 테이블이 어떻게 연결되나
-- Oracle: 특정 테이블에 걸린 FK 찾기
SELECT
    A.TABLE_NAME     AS "FK가 있는 테이블",
    A.COLUMN_NAME    AS "FK 컬럼",
    C.TABLE_NAME     AS "참조하는 테이블",
    C.COLUMN_NAME    AS "참조 컬럼(PK)"
FROM   ALL_CONS_COLUMNS   A
JOIN   ALL_CONSTRAINTS    B  ON A.CONSTRAINT_NAME = B.CONSTRAINT_NAME
JOIN   ALL_CONS_COLUMNS   C  ON B.R_CONSTRAINT_NAME = C.CONSTRAINT_NAME
WHERE  B.CONSTRAINT_TYPE = 'R'   -- R = Foreign Key
  AND  A.TABLE_NAME LIKE '%CLAIM%'
💡

실무 탐색 루틴

① ALL_TABLES에서 키워드로 관련 테이블 찾기 → ② ALL_TAB_COLUMNS로 컬럼 구조 보기 → ③ SELECT * … ROWNUM <= 5로 실제 데이터 확인 → ④ FK 쿼리로 연결 관계 파악 → ⑤ 기존 SQL 파일에서 FROM/JOIN 구조 확인. 이 순서로 돌리면 문서 없이도 DB 구조를 3시간 안에 파악할 수 있다.

느린 쿼리의 원인과
인덱스·실행계획 보는 법

쿼리가 돌긴 도는데 10초씩 걸린다면? 데이터가 많아서가 아닐 수 있다. 대부분 인덱스가 없거나 타지 않는 것이 원인이다.

인덱스란?
책 뒤의 색인처럼, 특정 컬럼 값으로 빠르게 행을 찾을 수 있게 미리 정렬해둔 구조. WHERE ACC_NO = '...'처럼 자주 쓰는 조건 컬럼에 인덱스가 있으면 수백만 행도 순식간에 찾는다.
실행계획 보기
-- Oracle: 실행계획 확인 (쿼리 앞에 붙임)
EXPLAIN PLAN FOR
SELECT * FROM CLAIM_PAY WHERE ACC_NO = 'ACC-2024-003847';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

-- MySQL: 실행계획
EXPLAIN
SELECT * FROM claim_pay WHERE acc_no = 'ACC-2024-003847';

실행계획에서 FULL TABLE SCAN 또는 type: ALL이 보이면 인덱스를 안 타고 있다는 뜻이다. INDEX RANGE SCAN 또는 type: ref가 보이면 인덱스를 잘 타고 있는 것이다.

인덱스가 안 타는 대표적인 패턴
WHERE TO_CHAR(ACC_DATE, 'YYYY') = '2024' — 컬럼에 함수를 씌우면 인덱스 무력화
WHERE ACC_DATE BETWEEN '2024-01-01' AND '2024-12-31' 로 바꿔야 함

WHERE ACC_NO LIKE '%3847' — 앞에 %가 붙은 LIKE는 인덱스 무력화
WHERE ACC_NO LIKE 'ACC-2024%' 처럼 앞부분이 고정이어야 함

WHERE PAY_AMT + 1000 > 500000 — 컬럼 연산도 인덱스 무력화
WHERE PAY_AMT > 499000 으로 바꿔야 함