๐ŸŒ Backend

๊ตฌ๊ฐ„๋ณ„ ์ง‘๊ณ„ SQL์˜ DB ์ด์‹์„ฑ ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ณผ์ •

DevPoong 2025. 9. 16. 16:33

1. ๋ฌธ์ œ ์ƒํ™ฉ

๋งค์žฅ์—์„œ ๋งค์›” ๊ณ ๊ฐ์˜ ์›”๋งค์ถœ๊ธˆ์•ก์„ ๊ธฐ์ค€์œผ๋กœ, ํ•ด๋‹น ๊ณ ๊ฐ์ด ์–ด๋А ๊ตฌ๊ฐ„(Decile, 10๋“ฑ๋ถ„)์— ์†ํ•˜๋Š”์ง€๋ฅผ ์ง‘๊ณ„ ํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์„ ๊ฐ€์ •

์˜ˆ๋ฅผ ๋“ค์–ด, ์›” ์ตœ๋Œ€ ๋งค์ถœ๊ธˆ์•ก์ด 20,000์›์ด๋ผ๋ฉด ์ด๋ฅผ 10๊ฐœ ๊ตฌ๊ฐ„์œผ๋กœ ๋‚˜๋ˆ„๊ฒŒ ๋œ๋‹ค.

  • ๊ตฌ๊ฐ„ 1: 1 ~ 2,000์›
  • ๊ตฌ๊ฐ„ 2: 2,001 ~ 4,000์›
  • ๊ตฌ๊ฐ„ 3: 4,001 ~ 6,000์›
  • ๊ตฌ๊ฐ„ 10: 18,001 ~ 20,000์›

๊ฒฐ๊ตญ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๋Š” ๊ฐ„๋‹จํ•˜๋‹ค.

  • ๊ฐ ๊ตฌ๊ฐ„๋ณ„ ๊ณ ๊ฐ ์ˆ˜
  • ๊ฐ ๊ตฌ๊ฐ„๋ณ„ ๊ณ ๊ฐ ์ˆ˜๊ฐ€ ์ „์ฒด์—์„œ ์ฐจ์ง€ํ•˜๋Š” ๋น„์œจ(%)

 

2. ์ดˆ๊ธฐ ์ ‘๊ทผ (Oracle ์ „์šฉ ๋ฌธ๋ฒ•)

์ฒ˜์Œ์—๋Š” Oracle ์ „์šฉ ๊ธฐ๋Šฅ์„ ์•Œ๊ฒŒ๋˜์–ด ์ด๋ฅผ ํ™œ์šฉํ•ด ๊ฐœ๋ฐœํ•˜๊ณ ์ž ํ–ˆ๋‹ค.

  • CONNECT BY LEVEL : 10๊ฐœ ๊ตฌ๊ฐ„ ์ƒ์„ฑ
  • WIDTH_BUCKET : ๊ฐ ๊ณ ๊ฐ์˜ ๋งค์ถœ๊ธˆ์•ก์„ ์ž๋™์œผ๋กœ ๊ตฌ๊ฐ„ ๋ฒˆํ˜ธ์— ๋งคํ•‘

๋น ๋ฅด๊ฒŒ ๊ฒฐ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ, ๋‹จ์ ๋„ ๋ช…ํ™•ํ–ˆ๋‹ค.

  • Oracle ์ข…์†์„ฑ → ๋‹ค๋ฅธ DBMS(PostgreSQL, MySQL ๋“ฑ)๋กœ ์ด์‹ ๋ถˆ๊ฐ€
  • ์ธ๋ฑ์Šค ํ™œ์šฉ ํ•œ๊ณ„ → WIDTH_BUCKET์€ ํ•จ์ˆ˜ ๊ธฐ๋ฐ˜์ด๋ผ RANGE SCAN ๋ถˆ๊ฐ€ (CPU ์—ฐ์‚ฐ ์ฆ๊ฐ€)
  • ์„ฑ๋Šฅ ์ €ํ•˜ → 100๋งŒ ๊ฑด ์ด์ƒ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ณ  CPU์—์„œ ํ•จ์ˆ˜ ๊ณ„์‚ฐ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค

 

3. ๊ฐœ์„  ์ „ ์ฟผ๋ฆฌ (Oracle ์ข…์† ๋ฌธ๋ฒ•)

