Theme:

MySQL 5.7에서 할 수 없었던 것 중 8.0에서 가능해진 것들은 무엇일까요? 단순한 버전 업이 아니라 SQL 작성 방식 자체가 달라지는 변화가 있었습니다.

MySQL 8.0 개요

MySQL 8.0은 2018년 출시 이후 가장 큰 변화를 가져온 메이저 버전입니다. 다른 RDBMS에서는 이미 제공하던 기능들이 드디어 MySQL에 추가되었습니다.

Window Function (윈도우 함수)

GROUP BY 없이 집계 결과를 각 행에 붙일 수 있는 기능입니다.

기본 문법

SQL
함수() OVER (
    PARTITION BY 그룹_컬럼
    ORDER BY 정렬_컬럼
    ROWS/RANGE 프레임_지정
)

ROW_NUMBER — 행 번호 매기기

SQL
-- 부서별로 급여 순위 매기기
SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept
FROM employees;
PLAINTEXT
| name   | department | salary | rank_in_dept |
|--------|-----------|--------|-------------|
| 김철수  | 개발       | 6000   | 1           |
| 이영희  | 개발       | 5500   | 2           |
| 박민수  | 개발       | 5000   | 3           |
| 정수진  | 마케팅     | 5800   | 1           |
| 최진우  | 마케팅     | 5200   | 2           |

RANK와 DENSE_RANK

SQL
SELECT
    name,
    salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
    RANK() OVER (ORDER BY salary DESC) AS ranking,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_ranking
FROM employees;
PLAINTEXT
| name | salary | row_num | ranking | dense_ranking |
|------|--------|---------|---------|--------------|
| A    | 6000   | 1       | 1       | 1            |
| B    | 6000   | 2       | 1       | 1            |
| C    | 5500   | 3       | 3       | 2            |  ← RANK: 3, DENSE_RANK: 2
| D    | 5000   | 4       | 4       | 3            |
  • ROW_NUMBER(): 항상 고유한 번호
  • RANK(): 동일 값은 같은 순위, 다음 순위를 건너뜀 (1, 1, 3)
  • DENSE_RANK(): 동일 값은 같은 순위, 건너뛰지 않음 (1, 1, 2)

LAG와 LEAD — 이전/다음 행 참조

SQL
-- 이전 달 대비 매출 비교
SELECT
    month,
    revenue,
    LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
    revenue - LAG(revenue, 1) OVER (ORDER BY month) AS diff
FROM monthly_revenue;
PLAINTEXT
| month   | revenue | prev_month_revenue | diff  |
|---------|---------|-------------------|-------|
| 2025-01 | 1000    | NULL              | NULL  |
| 2025-02 | 1200    | 1000              | 200   |
| 2025-03 | 1100    | 1200              | -100  |

SUM/AVG 등 집계 함수를 윈도우로

SQL
-- 누적 합계
SELECT
    date,
    amount,
    SUM(amount) OVER (ORDER BY date) AS running_total
FROM orders;

-- 이동 평균 (최근 3건)
SELECT
    date,
    amount,
    AVG(amount) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg
FROM orders;

CTE (Common Table Expression)

WITH 절로 임시 결과셋에 이름을 붙여 사용합니다.

기본 CTE

SQL
-- 서브쿼리 버전 (읽기 어려움)
SELECT * FROM (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees GROUP BY department
) dept_avg
WHERE avg_salary > 5000;

-- CTE 버전 (읽기 쉬움)
WITH dept_avg AS (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
)
SELECT * FROM dept_avg WHERE avg_salary > 5000;

여러 CTE 연결

SQL
WITH
active_users AS (
    SELECT id, name FROM users WHERE status = 'active'
),
user_orders AS (
    SELECT user_id, COUNT(*) AS order_count
    FROM orders
    GROUP BY user_id
)
SELECT
    u.name,
    COALESCE(o.order_count, 0) AS orders
FROM active_users u
LEFT JOIN user_orders o ON u.id = o.user_id;

재귀 CTE (Recursive CTE)

계층 구조 데이터를 하나의 쿼리로 조회합니다.

SQL
-- 조직도 테이블
CREATE TABLE org (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    manager_id INT
);

-- CEO부터 모든 하위 직원 조회
WITH RECURSIVE org_tree AS (
    -- 시작점 (Anchor)
    SELECT id, name, manager_id, 0 AS depth
    FROM org WHERE manager_id IS NULL

    UNION ALL

    -- 재귀 (하위 조직원 탐색)
    SELECT o.id, o.name, o.manager_id, t.depth + 1
    FROM org o
    JOIN org_tree t ON o.manager_id = t.id
)
SELECT CONCAT(REPEAT('  ', depth), name) AS org_chart
FROM org_tree;
PLAINTEXT
CEO
  VP Engineering
    Team Lead A
      Developer 1
      Developer 2
    Team Lead B
  VP Marketing
    Marketing Manager

Histogram 통계

인덱스가 없는 컬럼의 데이터 분포를 옵티마이저에게 알려주는 기능입니다.

