T-SQLpowered

UPDATE

Examples for SQL Server 2025. Each block is a separate example. Replace the table and column names as needed; Id is assumed to be unique.

Basics

Update one row by its Id:

UPDATE [dbo].[SampleTable]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

Update multiple columns in one statement:

UPDATE [dbo].[SampleTable]
SET [Col1] = N'New Value',
	[Col2] = NULL
WHERE [Id] = 1;

Update all rows:

UPDATE [dbo].[SampleTable]
SET [Status] = N'Archived';

Update using a variable:

DECLARE @Id INT = 1;
DECLARE @NewValue NVARCHAR(100) = N'New Value';

UPDATE [dbo].[SampleTable]
SET [Col1] = @NewValue
WHERE [Id] = @Id;

Set a column to NULL or its default value:

-- NULL
UPDATE [dbo].[SampleTable]
SET [Col1] = NULL
WHERE [Id] = 1;

-- column default
UPDATE [dbo].[SampleTable]
SET [Status] = DEFAULT
WHERE [Id] = 1;

Values and expressions

Set a value using CASE:

UPDATE [dbo].[SampleTable]
SET [Status] = CASE
	WHEN [Quantity] <= 0 THEN N'Empty'
	WHEN [Quantity] < 10 THEN N'Low'
	ELSE N'Available'
END;

Increase or decrease a numeric value:

-- add
UPDATE [dbo].[SampleTable]
SET [Value] += 10
WHERE [Id] = 1;

-- subtract
UPDATE [dbo].[SampleTable]
SET [Value] -= 5
WHERE [Id] = 1;

-- multiply
UPDATE [dbo].[SampleTable]
SET [Value] *= 1.10
WHERE [Id] = 1;

-- divide
UPDATE [dbo].[SampleTable]
SET [Value] /= 2
WHERE [Id] = 1;

-- remainder (integer column)
UPDATE [dbo].[SampleTable]
SET [Quantity] %= 10
WHERE [Id] = 1;

Set, clear or toggle a bit flag:

-- set flag 4
UPDATE [dbo].[SampleTable]
SET [Flags] |= 4
WHERE [Id] = 1;

-- clear flag 4
UPDATE [dbo].[SampleTable]
SET [Flags] &= ~4
WHERE [Id] = 1;

-- toggle flag 4
UPDATE [dbo].[SampleTable]
SET [Flags] ^= 4
WHERE [Id] = 1;

Replace NULL values with a fallback:

UPDATE [dbo].[SampleTable]
SET [Value] = COALESCE([Value], 0)
WHERE [Value] IS NULL;

Replace text or append a suffix:

-- replace text
UPDATE [dbo].[SampleTable]
SET [TextValue] = REPLACE([TextValue], N'Old', N'New')
WHERE [Id] = 1;

-- append text, treating NULL as empty text
UPDATE [dbo].[SampleTable]
SET [TextValue] = CONCAT([TextValue], N' suffix')
WHERE [Id] = 1;

Add one day or store the current UTC time:

-- add one day
UPDATE [dbo].[SampleTable]
SET [DueDate] = DATEADD(DAY, 1, [DueDate])
WHERE [Id] = 1;

-- UTC timestamp
UPDATE [dbo].[SampleTable]
SET [ModifiedAt] = SYSUTCDATETIME()
WHERE [Id] = 1;

Swap values between two columns:

UPDATE [dbo].[SampleTable]
SET [Col1] = [Col2],
	[Col2] = [Col1]
WHERE [Id] = 1;

Convert text to a number:

UPDATE [dbo].[SampleTable]
SET [Quantity] = TRY_CONVERT(INT, [TextValue])
WHERE [Id] = 1;

Invalid numeric text returns NULL. Quantity must allow NULL.

Replace part of a normal string using STUFF:

UPDATE [dbo].[SampleTable]
SET [Col1] = STUFF([Col1], 1, 3, N'New')
WHERE [Id] = 1;

STUFF starts at 1. This replaces the first three characters and also works with non-MAX string columns.

Update with the expression:

UPDATE [dbo].[SampleTable] SET [Col1] = @a + @b + @c

Large strings and binary data

.WRITE() works with VARCHAR(MAX), NVARCHAR(MAX) and VARBINARY(MAX). The value must not be NULL. Offsets start at 0: bytes for VARCHAR/VARBINARY and UTF-16 units for NVARCHAR. TextValue in these examples is NVARCHAR(MAX).

Update using the .WRITE() clause:

UPDATE [dbo].[SampleTable] SET [Col1].WRITE('New Value', 0, NULL)
UPDATE [dbo].[SampleTable] SET [Col1].WRITE(@a + @b + @c, 0, NULL)
UPDATE [dbo].[SampleTable] SET [Col1].WRITE(@x, 500, 1000)

Append text using .WRITE():

UPDATE [dbo].[SampleTable]
SET [TextValue].WRITE(N' appended text', NULL, 0)
WHERE [Id] = 1;

