Aktualisieren einer Tabelle mit mehr als 850 Millionen Datenzeilen

7

Ich wurde beauftragt, eine Aktualisierungsabfrage zu schreiben, um eine Tabelle mit mehr als 850 Millionen Datenzeilen zu aktualisieren. Hier sind die Tabellenstrukturen:

Quellentabellen:

    CREATE TABLE [dbo].[SourceTable1](
    [ProdClassID] [varchar](10) NOT NULL,
    [PriceListDate] [varchar](8) NOT NULL,
    [PriceListVersion] [smallint] NOT NULL,
    [MarketID] [varchar](10) NOT NULL,
    [ModelID] [varchar](20) NOT NULL,
    [VariantId] [varchar](20) NOT NULL,
    [VariantType] [tinyint] NULL,
    [Visibility] [tinyint] NULL,
 CONSTRAINT [PK_SourceTable1] PRIMARY KEY CLUSTERED 
(
    [VariantId] ASC,
    [ModelID] ASC,
    [MarketID] ASC,
    [ProdClassID] ASC,
    [PriceListDate] ASC,
    [PriceListVersion] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, 
ALLOW_PAGE_LOCKS  = ON, FILLFACTOR = 90)
    )

CREATE TABLE [dbo].[SourceTable2](
    [Id] [uniqueidentifier] NOT NULL,
    [ProdClassID] [varchar](10) NULL,
    [PriceListDate] [varchar](8) NULL,
    [PriceListVersion] [smallint] NULL,
    [MarketID] [varchar](10) NULL,
    [ModelID] [varchar](20) NULL,
 CONSTRAINT [PK_SourceTable2] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, 
ALLOW_PAGE_LOCKS  = ON, FILLFACTOR = 91) ON [PRIMARY]
    ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

SourceTable1enthält 52 Millionen Datenzeilen und SourceTable2enthält 400.000 Datenzeilen.

Hier ist die TargetTableStruktur

CREATE TABLE [dbo].[TargetTable](
    [ChassisSpecificationId] [uniqueidentifier] NOT NULL,
    [VariantId] [varchar](20) NOT NULL,
    [VariantType] [tinyint] NULL,
    [Visibility] [tinyint] NULL,
 CONSTRAINT [PK_TargetTable] PRIMARY KEY CLUSTERED 
(
    [ChassisSpecificationId] ASC,
    [VariantId] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON, FILLFACTOR = 71) ON [PRIMARY]
    ) ON [PRIMARY]

Die Beziehung zwischen diesen Tabellen ist wie folgt:

  • SourceTable1.VariantID bezieht sich auf TargetTable.VariantID
  • SourceTable2.ID bezieht sich auf TargetTable.ChassisSpecificationId

Die Aktualisierungsanforderung lautet wie folgt:

  1. Holen Sie sich die Werte für VariantTypeund Visibilityvon SourceTable1jedem VariantIDmit dem Maximalwert in der PriceListVersionSpalte.
  2. Holen Sie sich den Wert der IDSpalte von SourceTable2wo die Werte von ModelID, ProdClassID, PriceListDateund MarketIDÜbereinstimmung mit der SourceTable1.
  3. Aktualisieren Sie nun die TargetTablemit den Werten für VariantTypeund Visibilitywo die ChassisspecificationIDÜbereinstimmungen SourceTable2.IDund VariantIDÜbereinstimmungenSourceTable1.VariantID

Die Herausforderung besteht darin, dieses Update für die Live-Produktion mit minimaler Sperrung durchzuführen. Hier ist die Abfrage, die ich zusammengestellt habe.

-- Check if Temp table already exists and drop if it does
IF EXISTS(
        SELECT NULL 
        FROM tempdb.sys.tables
        WHERE name LIKE '#CSpec%'
      )
BEGIN
    DROP TABLE #CSpec;
END;