WITH ์ตœ๋Œ€๋งค์ถœ_CTE AS (
    SELECT MAX(์›”๋งค์ถœ๊ธˆ์•ก) ์ตœ๋Œ€๋งค์ถœ
      FROM ๋งค์ถœ์กฐ๊ฑด_T
     WHERE ๋งค์žฅ์ฝ”๋“œ = :๋งค์žฅ์ฝ”๋“œ
       AND :๊ธฐ์ค€๋…„์›” BETWEEN SUBSTR(์‹œ์ž‘์ผ์ž,0,6) AND SUBSTR(์ข…๋ฃŒ์ผ์ž,0,6)
),
๊ตฌ๊ฐ„๊ธฐ์ค€_CTE AS (
    SELECT CASE WHEN LEVEL = 1
                 THEN 1
                 ELSE (LEVEL-1) * (์ตœ๋Œ€๋งค์ถœ/10)
            END AS ์ตœ์†Œ๊ฐ’,
           CASE WHEN LEVEL = 11
                 THEN NULL
                 ELSE LEVEL * (์ตœ๋Œ€๋งค์ถœ/10) - 1
            END AS ์ตœ๋Œ€๊ฐ’
      FROM ์ตœ๋Œ€๋งค์ถœ_CTE
    CONNECT BY LEVEL <= 11
),
๋ฒ„ํ‚ท์ง‘๊ณ„_CTE AS (
    SELECT WIDTH_BUCKET(์›”๋งค์ถœ๊ธˆ์•ก, 1, V.์ตœ๋Œ€๋งค์ถœ, 10) AS ๋ฒ„ํ‚ท๋ฒˆํ˜ธ,
           COUNT(*) AS ๊ณ ๊ฐ์ˆ˜
      FROM ์›”๋ณ„๋งค์ถœ์ง‘๊ณ„_T A, ์ตœ๋Œ€๋งค์ถœ_CTE V
     WHERE A.๋งค์žฅ์ฝ”๋“œ = :๋งค์žฅ์ฝ”๋“œ
       AND A.๊ธฐ์ค€๋…„์›” = :๊ธฐ์ค€๋…„์›”
     GROUP BY WIDTH_BUCKET(์›”๋งค์ถœ๊ธˆ์•ก, 1, V.์ตœ๋Œ€๋งค์ถœ, 10)
)
SELECT B.์ตœ์†Œ๊ฐ’, B.์ตœ๋Œ€๊ฐ’,
       NVL(D.๊ณ ๊ฐ์ˆ˜,0) AS ๊ณ ๊ฐ์ˆ˜
  FROM ๊ตฌ๊ฐ„๊ธฐ์ค€_CTE B, ๋ฒ„ํ‚ท์ง‘๊ณ„_CTE D
 WHERE B.๊ตฌ๊ฐ„์ˆœ๋ฒˆ = D.๋ฒ„ํ‚ท๋ฒˆํ˜ธ(+)
 ORDER BY ์ตœ์†Œ๊ฐ’;

 

3-1. ๊ฐœ์„  ํ›„ ์ฟผ๋ฆฌ (ANSI SQL ๊ธฐ๋ฐ˜)

oracle์—์„œ๋Š” Recursive ๊ด€๋ จํ•ด์„œ ์ฝ”๋“œ๊ฐ€ ์ˆ˜์ •์ด ํ•„์š”ํ•˜๋‹ค. (์ด์‹์„ฑ 100%๋Š” ์•„๋‹Œ ์ ์„ ์ดํ•ดํ•ด์ฃผ์„ธ์š”..)