Insert text at the beginning using .WRITE():

UPDATE [dbo].[SampleTable]
SET [TextValue].WRITE(N'Prefix: ', 0, 0)
WHERE [Id] = 1;

Replace or remove part of a value using .WRITE():

-- replace the first 3 UTF-16 units
UPDATE [dbo].[SampleTable]
SET [TextValue].WRITE(N'New', 0, 3)
WHERE [Id] = 1;

-- remove the first 3 UTF-16 units
UPDATE [dbo].[SampleTable]
SET [TextValue].WRITE(N'', 0, 3)
WHERE [Id] = 1;

Truncate a value or replace everything after an offset:

-- keep the first 5 UTF-16 units
UPDATE [dbo].[SampleTable]
SET [TextValue].WRITE(NULL, 5, 0)
WHERE [Id] = 1;

-- replace everything after the first 5 UTF-16 units
UPDATE [dbo].[SampleTable]
SET [TextValue].WRITE(N'New ending', 5, NULL)
WHERE [Id] = 1;

The offset must not exceed the current value length.

Initialize a NULL value before using .WRITE():

UPDATE [dbo].[SampleTable]
SET [TextValue] = N''
WHERE [Id] = 1
	AND [TextValue] IS NULL;

UPDATE [dbo].[SampleTable]
SET [TextValue].WRITE(N'New Value', NULL, 0)
WHERE [Id] = 1;

Replace or append binary data:

-- BinaryData is VARBINARY(MAX)
UPDATE [dbo].[SampleTable]
SET [BinaryData] = 0x01020304
WHERE [Id] = 1;

UPDATE [dbo].[SampleTable]
SET [BinaryData].WRITE(0x0506, NULL, 0)
WHERE [Id] = 1;

Read a file into a binary column:

UPDATE [dbo].[SampleTable]
SET [BinaryData] = (
	SELECT [BulkColumn]
	FROM OPENROWSET(
		BULK N'C:\Data\Sample.bin',
		SINGLE_BLOB
	) AS [f]
)
WHERE [Id] = 1;

The file path is on the SQL Server host. Requires bulk-read permission and file access.

Replace or clear FILESTREAM data:

-- FileData is a VARBINARY(MAX) FILESTREAM column
UPDATE [dbo].[SampleTable]
SET [FileData] = 0x01020304
WHERE [Id] = 1;

UPDATE [dbo].[SampleTable]
SET [FileData] = NULL
WHERE [Id] = 1;

FILESTREAM does not support partial updates with .WRITE(). FileData must allow NULL for the second example.

Values from another source

Update using values from another table:

UPDATE [t]
SET [Col1] = [s].[Col1]
FROM [dbo].[SampleTable] AS [t]
INNER JOIN [dbo].[SourceTable] AS [s]
	ON [s].[Id] = [t].[Id];

The source must provide at most one matching row for each target row; otherwise the chosen source value is undefined.

Keep the current value when a LEFT JOIN has no match:

UPDATE [t]
SET [Col1] = CASE
	WHEN [s].[Id] IS NULL THEN [t].[Col1]
	ELSE [s].[Col1]
END
FROM [dbo].[SampleTable] AS [t]
LEFT JOIN [dbo].[SourceTable] AS [s]
	ON [s].[Id] = [t].[Id];

Source Id must be unique. A matched source row containing NULL still sets Col1 to NULL.

Update from another row in the same table:

UPDATE [child]
SET [ParentName] = [parent].[Name]
FROM [dbo].[SampleTable] AS [child]
INNER JOIN [dbo].[SampleTable] AS [parent]
	ON [parent].[Id] = [child].[ParentId];

Update using a correlated subquery:

UPDATE [t]
SET [Col1] = (
	SELECT [s].[Col1]
	FROM [dbo].[SourceTable] AS [s]
	WHERE [s].[Id] = [t].[Id]
)
FROM [dbo].[SampleTable] AS [t];

No matching row returns NULL. More than one matching row raises an error.

Update using aggregated source values:

UPDATE [t]
SET [TotalAmount] = [s].[TotalAmount]
FROM [dbo].[SampleTable] AS [t]
INNER JOIN (
	SELECT
		[SampleId],
		SUM([Amount]) AS [TotalAmount]
	FROM [dbo].[SourceTable]
	GROUP BY [SampleId]
) AS [s]
	ON [s].[SampleId] = [t].[Id];

Update multiple rows with different values using VALUES:

UPDATE [t]
SET [Col1] = [s].[Col1]
FROM [dbo].[SampleTable] AS [t]
INNER JOIN (VALUES
	(1, N'First value'),
	(2, N'Second value'),
	(3, N'Third value')
) AS [s] ([Id], [Col1])
	ON [s].[Id] = [t].[Id];

Use each Id once in the VALUES list.

Update using the latest matching source row:

UPDATE [t]
SET [Col1] = [s].[Col1]
FROM [dbo].[SampleTable] AS [t]
CROSS APPLY (
	SELECT TOP (1)
		[src].[Col1]
	FROM [dbo].[SourceTable] AS [src]
	WHERE [src].[SampleId] = [t].[Id]
	ORDER BY [src].[ModifiedAt] DESC, [src].[Id] DESC
) AS [s];

Source Id must be unique to break timestamp ties. CROSS APPLY skips targets with no match.

Update using a JSON array of changes:

DECLARE @Changes NVARCHAR(MAX) =
	N'[{"Id":1,"Col1":"First value"},{"Id":2,"Col1":"Second value"}]';

UPDATE [t]
SET [Col1] = [s].[Col1]
FROM [dbo].[SampleTable] AS [t]
INNER JOIN OPENJSON(@Changes)
WITH
(
	[Id] INT '$.Id',
	[Col1] NVARCHAR(100) '$.Col1'
) AS [s]
	ON [s].[Id] = [t].[Id];

Use one JSON item per Id. OPENJSON requires compatibility level 130 or higher.

Update from a table-valued parameter:

-- create once
CREATE TYPE [dbo].[utt_UpdateValues] AS TABLE
(
	[Id] INT NOT NULL PRIMARY KEY,
	[Col1] NVARCHAR(100) NULL
);
GO

CREATE OR ALTER PROCEDURE [dbo].[p_UpdateValues]
	@Changes [dbo].[utt_UpdateValues] READONLY
AS
BEGIN
	SET NOCOUNT ON;

	UPDATE [t]
	SET [Col1] = [s].[Col1]
	FROM [dbo].[SampleTable] AS [t]
	INNER JOIN @Changes AS [s]
		ON [s].[Id] = [t].[Id];
END;
GO

DECLARE @Changes [dbo].[utt_UpdateValues];

INSERT INTO @Changes ([Id], [Col1])
VALUES (1, N'First value'), (2, N'Second value');

EXEC [dbo].[p_UpdateValues] @Changes = @Changes;

Conditions and changed values

Update only changed values, including NULL (SQL Server 2022+):

DECLARE @NewValue NVARCHAR(100) = N'New Value';

UPDATE [dbo].[SampleTable]
SET [Col1] = @NewValue
WHERE [Id] = 1
	AND [Col1] IS DISTINCT FROM @NewValue;

IS DISTINCT FROM also detects changes to or from NULL.

Update a list of Ids:

UPDATE [dbo].[SampleTable]
SET [Status] = N'Archived'
WHERE [Id] IN (1, 2, 3);

Update rows from a date range:

DECLARE @FromDate DATE = '20250101';
DECLARE @ToDate DATE = '20250201';

UPDATE [dbo].[SampleTable]
SET [Status] = N'Archived'
WHERE [CreatedAt] >= @FromDate
	AND [CreatedAt] < @ToDate;

Includes January 2025. The upper boundary is excluded.

Update rows containing NULL or matching a text pattern:

-- NULL values
UPDATE [dbo].[SampleTable]
SET [Col1] = N'Unknown'
WHERE [Col1] IS NULL;

-- text pattern
UPDATE [dbo].[SampleTable]
SET [Status] = N'Archived'
WHERE [Name] LIKE N'Old%';

Update rows with or without a matching source row:

-- matching source row
UPDATE [t]
SET [Status] = N'Matched'
FROM [dbo].[SampleTable] AS [t]
WHERE EXISTS (
	SELECT 1
	FROM [dbo].[SourceTable] AS [s]
	WHERE [s].[Id] = [t].[Id]
);

-- no matching source row
UPDATE [t]
SET [Status] = N'Missing'
FROM [dbo].[SampleTable] AS [t]
WHERE NOT EXISTS (
	SELECT 1
	FROM [dbo].[SourceTable] AS [s]
	WHERE [s].[Id] = [t].[Id]
);

Update only when at least one column changed (SQL Server 2022+):

UPDATE [t]
SET [Col1] = [s].[Col1],
	[Col2] = [s].[Col2]
FROM [dbo].[SampleTable] AS [t]
INNER JOIN [dbo].[SourceTable] AS [s]
	ON [s].[Id] = [t].[Id]
WHERE [t].[Col1] IS DISTINCT FROM [s].[Col1]
	OR [t].[Col2] IS DISTINCT FROM [s].[Col2];

Source Id must be unique. NULL values are compared too.

CTEs and batches

Update the next 100 pending rows selected by Id:

;WITH [Batch] AS
(
	SELECT TOP (100)
		[Id],
		[Status]
	FROM [dbo].[SampleTable]
	WHERE [Status] = N'Pending'
	ORDER BY [Id]
)
UPDATE [Batch]
SET [Status] = N'Processed';

Assumes Id is unique. ORDER BY selects the batch; it does not guarantee modification order. UPDATE TOP (100) alone does not define which qualifying rows are selected.

Update a filtered CTE:

;WITH [RowsToUpdate] AS
(
	SELECT [Id], [Status]
	FROM [dbo].[SampleTable]
	WHERE [Status] = N'Pending'
)
UPDATE [RowsToUpdate]
SET [Status] = N'Ready';

