web-dev-qa-db-ja.com

SQL Serverテーブル内に数百万の行を効果的な方法で挿入/更新するにはどうすればよいですか?

何百万ものデータ行を処理し、それらを一時テーブル(ステージングテーブル)に保存し、最後にデータベース内のテーブルに保存するプロシージャをSQL Server内に作成しなければならないことがよくあります。

SSISのような代替ソリューションについては検討していません。

インデックスの無効化、制約、DBのオフライン化、リカバリモードの変更などはできません。

システムの混雑が少ないときにこのプロセスを実行するように設定することを検討しましたが、24時間年中無休のオンライン小売業者環境で作業しています。

非常によく似た質問があります: テーブルへの数百万行のパフォーマンスの挿入と更新

この質問も関連しています: 多数の行を挿入する最も速い方法は何ですか?

例1:

CREATE PROCEDURE [dbo].[udpstaging_page_import_fromFilter]
      @sourceDesc nvarchar(50) -- e.g. 'Coremetrics'
      ,@feedDesc nvarchar(50) -- e.g. 'Daily Exports'
      ,@process_job_task_logID bigint
AS BEGIN


      SET NOCOUNT ON;
      BEGIN TRY


            --truncate table prior INSERT
            exec dbo.udpstaging_page_truncateTable;

            declare @source_feedID int;
            exec crm_admin.dbo.udpsource_feedID_select @sourceDesc
                  ,@feedDesc
                  ,@source_feedID = @source_feedID OUTPUT;


            -- drop temp tables
            if exists (select * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#pageImport'))
                  drop table #pageImport;


            -- create temp tables
            create table #pageImport(
                  pageImportID [bigint] identity(1,1) NOT NULL
                  ,pageCode [varbinary](16) NOT NULL
            );


            insert into #pageImport(
                  pageCode
            )
            select pageCode
            from Coremetrics.PageView
            group by pageCode;


            -- add indexes to temp table
            CREATE CLUSTERED INDEX IDX_pageImport_pageImportID ON #pageImport(pageImportID);
            CREATE INDEX IDX_pageImport_pageCode ON #pageImport(pageCode);


            declare @recordCount bigint
                  ,@counter int
                  ,@maxCounter int
                  ,@updateRowCount int;

            select @counter = MIN(pageImportID)
                  ,@recordCount = MAX(pageImportID)
            from #pageImport;

            set @updateRowCount = 1000000;

            while @counter <= @recordCount
            begin

                  set @maxCounter = (@counter + @updateRowCount - 1);

                  with pageImport as (
                        select pv.pageCode
                              ,pv.websiteCode as 'pageCIV'
                              ,dbo.udfDerivePageName(pv.PAGE_ID, pv.CONTENT_CATEGORY_ID) as 'pageName'
                              ,dbo.udfDerivePageName(pv.PAGE_ID, pv.CONTENT_CATEGORY_ID) as 'pageDesc'
                              ,pv.[TIMESTAMP] as 'pageCreateDate'
                              ,pv.pageTypeCode
                              ,'' as 'pageTypeCIV'
                              ,pv.websiteCode
                              ,pv.marketID
                              ,@source_feedID as 'source_feedID'
                              ,@process_job_task_logID as 'process_job_task_logID'
                              ,GETDATE() as 'createdDate'
                              ,SUSER_NAME() as 'createdBy'
                              ,GETDATE() as 'modifiedDate'
                              ,SUSER_NAME() as 'modifiedBy'
                              ,ROW_NUMBER() over (
                                    PARTITION BY [pi].pageCode
                                    ORDER BY pv.[TIMESTAMP]
                              ) as 'is_masterPageImport'
                        from #pageImport [pi]
                        inner join Coremetrics.PageView pv on pv.pageCode = [pi].pageCode
                              and [pi].pageImportID between @counter and @maxCounter
                  )                 
                  insert into staging.[page](
                        pageCode
                        ,pageCIV
                        ,pageName
                        ,pageDesc
                        ,pageCreateDate
                        ,pageTypeCode
                        ,pageTypeCIV
                        ,websiteCode
                        ,marketID
                        ,source_feedID
                        ,process_job_task_logID
                        ,createdDate
                        ,createdBy
                        ,modifiedDate
                        ,modifiedBy
                  )
                  select pageCode
                        ,pageCIV
                        ,pageName
                        ,pageDesc
                        ,pageCreateDate
                        ,pageTypeCode
                        ,pageTypeCIV
                        ,websiteCode
                        ,marketID
                        ,source_feedID
                        ,process_job_task_logID
                        ,createdDate
                        ,createdBy
                        ,modifiedDate
                        ,modifiedBy 
                  from pageImport
                  where 1 = 1
                        and is_masterPageImport = 1;

                  set @counter = @counter + @updateRowCount;
            end;


            SET NOCOUNT OFF;
            RETURN 0;

      END TRY
      BEGIN CATCH
            print N'inner catch: ' + error_message();
            SET NOCOUNT OFF;
            RETURN -10;
      END CATCH

