I am trying to find the best way to copy a large amount of rows ( ~70million) from one table to another with the same schema inside the same database.

Our system currently runs with a set of A/B tables that are referenced elsewhere via a View. Our clients have the option to send us data either as a Full file or an Incremental file. When full files are ingested, the "inactive" table is truncated and loaded straight with the new data. For incremental files, we first copy all the data from "active" into "inactive" and then perform the incremental update. In both cases, when data loading is done we switch the view to the appropriate table.

This has been working fine for many years; typically clients send us data files at night time and their data is processed when nobody is using the system. However, one of our larger clients now wants to send us incremental files throughout the day. For a smaller sized client, the A->B copy operation is fairly quick (~30 seconds for 600k rows), but this large client's copy is taking 30-60+ minutes depending on load.

The schema for both tables is as follows (replace TableB with TableA):

CREATE TABLE [dbo].[TableA](
    [ClientId] NCHAR(10) NOT NULL,
    [ItemId] INT NOT NULL,
    [Amount] DECIMAL(19,6) NOT NULL,

REFERENCES [dbo].[Items] ([ItemId])


REFERENCES [dbo].[Clients] ([ClientId])


CREATE NONCLUSTERED INDEX [IX_TableA_ClientId_ItemID_INC_Amount] ON [dbo].[TableA] ([ClientId] ASC, [ItemId] ASC )  INCLUDE ([Amount]) 

The insert/select statement we are currently using is a simple:

INSERT INTO [dbo].[TableB]  ( ClientId, [ItemId], [Amount], [Loaded] )
SELECT ClientId, ItemId, Amount, Loaded FROM [dbo].[TableA]

I've tried various methods for optimizing this copy; I've added TabLock to the target table, tried setting Trace Flag 610 and tried many different batch sizes, but nothing seems to work. One variation of the queries I've tried is:

DECLARE @BatchSize BIGINT = 50000;
WHILE 1 = 1
      INSERT INTO TableB WITH ( TABLOCK ) ( [ClientId], [ItemId], [Amount], [Loaded] )
      SELECT TOP ( @BatchSize ) ClientId, ItemId,Amount, Loaded
      FROM      TableA t1
      WHERE     NOT EXISTS ( SELECT 1 FROM TableB WHERE ClientId = t1.ClientId AND ItemId = t1.ItemId );

      IF @@ROWCOUNT < @BatchSize

I've tried dropping/recreating indexesbut this too was proving costly. The best time I've come up with is about 5 minutes, but this involves dropping the target table and performing a select into, then adding the indexes and check constraints. This was purely "educational"--I can't go this route due to schema-bound views and some other items preventing this approach (in addition to our normal view, we have a "B" view which points to the other table for use during this process only)

While I know that SSIS is probably the best approach here, it is out of the question for now--I'm tasked with speeding up the existing process, if possible. I do have control over what procedures are called, etc from C#.

With that said, do I have any other options? I can do pretty much whatever I want to the inactive table as nothing is using it except these procedures. One thing I was considering is to somehow export the data with BCP from C# and then re-ingest it. Aside from not being sure if its possible, I've no idea if it would be any better (or worse for that matter)

  • Your version of sql server is limiting here to NOT use memory optimized tables as well as parallel insert .. select into features.
    – Kin Shah
    Commented Jul 18, 2016 at 15:57
  • We actually have some on 12 and 14 but the lowest version we still have and need to support is 2008 Commented Jul 18, 2016 at 16:02
  • If you are switching the view then why not just load into a fresh table?
    – paparazzo
    Commented Jul 18, 2016 at 16:37

1 Answer 1


How about keeping all the incremental data inside the inactive table until the end of the day, then load it into active and truncate inactive? Your view could reference both tables using a UNION ALL, that way you aren't switching between the two tables.

You could use partition swapping with the same method. You'd partition both tables, load inactive until 11 PM at night, then swap inactive's loaded partition into active. Your view would still need to reference both tables. This way, you have no downtime for loading besides the time it takes to initially import the file, and incremental files should be loading into an empty table every day.

  • The problem is that they are expecting it live (or as close to live as possible) and may during the day send multiple incrementals followed by a random full file. The idea behind the incremental load is that it will update the quantity and age of anything that came in the file. This strategy could work for this case where we only have the one column of actual data (amount) but wouldn't work in my other cases where there are multiple data points that can be loaded (10 columns, where partial loads--ex only 2/10 columns--are possible) Commented Jul 18, 2016 at 15:11
  • @pinkfloydx33 what about a MERGE statement? If it exists inside the base or temp table, update it. If not, INSERT to the inactive.
    – Arthur D
    Commented Jul 18, 2016 at 15:26
  • also problem with the view is that we don't use the view everywhere. a lot of existing processes use dynamic sql and select directly from the table based on a value stored in a lookup table indicating which to use... we're phasing it all out, but not all at once Commented Jul 18, 2016 at 16:18
  • Seems like you have significant system constraints. Maybe your first goal should be standardization of your data access methods, then focus on optimization?
    – Arthur D
    Commented Jul 19, 2016 at 12:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.