SQL
-- Histogram 생성
ANALYZE TABLE orders UPDATE HISTOGRAM ON status WITH 100 BUCKETS;

-- Histogram 확인
SELECT
    SCHEMA_NAME,
    TABLE_NAME,
    COLUMN_NAME,
    JSON_EXTRACT(HISTOGRAM, '$.\"histogram-type\"') AS type,
    JSON_EXTRACT(HISTOGRAM, '$.\"number-of-buckets-specified\"') AS buckets
FROM information_schema.COLUMN_STATISTICS;

-- Histogram 삭제
ANALYZE TABLE orders DROP HISTOGRAM ON status;

Histogram이 있으면 옵티마이저가 WHERE status = 'active'의 선택도를 정확히 추정할 수 있습니다.

역할 기반 권한 (Roles)

MySQL 8.0부터 역할(Role)을 사용하여 권한 관리를 체계화할 수 있습니다.

SQL
-- 역할 생성
CREATE ROLE 'read_only', 'read_write', 'admin';

-- 역할에 권한 부여
GRANT SELECT ON mydb.* TO 'read_only';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'read_write';
GRANT ALL PRIVILEGES ON mydb.* TO 'admin';

-- 사용자에게 역할 부여
GRANT 'read_only' TO 'analyst'@'%';
GRANT 'read_write' TO 'app_user'@'10.0.%';

-- 기본 역할 설정
SET DEFAULT ROLE 'read_only' TO 'analyst'@'%';

기타 주요 변경점

기본 문자셋 변경

SQL
-- 5.7: latin1 → 8.0: utf8mb4
SHOW VARIABLES LIKE 'character_set_server';  -- utf8mb4
SHOW VARIABLES LIKE 'collation_server';       -- utf8mb4_0900_ai_ci

기본 인증 플러그인 변경

SQL
-- 5.7: mysql_native_password
-- 8.0: caching_sha2_password
-- 호환성 문제 시 변경 가능
ALTER USER 'user'@'%' IDENTIFIED WITH mysql_native_password BY 'password';

원자적 DDL

MySQL 8.0부터 DDL(CREATE, ALTER, DROP)이 원자적으로 실행됩니다. DDL 도중 서버가 죽어도 반쯤 실행된 상태가 되지 않습니다.

SQL
-- 8.0에서는 이 명령이 원자적
DROP TABLE IF EXISTS t1, t2, t3;
-- 모두 삭제되거나, 하나도 삭제되지 않음

Invisible Index

인덱스를 삭제하지 않고 옵티마이저에게 보이지 않게 할 수 있습니다.

SQL
-- 인덱스를 보이지 않게
ALTER TABLE orders ALTER INDEX idx_status INVISIBLE;

-- 성능 테스트 후 다시 보이게
ALTER TABLE orders ALTER INDEX idx_status VISIBLE;

인덱스를 삭제했다가 다시 만드는 것은 시간이 오래 걸리므로, 먼저 INVISIBLE로 테스트할 수 있습니다.

Descending Index

SQL
-- 5.7에서는 DESC가 무시됨
-- 8.0에서는 실제로 내림차순 인덱스 생성
CREATE INDEX idx_created ON orders (created_at DESC);

-- 복합 인덱스에서도 방향 혼합 가능
CREATE INDEX idx_mixed ON orders (customer_id ASC, created_at DESC);

NOWAIT와 SKIP LOCKED

SQL
-- 락을 기다리지 않고 즉시 에러
SELECT * FROM orders WHERE id = 1 FOR UPDATE NOWAIT;

-- 잠긴 행을 건너뜀
SELECT * FROM orders WHERE status = 'pending'
FOR UPDATE SKIP LOCKED LIMIT 10;

JSON 기능 강화

SQL
-- JSON 값에 대한 집계
SELECT JSON_ARRAYAGG(name) FROM users WHERE department = '개발';
-- 결과: ["김철수", "이영희", "박민수"]

SELECT JSON_OBJECTAGG(name, salary) FROM users WHERE department = '개발';
-- 결과: {"김철수": 6000, "이영희": 5500, "박민수": 5000}

-- JSON 테이블 함수
SELECT *
FROM JSON_TABLE(
    '[{"name": "A", "score": 90}, {"name": "B", "score": 85}]',
    '$[*]' COLUMNS (
        name VARCHAR(10) PATH '$.name',
        score INT PATH '$.score'
    )
) AS jt;

정리

  • Window Function: GROUP BY 없이 집계 결과를 각 행에 붙이는 기능. ROW_NUMBER, RANK, LAG 등
  • CTE: WITH 절로 서브쿼리에 이름을 붙여 가독성을 높이고, 재귀 CTE로 계층 구조를 조회
  • Histogram: 인덱스 없는 컬럼의 데이터 분포를 옵티마이저에 제공
  • 역할(Roles): 권한 관리를 체계화
  • 그 외에도 원자적 DDL, Invisible Index, Descending Index 등 실무에서 유용한 기능이 다수 추가되었습니다
댓글 로딩 중...