WITH RECURSIVE
๊ตฌ๊ฐ„ํญ๊ณ„์‚ฐ_CTE AS (
    SELECT CEIL(:์ตœ๋Œ€๋งค์ถœ/10.0) AS ๊ตฌ๊ฐ„ํญ,
           :์ตœ๋Œ€๋งค์ถœ AS ์ตœ๋Œ€๋งค์ถœ
),
๊ตฌ๊ฐ„๊ธฐ์ค€_CTE AS (
    SELECT 1 AS ๊ตฌ๊ฐ„๋ฒˆํ˜ธ, 1 AS ์ตœ์†Œ๊ฐ’, ๊ตฌ๊ฐ„ํญ AS ์ตœ๋Œ€๊ฐ’, ๊ตฌ๊ฐ„ํญ, ์ตœ๋Œ€๋งค์ถœ
      FROM ๊ตฌ๊ฐ„ํญ๊ณ„์‚ฐ_CTE
    UNION ALL
    SELECT ๊ตฌ๊ฐ„๋ฒˆํ˜ธ+1,
           ์ตœ๋Œ€๊ฐ’+1,
           CASE WHEN ๊ตฌ๊ฐ„๋ฒˆํ˜ธ+1=10 THEN ์ตœ๋Œ€๋งค์ถœ ELSE ์ตœ๋Œ€๊ฐ’+๊ตฌ๊ฐ„ํญ END,
           ๊ตฌ๊ฐ„ํญ, ์ตœ๋Œ€๋งค์ถœ
      FROM ๊ตฌ๊ฐ„๊ธฐ์ค€_CTE
     WHERE ๊ตฌ๊ฐ„๋ฒˆํ˜ธ < 10
),
๊ตฌ๊ฐ„๋ณ„์ง‘๊ณ„_CTE AS (
    SELECT B.๊ตฌ๊ฐ„๋ฒˆํ˜ธ, B.์ตœ์†Œ๊ฐ’, B.์ตœ๋Œ€๊ฐ’,
           COUNT(C.๊ณ ๊ฐID) AS ๊ณ ๊ฐ์ˆ˜
      FROM ๊ตฌ๊ฐ„๊ธฐ์ค€_CTE B
      LEFT JOIN ์›”๋ณ„๋งค์ถœ์ง‘๊ณ„_T C
        ON C.์›”๋งค์ถœ๊ธˆ์•ก BETWEEN B.์ตœ์†Œ๊ฐ’ AND B.์ตœ๋Œ€๊ฐ’
       AND C.๋งค์žฅ์ฝ”๋“œ = :๋งค์žฅ์ฝ”๋“œ
       AND C.๊ธฐ์ค€๋…„์›” = :๊ธฐ์ค€๋…„์›”
     GROUP BY B.๊ตฌ๊ฐ„๋ฒˆํ˜ธ, B.์ตœ์†Œ๊ฐ’, B.์ตœ๋Œ€๊ฐ’
)
SELECT ๊ตฌ๊ฐ„๋ฒˆํ˜ธ, ์ตœ์†Œ๊ฐ’, ์ตœ๋Œ€๊ฐ’, ๊ณ ๊ฐ์ˆ˜,
       ROUND(๊ณ ๊ฐ์ˆ˜*100.0/NULLIF(SUM(๊ณ ๊ฐ์ˆ˜) OVER(),0),2) AS ๋น„์œจํผ์„ผํŠธ
  FROM ๊ตฌ๊ฐ„๋ณ„์ง‘๊ณ„_CTE
 ORDER BY ๊ตฌ๊ฐ„๋ฒˆํ˜ธ;

 

4. ๊ฐœ์„  ํฌ์ธํŠธ

  1. ANSI SQL ๋ฌธ๋ฒ•
    WITH RECURSIVE๋ฅผ ์‚ฌ์šฉํ•ด Oracle ์ „์šฉ ๋ฌธ๋ฒ•์„ ์ œ๊ฑฐํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.
    ์ด๋กœ์จ PostgreSQL, MySQL ๋“ฑ์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.
  2. ๊ตฌ๊ฐ„๋ณ„ ์กฐ์ธ ๊ตฌ์กฐ
    ๊ตฌ๊ฐ„(10ํ–‰)์„ ๊ธฐ์ค€์œผ๋กœ ๋งค์ถœ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ธํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.
    ๋•๋ถ„์— ๊ฐ ๊ตฌ๊ฐ„๋งˆ๋‹ค INDEX RANGE SCAN์ด ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค.
  3. ์„ฑ๋Šฅ ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ
    ํ•ญ์ƒ 10ํšŒ RANGE SCAN๋งŒ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋˜๋‹ˆ, ๋ฐ์ดํ„ฐ๋Ÿ‰์ด ์ปค์ ธ๋„ ์‹คํ–‰ ํŒจํ„ด์ด ์•ˆ์ •์ ์ด๊ฒŒ ๋˜์—ˆ๋‹ค.'

 

5. ์ธ๋ฑ์Šค ์„ค๊ณ„ ๊ณผ์ •

5-1. ์ดˆ๊ธฐ ์ธ๋ฑ์Šค

CREATE INDEX IDX_๋งค์ถœ_CLUB_YM_CUST ON ์›”๋ณ„๋งค์ถœ์ง‘๊ณ„_T (๋งค์žฅ์ฝ”๋“œ, ๊ธฐ์ค€๋…„์›”, ๊ณ ๊ฐID);
  • ๊ณ ๊ฐ ๋‹จ๊ฑด ์กฐํšŒ ์œ ๋ฆฌ
  • ๊ตฌ๊ฐ„ ์กฐ๊ฑด์—๋Š” ์›”๋งค์ถœ๊ธˆ์•ก์ด ์—†์–ด ํ…Œ์ด๋ธ” ๋žœ๋ค ์•ก์„ธ์Šค๋ฅผ ํ”ผํ•  ์ˆ˜ ์—†์Œ

 

