INSERT
문장이 동시에 실행되는 경우, INSERT
문장 자체보다는 테이블의 구조가 성능에 더 큰 영향을 미친다.INSERT
의 성능과 SELECT
의 성능을 동시에 빠르게 만들 수 있는 테이블 구조는 없다.INSERT IGNORE
옵션은 INSERT
도중 에러가 발생하면, 처리하던 내용을 포기하고 트랜잭션을 롤백하여 에러 대신 경고 메시지로 에러 수준을 낮춘다.INSERT IGNORE
옵션은 프라이머리 키나 유니크 인덱스와 중복되는 경우, 해당 레코드를 무시하고 다음 레코드를 처리할 수 있게 해준다.INSERT
를 할 수 없는 경우, 기본값으로 INSERT
한다.INSERT IGNORE
문장은 의도하지 않은 에러까지 모두 무시할 수 있으므로 주의하자.-- // 프라이머리 키 (emp_no, from_date)
INSERT IGNORE INTO salaries (emp_no, salary, from_date, to_date) VAlUES
(10001, 60117, '1986-06-26', '1987-06-26'),
(10001, 62102, '1987-06-26', '1988-06-25'),
(10001, 66074, '1988-06-25', '1989-06-25'),
(10001, 66596, '1989-06-25', '1990-06-25'),
(10001, 66961, '1990-06-25', '1991-06-25');
-- Query OK, 0 rows affected, 5 warnings (0.02 sec)
-- Records: 5 Duplicates: 5 Warnings: 5
-- // IGNORE 키워드가 없으면 INSERT 실패
INSERT INTO salaries VALUES(NULL, NULL, NULL, NULL);
-- ERROR 1048 (23000): Column 'emp_no' cannot be null
-- // INGORE 키워드가 있으면, NOT NULL 칼럼인 emp_no, from_date에 기본 값을 저장
INSERT IGNORE INTO salaries VALUES(NULL, NULL, NULL, NULL);
-- Query OK, 1 row affected, 4 warnings (0.02 sec)
SHOW WARNINGS\G
/*
*************************** 1. row ***************************
Level: Warning
Code: 1048
Message: Column 'emp_no' cannot be null
*************************** 2. row ***************************
Level: Warning
Code: 1048
Message: Column 'salary' cannot be null
*************************** 3. row ***************************
Level: Warning
Code: 1048
Message: Column 'from_date' cannot be null
*************************** 4. row ***************************
Level: Warning
Code: 1048
Message: Column 'to_date' cannot be null
*************************** 5. row ***************************
Level: Warning
Code: 1062
*/
INSERT ... ON DUPLICATE KEY UPDATE
옵션은 프라이머리 키나 유니크 인덱스와 중복되는 경우, 기존 레코드를 UPDATE
하는 방식으로 작동한다.REPLACE
문장도 비슷한 역할을 하지만 REPLACE
는 내부적으로 DELETE
와 INSERT
의 조합으로 작동한다. ➔ DELETE
와INSERT
조합은 성능상 이점이 없기 때문에 권장하지 않는다.CREATE TABLE daily_statistic(
target_date DATE NOT NULL,
stat_name VARCHAR(10) NOT NULL,
stat_value BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY(target_date, stat_name)
);
-- 해당 날짜의 레코드가 존재하면 stat_value를 1씩 증가
INSERT INTO daily_statistic (target_date, stat_name, stat_value)
VALUES (DATE(NOW()), 'VISIT', 1) ON DUPLICATE KEY UPDATE stat_value = (stat_value + 1);
INSERT ... ON DUPLICATE KEY UPDATE
절에서는 GROUP BY
결과인 COUNT(*)
를 참조할 수 없다.COUNT()
함수 대신 VALUES()
함수를 사용할 수 있다.VALUES()
함수는 MySQL 8.0.20 이후 버전에서는 지원되지 않을(Deprecated) 예정이다.INSERT ... SELECT ...
에 별칭을 주거나 INSERT
되는 레코드에 별칭을 부여해서 사용할 수 있다.--// ON DUPLICATE KEY UPDATE 절에서 COUNT(*)을 참조할 수 없음
INSERT INTO daily_statistic
SELECT DATE('2023-01-27'), 'VISIT', COUNT(*)
GROUP BY DATE('2023-01-27')
ON DUPLICATE KEY UPDATE stat_value = stat_value + COUNT(*);
-- ERROR 1111 (HY000): Invalid use of group function
--// COUNT() 함수 대신 VALUES() 함수를 사용할 수 있음
INSERT INTO daily_statistic
SELECT DATE('2023-01-27'), 'VISIT', COUNT(*)
GROUP BY DATE('2023-01-27')
ON DUPLICATE KEY UPDATE stat_value = stat_value + VALUES(stat_value);
SHOW WARNINGS\G
/*
*************************** 1. row ***************************
Level: Warning
Code: 1287
Message: 'VALUES function' is deprecated and will be removed in a future release.
Please use an alias (INSERT INTO ... VALUES (...) AS alias) and replace VALUES(col) in the ON DUPLICATE KEY UPDATE clause with alias.col instead
*/
--// MySQL 8.0.20 이후 버전
-- (1) SELECT 절에 별칭을 부여하는 방식
INSERT INTO daily_statistic
SELECT target_date, stat_name, stat_value
FROM (
SELECT DATE('2023-01-27') target_date, 'VISIT' stat_name, COUNT(*) stat_value
GROUP BY (target_date)
) new
ON DUPLICATE KEY UPDATE
daily_statistic.stat_value = daily_statistic.stat_value + new.stat_value;
-- (2) INSERT 되는 레코드에 별칭을 부여하는 방식
INSERT INTO daily_statistic
VALUES ('2023-01-27', 'VISIT', 1),
('2023-01-28', 'VISIT', 1) AS new
ON DUPLICATE KEY UPDATE
daily_statistic.stat_value = daily_statistic.stat_value + new.stat_value;
-- (3) INSERT 되는 레코드와 칼럼에 별칭을 부여하는 방식
INSERT INTO daily_statistic
SET target_date = '2023-01-28',
stat_name = 'VISIT',
stat_value = 1
AS new(f1, f2, f3)
ON DUPLICATE KEY UPDATE
daily_statistic.stat_value = daily_statistic.stat_value + new.f3;
LOAD DATA
명령은 일반적인 INSERT
명령과 비교했을 때 매우 빠르다.
하지만 단점도 있다.
INSERT
속도가 저하된다.LOAD DATA
문장으로 적재할 데이터 파일을 하나보다는 여러 개의 파일로 준비해서 여러 트랜잭션으로 나누어 실행하는 것이 좋다.
테이블 간 데이터 복사에는 LOAD DATA
보다는 INSERT ... SELECT ...
문장에서 WHERE
조건으로 잘라서 처리하는 것이 좋다.
INSERT
문장의 성능은 쿼리 문장 자체보다는 테이블의 구조에 의해 많이 결정된다.INSERT
한다면 프라이머리 키 값을 기준으로 미리 정렬해서 INSERT
문장을 구성하는 것이 성능에 도움이 될 수 있다.--// secure_file_priv 값이 "" 이어야 함
SELECT @@GLOBAL.secure_file_priv;
+---------------------------+
| @@GLOBAL.secure_file_priv |
+---------------------------+
| |
+---------------------------+
--// PK 값인 (emp_no, from_date) 정렬
SELECT *
FROM salaries
ORDER BY emp_no, from_date
INTO OUTFILE '/tmp/sorted_by_primary.csv'
CHARACTER SET euckr
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
--// (salary, to_date) 칼럼으로 정렬
SELECT *
FROM salaries
ORDER BY salary, to_date
INTO OUTFILE '/tmp/sorted_by_random.csv'
CHARACTER SET euckr
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
--// PK로 정렬된 csv 파일 LOAD
LOAD DATA INFILE '/tmp/sorted_by_primary.csv'
INTO TABLE salaries_temp_primary
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
-- Query OK, 2844047 rows affected (2 min 8.09 sec)
-- Records: 2844047 Deleted: 0 Skipped: 0 Warnings: 0
--// PK로 정렬되지 않은 csv 파일 LOAD
LOAD DATA INFILE '/tmp/sorted_by_random.csv'
INTO TABLE salaries_temp_random
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
-- Query OK, 2844047 rows affected (3 min 16.37 sec)
-- Records: 2844047 Deleted: 0 Skipped: 0 Warnings: 0
INSERT
성능을 결정하는 가장 중요한 부분이다.INSERT
와 SELECT
성능의 대립되는 두 가지 요소 중에서 하나를 선택해야 함을 의미한다.INSERT
가 빈번한 테이블SELECT
가 빈번한 테이블SELECT
쿼리의 조건을 기준으로 프라이머리 키로 선택자동 증가(Auto Increment) 칼럼을 이용하면 클러스터링되지 않는 테이블의 효과를 얻을 수 있다.
자동 증가 값의 채번을 위해서는 잠금이 필요하다.
innodb_autoinc_lock_mode
0
: 항상 AUTO_INC 잠금을 사용 ➔ 서비스용에서는 이 방식을 사용할 필요 없음1
: 한 건씩 처리할 때는 AUTO_INC 잠금을 사용하지 않고, 여러 레코드를 처리할 때는 AUTO_INC 잠금을 사용 ➔ INSERT 순서대로 채번된 자동 증가 값은 일관되고, 연속된 번호를 갖게 됨2
: AUTO_INC 잠금을 사용하지 않음 ➔ INSERT 순서와 채번된 번호의 연속성을 보장하지 않음쿼리 기반의 복제를 사용하는 경우, 소스 서버와 레플리카 서버의 자동 증가 값이 동기화되지 못할 수도 있으므로 주의해야 한다.
MySQL 5.7 버전의 기본 값은 1
, MySQL 8.0 버전의 기본 값은 2
이다.
➔ 복제의 바이너리 로그 포맷이 MySQL 5.7 버전에서는 STATEMENT
였지만, MySQL 8.0 버전에서 ROW
로 변경되었기 때문에, 복제를 STATEMENT
바이너리 로그 포맷으로 사용 중이라면 innodb_autoinc_lock_mode
또한 1
로 사용해야 한다.
LAST_INSERT_ID()
함수를 통해 가장 마지막에 증가된 AUTO_INCREMENT
값을 조회할 수 있다.
SELECT MAX() ...
를 사용하는 방법은 잘못된 결과를 반환할 수 있으므로 사용하지 않는 것이 좋다.CREATE TABLE tb_autoincrement (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(200),
PRIMARY KEY(id)
);
INSERT INTO tb_autoincrement VALUES (NULL, 'Georgi Fellona');
SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
| 1 |
+------------------+
UPDATE
나 DELETE
문장에 ORDER BY
절과 LIMIT
절을 사용하면 정렬된 칼럼을 차례대로 잘라서 처리할 수 있다.ROW
일 때는 문제가 되지 않지만, STATEMENT
일 때는 ORDER BY
에 의해 정렬되더라도 중복된 값의 순서가 복제 소스 서버와 레플리카 서버에서 달라질 수도 있기 때문이다.SET binlog_format=STATEMENT;
DELETE FROM employees ORDER BY last_name LIMIT 10;
SHOW WARNINGS \G
/*
*************************** 1. row ***************************
Level: Note
Code: 1592
Message: Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT.
The statement is unsafe because it uses a LIMIT clause.
This is unsafe because the set of rows included cannot be predicted.
*/
SET binlog_format=ROW;
JOIN UPDATE
라고 한다.JOIN UPDATE
는 조인되는 모든 테이블에 대해 읽기 참조만 되는 테이블은 읽기 잠금이 걸리고, 칼럼이 변경되는 테이블은 쓰기 잠금이 걸린다.CREATE TABLE tb_join_update (
emp_no INT,
first_name VARCHAR(14),
PRIMARY KEY(emp_no)
);
INSERT INTO tb_join_update
VALUES (10001, NULL), (10002, NULL), (10003, NULL), (10004, NULL);
+--------+------------+
| emp_no | first_name |
+--------+------------+
| 10001 | NULL |
| 10002 | NULL |
| 10003 | NULL |
| 10004 | NULL |
+--------+------------+
UPDATE tb_join_update t1, employees e
SET t1.first_name = e.first_name
WHERE e.emp_no = t1.emp_no;
+--------+------------+
| emp_no | first_name |
+--------+------------+
| 10001 | Georgi |
| 10002 | Bezalel |
| 10003 | Parto |
| 10004 | Chirstian |
+--------+------------+
JOIN UPDATE
문장에서는 GROUP BY
나 ORDER BY
절을 사용할 수 없다.ALTER TABLE departments ADD emp_count INT;
--// 부서별로 사원의 수를 emp_count 칼럼에 업데이트
-- JOIN UPDATE 문장에서는 GROUP BY나 ORDER BY 절을 사용할 수 없음
UPDATE departments d, dept_emp de
SET d.emp_count = COUNT(*)
WHERE de.dept_no = d.dept_no
GROUP BY de.dept_no;
-- check the manual that corresponds to your MySQL server version for the right syntax to use near 'GROUP BY de.dept_no' at line 4
-- JOIN UPDATE 문장에서 GROUP BY 절을 처리하기 위해 서브쿼리로 변경
UPDATE departments d,
(SELECT de.dept_no, COUNT(*) AS emp_count
FROM dept_emp de
GROUP BY de.dept_no
) dc
SET d.emp_count = dc.emp_count
WHERE dc.dept_no = d.dept_no;
STRAIGHT_JOIN
을 사용하거나 JOIN_ORDER
힌트를 사용할 수 있다.STRAIGHT_JOIN
키워드 왼쪽에 명시된 테이블이 드라이빙 테이블이 되고, 오른쪽 테이블이 드리븐 테이블이 된다.UPDATE
문장으로 여러 개의 레코드를 업데이트하는 경우, 모든 레코드를 동일한 값으로만 업데이트할 수 있었다.VALUES ROW(...), ...
문법을 사용하면 SQL 내에서 임시 테이블을 생성하는 효과를 낼 수 있다.CREATE TABLE user_level (
user_id BIGINT NOT NULL,
user_lv INT NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY(user_id)
);
INSERT INTO user_level
VALUES (1, 2, NOW()), (2, 4, NOW()), (3, 6, NOW()), (4, 8, NOW());
+---------+---------+---------------------+
| user_id | user_lv | created_at |
+---------+---------+---------------------+
| 1 | 2 | 2023-01-28 23:05:17 |
| 2 | 4 | 2023-01-28 23:05:17 |
| 3 | 6 | 2023-01-28 23:05:17 |
| 4 | 8 | 2023-01-28 23:05:17 |
+---------+---------+---------------------+
UPDATE user_level ul
INNER JOIN (VALUES ROW(1, 1),
ROW(2, 4)
) new_user_level (user_id, user_lv)
ON new_user_level.user_id = ul.user_id
SET ul.user_lv = ul.user_lv + new_user_level.user_lv;
+---------+---------+---------------------+
| user_id | user_lv | created_at |
+---------+---------+---------------------+
| 1 | 3 | 2023-01-28 23:05:17 |
| 2 | 8 | 2023-01-28 23:05:17 |
| 3 | 6 | 2023-01-28 23:05:17 |
| 4 | 8 | 2023-01-28 23:05:17 |
+---------+---------+---------------------+
JOIN DELETE
문장은 단일 테이블의 DELETE
문장과는 조금 다른 문법으로 쿼리를 작성해야 한다.DELETE FROM table ...
DELETE
와 FROM
절 사이에 삭제할 테이블명 명시-- 조인한 결과를 통해 employees 테이블의 레코드만 삭제
DELETE e
FROM employees e, dept_emp de, departments d
WHERE e.emp_no = de.emp_no
AND de.dept_no = d.dept_no
AND d.dept_no = 'd001';
-- 조인한 결과를 통해 여러 테이블의 레코드를 삭제
DELETE e, de
FROM employees e, dept_emp de, departments d
WHERE e.emp_no = de.emp_no
AND de.dept_no = d.dept_no
AND d.dept_no = 'd001';
STRAIGHT_JOIN
을 사용하거나 JOIN_ORDER
힌트를 사용할 수 있다.