SET STATISTICS IO ON
둘 다 의 출력은 비슷해 보입니다.
SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10
준다
V2
Table '#58B62A60'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#58B62A60'. Scan count 10, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
T2
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
아론이 코멘트에서 지적 하듯에서 추구 중첩 루프 인덱스에 의해 구동 계획이다 모두 동안으로 그리고 테이블 변수 버전에 대한 계획은 실제로 효율성이 떨어집니다 A가에 인덱스로 추구 테이블 버전 수행 잔류 조건과 테이블 변수 반면, version은 잔존 술어 로 인덱스 검색을 수행 하므로 더 많은 행을 처리합니다 (이 계획은 더 많은 수의 행에 대해 성능이 좋지 않은 이유)dbo.NUM
#temp
[#T].n = [dbo].[NUM].[n]
[#T].[n]<=[@total]
@V.n <= [@total]
@V.[n]=[dbo].[NUM].[n]
확장 이벤트 를 사용 하여 특정 spid에 대한 대기 유형을 확인하면이 결과가 10,000 번 실행됩니다.EXEC dbo.T2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| SOS_SCHEDULER_YIELD | 16 | 19 | 19 | 0 |
| PAGELATCH_SH | 39998 | 14 | 0 | 14 |
| PAGELATCH_EX | 1 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
이 결과는 10,000 회 실행 EXEC dbo.V2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| PAGELATCH_EX | 2 | 0 | 0 | 0 |
| PAGELATCH_SH | 1 | 0 | 0 | 0 |
| SOS_SCHEDULER_YIELD | 676 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
따라서 테이블 케이스 PAGELATCH_SH
에서 대기 수가 훨씬 더 높습니다 #temp
. 확장 이벤트 추적에 대기 자원을 추가하는 방법을 알지 못 하므로이 추가 조사를 수행했습니다.
WHILE 1=1
EXEC dbo.T2 10
다른 연결 폴링 중에 sys.dm_os_waiting_tasks
CREATE TABLE #T(resource_description NVARCHAR(2048))
WHILE 1=1
INSERT INTO #T
SELECT resource_description
FROM sys.dm_os_waiting_tasks
WHERE session_id=<spid_of_other_session> and wait_type='PAGELATCH_SH'
약 15 초 동안 실행 한 후 다음 결과를 수집했습니다.
+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
| 1098 | 2:1:150 |
| 1689 | 2:1:146 |
+-------+----------------------+
래칭되는이 두 페이지는 모두 tempdb.sys.sysschobjs
기본 테이블에서 ( 'nc1'
및 ) 다른 클러스터되지 않은 인덱스에 속합니다 'nc2'
.
tempdb.sys.fn_dblog
실행 중에 쿼리 하면 각 저장 프로 시저의 첫 번째 실행에 의해 추가 된 로그 레코드의 수는 다소 가변적이지만 후속 실행의 경우 각 반복에 의해 추가 된 수는 매우 일관되고 예측 가능합니다. 프로 시저 계획이 캐시되면 로그 항목 수는 #temp
버전에 필요한 것의 약 절반 입니다.
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
#temp
SP 의 테이블 버전 SP 에 대한 트랜잭션 로그 항목을 자세히 보면 저장 프로 시저를 호출 할 때마다 3 개의 트랜잭션과 테이블 변수가 1 개만 생성됩니다.
+---------------------------------+----+---------------------------------+----+
| #Temp Table | @Table Variable |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE | 9 | | |
| INSERT | 12 | TVQuery | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable | 5 |
+---------------------------------+----+---------------------------------+----+
INSERT
/ TVQUERY
트랜잭션 이름을 제외하고 동일합니다. 여기에는 임시 테이블 또는 테이블 변수에 삽입 된 10 개의 행 각각에 대한 로그 레코드와 LOP_BEGIN_XACT
/ LOP_COMMIT_XACT
항목이 포함됩니다.
CREATE TABLE
거래에만 나타납니다 #Temp
버전으로 다음과 같습니다.
+-----------------+-------------------+---------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+---------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_SHRINK_NOOP | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+---------------------+
FCheckAndCleanupCachedTempTable
트랜잭션이 모두 나타납니다하지만 6 개 추가 항목이 #temp
버전. 이들은 6 개의 행을 참조 sys.sysschobjs
하며 위와 정확히 동일한 패턴을 갖습니다.
+-----------------+-------------------+----------------------------------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+----------------------------------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_DELETE_ROWS | LCX_NONSYS_SPLIT | dbo.#7240F239.PK__#T________3BD0199374293AAB |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+----------------------------------------------+
두 트랜잭션에서이 6 개의 행을 보면 동일한 작업에 해당합니다. 첫 번째 LOP_MODIFY_ROW, LCX_CLUSTERED
는의 modify_date
열에 대한 업데이트 입니다 sys.objects
. 나머지 5 개 행은 모두 개체 이름 변경과 관련이 있습니다. name
영향을받는 NCI ( nc1
및 nc2
) 모두의 키 열 이기 때문에이 항목 은 삭제 / 삽입으로 수행되며 클러스터 된 인덱스로 돌아가서 업데이트됩니다.
을위한 것으로 보인다 #temp
테이블 버전에 의해 수행되는 정리의 저장 프로 시저의 끝 부분 때 FCheckAndCleanupCachedTempTable
트랜잭션이 같은 것을에서 임시 테이블의 이름을 변경하는 #T__________________________________________________________________________________________________________________00000000E316
등의 다른 내부 이름 #2F4A0079
과 입력 할 때 CREATE TABLE
트랜잭션이 다시 이름을 바꿉니다. 이 플립 플롭 이름은 한 연결에서 다른 연결 dbo.T2
에서는 루프에서 실행될 수 있습니다.
WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects
WHERE name LIKE '#%'
결과 예
따라서 Alex가 언급 한 성능 차이에 대한 잠재적 설명 중 하나는 시스템 테이블을 유지 관리하는 것이 추가 작업 tempdb
이라는 점입니다.
루프에서 Visual Studio Code 프로파일 러를 사용하여 두 절차를 모두 실행하면 다음이 드러납니다.
+-------------------------------+--------------------+-------+-----------+
| Function | Explanation | Temp | Table Var |
+-------------------------------+--------------------+-------+-----------+
| CXStmtDML::XretExecute | Insert ... Select | 16.93 | 37.31 |
| CXStmtQuery::ErsqExecuteQuery | Select Max | 8.77 | 23.19 |
+-------------------------------+--------------------+-------+-----------+
| Total | | 25.7 | 60.5 |
+-------------------------------+--------------------+-------+-----------+
테이블 변수 버전은 삽입 명령문 및 후속 선택을 수행하는 데 약 60 %의 시간을 소비하는 반면 임시 테이블은 절반 미만입니다. 이것은 OP에 표시된 타이밍과 일치하며 위의 결론과 함께 성능의 차이는 쿼리 실행 자체에 소비 된 시간이 아니라 보조 작업을 수행하는 데 소요되는 시간에 달려 있습니다.
임시 테이블 버전에서 "누락"75 %에 기여하는 가장 중요한 기능은 다음과 같습니다.
+------------------------------------+-------------------+
| Function | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute | 26.26% |
| CXStmtDDL::FinishNormalImp | 4.17% |
| TmpObject::Release | 27.77% |
+------------------------------------+-------------------+
| Total | 58.20% |
+------------------------------------+-------------------+
작성 및 해제 기능 모두에서이 함수 CMEDProxyObject::SetName
는 포함 샘플 값으로 표시 19.6%
됩니다. 여기서 나는 임시 테이블 케이스에서 39.2 %의 시간이 앞에서 설명한 이름 바꾸기를 사용한다고 추론합니다.
다른 40 %에 기여하는 테이블 변수 버전에서 가장 큰 것은
+-----------------------------------+-------------------+
| Function | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate | 7.41% |
| TmpObject::Release | 12.87% |
+-----------------------------------+-------------------+
| Total | 20.28% |
+-----------------------------------+-------------------+
임시 테이블 프로파일
테이블 변수 프로파일
#temp
가 지워지고 이후에 9,999 번 더 다시 채워지더라도 통계가 테이블에 한 번만 작성됨을 나타냅니다 .