MySQL 인덱스 및 성능 최적화

MySQLBeginner
지금 연습하기

소개

이 실습에서는 MySQL 인덱스와 성능 최적화 기법에 대해 배우게 됩니다. 이 실습은 데이터베이스 쿼리 성능 향상을 위해 인덱스를 생성하고 관리하는 데 중점을 둡니다.

먼저 users 테이블을 생성하고 샘플 데이터를 삽입합니다. 그런 다음 username 열에 단일 열 인덱스를 생성하고 생성 확인 방법을 배웁니다. 또한 EXPLAIN을 사용하여 쿼리 계획을 분석하고, 다중 열 쿼리를 위한 복합 인덱스를 추가하고, 사용되지 않는 인덱스를 제거하여 데이터베이스 효율성을 유지하는 방법을 다룹니다.

테이블에 단일 열 인덱스 생성

이 단계에서는 MySQL 에서 단일 열 인덱스를 생성하는 방법을 배우게 됩니다. 인덱스는 데이터베이스 쿼리 성능을 향상시키는 데 매우 중요하며, 특히 대규모 테이블을 다룰 때 더욱 그렇습니다. 열에 대한 인덱스는 데이터베이스가 전체 테이블을 스캔하지 않고도 해당 열의 특정 값과 일치하는 행을 빠르게 찾을 수 있도록 합니다.

인덱스 이해하기

인덱스를 책의 색인과 같다고 생각하면 됩니다. 특정 주제를 찾기 위해 책 전체를 읽는 대신, 색인을 사용하여 관련 페이지를 빠르게 찾을 수 있습니다. 마찬가지로 데이터베이스 인덱스는 데이터베이스 엔진이 특정 행을 빠르게 찾는 데 도움이 됩니다.

테이블 생성

먼저 인덱스 생성을 시연하기 위해 users라는 간단한 테이블을 생성해 보겠습니다. LabEx VM 에서 터미널을 엽니다. 바탕 화면의 Xfce Terminal 바로 가기를 사용할 수 있습니다.

루트 사용자로 MySQL 서버에 연결합니다.

sudo mysql -u root

먼저 이 실습을 위한 데이터베이스를 생성하고 선택합니다.

CREATE DATABASE lab_db;
USE lab_db;

이제 users 테이블을 생성합니다.

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

이 SQL 문은 id, username, email, created_at 열을 가진 users라는 테이블을 생성합니다. id 열은 기본 키로 설정되고 자동 증가합니다.

users 테이블에 샘플 데이터를 삽입해 보겠습니다.

INSERT INTO users (username, email) VALUES
('john_doe', 'john.doe@example.com'),
('jane_smith', 'jane.smith@example.com'),
('peter_jones', 'peter.jones@example.com');

단일 열 인덱스 생성

이제 username 열에 인덱스를 생성해 보겠습니다. 이렇게 하면 사용자 이름으로 사용자를 검색하는 쿼리가 더 빨라집니다.

CREATE INDEX idx_username ON users (username);

이 문은 users 테이블의 username 열에 idx_username이라는 인덱스를 생성합니다.

인덱스 확인

SHOW INDEXES 명령을 사용하여 인덱스가 생성되었는지 확인할 수 있습니다.

SHOW INDEXES FROM users;

출력에는 방금 생성한 idx_username 인덱스를 포함하여 users 테이블의 인덱스에 대한 세부 정보가 표시됩니다. Key_nameidx_username이고 Column_nameusername인 행을 볼 수 있어야 합니다.

+-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| Table | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Ignored |
+-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| users |          0 | PRIMARY      |            1 | id          | A         |           3 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| users |          1 | idx_username |            1 | username    | A         |           3 |     NULL | NULL   |      | BTREE      |         |               | NO      |
+-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+

이제 테이블에 단일 열 인덱스를 성공적으로 생성했습니다. 이렇게 하면 인덱싱된 열을 기준으로 데이터를 필터링하는 쿼리의 성능이 향상됩니다.

MySQL index creation example

EXPLAIN 을 사용하여 쿼리 계획 분석

이 단계에서는 MySQL 의 EXPLAIN 문을 사용하여 쿼리 실행 계획을 분석하는 방법을 배우게 됩니다. 쿼리 계획을 이해하는 것은 성능 병목 현상을 식별하고 쿼리를 최적화하는 데 필수적입니다.

쿼리 계획이란 무엇인가요?

쿼리 계획은 데이터베이스 엔진이 쿼리를 실행하는 데 사용하는 로드맵입니다. 테이블에 액세스하는 순서, 사용되는 인덱스, 데이터를 검색하는 데 적용되는 알고리즘을 설명합니다. 쿼리 계획을 분석함으로써 데이터베이스가 쿼리를 어떻게 실행하는지 이해하고 개선할 부분을 식별할 수 있습니다.

