오늘은 가장 많이 사용하는 SELECT 절에 대해 살펴보려고 합니다.
SELECT는 여러 개의 테이블에서 데이터를 조합하여 가져오기 때문에 테이블을 어떻게 읽을 것인가에 주의해서 살펴봅시다.
SELECT
문장에는 SELECT
절을 포함한 여러 절이 포함되어 있다.
이때, 어느 절이 먼저 실행되는지를 모르면 처리 내용이나 처리 결과를 예측할 수 없다.
실행 순서를 벗어난 쿼리가 필요하다면 서브쿼리로 작성된 인라인 뷰를 사용해야 한다.
➔ 하지만, 인라인 뷰가 사용되면 임시 테이블이 사용되기 때문에 주의해야 한다.
MySQL 8.0에 새로 도입된 WITH
절(CTE)은 항상 제일 먼저 실행되어 임시 테이블로 저장된다.
CTE란?
CTE(공통 테이블 식)는 SELECT, INSERT, UPDATE, DELETE 또는 CREATE VIEW 문 하나의 실행 범위 내에서 정의되는 임시 결과 집합이라고 볼 수 있습니다. CTE는 개체로 저장되지 않고 쿼리 지속 시간 동안만 존재한다는 점에서 파생 테이블과 비슷합니다. 그러나 CTE는 파생 테이블과 달리 자체 참조가 가능하며 동일 쿼리에서 여러 번 참조될 수 있습니다.
MySQL 8.0에 새로 추가된 윈도우 함수에서도 쿼리의 각 절이 실행되는 순서가 중요하다.
FROM
, WHERE
, GROUP BY
, HAVING
절에 의해 결정되고, 그 이후 윈도우 함수가 실행된다.SELECT
, ORDER BY
, LIMIT
절이 실행되어 최종 결과가 반환된다.WHERE
절의 조건뿐만 아니라 GROUP BY
나 ORDER BY
절도 인덱스를 이용해 빠르게 처리할 수 있다.
-- 인덱스를 변환해서 사용하면 안됨
SELECT * FROM salaries WHERE salary * 10 > 150000;
-- 인덱스를 변환하지 않는 방식으로 수정
SELECT * FROM salaries WHERE salary > 150000 / 10;
WHERE
절에 사용되는 비교 조건에서 연산자 양쪽의 두 비교 대상 값은 데이터 타입이 일치해야 한다.-- 테스트용 테이블 생성
CREATE TABLE tb_test (age VARCHAR(10), INDEX ix_age(age));
INSERT INTO tb_test VALUES ('1'), ('2'), ('3'), ('4'), ('5'), ('6'), ('7');
-- 타입이 일치하지 않게 사용하면 타입 변환이 발생하여 성능 저하
SELECT * FROM tb_test WHERE age = 2;
-- 타입이 일치하게 사용해야 함
SELECT * FROM tb_test WHERE age = '2';
WHERE
조건이 인덱스를 사용하는 방법은 크게 두 가지가 있다.WHERE
조건절에 나열된 순서가 인덱스와 다르더라도 옵티마이저는 인덱스를 사용할 수 있는 조건을 뽑아서 최적화를 수행한다.OR
연산자를 사용하면 읽어야할 레코드가 늘어나게 되어 풀 테이블 스캔을 하게 될 수 있으므로 주의해야 한다.GROUP BY
절이 인덱스를 사용할 수 있는 경우GROUP BY
절에 명시된 인덱스 칼럼의 순서와 위치가 같아야 한다.GROUP BY
절에 명시되지 않아도 인덱스를 사용할 수 있지만, 인덱스의 앞쪽에 있는 칼럼이 GROUP BY
절에 명시되지 않으면 인덱스를 사용할 수 없다.GROUP BY
절에 명시된 칼럼이 하나라도 인덱스에 없으면 인덱스를 사용할 수 없다.GROUP BY
에서 인덱스를 사용할 수 없는 경우-- 인덱스 칼럼의 순서
(COL_1, COL_2, COL_3, COL_4)
... GROUP BY COL_2, COL_1
... GROUP BY COL_1, COL_3, COL_2
... GROUP BY COL_1, COL_3
... GROUP BY COL_1, COL_2, COL_3, COL_4, COL_5
GROUP BY
에서 인덱스를 사용할 수 있는 경우-- 인덱스 칼럼의 순서
(COL_1, COL_2, COL_3, COL_4)
... GROUP BY COL_1
... GROUP BY COL_1, COL_2
... GROUP BY COL_1, COL_2, COL_3
... GROUP BY COL_1, COL_2, COL_3, COL_4
ORDER BY
절의 인덱스 사용은 GROUP BY
절과 거의 흡사하다. WHERE
절과 ORDER BY
절이 같이 사용된 하나의 쿼리 문장은 다음 3가지 중 한 가지 방법으로만 인덱스를 이용한다. (GROUP BY
도 마찬가지이다.)WHERE
절과 ORDER BY
절이 동시에 같은 인덱스를 사용WHERE
절과 ORDER BY
절의 대상 칼럼이 모두 하나의 인덱스에 연속해서 포함되어 있는 경우에 사용 가능WHERE
절만 인덱스를 이용WHERE
절에 일치하는 레코드의 건수가 많지 않을 때 효율적인 방식ORDER BY
절만 인덱스를 이용ORDER BY
절의 순서대로 인덱스를 읽으면서 WHERE
절의 조건에 일치하는지 비교하고, 일치하지 않을 때는 버리는 형태로 처리한다.GROUP BY
와 ORDER BY
절이 동시에 사용된 쿼리에서 하나의 인덱스를 사용하려면 칼럼의 순서와 내용이 모두 같아야 한다.GROUP BY
는 인덱스를 이용할 수 있지만 ORDER BY
가 인덱스를 이용할 수 없을 때는 모두 인덱스를 이용하지 못한다.-- 인덱스 칼럼의 순서
(COL_1, COL_2, COL_3, COL_4)
...GROUP BY COL_1, COL_2 ORDER BY COL_2
...GROUP BY COL_1, COL_2 ORDER BY COL_1, COL_3
WHERE
, GROUP BY
, ORDER BY
절에서 인덱스를 사용하는지 판단하는 방법WHERE
절이 인덱스를 사용할 수 있는가?GROUP BY
절이 인덱스를 사용할 수 있는가?GROUP BY
절과 ORDER BY
절이 동시에 인덱스를 사용할 수 있는가?NULL
값이 포함된 레코드도 인덱스로 관리된다.NULL
값을 동등 비교하는 것이 불가능하다. 한쪽이라도 NULL
이면 그 결과도 NULL
이다.NULL
인지 비교하려면 IS NULL
또는 <=>
연산자를 사용해야 한다.-- 결과: NULL
SELECT NULL = NULL;
-- 결과: 1
SELECT NULL <=> NULL;
ISNULL()
이라는 함수를 사용할 수도 있지만, 이 함수를 사용할 때는 인덱스를 사용하지 못할 수도 있다.-- 인덱스 레인지 스캔
SELECT * FROM titles WHERE to_date IS NULL;
SELECT * FROM titles WHERE ISNULL(to_date);
-- 인덱스 풀스캔 또는 테이블 풀스캔
SELECT * FROM titles WHERE ISNULL(to_date) = 1;
SELECT * FROM titles WHERE ISNULL(to_date) = true;
DATE
STR_TO_DATE()
함수를 사용하지 않아도 문자열을 DATE
타입으로 내부적으로 변환해준다.DATE()
함수는 DATETIME
값에서 시간 부분은 버리고 날짜 부분만 반환한다. DATETIME
DATE
와 마찬가지로 문자열을 내부적으로 DATETIME
으로 변환해준다.DATETIME
과 DATE
간의 타입 변환은 인덱스의 사용 여부에 영향을 미치지 않는다.DATETIME
도 마찬가지)-- STR_TO_DATE() 함수를 통해 명시적으로 변환
SELECT COUNT(*) FROM employees
WHERE hire_date > STR_TO_DATE('2011-07-23', '%Y-%m-%d');
-- 기본 날짜 형식으로 입력하면 자동 변환하여 비교
SELECT COUNT(*) FROM employees
WHERE hire_date > '2011-07-23';
TIMESTAMP
DATE
나 DATETIME
타입을 TIMESTAMP
타입과 비교할 때는 별도의 변환 작업이 없으면 인덱스를 제대로 사용하지 못한다.DATETIME
인 경우FROM_UNIXTIME()
함수를 이용해 TIMESTAMP
→ DATETIME
로 변환해서 비교해야 한다.TIMESTAMP
인 경우UNIX_TIMESTAMP()
함수를 이용해 DATETIME
→ TIMESTAMP
로 변환해서 비교해야 한다.AND
또는 OR
연산자로 연결된 경우 선행 표현식의 결과에 따라 후행 표현식을 평가할지 말지 결정하는 최적화를 "Short-Circuit Evaluation"라고 한다.-- 1번 조건(2,442,943 건)
SELECT COUNT(*) FROM salaries
WHERE CONVERT_TZ(from_date, '+00:00', '+09:00') > '1991-01-01';
-- 2번 조건(0 건)
SELECT COUNT(*) FROM salaries
WHERE to_date < '1985-01-01';
-- 1번 조건 체크 후 2번 조건 확인 (5.89 sec 소요)
SELECT * FROM salaries
WHERE CONVERT_TZ(from_date, '+00:00', '+09:00') > '1991-01-01' /* 1번 조건 */
AND to_date < '1985-01-01'; /* 2번 조건 */
-- 2번 조건 체크 후 1번 조건 확이 (4.50 sec 소요)
SELECT * FROM salaries
WHERE to_date < '1985-01-01' /* 2번 조건 */
AND CONVERT_TZ(from_date, '+00:00', '+09:00') > '1991-01-01'; /* 1번 조건 */
➔ 복잡한 연산 또는 다른 테이블의 레코드를 읽어야 하는 서브쿼리 조건 등은 WHERE
절의 뒤쪽으로 배치하는 것이 성능상 좋다.
단, WHERE
절의 조건 중에서 인덱스를 사용할 수 있는 조건은 나열 순서와 관계없이 가장 먼저 평가되기 때문에 고려하지 않아도 된다.
DISTINCT
를 남용하는 것은 성능적인 문제도 있지만, 쿼리의 결과도 의도한 바와 달라질 수 있다.DISTINCT
를 남용하는 경향이 있다.1:1
조인인지, 1:M
인인지 업무적인 특성을 잘 이해하는 것이 중요하다.LIMIT
의 중요한 특성은 필요한 레코드 건수만 준비되면 즉시 쿼리를 종료한다는 것이다.ORDER BY
, GROUP BY
, DISTINCT
가 인덱스를 이용해 처리될 수 있다면 LIMIT 절은 꼭 필요한 만큼의 레코드만 읽도록 하여 작업량을 상당히 줄여준다.LIMIT
에 n
과 m
이 주어질 때, n의 수치가 커지는 경우 주의해야 한다. (주로 페이징 처리)LIMIT 2000000, 10
은 2000010
건을 읽은 후 2000000
건은 버리고 마지막 10
건만 반환한다.LIMIT
조건의 페이징이 늘어날 경우, WHERE
조건절로 읽어야 할 위치를 찾고, 그 위치에서 10개만 읽는 형태의 쿼리를 사용하는 것이 좋다.-- 이런 형태는 성능 저하
SELECT * FROM salaries ORDER BY salary LIMIT 2000000, 10;
-- 10 rows in set (9.05 sec)
-- WHERE 조건절로 처음 위치를 찾음
SELECT * FROM salaries
WHERE salary >= 154888 AND NOT (salary = 154888 AND emp_no <= 109334)
ORDER BY salary LIMIT 0, 10;
-- 7 rows in set (0.04 sec)
COUNT()
함수는 칼럼이나 표현식을 인자로 받으며, "*"
를 사용할 수도 있다."*"
는 SELECT
절에 사용될 때처럼 모든 칼럼을 가져오라는 의미가 아니라 그냥 레코드 자체를 의미한다.COUNT(프라이머리 키 칼럼)
와 COUNT(*)
은 동일한 처리 성능을 보인다.WHERE
조건이 없는 COUNT(*)
쿼리는 실제 레코드 건수를 세어 보지 않아도 바로 결과를 반환할 수 있기 때문에 빠르게 처리된다.WHERE
조건이 있는 COUNT(*)
쿼리는 일반적인 방식으로 처리되며, 인덱스를 제대로 사용하지 못하면 COUNT(*)
쿼리도 많은 부하를 일으키기 때문에 주의해야 한다.COUNT()
함수에 칼럼명이나 표현식이 인자로 사용되면 그 칼럼이나 표현식의 결과가 NULL
이 아닌 레코드 건수만 반환한다.CHAR
타입과 VARCHAR
타입INT
타입과 BIGINT
타입DATE
타입과 DATETIME
타입CHAR
타입과 INT
타입의 비교와 같이 데이터 타입의 종류가 완전히 다른 경우CHAR
타입이더라도 문자 집합이나 콜레이션이 다른 경우 (COLLATE utf8mb4_general_ci
≠ COLLATE latin1_general_ci
)INT
타입이더라도 부호(Sign)의 존재 여부가 다른 경우INNER JOIN
으로 변환해서 실행한다.WHERE
절에 사용한다. -- 잘못된 아우터 조인 방법
SELECT * FROM employees e
LEFT JOIN dept_manager mgr ON mgr.emp_no = e.emp_no
WHERE mgr.dept_no = 'd001';
-- 옵티마이저가 이너 조인으로 변환
SELECT * FROM employees e
INNER JOIN dept_manager mgr ON mgr.emp_no = e.emp_no
WHERE mgr.dept_no = 'd001';
-- 정상적인 아우터 조인이 되는 경우
SELECT * FROM employees e
LEFT JOIN dept_manager mgr ON mgr.emp_no = e.emp_no AND mgr.dept_no = 'd001';
-- 안티 조인의 경우 아우터 조인에 WHERE 절 사용 가능
SELECT * FROM employees e
LEFT JOIN dept_manager mgr ON mgr.emp_no = e.emp_no
WHERE mgr.emp_no IS NULL
LIMIT 10;
GROUP BY
나 ORDER BY
를 처리하는 방식을 의미한다.GROUP BY
나 ORDER BY
를 사용하는 쿼리에서 지연된 조인을 사용하면 조인 횟수가 줄어들어 성능이 향샹된다.FROM
절에 사용된 서브쿼리가 외부 쿼리의 칼럼을 참조하기 위해서는 LETERAL
키워드가 명시되어야 한다.ORDER BY
절을 명시적으로 사용하는 것이 좋다.GROUP BY
와 함께 WITH ROLLUP
을 사용할 수 있다.-- GROUP BY의 전체 합계 조회
SELECT dept_no, COUNT(*)
FROM dept_emp
GROUP BY dept_no WITH ROLLUP;
+---------+----------+
| dept_no | COUNT(*) |
+---------+----------+
| d001 | 20211 |
| d002 | 17346 |
| d003 | 17786 |
| d004 | 73485 |
| d005 | 85707 |
| d006 | 20117 |
| d007 | 52245 |
| d008 | 21126 |
| d009 | 23580 |
| NULL | 331603 |
+---------+----------+
-- GROUP BY의 전체 합계에 NULL이 아닌 명시적인 값 부여
SELECT IF(GROUPING(dept_no), 'Total', dept_no) AS dept_no,
COUNT(*)
FROM dept_emp
GROUP BY dept_no WITH ROLLUP;
+---------+----------+
| dept_no | COUNT(*) |
+---------+----------+
| d001 | 20211 |
| d002 | 17346 |
| d003 | 17786 |
| d004 | 73485 |
| d005 | 85707 |
| d006 | 20117 |
| d007 | 52245 |
| d008 | 21126 |
| d009 | 23580 |
| Total | 331603 |
+---------+----------+
집합 함수
또는 CASE...WHEN
구문을 통해 SQL을 작성하면 된다.-- 레코드를 칼럼으로 변환
SELECT
SUM(CASE WHEN dept_no='d001' THEN emp_count ELSE 0 END) AS count_d001,
SUM(CASE WHEN dept_no='d002' THEN emp_count ELSE 0 END) AS count_d002,
SUM(CASE WHEN dept_no='d003' THEN emp_count ELSE 0 END) AS count_d003,
SUM(CASE WHEN dept_no='d004' THEN emp_count ELSE 0 END) AS count_d004,
SUM(CASE WHEN dept_no='d005' THEN emp_count ELSE 0 END) AS count_d005,
SUM(CASE WHEN dept_no='d006' THEN emp_count ELSE 0 END) AS count_d006,
SUM(CASE WHEN dept_no='d007' THEN emp_count ELSE 0 END) AS count_d007,
SUM(CASE WHEN dept_no='d008' THEN emp_count ELSE 0 END) AS count_d008,
SUM(CASE WHEN dept_no='d009' THEN emp_count ELSE 0 END) AS count_d009,
SUM(emp_count) as count_total
FROM (
SELECT dept_no, COUNT(*) as emp_count from dept_emp group by dept_no
) tb_derived;
+------------+------------+------------+------------+------------+------------+------------+------------+------------+-------------+
| count_d001 | count_d002 | count_d003 | count_d004 | count_d005 | count_d006 | count_d007 | count_d008 | count_d009 | count_total |
+------------+------------+------------+------------+------------+------------+------------+------------+------------+-------------+
| 20211 | 17346 | 17786 | 73485 | 85707 | 20117 | 52245 | 21126 | 23580 | 331603 |
+------------+------------+------------+------------+------------+------------+------------+------------+------------+-------------+
-- 하나의 칼럼을 여러 칼럼으로 분리
SELECT de.dept_no,
SUM(CASE WHEN e.hire_date BETWEEN '1980-01-01' AND '1989-12-31' THEN 1 ELSE 0 END) AS cnt_1980,
SUM(CASE WHEN e.hire_date BETWEEN '1990-01-01' AND '1999-12-31' THEN 1 ELSE 0 END) AS cnt_1990,
SUM(CASE WHEN e.hire_date BETWEEN '2000-01-01' AND '2009-12-31' THEN 1 ELSE 0 END) AS cnt_2000,
COUNT(*) AS cnt_total
FROM dept_emp de, employees e
WHERE e.emp_no = de.emp_no
GROUP BY de.dept_no;
+---------+----------+----------+----------+-----------+
| dept_no | cnt_1980 | cnt_1990 | cnt_2000 | cnt_total |
+---------+----------+----------+----------+-----------+
| d001 | 11038 | 9171 | 2 | 20211 |
| d002 | 9580 | 7765 | 1 | 17346 |
| d003 | 9714 | 8068 | 4 | 17786 |
| d004 | 40418 | 33065 | 2 | 73485 |
| d005 | 47007 | 38697 | 3 | 85707 |
| d006 | 11057 | 9059 | 1 | 20117 |
| d007 | 28673 | 23571 | 1 | 52245 |
| d008 | 11602 | 9524 | 0 | 21126 |
| d009 | 12979 | 10600 | 1 | 23580 |
+---------+----------+----------+----------+-----------+
SELECT
쿼리는 순서를 보장하지 않는다.ORDER BY
를 사용해야 한다."Using Filesort"
가 표시된다.ORDER BY
절에 문자 리터럴은 무시된다.ASC
방향으로 생성하면 ASC(정순 스캔)든 DESC(역순 스캔)든 비용 차이가 거의 없다.DESC
방향으로 생성하면 DESC(정순 스캔)가 ASC(역순 스캔)보다 더 빠르다.SELECT * FROM salaries ORDER BY COS(salary);
MySQL 5.6
버전까지는 서브쿼리를 최적으로 실행하지 못한다.MySQL 8.0
버전부터는 서브쿼리가 많이 최적화 되었다.SELECT
, FROM
, WHERE
)에 따라 최적화 방법이 달라진다.SELECT
절에 서브쿼리를 사용하면 그 서브쿼리는 항상 칼럼과 레코드가 하나인 결과를 반환해야 한다.-- 10건이 조회되는 서브쿼리 (로우 서브쿼리)
SELECT emp_no, (SELECT dept_name FROM departments)
FROM dept_emp LIMIT 10;
-- ERROR 1242 (21000): Subquery returns more than 1 row
MySQL 5.7
버전부터 FROM
절의 서브쿼리를 외부 쿼리로 병합하는 최적화를 수행한다.FROM
절의 모든 서브쿼리를 외부 쿼리로 병합할 수 있는 것은 아니다. FROM
절의 서브쿼리를 최적화 할 수 없는 경우SUM()
, MIM()
, MAX()
, COUNT()
)DISTINCT
GROUP BY
또는 HAVING
LIMIT
UNION(UNION DISTINCT)
또는 UNION ALL
SELECT
절에 서브쿼리가 사용된 경우WHERE
절의 서브쿼리는 연산자 형태로 사용될 수 있다.= (subquery)
IN (subquery)
NOT IN (subquery)
WHERE
절에 사용된 IN (subquery)
형태의 조건을 세미 조인이라고 한다.비 재귀적 CTE
와 재귀적 CTE
로 나눌 수 있다.FROM
절의 서브쿼리보다 효율적이다.WITH
절을 이용해 CTE를 정의한다.-- CTE를 사용한 쿼리
WITH cte_d AS (SELECT * FROM departments)
SELECT * FROM cte_d;
-- 위의 쿼리와 같은 서브 쿼리
SELECT *
FROM (SELECT * FROM departments) cte_de;
-- cte1 임시 테이블을 cte2에서 사용 가능
WITH
cte1 AS (SELECT emp_no, MIN(from_date) AS salary_from_date
FROM salaries
WHERE salary BETWEEN 50000 AND 51000
GROUP BY emp_no
),
cte2 AS (SELECT de.emp_no, min(from_date) as dept_from_date
FROM cte1
INNER JOIN dept_emp de on de.emp_no = cte1.emp_no
GROUP BY emp_no
)
SELECT * FROM employees e
INNER JOIN cte1 t1 ON t1.emp_no = e.emp_no
INNER JOIN cte2 t2 ON t2.emp_no = e.emp_no;
비 재귀적 쿼리
+ (UNION | UNION DISTINCT | UNION ALL)
+ 재귀적 쿼리
로 구성된다.cte_max_recursion_depth
변수를 낮추는 것을 권장한다.-- cte_max_recursion_depth 값을 제한
SET cte_max_recursion_depth = 10;
-- SET_VAR 힌트를 사용하여 cte_max_recursion_depth 값을 크게 설정
WITH RECURSIVE cte (no) AS (
SELECT 1 AS no
UNION ALL
SELECT (no + 1) FROM cte WHERE no < 1000)
SELECT /*+ SET_VAR(cte_max_recursion_depth=10000) */ * FROM cte;
-- 재귀적 CTE 테스트용 데이터베이스 생성
CREATE DATABASE tb_cte;
USE tb_cte
-- 테스트용 테이블 생성
CREATE TABLE employees (
id INT PRIMARY KEY NOT NULL,
name VARCHAR(100) NOT NULL,
manager_id INT NULL,
INDEX(manager_id),
FOREIGN KEY (manager_id) REFERENCES employees (id)
);
-- 데이터 추가
INSERT INTO employees
VALUES (333, "Yasmina", null),
(198, "John", 333),
(692, "Tarek", 333),
(29, "Pedro", 198),
(4610, "Sarah", 29),
(72, "Pierre", 29),
(123, "Adil", 692);
-- Adil(id=123)의 상위 조직장을 찾는 CTE 쿼리
WITH RECURSIVE managers AS (
SELECT *, 1 AS lv
FROM employees
WHERE id = 123
UNION ALL
SELECT e.*, (lv + 1)
FROM managers m
INNER JOIN employees e
ON e.id = m.manager_id
AND m.manager_id IS NOT NULL
)
SELECT * FROM managers ORDER BY lv DESC;
+------+---------+------------+------+
| id | name | manager_id | lv |
+------+---------+------------+------+
| 333 | Yasmina | NULL | 3 |
| 692 | Tarek | 333 | 2 |
| 123 | Adil | 692 | 1 |
+------+---------+------------+------+
-- 재귀적으로 상위 조직장의 순서를 나열
WITH RECURSIVE managers AS (
SELECT *,
CAST(id AS CHAR(100)) AS manager_path,
1 AS lv
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.*,
CONCAT(e.id, ' -> ', m.manager_path) AS manager_path,
lv + 1
FROM managers m
INNER JOIN employees e ON e.manager_id = m.id
)
SELECT * FROM managers ORDER BY lv ASC;
+------+---------+------------+--------------------------+------+
| id | name | manager_id | manager_path | lv |
+------+---------+------------+--------------------------+------+
| 333 | Yasmina | NULL | 333 | 1 |
| 198 | John | 333 | 198 -> 333 | 2 |
| 692 | Tarek | 333 | 692 -> 333 | 2 |
| 29 | Pedro | 198 | 29 -> 198 -> 333 | 3 |
| 123 | Adil | 692 | 123 -> 692 -> 333 | 3 |
| 72 | Pierre | 29 | 72 -> 29 -> 198 -> 333 | 4 |
| 4610 | Sarah | 29 | 4610 -> 29 -> 198 -> 333 | 4 |
+------+---------+------------+--------------------------+------+
WHERE
→ FROM
→ GROUP BY
→ ORDER BY
→ 윈도우 함수
→ SELECT
→ ORDER BY
→ LIMIT
AGGREGATE_FUNC() OVER(<partition> <order>) AS window_func_column
OVER()
절을 가지고 있어야 하며, 윈도우 함수로만 사용될 수 있다.SELECT
할 때 아무런 잠금도 걸지 않는다.SELECT
절에서 FOR SHARE
, FOR UPDATE
를 사용하면 잠금을 걸 수 있다. FOR SHARE
: 읽기 잠금FOR UPDATE
: 쓰기 잠금OF 테이블
옵션을 주면 명시적으로 테이블을 지정해서 잠금을 걸 수 있다.NO WAIT
는 레코드가 잠긴 상태라도 무시하고 즉시 에러를 반환한다.SKIP LOCKED
는 잠금이 걸리지 않은 레코드만 가져온다.