SQL Server Bulk Upsert using Table Valued Parameters

Not much background to this post. Had an interest in improving a part of some work code that did multiple upsert operations, and came up with a bulk insert stored procedure using the pattern described by Aaron Bertand. Uploading an example that uses the StackOverflow2010 database and Users table. Performance is greatly improved by using this over individual upsert statements.

Users table valued parameter: 

USE [StackOverflow2010]
GO

/****** Object:  Table Valued Parameter [dbo].[UsersTVP] ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TYPE [dbo].[UsersTVP] AS TABLE (
[Id] [int] NULL,
[AboutMe] [nvarchar](max) NULL,
[Age] [int] NULL,
[CreationDate] [datetime] NOT NULL DEFAULT GETDATE(),
[DisplayName] [nvarchar](40) NOT NULL DEFAULT '',
[DownVotes] [int] NOT NULL DEFAULT 0,
[EmailHash] [nvarchar](40) NULL,
[LastAccessDate] [datetime] NOT NULL DEFAULT GETDATE(),
[Location] [nvarchar](100) NULL,
[Reputation] [int] NOT NULL DEFAULT 1,
[UpVotes] [int] NOT NULL DEFAULT 0,
[Views] [int] NOT NULL DEFAULT 0,
[WebsiteUrl] [nvarchar](200) NULL,
[AccountId] [int] NULL
)
GO

UpsertUsers stored procedure: 

USE [StackOverflow2010]
GO

/****** Object:  StoredProcedure [dbo].[UpsertUsers] ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- Author: Tom Barrett
-- Create date: 2022-12-09
-- Description: Upserts a list of Users using table valued parameter
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[UpsertUsers]
-- Add the parameters for the stored procedure here
@Users UsersTVP READONLY
AS
BEGIN
/* SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements. */
SET NOCOUNT ON;
/* SET XACT_ABORT ON for proper rollback on errors */
SET XACT_ABORT ON;

IF NOT EXISTS (SELECT TOP 1 * FROM @Users)
/*THROW 51000, '@Users can not be empty', 1;*/
RETURN

BEGIN TRY
BEGIN TRANSACTION;

UPDATE [dbo].[Users] WITH (UPDLOCK, SERIALIZABLE) 
SET [AboutMe] = [tvp].[AboutMe]
, [Age] = [tvp].[Age]
, [CreationDate] = [tvp].[CreationDate]
, [DisplayName] = [tvp].[DisplayName]
, [DownVotes] = [tvp].[DownVotes]
, [EmailHash] = [tvp].[EmailHash]
, [LastAccessDate] = [tvp].[LastAccessDate]
, [Location] = [tvp].[Location]
, [Reputation] = [tvp].[Reputation]
, [UpVotes] = [tvp].[UpVotes]
, [Views] = [tvp].[Views]
, [WebsiteUrl] = [tvp].[WebsiteUrl]
, [AccountId] = [tvp].[AccountId]
FROM [dbo].[Users] AS [u]
INNER JOIN @Users AS [tvp]
ON [u].[Id] = [tvp].[Id];

INSERT [dbo].[Users]
           ( [AboutMe]
           , [Age]
           , [CreationDate]
           , [DisplayName]
           , [DownVotes]
           , [EmailHash]
           , [LastAccessDate]
           , [Location]
           , [Reputation]
           , [UpVotes]
           , [Views]
           , [WebsiteUrl]
           , [AccountId])
SELECT [AboutMe]
           , [Age]
           , [CreationDate]
           , [DisplayName]
           , [DownVotes]
           , [EmailHash]
           , [LastAccessDate]
           , [Location]
           , [Reputation]
           , [UpVotes]
           , [Views]
           , [WebsiteUrl]
           , [AccountId]
FROM @Users as [tvp]
WHERE NOT EXISTS (SELECT 1 FROM [dbo].[Users] WHERE [Id] = [tvp].[Id]);
 
COMMIT TRANSACTION;
END TRY

BEGIN CATCH
ROLLBACK;
END CATCH

IF @@TRANCOUNT > 0 ROLLBACK;
END
GO



Comments

Popular posts from this blog

Adding Font Awesome to .NET MAUI App

Comparing SELECT Performance of Joining Integers vs GUIDs in SQL Server

Performance: SQL Server vs MongoDB (JSON data)