VillageSQL is a drop-in replacement for MySQL with extensions.
All examples on this page work on VillageSQL. Install Now →
optimizer_switch 설정에서 나오므로 여러분의 수치는 다를 것입니다. 각 계획의 형태가 그대로 옮겨지는 부분입니다.
규칙: 병합 가능한가 아닌가
CTE는 외부 쿼리에 접혀 들어가거나 임시 테이블로 먼저 만들어지는데, 어느 쪽이 되는지는 CTE를 몇 번 참조하는지가 아니라 CTE에 무엇이 들어 있는지로 결정됩니다. 본문이WHERE와 JOIN이 있는 단순한 SELECT인 CTE는 병합되어 계획에서 완전히 사라집니다:
GROUP BY, DISTINCT, LIMIT, UNION, 또는 윈도 함수를 포함하는 CTE는 병합될 수 없습니다. 외부 쿼리의 조건을 그 연산들 앞에 적용하면 결과가 동등하지 않기 때문입니다. 이런 CTE는 구체화됩니다.
아래의 계획들은 그 두 번째 경우를 보여 줍니다. WITH라는 단어가 아니라 CTE의 내용에 관한 것으로 읽으세요.
어느 쪽을 얻었는지 확인하기
EXPLAIN FORMAT=TREE는 구체화 여부를 그 단어 그대로 말해 줍니다. 기본 EXPLAIN은 그렇지 않습니다: 여기의 쿼리들에서 Extra 열에는 그 단어가 나타나지 않습니다.
Materialize CTE counts입니다. 그 안의 GROUP BY가 이를 강제하며, 서버는 행을 임시 테이블에 쓰고 다시 읽습니다.
같은 GROUP BY를 포함하는 파생 테이블도 같은 계획을 얻습니다. FROM (SELECT rating, COUNT(*) ... GROUP BY rating) AS counts로 작성하면 노드 이름만 빼고 트리가 동일하고 비용도 같습니다. 따라서 단일 참조 쿼리에서 CTE가 파생 테이블보다 나은 점은 없습니다: 두 형태는 읽히는 방식이 다를 뿐, 옵티마이저는 둘을 똑같이 다룹니다.
같은 CTE를 두 번 참조하면 문구가 if needed로 바뀌고, 두 번째 참조는 같은 구체화된 결과를 재사용합니다:
구체화의 비용
구체화된 CTE는 임시 테이블이며, 이는 알아 둘 만한 두 가지를 뜻합니다. 기본 테이블의 인덱스를 포기합니다. 행이 임시 테이블에 들어가면 외부 쿼리는 그 테이블을 스캔합니다. 서버는 위의 조인에서 그랬듯 즉석에서auto_key를 만들 수 있지만, 그것은 실제 테이블에 있던 인덱스가 아닙니다. 외부 조건이 PRIMARY에 도달했던 이 페이지 맨 위의 병합된 계획과 비교해 보세요.
푸시다운이 이를 완화합니다. derived_condition_pushdown은 기본적으로 켜져 있고 옮길 수 있는 것을 안쪽으로 옮기는데, 이것이 위에서 Filter: (count(0) > 200)이 Materialize 노드 바깥이 아니라 안쪽에 나타나는 이유입니다. 모든 것을 옮길 수는 없으므로, 계획에서 CTE가 결과에 남는 것보다 훨씬 많은 행을 생산하는 것으로 보이면 제한 조건을 직접 CTE 안에 작성하세요.
CTE가 맞지 않는 형태인 경우
- 짧은 단일 단계. 파생 테이블은 사용되는 자리에 그대로 둡니다. 한 줄짜리 서브쿼리에 이름을 붙이면 읽는 사람에게 한 번 더 건너뛰게 합니다.
- 옵티마이저가 푸시다운하지 않은 필터. 계획에서 CTE가 모든 행을 구체화하고 외부 쿼리가 대부분을 버리는 것으로 보이면 필터를 안쪽에 작성하세요.
- 인덱스에 대한 행 단위 작업. 상관 서브쿼리나 조인은 각 조회마다 인덱스를 사용할 수 있습니다. 구체화된 CTE는 이미 그것을 포기했습니다.
SELECT하는 방식으로 단독 실행할 수 있어, 두 번째 단계를 보기 전에 첫 번째 단계가 맞는지 알려 줍니다. FROM 절 한가운데에서 파생 테이블을 꺼내어 직접 실행하는 것은 매번 수작업으로 하는 같은 일입니다.
실제 계층 구조에 대한 재귀 CTE
Sakila에는 계층 구조가 없으므로 이 절은 자체 테이블을 사용합니다. 샘플 데이터 옆에 만들고, 다 사용한 후에는DROP TABLE employees;를 실행하세요:
depth 열은 답이자 보호 장치입니다.
WHERE oc.depth < 100이 그것을 막습니다. 무한한 순회를 유한한 순회로 바꾸며, 데이터가 깨끗하든 아니든 동작합니다.
오류 메시지는 cte_max_recursion_depth를 올리라고 제안하는데, 이는 재귀가 실제로 종료되고 기본값 1,000보다 많은 반복이 필요할 때만 맞습니다. 정상적인 1,500단계 시리즈도 같은 오류에 부딪히며, 그 경우 SET SESSION cte_max_recursion_depth = 5000이 올바른 답입니다. 순환 데이터에서는 틀린 답입니다: 쿼리는 여전히 끝나지 않고, 그저 더 늦게 실패할 뿐입니다.
사용 가능 여부와 용어
CTE는 MySQL 8.0에서 도입되었고 8.4와 9.x를 포함한 이후 모든 릴리스에 있습니다.WITH 절에 ERROR 1064로 답하는 서버는 그 이전 버전입니다. SELECT VERSION()으로 확인하세요.
표준은 재귀 CTE의 첫 부분을 앵커 멤버, 두 번째 부분을 재귀 멤버라고 부릅니다. 튜토리얼은 첫 부분을 시드라고 부르는데 같은 것이며, MySQL 자체의 메시지는 표준 용어를 사용합니다.
CTE는 INSERT, UPDATE, DELETE에서도 동작합니다
문제 해결
함께 보기
- MySQL의 서브쿼리 대 JOIN — 이것과 경쟁하는 또 다른 형태
- MySQL GROUP BY 성능 — CTE가 피하도록 도울 수 있는 임시 테이블
- MySQL 뷰 — 이름 붙은 쿼리를 스키마 객체로 영구 저장하기
- 공통 테이블 표현식 — 구문을 먼저 보고 싶다면 튜토리얼