Sam Baek, The Dev's Corner

๐Ÿ” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค์™€ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

02 Nov 2025

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค์™€ ์ฟผ๋ฆฌ ์ตœ์ ํ™”๋ž€ ๋ฌด์—‡์ธ๊ฐ€


๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋ฉด ์ ์  ๋А๋ ค์ง€๋Š” ๊ฒฝํ—˜์„ ํ•˜๊ฒŒ ๋œ๋‹ค.
์ฒ˜์Œ์—” ๋น ๋ฅด๋˜ ์กฐํšŒ๊ฐ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์Œ“์ด๋ฉด์„œ
10์ดˆ, 30์ดˆ, ์‹ฌ์ง€์–ด 1๋ถ„ ์ด์ƒ ๊ฑธ๋ฆฌ๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•œ๋‹ค.

์ด๋Š” ๋งˆ์น˜ ์ฑ…์ด ์ •๋ฆฌ๋˜์ง€ ์•Š์€ ๋„์„œ๊ด€๊ณผ ๊ฐ™๋‹ค.
์ฑ…์ด 100๊ถŒ์ผ ๋•Œ๋Š” ํ•˜๋‚˜์”ฉ ์ฐพ์•„๋„ ๊ดœ์ฐฎ์ง€๋งŒ,
10๋งŒ ๊ถŒ์ด ๋˜๋ฉด ์›ํ•˜๋Š” ์ฑ…์„ ์ฐพ๊ธฐ ์œ„ํ•ด
๋ชจ๋“  ์ฑ…์„ ์ผ์ผ์ด ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค๋ฉด ์—„์ฒญ๋‚œ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฐ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค๋Š” ๋ฐ”๋กœ ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ
์ฑ…์˜ ๋ชฉ์ฐจ๋‚˜ ์ƒ‰์ธ์ฒ˜๋Ÿผ ๋น ๋ฅด๊ฒŒ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š”
ํ•ต์‹ฌ ๊ธฐ์ˆ ์ด๋‹ค.

์™œ ์ธ๋ฑ์Šค์™€ ์ฟผ๋ฆฌ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ• ๊นŒ?


์ตœ์ ํ™”๋˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚จ๋‹ค.

๋ฌธ์ œ 1: ๋А๋ฆฐ ์กฐํšŒ ์†๋„
์ธ๋ฑ์Šค ์—†์ด 100๋งŒ ๊ฑด์˜ ๋ฐ์ดํ„ฐ์—์„œ ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์œผ๋ ค๋ฉด
๋ชจ๋“  ํ–‰์„ ํ•˜๋‚˜์”ฉ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค. (Full Table Scan)

๋ฌธ์ œ 2: ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„
๋น„ํšจ์œจ์ ์ธ ์ฟผ๋ฆฌ๋Š” CPU์™€ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ณผ๋„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•œ๋‹ค.
ํ•˜๋‚˜์˜ ๋А๋ฆฐ ์ฟผ๋ฆฌ๊ฐ€ ์ „์ฒด ์„œ๋ฒ„๋ฅผ ๋А๋ฆฌ๊ฒŒ ๋งŒ๋“ ๋‹ค.

๋ฌธ์ œ 3: ๋™์‹œ ์ฒ˜๋ฆฌ ๋Šฅ๋ ฅ ์ €ํ•˜
์ฟผ๋ฆฌ ํ•˜๋‚˜๊ฐ€ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋ฉด ๋‹ค๋ฅธ ์š”์ฒญ๋“ค๋„ ๋Œ€๊ธฐํ•ด์•ผ ํ•œ๋‹ค.
๋ฝ(Lock)์ด ๊ธธ์–ด์ง€๋ฉด์„œ ๋™์‹œ์„ฑ์ด ๋–จ์–ด์ง„๋‹ค.

๋ฌธ์ œ 4: ์‚ฌ์šฉ์ž ์ดํƒˆ
ํŽ˜์ด์ง€ ๋กœ๋”ฉ์ด 3์ดˆ ์ด์ƒ ๊ฑธ๋ฆฌ๋ฉด ์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ ์ด ๊ธ‰์ฆํ•œ๋‹ค.

๊ธฐ๋ณธ ๊ฐœ๋… ์š”์•ฝ


๐Ÿท๏ธ ์ธ๋ฑ์Šค(Index)๋ž€?


๊ฐœ๋…: ๋ฐ์ดํ„ฐ๋ฅผ ๋น ๋ฅด๊ฒŒ ์ฐพ๊ธฐ ์œ„ํ•œ ์ •๋ ฌ๋œ ์ž๋ฃŒ๊ตฌ์กฐ