Store row numbers in a column:

;WITH [NumberedRows] AS
(
	SELECT
		[Id],
		ROW_NUMBER() OVER (ORDER BY [Id]) AS [SequenceNo]
	FROM [dbo].[SampleTable]
)
UPDATE [t]
SET [SequenceNo] = [s].[SequenceNo]
FROM [dbo].[SampleTable] AS [t]
INNER JOIN [NumberedRows] AS [s]
	ON [s].[Id] = [t].[Id];

Id must be unique. ROW_NUMBER returns BIGINT.

Mark the latest row in each group:

;WITH [RankedRows] AS
(
	SELECT
		[Id],
		ROW_NUMBER() OVER (
			PARTITION BY [GroupId]
			ORDER BY [CreatedAt] DESC, [Id] DESC
		) AS [RowNo]
	FROM [dbo].[SampleTable]
)
UPDATE [t]
SET [IsLatest] = CASE WHEN [s].[RowNo] = 1 THEN 1 ELSE 0 END
FROM [dbo].[SampleTable] AS [t]
INNER JOIN [RankedRows] AS [s]
	ON [s].[Id] = [t].[Id];

Id must be unique to break timestamp ties.

Update a parent row and all its descendants:

DECLARE @RootId INT = 1;

;WITH [Descendants] AS
(
	SELECT [Id]
	FROM [dbo].[SampleTable]
	WHERE [Id] = @RootId

	UNION ALL

	SELECT [child].[Id]
	FROM [dbo].[SampleTable] AS [child]
	INNER JOIN [Descendants] AS [parent]
		ON [child].[ParentId] = [parent].[Id]
)
UPDATE [t]
SET [Status] = N'Archived'
FROM [dbo].[SampleTable] AS [t]
INNER JOIN [Descendants] AS [d]
	ON [d].[Id] = [t].[Id]
OPTION (MAXRECURSION 100);

Assumes a tree without cycles. Increase MAXRECURSION if deeper branches are expected.

Limit the number or percentage of updated rows:

-- fixed count
UPDATE TOP (100) [dbo].[SampleTable]
SET [Status] = N'Processed'
WHERE [Status] = N'Pending';

-- variable count
DECLARE @BatchSize INT = 100;

UPDATE TOP (@BatchSize) [dbo].[SampleTable]
SET [Status] = N'Processed'
WHERE [Status] = N'Pending';

-- percentage
UPDATE TOP (10) PERCENT [dbo].[SampleTable]
SET [Status] = N'Processed'
WHERE [Status] = N'Pending';

These forms do not define row order.

Update in batches until no pending rows remain:

DECLARE @BatchSize INT = 1000;
DECLARE @RowsUpdated INT = 1;

WHILE @RowsUpdated > 0
BEGIN
	;WITH [Batch] AS
	(
		SELECT TOP (@BatchSize)
			[Id],
			[Status]
		FROM [dbo].[SampleTable]
		WHERE [Status] = N'Pending'
		ORDER BY [Id]
	)
	UPDATE [Batch]
	SET [Status] = N'Processed';

	SET @RowsUpdated = @@ROWCOUNT;
END;

Id must be unique. Run without an outer transaction for separate batch commits. Processed rows leave the filter.

OUTPUT and variables

Capture old and new values using OUTPUT:

DECLARE @Changes TABLE
(
	[Id] INT,
	[OldValue] NVARCHAR(MAX),
	[NewValue] NVARCHAR(MAX)
);

UPDATE [dbo].[SampleTable]
SET [Col1] = N'New Value'
OUTPUT
	[Inserted].[Id],
	[Deleted].[Col1],
	[Inserted].[Col1]
INTO @Changes ([Id], [OldValue], [NewValue])
WHERE [Id] = 1;

SELECT [Id], [OldValue], [NewValue]
FROM @Changes;

OUTPUT captures values before subsequent AFTER trigger changes. Returned values alone do not confirm that the transaction committed.

Return old and new values directly:

UPDATE [dbo].[SampleTable]
SET [Col1] = N'New Value'
OUTPUT
	[Inserted].[Id],
	[Deleted].[Col1] AS [OldValue],
	[Inserted].[Col1] AS [NewValue]
WHERE [Id] = 1;

Use OUTPUT INTO instead if the target has an enabled UPDATE trigger.

Return a value from the joined source:

UPDATE [t]
SET [Col1] = [s].[Col1]
OUTPUT
	[Inserted].[Id],
	[Deleted].[Col1] AS [OldValue],
	[Inserted].[Col1] AS [NewValue],
	[s].[SourceCode]
FROM [dbo].[SampleTable] AS [t]
INNER JOIN [dbo].[SourceTable] AS [s]
	ON [s].[Id] = [t].[Id];

Source Id must be unique. Direct OUTPUT requires a target without enabled UPDATE triggers.

Save changed rows into a temporary table:

