거대한 테이블에 몇 개의 행을 삽입하는 느린 성능


9

매장에서 데이터를 가져와 회사 전체의 재고 테이블을 업데이트하는 프로세스가 있습니다. 이 테이블에는 날짜 및 항목별로 모든 상점에 대한 행이 있습니다. 많은 상점을 보유한 고객의 경우이 테이블은 5 억 개의 행으로 매우 커질 수 있습니다.

이 재고 갱신 프로세스는 일반적으로 상점이 데이터를 입력함에 따라 하루에 여러 번 실행됩니다. 이 실행은 소수의 상점에서만 데이터를 업데이트합니다. 그러나 고객은 지난 30 일 동안 모든 상점을 업데이트하기 위해이를 실행할 수도 있습니다. 이 경우 프로세스는 10 개의 스레드를 가동시키고 각 저장소의 인벤토리를 별도의 스레드로 업데이트합니다.

고객이 처리하는 데 시간이 오래 걸린다고 불평하고 있습니다. 프로세스를 프로파일 링하고이 테이블에 INSERT하는 하나의 쿼리가 예상보다 훨씬 많은 시간을 소비하고 있음을 발견했습니다. 이 삽입은 때때로 30 초 안에 완료됩니다.

이 테이블에 대해 임시 SQL INSERT 명령을 실행하면 (BEGIN TRAN 및 ROLLBACK에 의해 제한됨) 임시 SQL이 밀리 초 단위로 완료됩니다.

느리게 수행되는 쿼리는 다음과 같습니다. 아이디어는 다양한 데이터 비트를 계산할 때 존재하지 않는 레코드를 삽입하고 나중에 업데이트하는 것입니다. 프로세스의 이전 단계에서 업데이트해야 할 항목을 식별하고 일부 계산을 수행 한 후 tempdb 테이블 Update_Item_Work에 결과를 입력했습니다. 이 프로세스는 10 개의 개별 스레드에서 실행 중이며 각 스레드에는 Update_Item_Work에 고유 한 GUID가 있습니다.

INSERT INTO Inventory
(
    Inv_Site_Key,
    Inv_Item_Key,
    Inv_Date,
    Inv_BusEnt_ID,
    Inv_End_WtAvg_Cost
)
SELECT DISTINCT
    UpdItemWrk_Site_Key,
    UpdItemWrk_Item_Key,
    UpdItemWrk_Date,
    UpdItemWrk_BusEnt_ID,
    (CASE UpdItemWrk_Set_WtAvg_Cost WHEN 1 THEN UpdItemWrk_WtAvg_Cost ELSE 0 END)
FROM tempdb..Update_Item_Work (NOLOCK)
WHERE UpdItemWrk_GUID = @GUID
AND NOT EXISTS
    -- Only insert for site/item/date combinations that don't exist
    (SELECT *
    FROM Inventory (NOLOCK)
    WHERE Inv_Site_Key = UpdItemWrk_Site_Key
    AND Inv_Item_Key = UpdItemWrk_Item_Key
    AND Inv_Date = UpdItemWrk_Date)

재고 테이블에는 42 개의 열이 있으며, 대부분의 열은 다양한 재고 조정에 대한 수량 및 개수를 추적합니다. sys.dm_db_index_physical_stats에 따르면 각 행은 약 242 바이트이므로 단일 8KB 페이지에 약 33 개의 행이 들어갈 것으로 예상합니다.

테이블은 고유 제한 조건 (Inv_Site_Key, Inv_Item_Key, Inv_Date)에 클러스터됩니다. 모든 키는 DECIMAL (15,0)이며 날짜는 SMALLDATETIME입니다. IDENTITY 기본 키 (비 클러스터형) 및 4 개의 다른 인덱스가 있습니다. 모든 인덱스 및 클러스터 제약 조건은 명시 적으로 정의됩니다 (FILLFACTOR = 90, PAD_INDEX = ON).

페이지 분할을 계산하기 위해 로그 파일을 조사했습니다. 클러스터형 인덱스에서 약 1,027 개의 분할을 측정하고 다른 인덱스에서 1,724 개의 분할을 측정했지만 이러한 간격이 발생한 간격에 대해서는 기록하지 않았습니다. 1 시간 반 후에 클러스터 된 인덱스에서 7,035 페이지 분할을 측정했습니다.

프로파일 러에서 캡처 한 쿼리 계획은 다음과 같습니다.