5-2. ๊ฐœ์„  ์ธ๋ฑ์Šค

CREATE INDEX IDX_๋งค์ถœ_YM_CLUB_AMT_CUST ON ์›”๋ณ„๋งค์ถœ์ง‘๊ณ„_T (๊ธฐ์ค€๋…„์›”, ๋งค์žฅ์ฝ”๋“œ, ์›”๋งค์ถœ๊ธˆ์•ก, ๊ณ ๊ฐID);
  • ์›”๋งค์ถœ๊ธˆ์•ก์„ ํฌํ•จํ•ด ๊ตฌ๊ฐ„ Range Scan์ด ๊ฐ€๋Šฅ
  • ํ…Œ์ด๋ธ” ์•ก์„ธ์Šค ์—†์ด ์ธ๋ฑ์Šค๋งŒ์œผ๋กœ ์ฒ˜๋ฆฌ(index-only)๊ฐ€ ๊ฐ€๋Šฅ

๋‹ค๋งŒ ๊ณ ๊ฐ ๋‹จ๊ฑด ์กฐํšŒ์—๋Š” ๊ธฐ์กด ์ธ๋ฑ์Šค๊ฐ€ ๋” ์ ํ•ฉํ–ˆ๊ธฐ์—,
๋‘ ์ธ๋ฑ์Šค๋ฅผ ์ด์›ํ™”ํ•ด ์ƒํ™ฉ๋ณ„๋กœ ์„ ํƒํ•˜๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด ๋” ํ•ฉ๋ฆฌ์ ์ผ ์ˆ˜ ์žˆ๋‹ค.

 

6. SQL ์‹คํ–‰๊ณ„ํš ๋น„๊ต

6-1. ๊ฐœ์„  ์ „ (Oracle ์ „์šฉ / WIDTH_BUCKET)

  • COST: ์•ฝ 1450
  • TIME: 3.5 ~ 4์ดˆ
  • ํŠน์ง•: ์ธ๋ฑ์Šค๋กœ ์กฐ๊ฑด ์ผ๋ถ€๋งŒ ๊ฑธ๋Ÿฌ๋‚ด๊ณ , ๋‚˜๋จธ์ง€๋Š” ํ…Œ์ด๋ธ” ๋žœ๋ค ์•ก์„ธ์Šค์™€ ํ•จ์ˆ˜ ๊ณ„์‚ฐ
------------------------------------------------------------------------------------------------------------
| Id | Operation                          | Name                                | Rows   | Cost | Time  |
------------------------------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT                   |                                     |     11 | 1450 | 3.8s  |
|  1 |  SORT ORDER BY                     |                                     |     11 |    3 | 0.0s  |
|  2 |   NESTED LOOPS OUTER               |                                     |     11 | 1447 | 3.8s  |
|  3 |    VIEW                            | (B: ๊ตฌ๊ฐ„๊ธฐ์ค€_CTE)                    |     11 |    2 | 0.0s  |
|  4 |     CONNECT BY WITH FILTERING      |                                     |     11 |    2 | 0.0s  |
|  5 |    VIEW                            | (D: ๋ฒ„ํ‚ท์ง‘๊ณ„_CTE)                    |     10 | 1445 | 3.8s  |
|  6 |     HASH GROUP BY                  |                                     |     10 | 1445 | 3.8s  |
|  7 |      TABLE ACCESS BY INDEX ROWID   | ์›”๋ณ„๋งค์ถœ์ง‘๊ณ„_T                       | 1,000K | 1400 | 3.6s  |
|  8 |       INDEX RANGE SCAN             | IDX_๋งค์ถœ_CLUB_YM_CUST                | 1,000K |  320 | 0.9s  |
------------------------------------------------------------------------------------------------------------
Predicate Information:
8 - access("๊ธฐ์ค€๋…„์›”" = :๊ธฐ์ค€๋…„์›” AND "๋งค์žฅ์ฝ”๋“œ" = :๋งค์žฅ์ฝ”๋“œ)
7 - filter(WIDTH_BUCKET("์›”๋งค์ถœ๊ธˆ์•ก", 1, :์ตœ๋Œ€๋งค์ถœ, 10) IS NOT NULL)

 

6-2. ๊ฐœ์„  ํ›„ (ANSI SQL / RECURSIVE, JOIN, RANGE SCAN, INDEX_ONLY)

  • COST: ์•ฝ 150
  • TIME: 0.4์ดˆ
  • ํŠน์ง•: ๊ตฌ๊ฐ„๋ณ„ 10ํšŒ INDEX RANGE SCAN๋งŒ ์ˆ˜ํ–‰, ํ…Œ์ด๋ธ” ์•ก์„ธ์Šค ๋ฐœ์ƒX