EXPLAIN 문 사용하기

EXPLAIN 문은 MySQL 이 쿼리를 실행하는 방법에 대한 정보를 제공합니다. 관련된 테이블, 사용된 인덱스, 조인 순서 및 쿼리 성능을 이해하는 데 도움이 되는 기타 세부 정보를 보여줍니다.

이제 EXPLAIN을 사용하여 간단한 쿼리를 분석해 보겠습니다.

EXPLAIN SELECT * FROM users WHERE username = 'john_doe';

EXPLAIN 문의 출력은 여러 열로 구성된 테이블입니다. 다음은 가장 중요한 열에 대한 설명입니다.

  • id: SELECT 문의 ID 입니다.
  • select_type: SELECT 쿼리의 유형입니다 (예: SIMPLE, PRIMARY, SUBQUERY).
  • table: 액세스되는 테이블입니다.
  • type: 조인 유형입니다. 이것은 가장 중요한 열 중 하나입니다. 일반적인 값은 다음과 같습니다.
    • system: 테이블에 행이 하나만 있습니다.
    • const: 테이블에 일치하는 행이 최대 하나 있으며, 쿼리 시작 시 읽힙니다.
    • eq_ref: 이전 테이블의 행 조합에 대해 이 테이블에서 한 행이 읽힙니다. 이는 인덱싱된 열에 조인할 때 사용됩니다.
    • ref: 이전 테이블의 행 조합에 대해 이 테이블에서 일치하는 모든 행이 읽힙니다. 이는 인덱싱된 열에 조인할 때 사용됩니다.
    • range: 인덱스를 사용하여 지정된 범위 내의 행만 검색합니다.
    • index: 전체 인덱스 스캔이 수행됩니다.
    • ALL: 전체 테이블 스캔이 수행됩니다. 이것은 가장 비효율적인 유형입니다.
  • possible_keys: MySQL 이 테이블에서 행을 찾기 위해 사용할 수 있는 인덱스입니다.
  • key: MySQL 이 실제로 사용한 인덱스입니다.
  • key_len: MySQL 이 사용한 키의 길이입니다.
  • ref: 인덱스와 비교되는 열 또는 상수입니다.
  • rows: MySQL 이 쿼리를 실행하기 위해 검사해야 할 것으로 추정하는 행의 수입니다.
  • Extra: MySQL 이 쿼리를 실행하는 방법에 대한 추가 정보입니다. 일반적인 값은 다음과 같습니다.
    • Using index: 쿼리는 인덱스만 사용하여 만족될 수 있습니다.
    • Using where: MySQL 은 테이블에 액세스한 후 행을 필터링해야 합니다.
    • Using temporary: MySQL 은 쿼리를 실행하기 위해 임시 테이블을 생성해야 합니다.
    • Using filesort: MySQL 은 테이블에 액세스한 후 행을 정렬해야 합니다.

EXPLAIN 출력 해석하기

SELECT * FROM users WHERE username = 'john_doe' 쿼리의 경우 EXPLAIN 출력은 다음과 유사해야 합니다.

+------+-------------+-------+------+---------------+--------------+---------+-------+------+-----------------------+
| id   | select_type | table | type | possible_keys | key          | key_len | ref   | rows | Extra                 |
+------+-------------+-------+------+---------------+--------------+---------+-------+------+-----------------------+
|    1 | SIMPLE      | users | ref  | idx_username  | idx_username | 767     | const | 1    | Using index condition |
+------+-------------+-------+------+---------------+--------------+---------+-------+------+-----------------------+

이 예시에서:

  • typeref이며, 이는 MySQL 이 인덱스를 사용하여 일치하는 행을 찾고 있음을 의미합니다.
  • possible_keyskey 모두 idx_username을 표시하며, 이는 이전 단계에서 생성한 idx_username 인덱스를 사용하고 있음을 의미합니다.
  • rows1이며, 이는 MySQL 이 쿼리를 실행하기 위해 단 하나의 행만 검사하면 될 것으로 추정함을 의미합니다.

인덱스가 없는 쿼리 분석하기

이제 인덱스를 사용하지 않는 쿼리를 분석해 보겠습니다. 먼저 users 테이블에 city라는 새 열을 추가해 보겠습니다.

ALTER TABLE users ADD COLUMN city VARCHAR(255);

이제 city로 검색하는 쿼리에 대해 EXPLAIN을 실행해 보겠습니다. 아직 city 열에 데이터를 추가하지 않았으므로 행 중 하나를 업데이트해 보겠습니다.

UPDATE users SET city = 'New York' WHERE username = 'john_doe';

이제 EXPLAIN 문을 다시 실행합니다.

EXPLAIN SELECT * FROM users WHERE city = 'New York';

