Do unused CTEs in queries affect performance and / or alter the generated query plan?
It doesn't appear that they do, but this really only applies to nested CTEs.
Create two temp tables:
CREATE TABLE #t1 (id INT);
INSERT #t1 ( id )
VALUES ( 1 );
CREATE TABLE #t2 (id INT);
INSERT #t2 ( id )
VALUES ( 1 );
Query 1:
WITH your_mom AS (
SELECT TOP 1 *
FROM #t1 AS t
),
also_your_mom AS (
SELECT TOP 1 *
FROM #t2 AS t
)
SELECT *
FROM your_mom;
Query 2:
WITH your_mom AS (
SELECT TOP 1 *
FROM #t1 AS t
),
also_your_mom AS (
SELECT TOP 1 *
FROM #t2 AS t
)
SELECT *
FROM also_your_mom;
Query plans:
[![NUTS][1]][1]
There is an overhead, but the unnecessary portion of the query is eliminated very early (during parsing in this case; the simplification stage in more complex cases), so the additional work is truly minimal, and does not contribute to potentially expensive cost-based optimization.
[1]: https://i.stack.imgur.com/Ryx9a.jpg
+1 to Erik, but wanted to add two things (which did not work well in a comment):
1. You don't even need to look at execution plans to see that they are ignored when not used. The following should produce a "divide by 0" error but does not due to `cte2` not being selected from at all:
;WITH cte1 AS
(
SELECT 1 AS [Bob]
),
cte2 AS (
SELECT 1 / 0 AS [Err]
FROM cte1
)
SELECT *
FROM cte1;
1. CTE's can be ignored, even if they are the only CTE, and even if they are selected from, _if_ logically all rows would be excluded anyway. The following is a case where the query optimizer knows ahead of time that no rows could be returned from the CTE, so it doesn't even bother to execute it:
;WITH cte AS
(
SELECT 1 / 0 AS [Bob]
)
SELECT TOP (1) [object_id]
FROM sys.objects
UNION ALL
SELECT cte.[Bob]
FROM cte
WHERE 1 = 0;
Regarding performance, the unused CTE is parsed and compiled (or at least compiled in the case below), so it is not 100% ignored, but the cost would have to be negligible and not worth being concerned about.
When only parsing, there is no error:
SET PARSEONLY ON;
;WITH cte1 AS
(
SELECT obj.[NotHere]
FROM sys.objects obj
)
SELECT TOP (1) so.[name]
FROM sys.objects so
GO
SET PARSEONLY OFF;
GO
When doing everything just short of execution, then there is a problem:
GO
SET NOEXEC ON;
GO
;WITH cte1 AS
(
SELECT obj.[NotHere]
FROM sys.objects obj
)
SELECT TOP (1) so.[name]
FROM sys.objects so
GO
SET NOEXEC OFF;
GO
/*
Msg 207, Level 16, State 1, Line XXXXX
Invalid column name 'NotHere'.
*/