Rows         Executes     StmtText                                                                                                                                             
----         --------     --------                                                                                                                                             
490          1            Sequence                                                                                                                                             
0            1              |--Index Update
0            1              |    |--Collapse
0            1              |         |--Sort
0            1              |              |--Filter
996          1              |                   |--Table Spool                                                                                                                 
996          1              |                        |--Split                                                                                                                  
498          1              |                             |--Assert
0            0              |                                  |--Compute Scalar
498          1              |                                       |--Clustered Index Update(UK_Inventory)
498          1              |                                            |--Compute Scalar
0            0              |                                                 |--Compute Scalar
0            0              |                                                      |--Compute Scalar
498          1              |                                                           |--Compute Scalar
498          1              |                                                                |--Top
498          1              |                                                                     |--Nested Loops
498          1              |                                                                          |--Stream Aggregate
0            0              |                                                                          |    |--Compute Scalar
498          1              |                                                                          |         |--Clustered Index Seek(tempdb..Update_Item_Work)
498          498            |                                                                          |--Clustered Index Seek(Inventory)
0            1              |--Index Update(UX_Inv_Exceptions_Date_Site_Item)
0            1              |    |--Collapse
0            1              |         |--Sort
0            1              |              |--Filter
996          1              |                   |--Table Spool
490          1              |--Index Update(UX_Inv_Date_Site_Item)
490          1                   |--Collapse
980          1                        |--Sort
980          1                             |--Filter
996          1                                  |--Table Spool                                                                                       

쿼리와 다양한 dmv를 살펴보면이 인벤토리 테이블의 페이지에서 쿼리가 0의 지속 시간 동안 PAGEIOLATCH_EX에서 대기하고 있음을 알 수 있습니다. 잠금이 대기 또는 차단되지 않습니다.

이 기계에는 약 32GB의 메모리가 있습니다. 2008 R2 Enterprise Edition으로 곧 업그레이드 될 예정이지만 SQL Server 2005 Standard Edition을 실행하고 있습니다. 디스크 사용량 측면에서 인벤토리 테이블의 크기에 대한 숫자는 없지만 필요한 경우 얻을 수 있습니다. 이 시스템에서 가장 큰 테이블 중 하나입니다.

sys.dm_io_virtual_file_stats에 대해 쿼리를 실행했으며 tempdb에 대한 평균 쓰기 대기 시간이 1.1 이상임을 알았 습니다 . 이 테이블이 저장된 데이터베이스의 평균 쓰기 대기 시간은 ~ 350ms입니다. 그러나 그들은 6 개월 정도마다 서버를 다시 시작하기 때문에이 정보가 관련이 있는지 모르겠습니다. tempdb는 4 개의 서로 다른 파일로 분산되어 있으며 인벤토리 테이블을 보유하는 데이터베이스에 대해 3 개의 서로 다른 파일이 있습니다.

단일 INSERT가 매우 빠를 때 많은 다른 스레드로 실행될 때이 쿼리가 왜 몇 개의 행을 삽입하는 데 시간이 오래 걸립니까?

-업데이트-

읽은 바이트를 포함하여 드라이브 당 대기 시간 수는 다음과 같습니다. 보시다시피 tempdb 성능에 문제가 있습니다. 재고 테이블은 PDICompany_252_01.mdf, PDICompany_252_01_Second.ndf 또는 PDICompany_252_01_Third.ndf에 있습니다.

ReadLatencyWriteLatencyLatencyAvgBPerRead AvgBPerWriteAvgBPerTransferDriveDB                     physical_name
         42        1112    623       62171       67654          65147R:   tempdb                 R:\Microsoft SQL Server\Tempdb\tempdev1.mdf
         38        1101    615       62122       67626          65109S:   tempdb                 S:\Microsoft SQL Server\Tempdb\tempdev2.ndf
         38        1101    615       62136       67639          65123T:   tempdb                 T:\Microsoft SQL Server\Tempdb\tempdev3.ndf
         38        1101    615       62140       67629          65119U:   tempdb                 U:\Microsoft SQL Server\Tempdb\tempdev4.ndf
         25         341     71       92767       53288          87009X:   PDICompany             X:\Program Files\PDI\Enterprise\Databases\PDICompany_Third.ndf
         26         339     71       90902       52507          85345X:   PDICompany             X:\Program Files\PDI\Enterprise\Databases\PDICompany_Second.ndf
         10         231     90       98544       60191          84618W:   PDICompany_FRx         W:\Program Files\PDI\Enterprise\Databases\PDICompany_FRx.mdf
         61         137     68        9120        9181           9125W:   model                  W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\modeldev.mdf
         36         113     97        9376        5663           6419V:   model                  V:\Microsoft SQL Server\Logs\modellog.ldf
         22          99     34       92233       52112          86304W:   PDICompany             W:\Program Files\PDI\Enterprise\Databases\PDICompany.mdf
          9          20     10       25188        9120          23538W:   master                 W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\master.mdf
         20          18     19       53419       10759          40850W:   msdb                   W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\MSDBData.mdf
         23          18     19      947956       58304         110123V:   PDICompany_FRx         V:\Program Files\PDI\Enterprise\Databases\PDICompany_FRx_1.ldf
         20          17     17      828123       55295         104730V:   PDICompany             V:\Program Files\PDI\Enterprise\Databases\PDICompany.ldf
          5          13     13       12308        4868           5129V:   master                 V:\Microsoft SQL Server\Logs\mastlog.ldf
         11          13     13       22233        7598           8513V:   PDIMaster              V:\Program Files\PDI\Enterprise\Databases\PDIMaster.ldf
         14          11     13       13846        9540          12598W:   PDIMaster              W:\Program Files\PDI\Enterprise\Databases\PDIMaster.mdf
         13          11     11       22350        1107           1110V:   msdb                   V:\Microsoft SQL Server\Logs\MSDBLog.ldf
         17           9      9      745437       11821          23249V:   PDIFoundation          V:\Program Files\PDI\Enterprise\Databases\PDIFoundation.ldf
         34           8     31       29490       33725          30031W:   PDIFoundation          W:\Program Files\PDI\Enterprise\Databases\PDIFoundation.mdf
          5           8      8       61560       61236          61237V:   tempdb                 V:\Microsoft SQL Server\Logs\templog.ldf
         13           6     11        8370       35087          16785W:   SAHost_Company01       W:\Program Files\PDI\Enterprise\Databases\SAHostCompany.mdf
          2           6      5       56235       33667          38911W:   SAHost_Company01       W:\Program Files\PDI\Enterprise\Databases\SAHost_Company_01_log.LDF