์ฑ…์˜ ์ƒ‰์ธ ๋น„์œ :
์ฑ… ๋’ค์ชฝ์˜ ์ƒ‰์ธ์„ ๋ณด๋ฉด โ€œ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค - 45์ชฝ, 123์ชฝโ€์ฒ˜๋Ÿผ
๋‹จ์–ด์™€ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๊ฐ€ ์ •๋ ฌ๋˜์–ด ์žˆ๋‹ค.
์ƒ‰์ธ์ด ์—†๋‹ค๋ฉด ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ๋’ค์ ธ์•ผ ํ•˜์ง€๋งŒ,
์ƒ‰์ธ์ด ์žˆ์œผ๋ฉด ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ๋ฐ”๋กœ ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.

์ธ๋ฑ์Šค์˜ ์žฅ๋‹จ์ 


์žฅ์ :

  • ์กฐํšŒ ์†๋„ ๋Œ€ํญ ํ–ฅ์ƒ: O(N) โ†’ O(log N)
  • ์ •๋ ฌ๊ณผ ๊ทธ๋ฃนํ•‘ ์„ฑ๋Šฅ ํ–ฅ์ƒ
  • WHERE, JOIN ์กฐ๊ฑด ๋น ๋ฅธ ์ฒ˜๋ฆฌ

๋‹จ์ :

  • ์ถ”๊ฐ€ ์ €์žฅ ๊ณต๊ฐ„ ํ•„์š”: ํ…Œ์ด๋ธ” ํฌ๊ธฐ์˜ 10-20%
  • INSERT/UPDATE/DELETE ๋А๋ ค์ง: ์ธ๋ฑ์Šค๋„ ํ•จ๊ป˜ ๊ฐฑ์‹ 
  • ์ž˜๋ชป ์„ค๊ณ„ํ•˜๋ฉด ์˜คํžˆ๋ ค ์„ฑ๋Šฅ ์ €ํ•˜


๐Ÿท๏ธ ์ธ๋ฑ์Šค์˜ ์ข…๋ฅ˜


1. B-Tree ์ธ๋ฑ์Šค (๊ฐ€์žฅ ์ผ๋ฐ˜์ )


ํŠน์ง•: ๊ท ํ˜• ์žกํžŒ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ์ •๋ ฌ๋œ ์ƒํƒœ ์œ ์ง€
์‚ฌ์šฉ: ๋Œ€๋ถ€๋ถ„์˜ ์กฐํšŒ, ์ •๋ ฌ, ๋ฒ”์œ„ ๊ฒ€์ƒ‰
์‹œ๊ฐ„ ๋ณต์žก๋„: O(log N)

์ ํ•ฉํ•œ ๊ฒฝ์šฐ:

  • WHERE id = 100
  • WHERE age BETWEEN 20 AND 30
  • ORDER BY created_at

๋ถ€์ ํ•ฉํ•œ ๊ฒฝ์šฐ:

  • WHERE name LIKE โ€˜%๊น€%โ€™ (์•ž์— ์™€์ผ๋“œ์นด๋“œ)
  • ์นด๋””๋„๋ฆฌํ‹ฐ๊ฐ€ ๋งค์šฐ ๋‚ฎ์€ ์ปฌ๋Ÿผ (์„ฑ๋ณ„, ํƒ€์ž… ๋“ฑ)


2. Hash ์ธ๋ฑ์Šค


ํŠน์ง•: ํ•ด์‹œ ํ•จ์ˆ˜๋กœ ๋น ๋ฅธ ๊ฒ€์ƒ‰ (๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜)
์‚ฌ์šฉ: ๋™๋“ฑ ๋น„๊ต (=)
์‹œ๊ฐ„ ๋ณต์žก๋„: O(1)

์ ํ•ฉํ•œ ๊ฒฝ์šฐ: WHERE id = 100
๋ถ€์ ํ•ฉํ•œ ๊ฒฝ์šฐ: ๋ฒ”์œ„ ๊ฒ€์ƒ‰, ์ •๋ ฌ (BETWEEN, ORDER BY ๋ถˆ๊ฐ€)

3. Full-Text ์ธ๋ฑ์Šค


ํŠน์ง•: ํ…์ŠคํŠธ ์ „๋ฌธ ๊ฒ€์ƒ‰์— ํŠนํ™”
์‚ฌ์šฉ: ๊ธด ํ…์ŠคํŠธ ๊ฒ€์ƒ‰

์ ํ•ฉํ•œ ๊ฒฝ์šฐ: ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ ๊ฒ€์ƒ‰, ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰

4. ๋ณตํ•ฉ ์ธ๋ฑ์Šค (Composite Index)


ํŠน์ง•: ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ์„ ์กฐํ•ฉํ•œ ์ธ๋ฑ์Šค

CREATE INDEX idx_user_name_age ON users (name, age);


์ค‘์š” ์›์น™: ์ปฌ๋Ÿผ ์ˆœ์„œ๊ฐ€ ๋งค์šฐ ์ค‘์š”!

ํ™œ์šฉ ๊ฐ€๋Šฅ:

  • WHERE name = โ€˜๊น€์ฒ ์ˆ˜โ€™
  • WHERE name = โ€˜๊น€์ฒ ์ˆ˜โ€™ AND age = 25

ํ™œ์šฉ ๋ถˆ๊ฐ€:

  • WHERE age = 25 (์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ ๋ˆ„๋ฝ)


๐Ÿท๏ธ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ํ•ต์‹ฌ ๊ฐœ๋…


1. ์‹คํ–‰ ๊ณ„ํš (Execution Plan)


๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ฟผ๋ฆฌ๋ฅผ ์–ด๋–ป๊ฒŒ ์‹คํ–‰ํ• ์ง€ ๊ณ„ํšํ•œ ๊ฒƒ

ํ™•์ธ ๋ฐฉ๋ฒ•: EXPLAIN ๋ช…๋ น์–ด ์‚ฌ์šฉ

2. ์นด๋””๋„๋ฆฌํ‹ฐ (Cardinality)


์ปฌ๋Ÿผ์˜ ๊ณ ์œ ํ•œ ๊ฐ’์˜ ๊ฐœ์ˆ˜

  • ๋†’์€ ์นด๋””๋„๋ฆฌํ‹ฐ: ์ด๋ฉ”์ผ, ์ฃผ๋ฏผ๋ฒˆํ˜ธ (์ธ๋ฑ์Šค ํšจ๊ณผ โ†‘)
  • ๋‚ฎ์€ ์นด๋””๋„๋ฆฌํ‹ฐ: ์„ฑ๋ณ„, ์ƒํƒœ์ฝ”๋“œ (์ธ๋ฑ์Šค ํšจ๊ณผ โ†“)


3. ์„ ํƒ๋„ (Selectivity)


์ „์ฒด ๋ฐ์ดํ„ฐ ์ค‘ ์กฐ๊ฑด์— ๋งž๋Š” ๋น„์œจ

  • ๋‚ฎ์€ ์„ ํƒ๋„: ์ „์ฒด์˜ 5% ์„ ํƒ (์ธ๋ฑ์Šค ํšจ๊ณผ โ†‘)
  • ๋†’์€ ์„ ํƒ๋„: ์ „์ฒด์˜ 80% ์„ ํƒ (Full Scan์ด ๋‚˜์„ ์ˆ˜๋„)


4. ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค (Covering Index)


์ฟผ๋ฆฌ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ปฌ๋Ÿผ์ด ์ธ๋ฑ์Šค์— ํฌํ•จ๋˜์–ด
ํ…Œ์ด๋ธ”์„ ์กฐํšŒํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ์ƒํƒœ

-- ์ธ๋ฑ์Šค: (name, age, email)
SELECT name, age, email FROM users WHERE name = '๊น€์ฒ ์ˆ˜';
-- ํ…Œ์ด๋ธ” ์ ‘๊ทผ ์—†์ด ์ธ๋ฑ์Šค๋งŒ์œผ๋กœ ์กฐํšŒ ์™„๋ฃŒ!


์ธ๋ฑ์Šค ์ƒ์„ฑ ์˜ˆ์‹œ


MySQL ์ธ๋ฑ์Šค ์ƒ์„ฑ


๋‹จ์ผ ์ปฌ๋Ÿผ ์ธ๋ฑ์Šค


-- ๊ธฐ๋ณธ ์ธ๋ฑ์Šค ์ƒ์„ฑ
CREATE INDEX idx_email ON users (email);

-- ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค
CREATE UNIQUE INDEX idx_unique_email ON users (email);

-- ์ธ๋ฑ์Šค ์‚ญ์ œ
DROP INDEX idx_email ON users;

-- ์ธ๋ฑ์Šค ๋ชฉ๋ก ํ™•์ธ
SHOW INDEX FROM users;


๋ณตํ•ฉ ์ธ๋ฑ์Šค (์ปฌ๋Ÿผ ์ˆœ์„œ ์ค‘์š”!)


-- ์ด๋ฆ„ + ๋‚˜์ด ๋ณตํ•ฉ ์ธ๋ฑ์Šค
CREATE INDEX idx_name_age ON users (name, age);