END;

例2:

これは、ここに投稿するには大きすぎるストアドプロシージャの一部です。

IF OBJECT_ID('tempdb.dbo.#ztblOrgProductStockView', 'U') IS NOT NULL
    DROP TABLE #ztblOrgProductStockView;

CREATE TABLE #ztblOrgProductStockView (
    [lngID] [int] NOT NULL IDENTITY PRIMARY KEY,
    [sintMarketId] [smallint] NOT NULL,
    [sintChannelId] [smallint] NOT NULL,
    [strOrgVwName] [varchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
    [tintSequence] [tinyint] NOT NULL,
    [tintOrgGrpId] [tinyint] NOT NULL,
    [strTier1] [varchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [strTier2] [varchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [strTier3] [varchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [strTier4] [varchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [strTier5] [varchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [strTier6] [varchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [strItemNo] [varchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [strStockTypeName] [varchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
    [tintStockTypeId] [tinyint] NOT NULL,
    [sintDueWithinDays] [tinyint] NOT NULL,
    [bitOverSellingAllowed] [bit] NOT NULL,
    [dtmStartDate] [datetime] NULL,
    [dtmEndDate] [datetime] NULL,
    [dtmExpected] [datetime] NULL,
    [blnIsLocalToMarket] [bit] NULL,
    [blnPremiunDelvAllowed] [bit] NULL,
    [strStdDeliveryDaysCode] [varchar](20)
)



INSERT into #ztblOrgProductStockView (
    sintMarketId
    ,sintChannelId
    ,strOrgVwName
    ,tintSequence
    ,tintOrgGrpId
    ,strTier1
    ,strTier2
    ,strTier3
    ,strTier4
    ,strTier5
    ,strTier6
    ,strItemNo
    ,strStockTypeName
    ,tintStockTypeId
    ,sintDueWithinDays
    ,bitOverSellingAllowed
    ,dtmStartDate
    ,dtmEndDate
    ,dtmExpected
    ,blnIsLocalToMarket
    ,blnPremiunDelvAllowed
    ,strStdDeliveryDaysCode
)
    select
        rv.sintMarketId
        ,rv.sintChannelId
        ,rv.strOrgVwName
        ,tintSequence
        ,tintOrgGrpId
        ,ISNULL(rv.pnTier1,'ÿ')
        ,ISNULL(rv.pnTier2,'ÿ')
        ,ISNULL(rv.pnTier3,'ÿ')
        ,ISNULL(rv.strTier4,'ÿ')
        ,ISNULL(rv.strTier5,'ÿ')
        ,ISNULL(rv.strTier6,'ÿ')
        ,rv.strItemNo
        ,strStockTypeName
        ,tintStockTypeId
        ,sintDueWithinDays
        ,bitOverSellingAllowed
        ,dtmStartDate
        ,dtmEndDate
        ,dtmExpected
        ,blnIsLocalToMarket
        ,blnPremiunDelvAllowed
        ,strStdDeliveryDaysCode
    from #ztblOrgProductRangeView_1 rv
    inner join #ztblOrgProductSeqView_1 sv on rv.strItemNo = sv.strItemNo
        and rv.lngOrgVwId = sv.lngOrgVwId
    --order by rv.sintMarketId, rv.sintChannelId, sv.tintOrgGrpId, rv.strItemNo, sv.tintStockTypeId

--set @DebugDate = convert(nvarchar(10),getdate(),108)
--raiserror('%s [%s]', 0, 1, N'Populated #ztblOrgProductStockView', @DebugDate) with nowait

--select [sintMarketId], [sintChannelId], [tintOrgGrpId], [strItemNo], [tintStockTypeId], count(*)
--from [#ztblOrgProductStockView]
--group by [sintMarketId], [sintChannelId], [tintOrgGrpId], [strItemNo], [tintStockTypeId]
--having count(*) > 1

    set @lngRowcount = @@ROWCOUNT
    set nocount on;

        While @lngRowcount > 0
        Begin
            Set @lngMinID = @lngMaxID
            Set @lngMaxID = @lngMaxID + 5000

            INSERT INTO [ztblOrgProductStockView]
                   ([sintActiveView]    
                   ,[sintMarketId]
                   ,[sintChannelId]
                   ,[strOrgVwName]  
                   ,[tintSequence]
                   ,[tintOrgGrpId]
                   ,[strTier1]
                   ,[strTier2]
                   ,[strTier3]
                   ,[strTier4]
                   ,[strTier5]
                   ,[strTier6]
                   ,[strItemNo]
                   ,[strStockTypeName]
                   ,[tintStockTypeId]
                   ,[sintDueWithinDays]
                   ,[bitOverSellingAllowed]
                   ,[dtmStartDate]
                   ,[dtmEndDate]
                   ,[dtmExpected]
                   ,[blnIsLocalToMarket]
                   ,[blnPremiunDelvAllowed]
                   ,[strStdDeliveryDaysCode])
            Select
                    @sintActiveView_new
                   ,[sintMarketId]
                   ,[sintChannelId]
                   ,[strOrgVwName]
                   ,[tintSequence]
                   ,[tintOrgGrpId]
                   ,[strTier1]
                   ,[strTier2]
                   ,[strTier3]
                   ,[strTier4]
                   ,[strTier5]
                   ,[strTier6]
                   ,[strItemNo]
                   ,[strStockTypeName]
                   ,[tintStockTypeId]
                   ,[sintDueWithinDays]
                   ,[bitOverSellingAllowed]
                   ,[dtmStartDate]
                    ,[dtmEndDate]
                   ,[dtmExpected]
                   ,[blnIsLocalToMarket]
                   ,[blnPremiunDelvAllowed]
                   ,[strStdDeliveryDaysCode]
            From #ztblOrgProductStockView
            Where lngID >= @lngMinID
            And   lngID <  @lngMaxID

            set @lngRowcount = @@ROWCOUNT

        End

質問ここでは意見に基づく回答が最も人気があるわけではないことに注意してください。可能な場合は証拠を提示してください。

1)バッチのサイズを整理する最良の方法を決定する方法は?たとえば、例2では5000です。

2)一般的に、パフォーマンスが向上する可能性はBEGIN TRANSACTIONおよびCOMMIT TRANSACTION 以内 while loop?バッチの1つのトランザクション。

3)バッチのサイズを変更したい場合、バッチのサイズを増やすことができるかどうかを判断するために監視できることまたは、I/Oレイテンシを引き起こしていますか?

現在、以下のスクリプトを使用してI/Oレイテンシを見つけています。

-- How to identify I/O latency issues
-- Below SQL code helps in identifying the I/O latency issues in a SQL Server system on a per-file basis.
-- http://sqlserverdbknowledge.wordpress.com/2011/11/08/how-to-identify-io-latency-issues/
--http://www.sqlskills.com/blogs/paul/how-to-examine-io-subsystem-latencies-from-within-sql-server/

--MARCELO MIORELLI 26-JULY-2013

SELECT 
--- virtual file latency
ReadLatency = CASE WHEN num_of_reads = 0
THEN 0 ELSE (io_stall_read_ms / num_of_reads) END,
WriteLatency = CASE WHEN num_of_writes = 0 
THEN 0 ELSE (io_stall_write_ms / num_of_writes) END,
Latency = CASE WHEN (num_of_reads = 0 AND num_of_writes = 0)
THEN 0 ELSE (io_stall / (num_of_reads + num_of_writes)) END,
--– avg bytes per IOP
AvgBPerRead = CASE WHEN num_of_reads = 0 
THEN 0 ELSE (num_of_bytes_read / num_of_reads) END,
AvgBPerWrite = CASE WHEN io_stall_write_ms = 0 
THEN 0 ELSE (num_of_bytes_written / num_of_writes) END,
AvgBPerTransfer = CASE WHEN (num_of_reads = 0 AND num_of_writes = 0)
THEN 0 ELSE ((num_of_bytes_read + num_of_bytes_written) / 
(num_of_reads + num_of_writes)) END, 
LEFT (mf.physical_name, 2) AS Drive,
DB_NAME (vfs.database_id) AS DB,
--- –vfs.*,
mf.physical_name
FROM sys.dm_io_virtual_file_stats (NULL,NULL) AS vfs
JOIN sys.master_files AS mf
ON vfs.database_id = mf.database_id
AND vfs.file_id = mf.file_id
--WHERE vfs.file_id = 2 — log files
-- ORDER BY Latency DESC
-- ORDER BY ReadLatency DESC
ORDER BY WriteLatency DESC;
GO 
5

ここでは意見に基づく回答が最も一般的ではありません。可能な場合は証拠を提示してください。

まあ、それが完全に公平であるとは限りません。最終的に、何が最もうまくいくかについての「証拠」はyourシステムから得られます;-)。ハードウェア、データ、システムの負荷などによって、最適なものが決まります。物事への取り組み方やシステムの仕組みには多くの変数があるため、あるシステムで最適に機能するものは、別のシステムではそれほど優れていない場合があります。

1)バッチのサイズを整理する最良の方法をどのように決定するのですか?たとえば、例2では5000です。

これは主に試行錯誤の問題であり、何が最適に機能するかを確認します。ただし、ロックのエスカレーションは通常5000ロックで発生することに注意してください。データの編成方法に応じて、5000の変更は5000の行ロックまたはいくつかのページロックになる可能性があります。行の順序は異なるインデックス間で異なる可能性があるため、これはオブジェクトごとです。重要なのは、これらの操作中に他のプロセスが使用できるようにする必要があるテーブルに加えられた変更は、テーブルロック(つまり、ロックのエスカレーションの結果)を回避しようとすることです。ただし、ステージングテーブルや一時テーブルなどのテーブルは、単一の操作であり、競合がないため、テーブルロックのメリットがあります。したがって、「バルク」TABLOCKを実行するときのINSERTヒントです。

2) whileループ内でBEGIN TRANSACTIONとCOMMIT TRANSACTIONを実行すると、通常はパフォーマンスが向上する可能性が高くなりますか?バッチの1つのトランザクション。

複数のDML操作を明示的なトランザクションにラップすると、それらの操作が行ごとの場合のパフォーマンスが大幅に向上します。ここに投稿したコードから、すでにセットベースの操作を実行しているため、トランザクションに何かを組み合わせてもタイミングのメリットはわずかです。また、両方の例のWHILEループでは、単一のINSERT操作を実行しています。これは、独自のトランザクションであるため、WHILEループ内にトランザクションを追加しても、何も得られません。 。そして、WHILEループの周りに明示的なトランザクションを追加すると、セット全体が単一のトランザクションに入れられます。これも、タイミングを少し改善する可能性がありますが、ブロッキングの可能性を高める巨大なトランザクションもあります。このトランザクションがより長くアクティブになるため、LOGファイルの増加に貢献します。

バッチのサイズを変更したい場合、バッチのサイズを増やすことができるかどうか、またはI/Oレイテンシを引き起こしているかどうかを判断するために何を監視できますか?

プロセスの実行速度が速いか遅いかを監視します。いくつかの異なるバッチサイズを試して、それぞれをプロセスの複数の反復で実行させます。バッチサイズごとにプロセスが実行された時間を追跡すると、最適な方法がわかります。


これらの線に沿って、例1では少なくとも100万行のバッチサイズを減らしてみます。

また、スカラーUDF dbo.udfDerivePageNameをインラインTVFに変換し、CROSS APPLYまたはOUTER APPLYを使用してクエリに組み込みます(例1)。また、UDFへの両方の呼び出しが同じ2つのパラメーターで渡されることを考えると、2回の呼び出しではなく、返されたフィールドを2回(1回はpageNameとして、1回はpageDescとして)参照するだけです。 iTVF。

宛先テーブルの競合を減らす別のオプション(これが新しい行を挿入するだけで既存の行を更新しない場合)は、テーブルパーティションを使用することです。これにより、現在行っているようにデータをステージングできますが、その新しいデータをライブテーブルに挿入するのではなく、比較的迅速な操作で新しいパーティションをSWITCHします。これは、最初の段階でデータをステージングするのにかかる時間やI/Oには役立ちませんが、ステージングされたデータをライブテーブルに「マージ」することによる時間と競合を排除できます。調査する必要があります。

4
Solomon Rutzky