의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Paul White 9

답변:


4

클러스터 된 인덱스가 실제 데이터를 보유하고 있기 때문에 클러스터 된 인덱스 페이지 분할이 어려움을 겪고있는 것으로 보입니다. 그러면 새 페이지를 할당하고 데이터를 이들로 이동해야합니다. 이로 인해 페이지 잠금이 발생하여 차단 될 수 있습니다.

또한 클러스터형 인덱스 키는 21 바이트이며 모든 보조 인덱스에 책갈피로 저장해야합니다.

기본 키 ID 열을 클러스터형 인덱스로 만드는 것을 고려해 보았을 때 다른 인덱스의 크기가 줄어들뿐만 아니라 클러스터형 인덱스의 페이지 분할 수가 줄어 듭니다. 위장을 재건 할 수 있다면 시도해 볼 가치가 있습니다.


1

멀티 스레드 방식을 사용하면 먼저 키가 있는지 확인 해야하는 테이블에 삽입하는 것이 중요합니다. 그런 종류의 스레드가 있더라도 해당 테이블에 대한 PK 인덱스에 동시성 문제가 있다고 말합니다. 같은 이유로, 인벤토리 테이블에서 NOLOCK 힌트가 마음에 들지 않습니다. 다른 스레드가 동일한 키를 쓸 수 있으면 오류가 발생하는 것처럼 보입니다 (파티션 구성표가 해당 가능성을 제거합니까?). 여러 스레드의 초기 도입 속도가 어느 시점에서 잘 작동했기 때문에 속도가 얼마나 큰지 궁금합니다.

시도해야 할 것은 쿼리를 대량 작업처럼 읽히고 "존재하지 않는 곳"을 "조인 방지"로 변환하는 것입니다. (궁극적으로 옵티마이 저는이 노력을 무시하도록 선택할 수 있습니다). 위에서 언급했듯이 파티션이 스레드간에 키 충돌을 보장하지 않는 한 대상 테이블에서 NOLOCK 힌트를 제거합니다.

 INSERT INTO i (...)
 SELECT DISTINCT ...             
   FROM tempdb..Update_Item_Work t (NOLOCK) -- nolock okay on read table
   left join Inventory i -- use without NOLOCK because PK is written inter-thread
     on i.Inv_Site_Key = t.UpdItemWrk_Site_Key
    and i.Inv_Item_Key = t.UpdItemWrk_Item_Key
    and i.Inv_Date = t.UpdItemWrk_Date
  where i.Inv_Site_Key is null   -- where not exist in inventory
    and UpdItemWrk_GUID = @GUID  -- for this thread

기본으로 실행되는 타이밍의 경우 다른 가능성으로 병합 힌트 ( "왼쪽 조인"-> "왼쪽 병합 조인")를 사용하여 다시 실행할 수 있습니다. 병합 힌트에 대한 임시 테이블 (UpdItemWrk_Site_Key, UpdItemWrk_Item_Key, UpdItemWrk_Date)에 인덱스가 있어야합니다.

최신 비 표현 버전의 SQL Server 2008/2012가이 양식의 더 큰 병합을 자동으로 병렬화하여 GUID 기반 분할을 제거 할 수 있는지 여부는 알 수 없습니다.

조인이 모든 항목이 아닌 개별 항목에서만 발생하도록 장려하기 위해 "select distinct ... from ..."절을 "select * from (select distinct ... from ...)"로 변환 할 수 있습니다. 조인을 계속합니다. 구별이 많은 행을 필터링하는 경우에만 눈에 띄는 차이를 만들 수 있습니다. 다시 한 번 옵티마이 저는이 노력을 무시할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.