-- ํ™œ์šฉ ๊ฐ€๋Šฅํ•œ ์ฟผ๋ฆฌ
SELECT * FROM users WHERE name = '๊น€์ฒ ์ˆ˜';
SELECT * FROM users WHERE name = '๊น€์ฒ ์ˆ˜' AND age = 25;
SELECT * FROM users WHERE name = '๊น€์ฒ ์ˆ˜' AND age > 20;

-- ํ™œ์šฉ ๋ถˆ๊ฐ€ (์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ ๋ˆ„๋ฝ)
SELECT * FROM users WHERE age = 25;


์ธ๋ฑ์Šค ํžŒํŠธ


-- ํŠน์ • ์ธ๋ฑ์Šค ๊ฐ•์ œ ์‚ฌ์šฉ
SELECT * FROM users USE INDEX (idx_name_age)
WHERE name = '๊น€์ฒ ์ˆ˜';

-- ์ธ๋ฑ์Šค ๋ฌด์‹œ
SELECT * FROM users IGNORE INDEX (idx_email)
WHERE email = 'test@example.com';


PostgreSQL ์ธ๋ฑ์Šค ์ƒ์„ฑ


๊ธฐ๋ณธ ์ธ๋ฑ์Šค


-- B-Tree ์ธ๋ฑ์Šค (๊ธฐ๋ณธ)
CREATE INDEX idx_email ON users (email);

-- ๋ถ€๋ถ„ ์ธ๋ฑ์Šค (์กฐ๊ฑด๋ถ€)
CREATE INDEX idx_active_users ON users (email)
WHERE status = 'active';

-- ํ‘œํ˜„์‹ ์ธ๋ฑ์Šค
CREATE INDEX idx_lower_email ON users (LOWER(email));


๊ณ ๊ธ‰ ์ธ๋ฑ์Šค


-- GIN ์ธ๋ฑ์Šค (๋ฐฐ์—ด, JSONB ๊ฒ€์ƒ‰)
CREATE INDEX idx_tags ON posts USING GIN (tags);

-- GiST ์ธ๋ฑ์Šค (์ง€๋ฆฌ ์ •๋ณด, ๋ฒ”์œ„)
CREATE INDEX idx_location ON stores USING GiST (location);

-- BRIN ์ธ๋ฑ์Šค (๋Œ€์šฉ๋Ÿ‰ ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ)
CREATE INDEX idx_created_at ON logs USING BRIN (created_at);


์ฟผ๋ฆฌ ์ตœ์ ํ™” ์‹ค์ „ ์˜ˆ์‹œ


EXPLAIN์œผ๋กœ ์‹คํ–‰ ๊ณ„ํš ๋ถ„์„


MySQL EXPLAIN


EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';


์ฃผ์š” ํ™•์ธ ํ•ญ๋ชฉ:

ํ•ญ๋ชฉ ์˜๋ฏธ ์ข‹์€ ๊ฐ’
type ์กฐ์ธ ํƒ€์ž… const, eq_ref
possible_keys ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ธ๋ฑ์Šค -
key ์‹ค์ œ ์‚ฌ์šฉ๋œ ์ธ๋ฑ์Šค NULL์ด ์•„๋‹˜
rows ์˜ˆ์ƒ ๊ฒ€์ƒ‰ ํ–‰ ์ˆ˜ ์ ์„์ˆ˜๋ก ์ข‹์Œ
Extra ์ถ”๊ฐ€ ์ •๋ณด Using index


type ๊ฐ’ ํ•ด์„ (์ข‹์€ ์ˆœ์„œ):

  • const: PRIMARY KEY๋‚˜ UNIQUE ์ธ๋ฑ์Šค๋กœ ๋‹จ์ผ ํ–‰ ์กฐํšŒ (์ตœ๊ณ )
  • eq_ref: ์กฐ์ธ ์‹œ PRIMARY KEY ์‚ฌ์šฉ
  • ref: ์ผ๋ฐ˜ ์ธ๋ฑ์Šค ์‚ฌ์šฉ
  • range: ๋ฒ”์œ„ ๊ฒ€์ƒ‰ (BETWEEN, >, <)
  • index: ์ธ๋ฑ์Šค ํ’€ ์Šค์บ”
  • ALL: ํ…Œ์ด๋ธ” ํ’€ ์Šค์บ” (์ตœ์•…)


PostgreSQL EXPLAIN ANALYZE


EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';


์ถœ๋ ฅ ์˜ˆ์‹œ:

Index Scan using idx_email on users  (cost=0.42..8.44 rows=1 width=100)
  Index Cond: (email = 'test@example.com'::text)
  Planning Time: 0.123 ms
  Execution Time: 0.045 ms


์ฃผ์š” ํ™•์ธ ํ•ญ๋ชฉ:

  • Seq Scan: ํ…Œ์ด๋ธ” ํ’€ ์Šค์บ” (๋‚˜์จ)
  • Index Scan: ์ธ๋ฑ์Šค ์‚ฌ์šฉ (์ข‹์Œ)
  • Index Only Scan: ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค (์ตœ๊ณ )
  • rows: ์˜ˆ์ƒ ํ–‰ ์ˆ˜
  • cost: ์˜ˆ์ƒ ๋น„์šฉ


๋А๋ฆฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ์˜ˆ์‹œ


Before: ์ตœ์ ํ™” ์ „ (๋А๋ฆผ)


-- ์ธ๋ฑ์Šค๊ฐ€ ์—†๋Š” ์ƒํƒœ
SELECT * FROM orders
WHERE customer_id = 12345
AND order_date >= '2024-01-01'
AND status = 'completed';

-- EXPLAIN ๊ฒฐ๊ณผ: type = ALL (ํ…Œ์ด๋ธ” ํ’€ ์Šค์บ”)
-- ์‹คํ–‰ ์‹œ๊ฐ„: 5.2์ดˆ (100๋งŒ ๊ฑด ๊ธฐ์ค€)


After: ์ตœ์ ํ™” ํ›„ (๋น ๋ฆ„)


-- ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ƒ์„ฑ
CREATE INDEX idx_orders_optimization
ON orders (customer_id, order_date, status);

-- ๊ฐ™์€ ์ฟผ๋ฆฌ ์‹คํ–‰
SELECT * FROM orders
WHERE customer_id = 12345
AND order_date >= '2024-01-01'
AND status = 'completed';

-- EXPLAIN ๊ฒฐ๊ณผ: type = ref (์ธ๋ฑ์Šค ์‚ฌ์šฉ)
-- ์‹คํ–‰ ์‹œ๊ฐ„: 0.05์ดˆ (104๋ฐฐ ๋น ๋ฆ„!)


์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค๋กœ ์ถ”๊ฐ€ ์ตœ์ ํ™”


-- ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์กฐํšŒ
SELECT order_id, customer_id, order_date, status, total_amount
FROM orders
WHERE customer_id = 12345
AND order_date >= '2024-01-01'
AND status = 'completed';

-- ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค ์ƒ์„ฑ
CREATE INDEX idx_orders_covering
ON orders (customer_id, order_date, status, order_id, total_amount);

-- EXPLAIN ๊ฒฐ๊ณผ: Extra = Using index (ํ…Œ์ด๋ธ” ์ ‘๊ทผ ์—†์Œ)
-- ์‹คํ–‰ ์‹œ๊ฐ„: 0.02์ดˆ (์ถ”๊ฐ€๋กœ 2.5๋ฐฐ ๋น ๋ฆ„!)


N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ


Before: N+1 ๋ฌธ์ œ ๋ฐœ์ƒ


# ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ (1๋ฒˆ)
posts = db.query("SELECT * FROM posts LIMIT 10")

# ๊ฐ ๊ฒŒ์‹œ๊ธ€์˜ ์ž‘์„ฑ์ž ์กฐํšŒ (N๋ฒˆ)
for post in posts:
    author = db.query(f"SELECT * FROM users WHERE id = {post.author_id}")
    post.author_name = author.name

# ์ด ์ฟผ๋ฆฌ ์ˆ˜: 1 + 10 = 11๋ฒˆ


After: JOIN์œผ๋กœ ํ•ด๊ฒฐ


-- ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ํ•ด๊ฒฐ
SELECT
    p.*,
    u.name as author_name,
    u.email as author_email
FROM posts p
INNER JOIN users u ON p.author_id = u.id
LIMIT 10;

-- ์ด ์ฟผ๋ฆฌ ์ˆ˜: 1๋ฒˆ (11๋ฐฐ ๊ฐ์†Œ!)


๋Œ€์•ˆ: IN ์ ˆ ์‚ฌ์šฉ


# ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ (1๋ฒˆ)
posts = db.query("SELECT * FROM posts LIMIT 10")
author_ids = [post.author_id for post in posts]

# ์ž‘์„ฑ์ž ์ผ๊ด„ ์กฐํšŒ (1๋ฒˆ)
authors = db.query(f"SELECT * FROM users WHERE id IN ({','.join(map(str, author_ids))})")
author_map = {author.id: author for author in authors}

# ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋งคํ•‘
for post in posts:
    post.author = author_map[post.author_id]

# ์ด ์ฟผ๋ฆฌ ์ˆ˜: 2๋ฒˆ (5.5๋ฐฐ ๊ฐ์†Œ)


ํŽ˜์ด์ง• ์ตœ์ ํ™”


Before: OFFSET ์‚ฌ์šฉ (๋А๋ฆผ)


-- 10๋งŒ ๋ฒˆ์งธ ํŽ˜์ด์ง€ ์กฐํšŒ (๋งค์šฐ ๋А๋ฆผ)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100000;

-- ๋ฌธ์ œ: 10๋งŒ ๊ฑด์„ ์ฝ๊ณ  ๋ฒ„๋ ค์•ผ ํ•จ
-- ์‹คํ–‰ ์‹œ๊ฐ„: 2.3์ดˆ


After: Cursor ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• (๋น ๋ฆ„)


-- ์ฒซ ํŽ˜์ด์ง€
SELECT * FROM posts
ORDER BY id
LIMIT 20;
-- ๋งˆ์ง€๋ง‰ id: 20

-- ๋‹ค์Œ ํŽ˜์ด์ง€ (WHERE ์กฐ๊ฑด ์‚ฌ์šฉ)
SELECT * FROM posts
WHERE id > 20
ORDER BY id
LIMIT 20;

-- ์‹คํ–‰ ์‹œ๊ฐ„: 0.01์ดˆ (230๋ฐฐ ๋น ๋ฆ„!)


์ฟผ๋ฆฌ ์ตœ์ ํ™” ์•ˆํ‹ฐํŒจํ„ด๊ณผ ํ•ด๊ฒฐ์ฑ…


์•ˆํ‹ฐํŒจํ„ด 1: SELECT *


๋ฌธ์ œ


-- ํ•„์š” ์—†๋Š” ์ปฌ๋Ÿผ๊นŒ์ง€ ๋ชจ๋‘ ์กฐํšŒ
SELECT * FROM users WHERE id = 100;


ํ•ด๊ฒฐ


-- ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์กฐํšŒ
SELECT id, name, email FROM users WHERE id = 100;


์ด์œ : ๋„คํŠธ์›Œํฌ ์ „์†ก๋Ÿ‰ ๊ฐ์†Œ, ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค ํ™œ์šฉ ๊ฐ€๋Šฅ

์•ˆํ‹ฐํŒจํ„ด 2: ํ•จ์ˆ˜ ์‚ฌ์šฉ์œผ๋กœ ์ธ๋ฑ์Šค ๋ฌดํšจํ™”


๋ฌธ์ œ


-- ์ธ๋ฑ์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ
SELECT * FROM users WHERE YEAR(created_at) = 2024;
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';


ํ•ด๊ฒฐ


-- ๋ฒ”์œ„ ๊ฒ€์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝ (์ธ๋ฑ์Šค ํ™œ์šฉ)
SELECT * FROM users
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01';

-- ๋ฐ์ดํ„ฐ๋ฅผ ์†Œ๋ฌธ์ž๋กœ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ํ•จ์ˆ˜ ์ธ๋ฑ์Šค ์ƒ์„ฑ
CREATE INDEX idx_lower_email ON users (LOWER(email));


์•ˆํ‹ฐํŒจํ„ด 3: OR ์กฐ๊ฑด ๋‚จ์šฉ


๋ฌธ์ œ


-- ์ธ๋ฑ์Šค ํ™œ์šฉ ์–ด๋ ค์›€
SELECT * FROM users
WHERE name = '๊น€์ฒ ์ˆ˜' OR email = 'kim@example.com';


ํ•ด๊ฒฐ


-- UNION์œผ๋กœ ๋ถ„๋ฆฌ (๊ฐ๊ฐ ์ธ๋ฑ์Šค ์‚ฌ์šฉ)
SELECT * FROM users WHERE name = '๊น€์ฒ ์ˆ˜'
UNION
SELECT * FROM users WHERE email = 'kim@example.com';


์•ˆํ‹ฐํŒจํ„ด 4: ์™€์ผ๋“œ์นด๋“œ ์•ž์ชฝ ์‚ฌ์šฉ


๋ฌธ์ œ


-- ์ธ๋ฑ์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ
SELECT * FROM users WHERE name LIKE '%์ฒ ์ˆ˜%';


ํ•ด๊ฒฐ


-- ์™€์ผ๋“œ์นด๋“œ๋ฅผ ๋’ค์ชฝ์—๋งŒ ์‚ฌ์šฉ
SELECT * FROM users WHERE name LIKE '๊น€์ฒ ์ˆ˜%';

