
MySQL 아키텍처 개요
┌─────────────────────────────────────┐
│ Client (JDBC, CLI, etc.) │
└──────────────┬──────────────────────┘
↓
┌─────────────────────────────────────┐
│ MySQL Server Layer │
│ ├─ Connection Handler │
│ ├─ SQL Parser │
│ ├─ Optimizer │
│ ├─ Query Cache (8.0에서 제거됨) │
│ └─ Query Executor │
└──────────────┬──────────────────────┘
↓
┌─────────────────────────────────────┐
│ Storage Engine Layer │
│ ├─ InnoDB (주로 사용) │
│ ├─ MyISAM │
│ └─ Memory, etc. │
└─────────────────────────────────────┘
SQL 실행과정 개요(Write)
INSERT INTO users (name, email) VALUES ('Chu', 'Chu@test.com');
실행 단계:
1. Connection Handler
↓ 인증 & 권한 확인
2. Query Parser
↓ SQL 문법 분석 (Syntax Tree 생성)
3. Preprocessor
↓ 테이블/컬럼 존재 여부, 권한 재확인
4. Optimizer
↓ 실행 계획 수립 (Write는 단순)
5. Storage Engine (InnoDB)
↓ a. Undo Log 작성 (롤백 대비)
↓ b. Buffer Pool에서 데이터 페이지 찾기/로드
↓ c. 데이터 수정 (Dirty Page 생성)
↓ d. Redo Log 작성 (크래시 복구용)
6. Transaction Commit
↓ Redo Log를 디스크에 플러시
7. Background Thread
↓ 나중에 Dirty Page를 디스크에 기록
실제동작
mysql -h localhost -u root -p
SQL 문자열을 MySQL이 이해할 수 있는 구조로 변환
// 입력
SELECT id, name FROM users WHERE age > 23;
// step1: Lexical Analysis (어휘 분석) -> 토큰 분리
[SELECT] [id] [,] [name] [FROM] [users] [WHERE] [age] [>] [23] [;]
// step2: Syntac Analysis(구문 분석) -> 문법 규칙 확인
SELECT_STATEMENT
- SELECT_LIST
- COLUMN : id
- COLUMN : name
- FROM_CLAUSE
- TABLE: users
- WHERE_CLAUSE
- CONDITION: age > 23
// step3: Parse Tree 생성
Query
|
SELECT_STMT
/ | \
FIELDS FROM WHERE
/ \ | |
id name users age>23
→ 잘못된 SQL 문은 Parser 가 체크
→ 존재하지 않는 테이블 → Parser 통과 → Preprocessor에서 에러 발생!
어떻게 실행할 것인가? 를 결정
판단 기준
Optimizer가 사용하는 정보
- 통계정보 -> ANALYZE TABLE users;
- 확인 가능한 통계 -> SHOW TABLE STATUS LIKE ‘users’;
- 인덱스 고유값 개수 -> SHOW INDEX FROM users;
Optimizer가 하는 결정들 예시
SELECT * FROM users WHERE age = 23 AND city = 'Seoul'
→ city는 몇개 안되고 age는 많으므로 idx_city로 좁힌 후 age 필터링
SELECT *
FROM users U
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id
WHERE u.age > 30
Optimizer의 선택
→ orders 부터 하면 전체 100만 행 부터 처리 해야할수도 있음
SELECT *
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);
// Optimizer 변환
SELECT u.*
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.total > 1000;
Optimizer가 만든 실행계획을 실제로 수행 → Storage Engine API를 호출하여 데이터 처리
Storage Engine에 요청: idx_age 인덱스
handler → index_init(idx_age)
age = 25 인 행 찾아줘
handler → index_read(key = 25)
다음행 있음?
handler → index_next()
없으면 중단
→ 종료
모든 행을 클라이언트에 전송
SELECT name, email
FROM users
WHERE age = 23
ORDER BY created_at DESC
LIMIT 10;
INSERT INTO users(name, email, age)
VALUES ('Chu', 'Chu@test.com', 23);
1~3. Connection, Parse, Preprocess는 SELECT와 동일
Optimizer
InnoDB 트랜잭션 시작
Undo Log 작성 (롤백 대비)
Buffer Pool에서 페이지 찾기
데이터 페이지에 행 삽입
인덱스 업데이트
Redo Log 작성 (WAL(디스크 쓰기 전 로그부터 쓰는것) 방식)
COMMIT 처리
Background 처리
Dirty Page Flush
Undo Log 정리
SET profiling = 1;
Profiling vs Performance Schema
세션 레벨로 동작 (SET profiling = 1)
실행된 쿼리와 각 단계별 시간 기록
SHOW PROFILES, SHOW PROFILE FOR QUERY n; 확인
장점
설정 간단
바로바로 눈으로 보기 쉬움
학습용으로 적합
performance-schema 데이터베이스 내 여러 테이블을 통해 쿼리 실행 정보를 수집
MySQL이 내부적으로 쿼리 실행의 모든 단계를 event로 추적
장점
운영 환경에서도 사용 가능
쿼리 외에도 IO, CPU, 메모리 Lock 등 전부 추적 가능
MySQL Workbench, Grafana 등 외부 툴이 이를 기반으로 시각화
단점
초반에 데이터 구조가 복잡하게 느껴짐
기본적으로 꺼져 있을 수도 있어서 활성화 설정 필요
-- 테이블 생성
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
email VARCHAR(100),
age INT,
INDEX idx_age (age)
) ENGINE=InnoDB;
-- 10만 건 데이터 생성 프로시저
DELIMITER $$
CREATE PROCEDURE gen_data(IN num INT)
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= num DO
INSERT INTO users VALUES (
i,
CONCAT('user', i),
CONCAT('user', i, '@test.com'),
20 + (i % 50)
);
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
-- 생성 (30초 소요)
CALL gen_data(100000);
DELIMITER $$ - 문장 구분자 변경 (MySQL이 ; 과 혼동하는 것을 방지CREATE PROCEDURE - 프로시저 정의 시작DECLARE - 변수 선언WHILE - 반복문SET - 변수값 변경CALL gen_data(100000) - 프로시저 실행SELECT COUNT(*) FROM users WHERE name = 'user50000';
SELECT COUNT(*) FROM users WHERE age = 25;
| Query_ID | Duration | Query
| 100006 | 0.02376075 | SELECT COUNT(*) FROM users WHERE name = 'user50000' |
| 100007 | 0.00139775 | SELECT COUNT(*) FROM users WHERE age = 25
EXPLAIN SELECT * FROM users WHERE name = 'user50000';
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | users | NULL | ALL | NULL | NULL | NULL | NULL | 99858 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
EXPLAIN SELECT * FROM users WHERE age = 25;
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | users | NULL | ref | idx_age | idx_age | 5 | const | 2000 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
Type
성능 좋은순
Key
실제 사용된 인덱스 이름
rows
검사할 예상 행 수
Extra
빠름
Using index ← 커버링 인덱스
NULL ← 깔끔
느림
Using filesort ← 디스크 정렬
Using temporary ← 임시 테이블
Using where ← 추가 필터 (1번 쿼리)
select_type
possible_keys
사용가능한 인덱스 목록
SHOW ENGINE INNODB STATUS\G
Log sequence number 66135635
Log buffer assigned up to 66135635
Log buffer completed up to 66135635
Log written up to 66135635
Log flushed up to 66135635
Added dirty pages up to 66135635
Pages flushed up to 66135635
Last checkpoint at 66135635Log sequence number ← 현재까지 쓴 로그
Log flushed up to ← 디스크에 쓴 로그
Last checkpoint at ← 실제 데이터 쓴 시점
→ 셋이 같은 경우 Drity Page 없음
Buffer Pool
Total large memory allocated 0
Dictionary memory allocated 554450
Buffer pool size 8191 <- 전체 8191 페이지
Free buffers 6329 <- 비어있음: 6329
Database pages 1856 <- 사용중 1856
Old database pages 665
Modified db pages 0 <- Dirty Page: 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 1088, created 768, written 2488
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1856, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
ROW OPERATIONS
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1, Main thread ID=281472078507776 , state=sleeping
Number of rows inserted 100000, updated 0, deleted 0, read 206000
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
Number of system rows inserted 31, updated 348, deleted 8, read 5152
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
INSERT INTO users VALUES (100001, 'test', 'test@test.com', 30);
INSERT INTO users VALUES
(200001, 'a', 'a@test.com', 30),
(200002, 'b', 'b@test.com', 31),
(200003, 'c', 'c@test.com', 32);
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 100012 | 0.00925475 | INSERT INTO users VALUES (100001, 'test', 'test@test.com', 30) |
| 100013 | 0.00507650 | INSERT INTO users VALUES (200001, 'a', 'a@test.com', 30),
(200002, 'b', 'b@test.com', 31),
(200003, 'c', 'c@test.com', 32)