출력은 다음과 같을 수 있습니다.

+------+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id   | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+-------+------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 3    | Using where |
+------+-------------+-------+------+---------------+------+---------+------+------+-------------+

이 예시에서:

  • typeALL이며, 이는 MySQL 이 전체 테이블 스캔을 수행하고 있음을 의미합니다.
  • possible_keyskey는 모두 NULL이며, 이는 MySQL 이 어떤 인덱스도 사용하고 있지 않음을 의미합니다.
  • rows3이며, 이는 MySQL 이 쿼리를 실행하기 위해 테이블의 모든 3 개 행을 검사해야 할 것으로 추정함을 의미합니다.
  • ExtraUsing where를 표시하며, 이는 MySQL 이 테이블에 액세스한 후 행을 필터링해야 함을 의미합니다.

이는 쿼리가 최적화되지 않았으며 city 열에 대한 인덱스의 이점을 얻을 수 있음을 나타냅니다.

MySQL EXPLAIN query plan output

다중 열 쿼리를 위한 복합 인덱스 추가

이 단계에서는 MySQL 에서 복합 인덱스를 생성하는 방법을 배우게 됩니다. 복합 인덱스는 테이블의 두 개 이상의 열에 대한 인덱스입니다. 여러 열을 기준으로 데이터를 필터링하는 쿼리의 성능을 크게 향상시킬 수 있습니다.

복합 인덱스란 무엇인가요?

복합 인덱스는 여러 열을 포함하는 인덱스입니다. 쿼리에서 WHERE 절에 여러 열을 자주 사용하는 경우 유용합니다. 복합 인덱스에서 열의 순서는 중요합니다. 쿼리의 WHERE 절에 열이 동일한 순서로 지정될 때 인덱스가 가장 효과적입니다.

다른 도시를 포함하여 users 테이블에 더 많은 데이터를 추가해 보겠습니다.

INSERT INTO users (username, email, city) VALUES
('alice_brown', 'alice.brown@example.com', 'Los Angeles'),
('bob_davis', 'bob.davis@example.com', 'Chicago'),
('charlie_wilson', 'charlie.wilson@example.com', 'New York'),
('david_garcia', 'david.garcia@example.com', 'Los Angeles');

복합 인덱스 생성

cityusername 모두로 사용자를 필터링하는 쿼리를 자주 실행한다고 가정해 보겠습니다. 이 경우 cityusername 열에 복합 인덱스를 생성할 수 있습니다.

CREATE INDEX idx_city_username ON users (city, username);

이 문은 users 테이블의 cityusername 열에 idx_city_username이라는 인덱스를 생성합니다.

인덱스 확인

SHOW INDEXES 명령을 사용하여 인덱스가 생성되었는지 확인할 수 있습니다.

SHOW INDEXES FROM users;

출력에는 방금 생성한 idx_city_username 인덱스를 포함하여 users 테이블의 인덱스에 대한 세부 정보가 표시됩니다. idx_city_username에 대해 city 열과 username 열 각각에 대한 두 개의 행이 표시되어야 합니다.

+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| Table | Non_unique | Key_name          | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Ignored |
+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| users |          0 | PRIMARY           |            1 | id          | A         |           7 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| users |          1 | idx_username      |            1 | username    | A         |           7 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| users |          1 | idx_city_username |            1 | city        | A         |           7 |     NULL | NULL   | YES  | BTREE      |         |               | NO      |
| users |          1 | idx_city_username |            2 | username    | A         |           7 |     NULL | NULL   |      | BTREE      |         |               | NO      |
+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+

복합 인덱스 사용하기

복합 인덱스의 이점을 확인하려면 EXPLAIN 명령을 사용하여 WHERE 절에 cityusername 열을 모두 사용하는 쿼리를 분석할 수 있습니다.

EXPLAIN SELECT * FROM users WHERE city = 'New York' AND username = 'charlie_wilson';

EXPLAIN 출력은 쿼리가 idx_city_username 인덱스를 사용하고 있음을 보여줍니다. 이는 데이터베이스가 전체 테이블을 스캔하지 않고 일치하는 행을 빠르게 찾을 수 있음을 의미합니다. 출력에서 possible_keyskey 열을 확인하십시오. 인덱스가 사용되고 있다면 이 열에서 idx_city_username을 볼 수 있습니다.

+------+-------------+-------+------+--------------------------------+--------------+---------+-------+------+------------------------------------+
| id   | select_type | table | type | possible_keys                  | key          | key_len | ref   | rows | Extra                              |
+------+-------------+-------+------+--------------------------------+--------------+---------+-------+------+------------------------------------+
|    1 | SIMPLE      | users | ref  | idx_username,idx_city_username | idx_username | 767     | const | 1    | Using index condition; Using where |
+------+-------------+-------+------+--------------------------------+--------------+---------+-------+------+------------------------------------+