CREATE TABLE #Changes
(
	[Id] INT,
	[OldValue] NVARCHAR(MAX),
	[NewValue] NVARCHAR(MAX)
);

UPDATE [dbo].[SampleTable]
SET [Col1] = N'New Value'
OUTPUT
	[Inserted].[Id],
	[Deleted].[Col1],
	[Inserted].[Col1]
INTO #Changes ([Id], [OldValue], [NewValue])
WHERE [Id] IN (1, 2, 3);

SELECT [Id], [OldValue], [NewValue]
FROM #Changes;

DROP TABLE #Changes;

A permanent capture table can be used too. The OUTPUT INTO table cannot have enabled triggers, CHECK constraints, or foreign-key participation.

Get the number of updated rows:

DECLARE @RowsUpdated INT;

UPDATE [dbo].[SampleTable]
SET [Status] = N'Archived'
WHERE [Id] IN (1, 2, 3);

SET @RowsUpdated = @@ROWCOUNT;

SELECT @RowsUpdated AS [RowsUpdated];

Read @@ROWCOUNT immediately after UPDATE. It counts affected rows, including unchanged values.

Capture a single row’s new or previous value into a variable:

-- new value
DECLARE @NewValue NVARCHAR(100);

UPDATE [dbo].[SampleTable]
SET @NewValue = [Col1] = N'New Value'
WHERE [Id] = 1;

SELECT @NewValue AS [NewValue];

-- previous value
DECLARE @OldValue NVARCHAR(100);

UPDATE [dbo].[SampleTable]
SET @OldValue = [Col1],
	[Col1] = N'Another Value'
WHERE [Id] = 1;

SELECT @OldValue AS [OldValue];

-- new value after a compound assignment
DECLARE @NewQuantity INT;

UPDATE [dbo].[SampleTable]
SET @NewQuantity = [Quantity] += 1
WHERE [Id] = 1;

SELECT @NewQuantity AS [NewQuantity];

Id must identify one row. Use OUTPUT for multiple rows.

Assign a variable from the updated row:

DECLARE @Id INT = 1;
DECLARE @PreviousQuantity INT;

UPDATE [dbo].[SampleTable]
SET @PreviousQuantity = [Quantity],
	[Status] = N'Processed'
WHERE [Id] = @Id;

SELECT @PreviousQuantity AS [PreviousQuantity];

Id must identify one row. If no row matches, the variable keeps its previous value.

SQL Server 2025

Collapse repeated spaces using a regular expression (SQL Server 2025):

UPDATE [dbo].[SampleTable]
SET [TextValue] =
	REGEXP_REPLACE([TextValue], N' {2,}', N' ')
WHERE [Id] = 1;

Replaces runs of two or more ordinary spaces with one space.
REGEXP_REPLACE reference on Microsoft Learn.

Remove non-digit characters (SQL Server 2025):

UPDATE [dbo].[SampleTable]
SET [Phone] = REGEXP_REPLACE([Phone], N'[^0-9]', N'')
WHERE [Id] = 1;

Replace a native JSON document or modify one property (SQL Server 2025):

-- JsonData has the native JSON data type
UPDATE [dbo].[SampleTable]
SET [JsonData] = CAST(N'{"status":"Pending","count":1}' AS JSON)
WHERE [Id] = 1;

UPDATE [dbo].[SampleTable]
SET [JsonData].modify('$.status', N'Processed')
WHERE [Id] = 1;

Native JSON columns use the .modify() method. JSON data type reference.

Replace a vector value (SQL Server 2025):

-- Embedding is VECTOR(3)
DECLARE @Embedding VECTOR(3) = '[0.15, 0.25, 0.35]';

UPDATE [dbo].[SampleTable]
SET [Embedding] = @Embedding
WHERE [Id] = 1;

Match the column dimension. Vector-index update support depends on the index version. Current vector-index limitations.

JSON and XML

Change or add a property in JSON text:

-- JsonText is NVARCHAR(MAX) containing a JSON object
UPDATE [dbo].[SampleTable]
SET [JsonText] = JSON_MODIFY([JsonText], '$.status', N'Processed')
WHERE [Id] = 1;

Adds status when missing from the root object. JsonText must contain valid JSON.

Delete a JSON property:

UPDATE [dbo].[SampleTable]
SET [JsonText] = JSON_MODIFY([JsonText], '$.status', NULL)
WHERE [Id] = 1;

In the default lax mode, SQL NULL removes the property.

Set an existing JSON property to JSON null:

UPDATE [dbo].[SampleTable]
SET [JsonText] = JSON_MODIFY([JsonText], 'strict $.status', NULL)
WHERE [Id] = 1;

Strict mode keeps the property with a JSON null value. The property must exist.

Append a value to a JSON array:

UPDATE [dbo].[SampleTable]
SET [JsonText] = JSON_MODIFY([JsonText], 'append $.tags', N'SQL Server')
WHERE [Id] = 1;

Assumes tags is an existing array.

Store an object instead of an escaped JSON string:

UPDATE [dbo].[SampleTable]
SET [JsonText] = JSON_MODIFY(
	[JsonText],
	'$.address',
	JSON_QUERY(N'{"city":"Prague","country":"CZ"}')
)
WHERE [Id] = 1;

Change multiple JSON properties:

UPDATE [dbo].[SampleTable]
SET [JsonText] = JSON_MODIFY(
	JSON_MODIFY([JsonText], '$.status', N'Processed'),
	'$.count',
	2
)
WHERE [Id] = 1;

The number stays numeric. JSON_MODIFY reference.

Replace XML text or an attribute value:

-- XmlData is XML, for example:
-- <Root Status="Pending"><Value>Old</Value></Root>

UPDATE [dbo].[SampleTable]
SET [XmlData].modify('
	replace value of (/Root/Value/text())[1]
	with "New"
')
WHERE [Id] = 1
	AND [XmlData] IS NOT NULL;

UPDATE [dbo].[SampleTable]
SET [XmlData].modify('
	replace value of (/Root/@Status)[1]
	with "Processed"
')
WHERE [Id] = 1
	AND [XmlData] IS NOT NULL;

The [1] selects a single node.

Insert or delete an XML element:

-- insert under the existing Root element
UPDATE [dbo].[SampleTable]
SET [XmlData].modify('
	insert <Note>New note</Note>
	as last into (/Root)[1]
')
WHERE [Id] = 1
	AND [XmlData] IS NOT NULL;

-- delete Note elements
UPDATE [dbo].[SampleTable]
SET [XmlData].modify('delete /Root/Note')
WHERE [Id] = 1
	AND [XmlData] IS NOT NULL;

Examples use untyped XML without namespaces. XML modify() reference.

Other update targets

Update a temporary table or a table variable:

-- temporary table
CREATE TABLE #SampleTable
(
	[Id] INT NOT NULL PRIMARY KEY,
	[Col1] NVARCHAR(100) NULL
);

INSERT INTO #SampleTable ([Id], [Col1])
VALUES (1, N'Old Value');

UPDATE #SampleTable
SET [Col1] = N'New Value'
WHERE [Id] = 1;

SELECT * FROM #SampleTable;

DROP TABLE #SampleTable;

-- table variable
DECLARE @SampleTable TABLE
(
	[Id] INT NOT NULL PRIMARY KEY,
	[Col1] NVARCHAR(100) NULL
);

INSERT INTO @SampleTable ([Id], [Col1])
VALUES (1, N'Old Value');

UPDATE @SampleTable
SET [Col1] = N'New Value'
WHERE [Id] = 1;

SELECT * FROM @SampleTable;

Update through a view:

CREATE OR ALTER VIEW [dbo].[v_SampleTable]
AS
	SELECT [Id], [Col1]
	FROM [dbo].[SampleTable];
GO

UPDATE [dbo].[v_SampleTable]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

The change is stored in SampleTable. The updated columns must belong to one base table and meet the view-update rules.

Update through a synonym:

-- create once
CREATE SYNONYM [dbo].[SampleAlias]
FOR [dbo].[SampleTable];
GO

UPDATE [dbo].[SampleAlias]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

Update a table in another database or on a linked server:

-- another database on the same instance
UPDATE [SampleDb].[dbo].[SampleTable]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

-- configured linked server
UPDATE [RemoteServer].[SampleDb].[dbo].[SampleTable]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

Remote updates require a writable provider and the appropriate login mapping. OUTPUT is not supported for remote targets.

Update through OPENQUERY:

UPDATE OPENQUERY(
	[RemoteServer],
	'SELECT [Id], [Col1]
	 FROM [SampleDb].[dbo].[SampleTable]
	 WHERE [Id] = 1'
)
SET [Col1] = N'New Value';

Uses a configured linked server. The provider must return an updatable rowset.

Update through OPENROWSET:

UPDATE OPENROWSET(
	'MSOLEDBSQL19',
	'Data Source=RemoteServer;Initial Catalog=SampleDb;Integrated Security=SSPI;Use Encryption for Data=Mandatory;',
	'SELECT [Id], [Col1]
	 FROM [dbo].[SampleTable]
	 WHERE [Id] = 1'
)
SET [Col1] = N'New Value';

Requires OLE DB Driver 19, allowed ad hoc access, a trusted server certificate and remote access for the Windows login. OPENROWSET reference.

Update through OPENDATASOURCE:

UPDATE OPENDATASOURCE(
	'MSOLEDBSQL19',
	'Data Source=RemoteServer;Integrated Security=SSPI;Use Encryption for Data=Mandatory;'
).[SampleDb].[dbo].[SampleTable]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

Uses the same driver, ad hoc access and authentication requirements as the OPENROWSET example. OPENDATASOURCE reference.

Update a system-versioned temporal table:

-- SampleTemporalTable already has SYSTEM_VERSIONING = ON
UPDATE [dbo].[SampleTemporalTable]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

Update the current table. SQL Server stores the previous version in the history table. Period columns and the history table cannot be updated directly while system versioning is on. Temporal update reference.

Restore a current row from temporal history:

DECLARE @Id INT = 1;
DECLARE @AsOf DATETIME2 = '2025-01-15T12:00:00';

UPDATE [t]
SET [Col1] = [h].[Col1]
FROM [dbo].[SampleTemporalTable] AS [t]
INNER JOIN [dbo].[SampleTemporalTable]
	FOR SYSTEM_TIME AS OF @AsOf AS [h]
	ON [h].[Id] = [t].[Id]
WHERE [t].[Id] = @Id;

Updates the current row and creates another history version. It does not insert a row that has already been deleted.

Transactions and concurrency

Inspect an update and roll it back:

BEGIN TRANSACTION;

UPDATE [dbo].[SampleTable]
SET [Col1] = N'New Value'
WHERE [Id] = 1;

SELECT [Id], [Col1]
FROM [dbo].[SampleTable]
WHERE [Id] = 1;

ROLLBACK TRANSACTION;
-- use COMMIT TRANSACTION instead to save the change

Run as a separate transaction.

Update two tables in one transaction:

SET XACT_ABORT ON;

BEGIN TRY
	BEGIN TRANSACTION;

	UPDATE [dbo].[SampleTable]
	SET [Status] = N'Processed'
	WHERE [Id] = 1;

	UPDATE [dbo].[SourceTable]
	SET [Status] = N'Processed'
	WHERE [Id] = 1;

	COMMIT TRANSACTION;
END TRY
BEGIN CATCH
	IF XACT_STATE() <> 0
		ROLLBACK TRANSACTION;

	THROW;
END CATCH;

Assumes no transaction is already open. An error rolls back both changes.

Decrease stock only when enough stock is available:

DECLARE @Id INT = 1;
DECLARE @Requested INT = 3;

IF @Requested <= 0
	THROW 50000, 'Requested quantity must be positive.', 1;

UPDATE [dbo].[SampleTable]
SET [Quantity] -= @Requested
WHERE [Id] = @Id
	AND [Quantity] >= @Requested;

IF @@ROWCOUNT = 0
	THROW 50001, 'Row not found or insufficient stock.', 1;

Id must be unique. The check and decrease happen in one statement.

Update only if the rowversion still matches:

DECLARE @Id INT = 1;
-- rowversion returned by the previous read
DECLARE @ExpectedVersion BINARY(8) = 0x0000000000000123;

UPDATE [dbo].[SampleTable]
SET [Col1] = N'New Value'
WHERE [Id] = @Id
	AND [RowVersion] = @ExpectedVersion;

IF @@ROWCOUNT = 0
	THROW 50002, 'Row was changed or deleted.', 1;

RowVersion must be a ROWVERSION column. SQL Server generates its value; it is not a date or time. ROWVERSION reference.

Update an existing row or insert a missing row:

DECLARE @Id INT = 1;
DECLARE @NewValue NVARCHAR(100) = N'New Value';

SET XACT_ABORT ON;

BEGIN TRY
	BEGIN TRANSACTION;

	UPDATE [dbo].[SampleTable] WITH (UPDLOCK, SERIALIZABLE)
	SET [Col1] = @NewValue
	WHERE [Id] = @Id;

	IF @@ROWCOUNT = 0
	BEGIN
		INSERT INTO [dbo].[SampleTable] ([Id], [Col1])
		VALUES (@Id, @NewValue);
	END;

	COMMIT TRANSACTION;
END TRY
BEGIN CATCH
	IF XACT_STATE() <> 0
		ROLLBACK TRANSACTION;

	THROW;
END CATCH;

Id needs a unique key and must allow the explicit inserted value. Assumes no transaction is already open. The lock protects the missing-key check until commit.

Claim the next pending row while skipping locked rows:

-- READ COMMITTED isolation, READ_COMMITTED_SNAPSHOT OFF
DECLARE @Claimed TABLE
(
	[Id] INT,
	[Col1] NVARCHAR(100)
);

SET XACT_ABORT ON;

BEGIN TRY
	BEGIN TRANSACTION;

	;WITH [NextRow] AS
	(
		SELECT TOP (1) [Id], [Col1], [Status]
		FROM [dbo].[SampleTable] WITH (UPDLOCK, READPAST, ROWLOCK)
		WHERE [Status] = N'Pending'
		ORDER BY [Id]
	)
	UPDATE [NextRow]
	SET [Status] = N'Processing'
	OUTPUT [Inserted].[Id], [Inserted].[Col1]
	INTO @Claimed ([Id], [Col1]);

	COMMIT TRANSACTION;

	SELECT [Id], [Col1]
	FROM @Claimed;
END TRY
BEGIN CATCH
	IF XACT_STATE() <> 0
		ROLLBACK TRANSACTION;

	THROW;
END CATCH;

Use a rowstore table with unique Id and no outer transaction. READPAST skips row locks, not page locks. RCSI needs a different hint combination. Table-hint reference.

Other forms

Update the current cursor row:

DECLARE @Id INT;
DECLARE @Col1 NVARCHAR(100);

DECLARE [UpdateCursor] CURSOR LOCAL KEYSET
FOR
	SELECT [Id], [Col1]
	FROM [dbo].[SampleTable]
	WHERE [Status] = N'Pending'
FOR UPDATE OF [Col1];

OPEN [UpdateCursor];

FETCH NEXT FROM [UpdateCursor] INTO @Id, @Col1;

WHILE @@FETCH_STATUS = 0
BEGIN
	UPDATE [dbo].[SampleTable]
	SET [Col1] = N'New Value'
	WHERE CURRENT OF [UpdateCursor];

	FETCH NEXT FROM [UpdateCursor] INTO @Id, @Col1;
END;

CLOSE [UpdateCursor];
DEALLOCATE [UpdateCursor];

The table needs a unique index for this KEYSET cursor. FAST_FORWARD cursors are read-only.

Update a CLR user-defined type property or call its mutator method:

-- Location is a custom CLR type with a writable X property
UPDATE [dbo].[SampleTable]
SET [Location].[X] = 10
WHERE [Id] = 1;

-- the CLR type exposes a SetXY mutator method
UPDATE [dbo].[SampleTable]
SET [Location].SetXY(10, 20)
WHERE [Id] = 1;

Requires an installed CLR type defining these members and a non-NULL Location. SetXY must be marked as a mutator. CLR type update reference.

Use a table hint or query hint:

-- request row locks
UPDATE [dbo].[SampleTable] WITH (ROWLOCK)
SET [Col1] = N'New Value'
WHERE [Id] = 1;

-- recompile this statement and limit parallelism
UPDATE [dbo].[SampleTable]
SET [Status] = N'Archived'
WHERE [GroupId] = 1
OPTION (RECOMPILE, MAXDOP 2);

ROWLOCK does not prevent lock escalation and cannot be used on a table with a clustered columnstore index.

Update a table selected by name using dynamic SQL:

DECLARE @SchemaName SYSNAME = N'dbo';
DECLARE @TableName SYSNAME = N'SampleTable';
DECLARE @Id INT = 1;
DECLARE @NewValue NVARCHAR(100) = N'New Value';

DECLARE @Sql NVARCHAR(MAX) =
	N'UPDATE ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N'
	SET [Col1] = @NewValue
	WHERE [Id] = @Id;';

EXEC [sys].[sp_executesql]
	@Sql,
	N'@Id INT, @NewValue NVARCHAR(100)',
	@Id = @Id,
	@NewValue = @NewValue;

QUOTENAME handles object names. Pass data values as parameters.

Return the affected-row count from a stored procedure:

CREATE OR ALTER PROCEDURE [dbo].[p_UpdateSample]
	@Id INT,
	@NewValue NVARCHAR(100),
	@RowsUpdated INT OUTPUT
AS
BEGIN
	SET NOCOUNT ON;

	UPDATE [dbo].[SampleTable]
	SET [Col1] = @NewValue
	WHERE [Id] = @Id;

	SET @RowsUpdated = @@ROWCOUNT;
END;
GO

DECLARE @RowsUpdated INT;

EXEC [dbo].[p_UpdateSample]
	@Id = 1,
	@NewValue = N'New Value',
	@RowsUpdated = @RowsUpdated OUTPUT;

SELECT @RowsUpdated AS [RowsUpdated];

Update inside a natively compiled stored procedure:

-- SampleMemoryTable is an existing memory-optimized table
CREATE PROCEDURE [dbo].[p_UpdateSampleNative]
	@Id INT,
	@NewValue NVARCHAR(100)
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER
AS
BEGIN ATOMIC WITH
(
	TRANSACTION ISOLATION LEVEL = SNAPSHOT,
	LANGUAGE = N'us_english'
)
	UPDATE [dbo].[SampleMemoryTable]
	SET [Col1] = @NewValue
	WHERE [Id] = @Id;
END;
GO

EXEC [dbo].[p_UpdateSampleNative]
	@Id = 1,
	@NewValue = N'New Value';

Create the procedure once. Native procedures have a smaller supported T-SQL surface. Native procedure reference.

Update a local return table inside a function:

CREATE OR ALTER FUNCTION [dbo].[f_SampleValues]()
RETURNS @Result TABLE
(
	[Id] INT,
	[Col1] NVARCHAR(100)
)
AS
BEGIN
	INSERT INTO @Result ([Id], [Col1])
	SELECT [Id], [Col1]
	FROM [dbo].[SampleTable];

	UPDATE @Result
	SET [Col1] = N'Unknown'
	WHERE [Col1] IS NULL;

	RETURN;
END;
GO

SELECT [Id], [Col1]
FROM [dbo].[f_SampleValues]();

A T-SQL function can update its local table variables. It cannot update permanent tables.

Full UPDATE syntax and restrictions on Microsoft Learn.