Plan hash value: 7xxx4321

------------------------------------------------------------------------------------------------------------
| Id | Operation                          | Name                                | Rows  | Cost | Time  |
------------------------------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT                   |                                     |    10 |  150 | 0.4s  |
|  1 |  SORT ORDER BY                     |                                     |    10 |    5 | 0.0s  |
|  2 |   VIEW                             | (D: ๊ตฌ๊ฐ„๋ณ„์ง‘๊ณ„_CTE)                  |    10 |  145 | 0.4s  |
|  3 |    HASH GROUP BY                   |                                     |    10 |  145 | 0.4s  |
|  4 |     NESTED LOOPS OUTER             |                                     |   1M  |  140 | 0.4s  |
|  5 |      RECURSIVE WITH PUMP           | (B: ๊ตฌ๊ฐ„๊ธฐ์ค€_CTE, 10ํ–‰)               |    10 |    1 | 0.0s  |
|  6 |      PARTITION RANGE SINGLE        |                                     |  100K |   14 | 0.05s |
|  7 |       INDEX RANGE SCAN             | IDX_๋งค์ถœ_YM_CLUB_AMT_CUST            |  100K |   14 | 0.05s |
------------------------------------------------------------------------------------------------------------
Predicate Information:
7 - access("C"."๊ธฐ์ค€๋…„์›”"=:๊ธฐ์ค€๋…„์›” AND "C"."๋งค์žฅ์ฝ”๋“œ"=:๋งค์žฅ์ฝ”๋“œ
            AND "C"."์›”๋งค์ถœ๊ธˆ์•ก" BETWEEN "B"."์ตœ์†Œ๊ฐ’" AND "B"."์ตœ๋Œ€๊ฐ’")

6-3. ์„ฑ๋Šฅ ๋น„๊ต ์š”์•ฝ

๊ฐœ์„  ์ „ (๋งค์žฅ์ฝ”๋“œ, ๊ธฐ์ค€๋…„์›”, ๊ณ ๊ฐID) RANGE SCAN ํ›„ ROWID ๋žœ๋ค ์•ก์„ธ์Šค + Width Bucket ํ•จ์ˆ˜๊ณ„์‚ฐ cost : 1450 3.5~4s
๊ฐœ์„  ํ›„ (๊ธฐ์ค€๋…„์›”, ๋งค์žฅ์ฝ”๋“œ, ์›”๋งค์ถœ๊ธˆ์•ก, ๊ณ ๊ฐID) ๊ตฌ๊ฐ„๋ณ„ INDEX RANGE SCAN (index-only) cost : 150 0.4s

 

7. ๊ฒฐ๋ก 

  1. Oracle ์ข…์†์„ฑ ์ œ๊ฑฐ
    1. ์ „์šฉ ํ•จ์ˆ˜(WIDTH_BUCKET, CONNECT BY) ๋Œ€์‹  ANSI SQL(WITH RECURSIVE)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ WIDTH_BUCKET์—์„œ์˜ ๋ถˆํ•„์š”ํ•œ CPU ์—ฐ์‚ฐ์„ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
    2. ๊ฐ DB์— ์ข…์†์ ์ธ ํ•จ์ˆ˜์˜ ์‚ฌ์šฉ์„ ์ง€์–‘ํ•ด์•ผ ํ•˜๋Š” ์ผ€์ด์Šค๋ฅผ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.
  2. ์ธ๋ฑ์Šค ์ตœ์ ํ™”
    1. ๋ฒ”์œ„ ์กฐ๊ฑด ์ปฌ๋Ÿผ(์›”๋งค์ถœ๊ธˆ์•ก)์„ ์ธ๋ฑ์Šค์— ํฌํ•จ์‹œํ‚ด์œผ๋กœ์จ ํ…Œ์ด๋ธ” ์—‘์„ธ์Šค๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š๊ฒŒ ๋˜์—ˆ๊ณ , ๊ตฌ๊ฐ„๋ณ„ ์ง‘๊ณ„ ์ฟผ๋ฆฌ์—์„œ์˜ ํšจ์œจ์ด ์ฆ๊ฐ€ํ•˜์˜€๋‹ค.
  3. ์„ฑ๋Šฅ ๊ฐœ์„ 
    1. 3~4์ดˆ -> 0.4์ดˆ, ์•ฝ 10๋ฐฐ ๊ฐœ์„