RealMySQL 책을 읽고 노션에 정리한 글을 옮긴 것입니다.
MySQL 5.6 버전부터는 통계정보를 휘발성이 아닌 영구적으로 보관할 수 있도록 개선되었다.
테이블을 생성할 때는 STATS_PERSISTENT 옵션을 통해서 테이블 단위의 영구적인 통계 정보를 보관할 지 말지를 결정할 수 있다.
mysql> CREATE TABLE tab_test (fd1 INT, fd2 VARCHAR(20), PRIMARY KEY(fd1))
ENGINE = InnoDB STATS_PERSISTENT={ DEFAULT | 0 | 1 }
STATS_PERSISTENT=0 : 테이블 통계 정보를 5.5 이번방식대로 관리함, 메모리로 관리
STATS_PERSISTENT=1 : 테이블 통계 정보를 innodb_index_status, innodb_table_stats 테이블로 관리함
STATS_PERSISTNET=DEFAULT : innodb_stats_persistent 시스템 변수에 의존한다.
MySQL 5.5 이전 버전에서는 이벤트가 발생하면 자동으로 통계 정보가 갱신되었다.
메모리에 있는 통계정보가 자동으로 업데이트 되는 것을 막기위해 innodb_stats_auto_recalc
시스템 변수를 통해 조정할 수 있다.
innodb_stats_transient_sample_pages
innodb_stats_persistent_sample_pages
SQL 8.0 버전에서 히스토그램은 자동으로 수집되지 않고 수동으로 ANALYZIE TABLE ... UPDATE HISTOGRAM
명령어를 통해서 수집된다.
MySQL 8.0 버전에서는 2종류의 히스토그램 타입이 지원된다.
Singletom(싱글톤 히스토그램) : 칼럼값 개별로 레코드 건수를 관리하는 히스토그램으로, Value-Based 히스토그램 또는 도수 분포라고도 불린다.
싱글톤 히스토그램은유니크한 값이 상대적으로 적을 경우 사용한다.
Equi-Height(높이 균형 히스토그램) : 칼럼값의 범위를 균등한 개수로 구분해서 관리하는 히스토그램으로, Height-Balanced 히스토그램이라고도 불린다.
레코드 건수 비율이 누적되어 표시된다. 그래프 기울기가 비슷하게 증가하므로 비슷한 값을 가지는것을 알 수 있다.
information_schema.column_statistics 테이블의 HISTOGRAM 칼럼이 가진 필드들의 의미
- sampling-rate : 히스토그램 정보를 수집하기 위해 스캔한 페이지의 비율을 저장한다. 샘플링 비율이 0.35라면 전체 데이터 페이지의 35%를 스캔해서 이 정보가 수집됐다는 것을 의미한다.
- histogram-type : 히스토그램의 종류를 저장한다.
- number-of-buckets-specified : 히스토그램을 생성할 때 설정했던 버킷의 개수를 저장한다. 히스토그램을 생성할 때 별도의 버킷의 개수를 지정하지 않았다면 기본으로 100개의 버킷이 사용된다.
mysql> ANALYZE TABLE employees.employees DROP HISTOGRAM ON gender, hire_date;
히스토그램 삭제시 테이블을 참조하지 않고 딕셔너리 내용만 삭제하지만, 히스토그램을 삭제하면 실행계획이 변경될 수 있다.
히스토그램을 삭제하지 않고 히스토그램을 사용하지 않게 하는 방법
mysql> SET GLOBAL optimizer_switch='condition_fanout_filter=off';
히스토그램이 사용되기 이전에는 인덱스의 분포에 상관없이 동일하다고 생각을 하고 실행계획을 작성했다.
예를 들어, 테이블 레코드가 1000건이고 어떤 칼럼의 유니크한 값의 개수가 100개 라면 히스토그램을 사용하지 않았을때 동등 비교 검색으로 대략 10개의 레코드가 일치할 것이라 예상한다.
하지만 데이터는 특정값을 골고루 분포하지 않는다.
인덱스 다이브(Index Dive)
- 쿼리 옵티마이저가 인덱스를 조사(스캔) 하면서 쿼리 실행 계획을 최적화하는 과정에서 사용되는 기법이다.
- 옵티마이저는 특정 인덱스를 사용할때 해당 인덱스의 일부만 샘플링(Sampling)하여 데이터 분포를 추정하고, 이를 기반으로 실행비용을 계산한다.
mysql> SELECT * FROM employees WHERE first_name='Tonny' AND birth_date
BETWEEN '1954-01-01' AND '1955-01-01'
옵티마이저는 이 쿼리를 테이블 풀 스캔을 사용할지 first_name 의 인덱스를 사용할지 고민한다.
fisrt_name 에 히스토그램이 있다고 하더라도 SQL 8.0 서버에서는 인덱스 다이브를 선호한다.
실제 인덱스를 샘플링해서 진행하므로 히스토그램보다 더 정확한 통계를 가지고 있다는 이유때문이다.
MySQL 실행 계획은 DESC
또는 EXPLAIN
명령으로 확인할 수 있다.
EXPLAIN ANALYZE
Index lookup on s using PRIMARY (emp_no=e.emp_no) (cost=0.98 rows=10)
(actual time=0.007..0.009 rows=10 loops=233)
mysql> EXPLAIN FORMAT=TREE
SELECT * FROM dept_emp de
WHERE de.emp_no = (SELECT e.emp_no FROM employees e WHERE e.first_name='Georgi'
AND e.last_name='Facello' LIMIT 1);
+----+-------------+--------+---------+-------------------+------------+-------------+
| id | select_type | table | type | key | rows | Extra |
+----+-------------+--------+---------+-------------------+------------+-------------+
| 1 | PRIMARY | de | ref | ix_empno_fromdate | 1 | Using where |
| 2 | SUBQUERY | e | ref | ix_firstname | 253 | Using where |
+----+-------------+--------+---------+-------------------+------------+-------------+
-> Filter : (de.emp_no = (select #2)) (cost=1.10 rows=1)
-> Index lookup on de using ix_empno_fromdate (emp_no=(select #2)) (cost=1.10 rows=1)
-> Select #2 (subquery in condition; run only once)
-> Limit: 1 row(s)
-> Filter: (e.last_name = 'Facello') (cost=70.49 rows=25)
-> Index lookup on e using ix_firstname (first_name='Georgi') (cost=70.49 rows=253)
mysql> EXPLAIN
SELECT * FROM (
(SELECT emp_no FROM employees e1 LIMIT 10) UNION ALL
(SELECT emp_no FROM employees e2 LIMIT 10) UNION ALL
(SELECT emp_no FROM employees e3 LIMIT 10) ) tb;
+----+-------------+------------+-------+-------------+------+--------+-------------+
| id | select_type | table | type | key | ref | rows | Extra |
+----+-------------+------------+-------+-------------+------+--------+-------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | 30 | NULL |
| 2 | DERIVED | e1 | index | ix_hiredate | NULL | 300252 | Using index |
| 3 | UNION | e2 | index | ix_hiredate | NULL | 300252 | Using index |
| 4 | UNION | e3 | index | ix_hiredate | NULL | 300252 | Using index |
+----+-------------+------------+-------+-------------+------+--------+-------------+
mysql> EXPLAIN
SELECT e.first_name,
(SELECT COUNT(*)
FROM dept_emp de, dept_manager dm
WHERE dm.dept_no=dept_no AND de.emp_no=e.emp_no) AS cnt
FROM employees e
WHERE e.first_name='Matt';
+-----+--------------------+-------+------+-------------------+------+-------------+
| id | select_type | table | type | key | rows | Extra |
+-----+--------------------+-------+------+-------------------+------+-------------+
| 1 | PRIMARY | e | ref | ix_fristname | 233 | Using index |
| 2 | DEPENDENT SUBQUERY | de | ref | ix_empno_fromdate | 1 | Using index |
| 3 | DEPENDENT SUBQUERY | dm | ref | PRIMARY | 2 | Using index |
+-----+--------------------+-------+------+-------------------+------+-------------+
mysql> EXPLAIN
SELECT * FROM dept_emp WHERE dept_no='d005';
+-----+-------------+-----------+--------+---------+------------+-------------+
| id | select_type | table | type | key | key_len | ref |
+-----+-------------+-----------+--------+---------+------------+-------------+
| 1 | SIMPLE | dept_emp | ref | PRIMARY | 16 | const |
+-----+-------------+-----------+--------+---------+------------+-------------+
mysql> EXPLAIN
SELECT * FROM departments
WHERE dept_no IN (SELECT dept_no FROM dept_emp WHERE emp_no=10001);
+----+--------------------+-------------+-----------------+-------------+---------+
| id | select_type | table | type | key | key_len |
+----+--------------------+-------------+-----------------+-------------+---------+
| 1 | PRIMARY | departments | index | ux_deptname | 162 |
| 2 | DEPENDENT SUBQUERY | dept_emp | unique_subquery | PRIMARY | 20 |
+----+--------------------+-------------+-----------------+-------------+---------+
mysql> EXPLAIN
SELECT DISTINCT d.dept_no
FROM departments d, dept_emp de WHERE de.dept_no=d.dept_no;
+----+-------------+-------+-------+-------------+------------------------------+
| id | select_type | table | type | key | Extra |
+----+-------------+-------+-------+-------------+------------------------------+
| 1 | SIMPLE | d | index | ux_deptname | Using index; Using temporary |
| 1 | SIMPLE | de | ref | PRIMARY | Using index; Distinct |
+----+-------------+-------+-------+-------------+------------------------------+
col1 IN (SELECT col2 FROM …)
같은 조건을 가진 쿼리에서 col1 의 값이 NULL 이된다면 NULL IN (SELECT col2 FROM ...)
로 된다.mysql> EXPLAIN
SELECT d.dept_no,
NULL IN (SELECT id.dept_name FROM departments d2) FROM departments d1;
+-----+-------------+---------+----------------+-------------------------------------------------+
| id | select_type | table | type | Extra |
+-----+-------------+---------+----------------+-------------------------------------------------+
| 1 | PRIMARY | d1 | index | Using index |
| 2 | SUBQUERY | d2 | index_subquery | Using where; Using index; Full scan on NULL key |
+-----+-------------+---------+----------------+-------------------------------------------------+
mysql> SELECT *
FROM tb_test1
WHERE col1 IS NOT NULL AND col1 IN (SELECT col2 FROM tb_test2);
LooseScan
최적화 전략이 사용됐을 경우mysql> EXPLAIN
SELECT *
FROM dept_emp de
LEFT JOIN departments d ON de.dept_no=d.dept_no
WHERE d.dept_no IS NULL;
+------+-------------+-------+-----------+-----------+-----------------------------+
| id | select_type | table | type | key | Extra |
+------+-------------+-------+-----------+-----------+-----------------------------+
| 1 | SIMPLE | de | ALL | NULL | NULL |
| 1 | SIMPLE | d | eq_ref | PRIMARY | Using where; Not exists |
+------+-------------+-------+-----------+-----------+-----------------------------+
EXPLAIN FOR CONNECT
명령어를 실행했을때 해당 커넥션에서 아직 쿼리의 실행 계획을 수립하지 못했을때mysql> WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte WHERE n < 5
)
SELECT * FROM cte;
인덱스를 활용하여 GROUP BY를 효율적으로 수행하는 방법
전체 데이터를 스캔하지 않고, 그룹별로 필요한 데이터만 인덱스를 통해 빠르게 찾음
불필요한 행을 읽지 않기 때문에 성능이 향상됨
루스 인덱스 스캔이 적용되는 경우
WHERE 조건이 인덱스의 "선두 컬럼"을 포함해야 함
GROUP BY에 사용되는 컬럼이 "인덱스의 연속적인 컬럼"이어야 함
WHERE 조건이 "범위 조건(>, <, BETWEEN)"이 아니라, "동등 조건(=, IN)"일 때 최적화 가능
예제: 루스 인덱스 스캔이 적용되는 경우
sql
복사편집
SELECT category, MIN(price)
FROM products
WHERE category IN ('A', 'B', 'C')
GROUP BY category;
여기서 category
가 인덱스의 선두 컬럼이라면, 루스 인덱스 스캔이 가능하다!
즉, WHERE 조건이 category
의 동등 조건(IN
또는 =
)이기 때문에, 인덱스를 효율적으로 사용할 수 있음.
루스 인덱스 스캔이 적용되려면, WHERE 조건이 특정한 패턴을 따라야 한다.
예제 (적용 가능)
sql
복사편집
CREATE INDEX idx_category_price ON products (category, price);
sql
복사편집
SELECT category, MIN(price)
FROM products
WHERE category IN ('A', 'B', 'C') -- 인덱스 선두 컬럼 사용
GROUP BY category;
category
가 인덱스의 첫 번째 컬럼이므로, 루스 인덱스 스캔이 적용될 수 있음.
즉, WHERE category IN (...)
이 인덱스의 선두 컬럼에 대한 조건이므로, 불필요한 행을 읽지 않고 최적화할 수 있음.
예제 (적용 불가능)
sql
복사편집
SELECT category, MIN(price)
FROM products
WHERE price > 100 -- 인덱스의 첫 번째 컬럼이 아님
GROUP BY category;
price
는 idx_category_price
에서 두 번째 컬럼이므로, 루스 인덱스 스캔이 불가능함.
이 경우, 타이트 인덱스 스캔(Tight Index Scan) 또는 풀 테이블 스캔이 사용될 가능성이 높음.
예제 (적용 가능)
sql
복사편집
SELECT category, MIN(price)
FROM products
WHERE category IN ('A', 'B', 'C') -- 동등 조건 (IN)
GROUP BY category;
WHERE 조건이 =
또는 IN
이므로, 루스 인덱스 스캔이 적용될 수 있음.
예제 (적용 불가능)
sql
복사편집
SELECT category, MIN(price)
FROM products
WHERE category BETWEEN 'A' AND 'C' -- 범위 조건 (BETWEEN)
GROUP BY category;
BETWEEN과 같은 범위 조건이 들어가면, 루스 인덱스 스캔이 불가능해짐.
이 경우, MySQL은 타이트 인덱스 스캔(Tight Index Scan) 또는 풀 테이블 스캔을 사용할 가능성이 높음.
예제 (적용 가능)
sql
복사편집
CREATE INDEX idx_category_price ON products (category, price);
sql
복사편집
SELECT category, MIN(price)
FROM products
WHERE category IN ('A', 'B', 'C')
GROUP BY category;
GROUP BY 컬럼이 인덱스의 연속된 컬럼이므로, 루스 인덱스 스캔이 가능함.
예제 (적용 불가능)
sql
복사편집
SELECT category, brand, MIN(price)
FROM products
WHERE category IN ('A', 'B', 'C')
GROUP BY category, brand;
brand
컬럼이 인덱스의 연속된 컬럼이 아니면, 루스 인덱스 스캔이 적용되지 않음.
이 경우, 타이트 인덱스 스캔(Tight Index Scan) 또는 풀 테이블 스캔이 사용될 가능성이 높음.
EXPLAIN을 사용하여 실행 계획을 확인하면 루스 인덱스 스캔이 적용되었는지 확인할 수 있다.
예제: 실행 계획 확인
sql
복사편집
EXPLAIN SELECT category, MIN(price)
FROM products
WHERE category IN ('A', 'B', 'C')
GROUP BY category;
루스 인덱스 스캔이 적용된 경우
pgsql
복사편집
id | select_type | table | type | key | key_len | rows | Extra
-------------------------------------------------------------------------------------
1 | SIMPLE | products | index | idx_category_price | 50 | 3 | Using index for group-by
Using index for group-by
가 나오면 루스 인덱스 스캔이 적용된 것이다.
루스 인덱스 스캔이 적용되지 않은 경우
pgsql
복사편집
id | select_type | table | type | key | key_len | rows | Extra
---------------------------------------------------------------------------------
1 | SIMPLE | products | index | NULL | NULL | 10000 | Using temporary; Using filesort
Using temporary; Using filesort
가 나오면 루스 인덱스 스캔이 적용되지 않은 것이다.
즉, MySQL이 임시 테이블과 정렬을 사용하여 GROUP BY를 처리하고 있음.
✔ 루스 인덱스 스캔을 사용하려면, WHERE 조건이 인덱스의 "선두 컬럼"을 포함해야 한다.
✔ WHERE 조건이 =
, IN
같은 동등 조건이어야 하며, BETWEEN
, <
, >
같은 범위 조건이면 루스 인덱스 스캔이 불가능하다.
✔ GROUP BY 컬럼이 인덱스의 연속된 컬럼이어야 한다.
✔ EXPLAIN 실행 계획에서 Using index for group-by
가 나오면 루스 인덱스 스캔이 적용된 것이다.
"루스 인덱스 스캔을 사용하려면 WHERE 조건이 인덱스의 선두 컬럼을 포함하고, 동등 조건(= 또는 IN)이어야 하며, GROUP BY 컬럼이 인덱스의 연속된 컬럼이어야 한다. EXPLAIN 실행 계획에서 Using index for group-by가 나오면 루스 인덱스 스캔이 적용된 것이다!"
mysql> ALTER TABLE employees ADD INDEX ix_gender_birthdate (gender, birth_date);
mysql> EXPLAIN SELECT gender, birth_date FROM employees
WHERE birth_date>='1965-02-01'
+----+-----------+------+---------------------+----------------------------------------+
| id | table | type | key | Extra |
+----+-----------+------+---------------------+----------------------------------------+
| 1 | employees | range| ix_gender_birthdate | Using where; Using index for skip skan |
+----+-----------+------+---------------------+----------------------------------------+
COUNT(DISTINCT column1
를 포함하는 쿼리도 인덱스를 사용할 수 없는 경우mysql> EXPLAIN
SELECT *
FROM employees
WHERE emp_no BETWEEN 10001 AND 10100
AND gender='F';
+----+-------------+-----------+--------+---------+------+-----------+-------------+
| id | select_type | table | type | key | rows | filtered | Extra |
+----+-------------+-----------+--------+---------+------+-----------+-------------+
| 1 | SIMPLE | employees | range | PRIMARY | 100 | 50.00 | Using where |
+----+-------------+-----------+--------+---------+------+-----------+-------------+
emp_no BETWEEN 10001 AND 10100
이며 gender='F'
는 체크 조건이다.LIMIT 0
을 사용했을 경우