-- ๋˜๋Š” Full-Text ์ธ๋ฑ์Šค ์‚ฌ์šฉ
CREATE FULLTEXT INDEX idx_name_fulltext ON users (name);
SELECT * FROM users WHERE MATCH(name) AGAINST('์ฒ ์ˆ˜');


์•ˆํ‹ฐํŒจํ„ด 5: ์„œ๋ธŒ์ฟผ๋ฆฌ ๋‚จ์šฉ


๋ฌธ์ œ


-- ์„œ๋ธŒ์ฟผ๋ฆฌ๊ฐ€ ๋งค ํ–‰๋งˆ๋‹ค ์‹คํ–‰๋จ
SELECT *,
    (SELECT COUNT(*) FROM orders WHERE customer_id = u.id) as order_count
FROM users u;


ํ•ด๊ฒฐ


-- JOIN์œผ๋กœ ๋ณ€๊ฒฝ
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.customer_id
GROUP BY u.id;


์‹ค์ „ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋ถ„์„


๋А๋ฆฐ ์ฟผ๋ฆฌ ๋กœ๊ทธ ํ™œ์„ฑํ™”


MySQL


-- ๋А๋ฆฐ ์ฟผ๋ฆฌ ๋กœ๊ทธ ํ™œ์„ฑํ™”
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 1์ดˆ ์ด์ƒ ๊ฑธ๋ฆฌ๋Š” ์ฟผ๋ฆฌ ๊ธฐ๋ก

-- ๋กœ๊ทธ ํŒŒ์ผ ์œ„์น˜ ํ™•์ธ
SHOW VARIABLES LIKE 'slow_query_log_file';


PostgreSQL


-- postgresql.conf ์„ค์ •
-- log_min_duration_statement = 1000  (1์ดˆ)

-- ์‹คํ–‰ ์‹œ๊ฐ„์ด ๊ธด ์ฟผ๋ฆฌ ํ™•์ธ
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;


์ธ๋ฑ์Šค ์‚ฌ์šฉ๋ฅ  ํ™•์ธ


MySQL


-- ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ธ๋ฑ์Šค ์ฐพ๊ธฐ
SELECT
    t.TABLE_SCHEMA,
    t.TABLE_NAME,
    s.INDEX_NAME,
    s.COLUMN_NAME
FROM information_schema.TABLES t
INNER JOIN information_schema.STATISTICS s
    ON t.TABLE_SCHEMA = s.TABLE_SCHEMA
    AND t.TABLE_NAME = s.TABLE_NAME
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage i
    ON i.OBJECT_SCHEMA = s.TABLE_SCHEMA
    AND i.OBJECT_NAME = s.TABLE_NAME
    AND i.INDEX_NAME = s.INDEX_NAME
WHERE t.TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema')
AND i.INDEX_NAME IS NULL;


PostgreSQL


-- ์ธ๋ฑ์Šค ์‚ฌ์šฉ ํ†ต๊ณ„
SELECT
    schemaname,
    tablename,
    indexname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan;


์‹ค์ „ ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ


โœ… ์ธ๋ฑ์Šค ์„ค๊ณ„


  • WHERE ์ ˆ์— ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ƒ์„ฑ
  • JOIN ์กฐ๊ฑด ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ƒ์„ฑ
  • ORDER BY ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ๊ณ ๋ ค
  • ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ปฌ๋Ÿผ ์ˆœ์„œ ์ตœ์ ํ™” (์นด๋””๋„๋ฆฌํ‹ฐ ๋†’์€ ์ˆœ)
  • ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค ํ™œ์šฉ
  • ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ธ๋ฑ์Šค ์ œ๊ฑฐ


โœ… ์ฟผ๋ฆฌ ์ž‘์„ฑ


  • SELECT * ๋Œ€์‹  ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์กฐํšŒ
  • N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ (JOIN ๋˜๋Š” IN ์‚ฌ์šฉ)
  • WHERE ์ ˆ์—์„œ ํ•จ์ˆ˜ ์‚ฌ์šฉ ์ง€์–‘
  • OR ๋Œ€์‹  UNION ๊ณ ๋ ค
  • LIKE โ€˜%keyword%โ€™ ์ง€์–‘
  • ์„œ๋ธŒ์ฟผ๋ฆฌ ๋Œ€์‹  JOIN ๊ณ ๋ ค