-- Create Temp table to assign sequence numbers
CREATE Table #CSpec
(
    RowID int,
    ID uniqueidentifier,
    PriceListDate VarChar(8),
    ProdClassID VarChar(10),
    ModelID VarChar(20),
    MarketID Varchar(10)
 );

-- Populate temp table 
INSERT INTO #CSpec
SELECT ROW_NUMBER() OVER (ORDER BY MarketID) RowID,
       CS.id, 
       CS.pricelistdate, 
       CS.prodclassid, 
       CS.modelid, 
       CS.marketid 
FROM   dbo.SourceTable2 CS 
WHERE CS.MarketID IS NOT NULL;

-- Declare variables to hold values used for updates
DECLARE @min            int, 
        @max            int,
        @ID             uniqueidentifier,
        @PriceListDate  varchar(8),
        @ProdClassID    varchar(10),
        @ModelID        varchar(20),
        @MarketID       varchar(10);
-- Set minimum and maximum values for looping
SET @min = 1;
SET @max = (SELECT MAX(RowID) From #CSpec);

-- Populate other variables in a loop
WHILE @min <= @max
BEGIN
    SELECT 
        @ID = ID,
        @PriceListDate = PriceListDate,
        @ProdClassID = ProdClassID,
        @ModelID = ModelID,
        @MarketID = MarketID
    FROM #CSpec
    WHERE RowID = @min;  

-- Use CTE to get relevant values from SourceTable1 
    ;WITH Variant_CTE AS
    (
    SELECT  V.variantid, 
            V.varianttype, 
            V.visibility,
            MAX(V.PriceListVersion) LatestPriceVersion
    FROM    SourceTable1 V 
    WHERE       V.ModelID = @ModelID
            AND V.ProdClassID = @ProdClassID
            AND V.PriceListDate = @PriceListDate
            AND V.MarketID = @MarketID
    GROUP BY
            V.variantid, 
            V.varianttype, 
            V.visibility
    )

-- Update the TargetTable with the values obtained in the CTE
    UPDATE      SV 
        SET     SV.VariantType = VC.VariantType, 
                SV.Visibility = VC.Visibility
    FROM        spec_variant SV 
    INNER JOIN  TargetTable VC
    ON          SV.VariantId = VC.VariantId
    WHERE       SV.ChassisSpecificationId = @ID
                AND SV.VariantType IS NULL
                AND SV.Visibility IS NULL;

    -- Increment the value of loop variable
    SET @min = @min+1;
END
-- Clean up
DROP TABLE #CSpec

Es dauert ungefähr 30 Sekunden, wenn ich das Limit der Iterationen auf 10 setze, indem ich den Wert der @maxVariablen fest codiere . Wenn ich jedoch das Limit auf 50 Iterationen erhöhe, dauert der Abschluss fast 4 Minuten. Ich befürchte, dass die Ausführungszeit für 400.000 Iterationen bei der Produktion mehrere Tage betragen wird. Dies ist jedoch möglicherweise noch akzeptabel, wenn das TargetTablenicht gesperrt wird und Benutzer nicht darauf zugreifen können.

Alle Eingaben sind willkommen.

Danke, Raj

Raj
quelle
2 Personen haben für das Schließen dieser Frage gestimmt. Möchtest du erklären warum?
Raj
1
Die Leute haben nicht für das Schließen gestimmt, sondern für die Migration zur DBA.SE-Site, wo es bessere Chancen gibt, dass Sie eine Expertenantwort erhalten.
Ypercubeᵀᴹ
2
Einige andere Dinge zu beachten: Statistiken und Stapelaktualisierungen. Einen informativen Artikel finden Sie hier im MSDN-Blog SQL Repl .
Marian
1
Vielen Dank für all die Inputs Jungs. Ich konnte das Skript weiter optimieren, relevante Indizes erstellen und dank des Links von @ Marian mehrere Sitzungen gleichzeitig verwenden und gerade die Aktualisierung der Test-DB mit 175 Millionen Zeilen in etwa 40 Minuten abgeschlossen haben. Das ist eine enorme Verbesserung gegenüber den 9,5 Stunden, die mein Vorgänger mit derselben Test-DB verbracht hat. Sehr geschätzt.
Raj

Antworten:

5

Um die Dinge zu beschleunigen, könnten Sie versuchen

  • Hinzufügen eines Primärschlüssels zu # CSpec.RowID, damit Sie ihn nicht bei jeder Iteration scannen
  • Ändern Sie den CTE in eine temporäre Tabelle mit geeigneter PK. Siehe auch den nächsten Punkt
  • Fügen Sie einen Index für SourceTable1 hinzu, der der CTE WHERE-Klausel entspricht: Derzeit wird die PK gescannt, dh alle SourceTable1-Zeilen werden bei jeder Iteration gescannt. Alle 52 Millionen Zeilen
  • SourceTable2.MarketID hat auch keinen Index, aber ich würde mir darüber keine Sorgen machen, da er nur einmal gescannt wird (so wie ich es verstehe).

Die Abfragepläne hier sollten viele Scans anzeigen, da Sie schlechte Indizes für die von Ihnen ausgeführten Vorgänge haben.

Die Indizierung der Zieltabelle wird in Ordnung angezeigt

Eine weitere Beobachtung: Uniqueidentifier und Varchar sind eine schlechte Wahl für Clustered-Indizes (Ihre PKs hier): Zumindest der Aufwand für Sammlungsvergleiche ist zu groß, nicht erhöht

Bearbeiten, eine weitere Beobachtung (danke an @Marian)

Ihr Clustered-Index ist im Allgemeinen breit. Jeder nicht gruppierte Index zeigt auf den gruppierten Index, was auch einen riesigen NC-Index bedeutet

Sie könnten wahrscheinlich das gleiche Ergebnis erzielen, indem Sie die Cluster-PK neu anordnen.

gbn
quelle
Danke für die Antwort. Es ist sinnvoll, PK zur temporären Tabelle #CSpec hinzuzufügen. Ich habe mich für einen CTE entschieden, da bei jeder Iteration nur durchschnittlich 16 bis 20 Datenzeilen aktualisiert werden. Index für SourceTable1 erstellt. Was Ihre Beobachtungen zur Auswahl der PKs betrifft, stimme ich zu. Leider wird dies vererbt und Änderungen am DB-Schema sind möglicherweise nicht möglich. Vielen Dank.
Raj
4

Veröffentlichung der endgültigen SQL für diesen Prozess zum Nutzen der Community

/********************************************************************************************************************
*  Notes: Since this approach executes in a loop inside an explicit transaction, locks will be obtained and         *
*  released for each iteration, thus minimizing impact on other users accessing the same table at the same time.    *
*                                                                                                                   *
*  This process would update 10,000 to 12,000 rows per second, and thus is estimated to run for approximately       *
*  23 hours on production with 850 million rows in Spec_Variant table. However, we can harness the power of         *
*  mutli-threading, by statically defining the @min and @max variable values and then running multiple sessions     *
*  of this update. This will reduce the time required to 23 hours divided by the number of sessions. In other words,* 
*  if we run 8 sessions of this update query parallelly, it should complete in 23/8 ~ 3 hours. If multiple sessions *
*  are possible, then the temp table needs to be created as a global temp table and populated in its own session.   *
*  Additionally, each sessions @max and @min values need to be hard coded,for example, 1-50000, 50001-100000, etc.  *
*********************************************************************************************************************/

-- However, to make this possible, we will have to use...

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

-- ... this would be the ideal setting to minimize locking. Before using this, we will need to execute
-- ALTER DATABASE MyDatabase
-- SET ALLOW_SNAPSHOT_ISOLATION ON

-- Alternately, if access rights permit, executing 
-- DBCC TRACEON(1211,-1) will disable lock escalation. Else, the TRANSACTION ISOLATION LEVEL can be left at 
-- default (READ COMMITTED), but will not allow us to run multiple sessions.

SET NOCOUNT ON;

-- Check if Temp table already exists and drop if it does
IF EXISTS(
        SELECT NULL 
        FROM tempdb.sys.tables
        WHERE name LIKE '#CSpec%'
      )
BEGIN
    DROP TABLE #CSpec;
END;

-- Create Temp table to assign sequence numbers
CREATE Table #CSpec
    (
    RowID           int PRIMARY KEY,
    ID              uniqueidentifier,
    PriceListDate   VarChar(8),
    ProdClassID     VarChar(10),
    ModelID         VarChar(20),
    MarketID        Varchar(10)
    );

-- Populate temp table 
INSERT INTO #CSpec
SELECT ROW_NUMBER() OVER (ORDER BY MarketID) RowID,
       CS.id, 
       CS.pricelistdate, 
       CS.prodclassid, 
       CS.modelid, 
       CS.marketid 
FROM   dbo.SourceTable2 CS 
WHERE CS.MarketID IS NOT NULL
-- This AND clause will allow this process to be run multiple times in timed sessions and will prevent
-- an attempt to update rows that were already updated in an earlier session. If the process will be run 
-- only once from start to finish, this block can be commented out
AND CS.Id NOT IN 
            (
                SELECT DISTINCT ChassisSpecificationId
                FROM TargetTable
                WHERE VariantType IS NOT NULL AND Visibility IS NOT NULL
            );

-- Declare variables to hold values used for updates
DECLARE @min            int, 
        @max            int,
        @ID             uniqueidentifier,
        @PriceListDate  varchar(8),
        @ProdClassID    varchar(10),
        @ModelID        varchar(20),
        @MarketID       varchar(10);

-- Set minimum and maximum values for looping. See comments in the notes section on top.
SELECT @min = 1,@max = MAX(RowID) From #CSpec;

-- Populate other variables in a loop
WHILE @min <= @max
BEGIN
    BEGIN TRY
    BEGIN TRANSACTION;
    SELECT 
        @ID = ID,
        @PriceListDate = PriceListDate,
        @ProdClassID = ProdClassID,
        @ModelID = ModelID,
        @MarketID = MarketID
    FROM #CSpec
    WHERE RowID = @min;  

-- Use CTE to get relevant values from SourceTable1
    ;WITH CTE AS
    (
    SELECT  V.variantid, 
            V.varianttype, 
            V.visibility,
            MAX(V.PriceListVersion) LatestPriceVersion
    FROM    SourceTable1 V 
    WHERE       V.ModelID = @ModelID
            AND V.ProdClassID = @ProdClassID
            AND V.PriceListDate = @PriceListDate
            AND V.MarketID = @MarketID
    GROUP BY
            V.variantid, 
            V.varianttype, 
            V.visibility
    )

-- Update the TargetTable with the values obtained in the CTE
    UPDATE      SV 
    SET         SV.VariantType = VC.VariantType, 
                SV.Visibility = VC.Visibility
    FROM        spec_variant SV 
    INNER JOIN  CTE VC
    ON          SV.VariantId = VC.VariantId
    WHERE       SV.ChassisSpecificationId = @ID
                AND SV.VariantType IS NULL
                AND SV.Visibility IS NULL;

   -- Check for errors and commit transaction
        IF @@ERROR = 0
            BEGIN
                COMMIT TRANSACTION;
                 -- Increment the value of loop variable
                SET @min = @min+1;
            END
    END TRY
    BEGIN CATCH
        IF @@ERROR <> 0
            BEGIN
                ROLLBACK;
            END
    END CATCH
END
-- Clean up
SET NOCOUNT OFF; 
DROP TABLE #CSpec;
Raj
quelle