인덱스에서 열의 순서

복합 인덱스에서 열의 순서는 중요합니다. (city, username) 대신 (username, city)에 인덱스를 생성하면 city로 필터링한 다음 username으로 필터링하는 쿼리에 대해 인덱스가 덜 효과적입니다.

예를 들어, (username, city)에 인덱스가 있고 다음 쿼리를 실행한다고 가정해 보겠습니다.

EXPLAIN SELECT * FROM users WHERE city = 'New York' AND username = 'charlie_wilson';

MySQL 은 인덱스를 사용하지 않거나 부분적으로만 사용할 수 있습니다. 왜냐하면 city 열이 인덱스의 선행 열이 아니기 때문입니다.

MySQL composite index example

사용하지 않는 인덱스 제거

이 단계에서는 MySQL 에서 사용되지 않는 인덱스를 제거하는 방법을 배우게 됩니다. 인덱스는 쿼리 성능을 크게 향상시킬 수 있지만, 쓰기 작업 (삽입, 업데이트, 삭제) 에 오버헤드를 추가하기도 합니다. 따라서 더 이상 사용되지 않는 인덱스를 식별하고 제거하는 것이 중요합니다.

사용되지 않는 인덱스를 제거하는 이유는 무엇인가요?

사용되지 않는 인덱스는 디스크 공간을 차지하고 쓰기 작업을 느리게 할 수 있습니다. 테이블의 데이터가 수정될 때마다 데이터베이스 엔진은 해당 테이블의 모든 인덱스도 업데이트해야 합니다. 인덱스가 어떤 쿼리에서도 사용되지 않는다면 불필요한 오버헤드만 추가하는 것입니다.

이전 단계에서 username 열에 idx_username이라는 인덱스를 생성했습니다. 쿼리 패턴을 분석한 후 이 인덱스가 더 이상 사용되지 않는다고 판단한다고 가정해 보겠습니다.

인덱스 제거

idx_username 인덱스를 제거하려면 DROP INDEX 문을 사용할 수 있습니다.

DROP INDEX idx_username ON users;

이 문은 users 테이블에서 idx_username 인덱스를 제거합니다.

인덱스 제거 확인

SHOW INDEXES 명령을 사용하여 인덱스가 제거되었는지 확인할 수 있습니다.

SHOW INDEXES FROM users;

출력에는 users 테이블의 인덱스에 대한 세부 정보가 표시됩니다. 출력에서 idx_username 인덱스가 더 이상 표시되지 않아야 합니다.

+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| Table | Non_unique | Key_name          | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Ignored |
+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| users |          0 | PRIMARY           |            1 | id          | A         |           7 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| users |          1 | idx_city_username |            1 | city        | A         |           7 |     NULL | NULL   | YES  | BTREE      |         |               | NO      |
| users |          1 | idx_city_username |            2 | username    | A         |           7 |     NULL | NULL   |      | BTREE      |         |               | NO      |
+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+

사용되지 않는 인덱스 식별

실제 시나리오에서는 사용되지 않는 인덱스를 식별하는 것이 어려울 수 있습니다. MySQL 은 이 작업을 돕기 위해 여러 도구와 기술을 제공합니다.

  • MySQL Enterprise Audit: 이 기능을 사용하면 서버에서 실행된 모든 쿼리를 기록할 수 있습니다. 그런 다음 쿼리 로그를 분석하여 어떤 인덱스가 사용되고 있는지 식별할 수 있습니다.
  • Performance Schema: Performance Schema 는 인덱스 사용을 포함하여 서버 성능에 대한 자세한 정보를 제공합니다.
  • 타사 도구: 여러 타사 도구를 사용하여 인덱스 사용을 모니터링하고 사용되지 않는 인덱스를 식별할 수 있습니다.

정기적으로 인덱스 사용을 모니터링하고 사용되지 않는 인덱스를 제거함으로써 데이터베이스의 전반적인 성능을 향상시킬 수 있습니다.

이제 모든 단계를 완료했으므로 MySQL 콘솔을 종료합니다.

exit;

MySQL users table index removal example

요약

이 실습에서는 쿼리 성능을 향상시키기 위해 MySQL 에서 단일 열 인덱스를 생성하는 방법을 배웠습니다. 또한 EXPLAIN 문을 사용하여 쿼리 실행 계획을 분석하고 성능 병목 현상을 식별하는 방법을 배웠습니다. 또한 다중 열 쿼리를 위한 복합 인덱스를 생성하고 데이터베이스 효율성을 유지하기 위해 사용되지 않는 인덱스를 제거하는 연습을 했습니다. 인덱스를 효과적으로 이해하고 활용하는 것은 데이터베이스 성능 최적화를 위한 기본적인 기술입니다.