โœ… ์„ฑ๋Šฅ ๋ถ„์„


  • EXPLAIN์œผ๋กœ ์‹คํ–‰ ๊ณ„ํš ํ™•์ธ
  • ๋А๋ฆฐ ์ฟผ๋ฆฌ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ์ธ๋ฑ์Šค ์‚ฌ์šฉ๋ฅ  ํ™•์ธ
  • ์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ๊ฐ„ ์ธก์ •


โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •


  • ์ปค๋„ฅ์…˜ ํ’€ ํฌ๊ธฐ ์ตœ์ ํ™”
  • ์ฟผ๋ฆฌ ์บ์‹œ ํ™œ์šฉ (MySQL 5.7 ์ดํ•˜)
  • ๋ฒ„ํผ ํ’€ ํฌ๊ธฐ ์กฐ์ •
  • ํ†ต๊ณ„ ์ •๋ณด ์ฃผ๊ธฐ์  ๊ฐฑ์‹ 


์š”์•ฝ


๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ์˜ 90%๋Š” ์ธ๋ฑ์Šค์™€ ์ฟผ๋ฆฌ ์ตœ์ ํ™”์—์„œ ๊ฒฐ์ •๋œ๋‹ค.

๐Ÿ’Ž ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. ์ธ๋ฑ์Šค๋Š” ์ฑ…์˜ ์ƒ‰์ธ๊ณผ ๊ฐ™๋‹ค - ๋น ๋ฅธ ๊ฒ€์ƒ‰์˜ ํ•ต์‹ฌ
  2. B-Tree ์ธ๋ฑ์Šค๊ฐ€ ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ด๊ณ  ํšจ๊ณผ์ 
  3. ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋Š” ์ปฌ๋Ÿผ ์ˆœ์„œ๊ฐ€ ๋งค์šฐ ์ค‘์š”
  4. EXPLAIN์œผ๋กœ ์‹คํ–‰ ๊ณ„ํš ๋ฐ˜๋“œ์‹œ ํ™•์ธ
  5. ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค๋กœ ํ…Œ์ด๋ธ” ์ ‘๊ทผ ์ œ๊ฑฐ
  6. N+1 ๋ฌธ์ œ๋Š” JOIN์ด๋‚˜ IN์œผ๋กœ ํ•ด๊ฒฐ


๐Ÿš€ ์ตœ์ ํ™” ์šฐ์„ ์ˆœ์œ„:

1๋‹จ๊ณ„: EXPLAIN์œผ๋กœ ๋А๋ฆฐ ์ฟผ๋ฆฌ ์ฐพ๊ธฐ
2๋‹จ๊ณ„: WHERE/JOIN ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ƒ์„ฑ
3๋‹จ๊ณ„: ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋กœ ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค ๊ตฌ์„ฑ
4๋‹จ๊ณ„: N+1 ๋ฌธ์ œ ์ œ๊ฑฐ
5๋‹จ๊ณ„: ์ฟผ๋ฆฌ ๋ฆฌํŒฉํ† ๋ง (ํ•จ์ˆ˜ ์ œ๊ฑฐ, OR ์ œ๊ฑฐ)

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ:

  • ์ธ๋ฑ์Šค๋Š” ๋งŒ๋Šฅ์ด ์•„๋‹ˆ๋‹ค (์“ฐ๊ธฐ ์„ฑ๋Šฅ ์ €ํ•˜)
  • ๋ชจ๋“  ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ๋งŒ๋“ค์ง€ ๋ง ๊ฒƒ
  • ์ •๊ธฐ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ธ๋ฑ์Šค ์ œ๊ฑฐ
  • ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐฑ์‹ 


๐Ÿ“Š ์„ฑ๊ณผ ์ธก์ •:

์ข‹์€ ์ตœ์ ํ™”๋Š” ์ˆซ์ž๋กœ ์ฆ๋ช…๋œ๋‹ค.
Before/After ์‹คํ–‰ ์‹œ๊ฐ„์„ ๋ฐ˜๋“œ์‹œ ์ธก์ •ํ•˜๊ณ 
10๋ฐฐ ์ด์ƒ์˜ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜์ž.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™”๋Š” ํ•œ ๋ฒˆ์— ๋๋‚˜๋Š” ์ž‘์—…์ด ์•„๋‹ˆ๋‹ค.
๋ฐ์ดํ„ฐ๊ฐ€ ์Œ“์ด๊ณ  ํŠธ๋ž˜ํ”ฝ์ด ์ฆ๊ฐ€ํ•˜๋ฉด์„œ
๊ณ„์†ํ•ด์„œ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ๊ฐœ์„ ํ•ด์•ผ ํ•˜๋Š” ์ง€์†์ ์ธ ๊ณผ์ •์ด๋‹ค.