SQL Server CTE loop; insert all record together
You could use MERGE
:
select * into #t1
from(
select 'va1'c1,'vb1'c2,'vc1'c3 union all
select 'va2'c1,'vb2'c2,'vc2'c3 union all
select 'va3'c1,'vb3'c2,'vc3'c3 union all
select 'va1'c1,'vb1'c2,'vc1'c3 union all
select 'va2'c1,'vb2'c2,'vc2'c3 union all
select 'va3'c1,'vb3'c2,'vc3'c3 union all
select 'va1'c1,'vb1'c2,'vc1'c3 union all
select 'va2'c1,'vb2'c2,'vc2'c3 union all
select 'va3'c1,'vb3'c2,'vc3'c3 union all
select 'va1'c1,'vb1'c2,'vc1'c3 union all
select 'va2'c1,'vb2'c2,'vc2'c3 union all
select 'va3'c1,'vb3'c2,'vc3'c3 union all
select 'va4'c1,'vb4'c2,'vc4'c3
)t;
select * into #t2 from #t1 where 0=1;
;with tmp1 as(
select t1.*, ROW_NUMBER()over(partition by t1.c1 order by(select null))r
from #t1 t1
left join #t2 t2
on t1.c1=t2.c1
where t2.c1 is null
),tmp2 as (
select 0 n,*
from tmp1
union all
select n+1 n,t1.c1,t1.c2,t1.c3,t1.r
from tmp2 t1
join tmp1 t2
on t1.c1=t2.c1
and t2.r=t1.r+1
where n<10
)
MERGE #t2
USING tmp2
ON #t2.c1 = tmp2.c1
WHEN NOT MATCHED THEN
INSERT VALUES (tmp2.c1, tmp2.c2, tmp2.c3);
SELECT @@ROWCOUNT;
-- 103
DBFiddle Demo
EDIT:
Kudos to Bartosz Ratajczyk for examining this case:
It turns out it is related to lazy/eager table/index spooling. There are at least two more ways to force SQL Server to generate different execution plan:
a) By using TOP (100) PERCENT
DECLARE @n INT = 100;
;with tmp1 as (
select t1.*,
ROW_NUMBER()over(partition by t1.c1 order by(select null))r
from #t1 t1
left join #t2 t2
on t1.c1=t2.c1
where t2.c1 is null
),tmp2 as
(
select 0 n,*
from tmp1
union all
select n+1 n,t1.c1,t1.c2,t1.c3,t1.r
from tmp2 t1
join tmp1 t2
on t1.c1=t2.c1
and t2.r=t1.r+1
where n<10
)
insert #t2
select TOP (@n) PERCENT c1, c2, c3 --,r
from tmp2
SELECT @@ROWCOUNT;
b) By using ORDER BY .. OFFSET 0 ROWS
:
;with tmp1 as(
select t1.*,
ROW_NUMBER()over(partition by t1.c1 order by(select null))r
from #t1 t1
left join #t2 t2
on t1.c1=t2.c1
where t2.c1 is null
),tmp2 as
(
select 0 n,*
from tmp1
union all
select n+1 n,t1.c1,t1.c2,t1.c3,t1.r
from tmp2 t1
join tmp1 t2
on t1.c1=t2.c1
and t2.r=t1.r+1
where n<10
)
insert #t2
select c1, c2, c3 --,r
from tmp2
ORDER BY 1 OFFSET 0 ROWS;
SELECT @@ROWCOUNT;
db<>fiddle demo2
Addendum: How does the recursive CTE work? by Bartosz Ratajczyk
You hit a peculiarity in MS SQL server's CTE implementation. It is not handled that way in all backends. You have to select first into a temporary cursor and then insert from it. ie:
SELECT *
INTO #t1
FROM(
SELECT 'va1' c1, 'vb1' c2, 'vc1' c3
UNION ALL
SELECT 'va2' c1, 'vb2' c2, 'vc2' c3
UNION ALL
SELECT 'va3' c1, 'vb3' c2, 'vc3' c3
UNION ALL
SELECT 'va1' c1, 'vb1' c2, 'vc1' c3
UNION ALL
SELECT 'va2' c1, 'vb2' c2, 'vc2' c3
UNION ALL
SELECT 'va3' c1, 'vb3' c2, 'vc3' c3
UNION ALL
SELECT 'va1' c1, 'vb1' c2, 'vc1' c3
UNION ALL
SELECT 'va2' c1, 'vb2' c2, 'vc2' c3
UNION ALL
SELECT 'va3' c1, 'vb3' c2, 'vc3' c3
UNION ALL
SELECT 'va1' c1, 'vb1' c2, 'vc1' c3
UNION ALL
SELECT 'va2' c1, 'vb2' c2, 'vc2' c3
UNION ALL
SELECT 'va3' c1, 'vb3' c2, 'vc3' c3
UNION ALL
SELECT 'va4' c1, 'vb4' c2, 'vc4' c3
)t;
SELECT * INTO #t2 FROM #t1 WHERE 0=1;
DECLARE @tmp TABLE(c1 VARCHAR(10), c2 VARCHAR(10), c3 VARCHAR(10));
WITH
tmp1 AS (
SELECT t1.*, ROW_NUMBER() OVER (PARTITION BY t1.c1 ORDER BY(SELECT NULL)) r
FROM #t1 t1
LEFT JOIN #t2 t2 ON t1.c1=t2.c1
WHERE t2.c1 IS NULL
),
tmp2 AS (
SELECT 0 n, * FROM tmp1
UNION ALL
SELECT n+1 n, t1.c1, t1.c2, t1.c3, t1.r
FROM tmp2 t1
JOIN tmp1 t2 ON t1.c1=t2.c1
AND t2.r=t1.r+1
WHERE n<10
)
INSERT @tmp(c1, c2, c3)
SELECT c1, c2, c3 --,r
FROM tmp2;
INSERT #t2 SELECT * FROM @tmp;
SELECT * FROM #t2;
DROP TABLE #t1;
DROP TABLE #t2;