Dropping and creating persisted computed columns causes SQL Server to think the 8060 is reached while it is not

Viewed 205

In SQL Server the max length of a row is 8060 bytes. In my case the dba.CostCenter rows can be max 1383 bytes. So far so good. When updating our Cloud software on a daily basis the table definition gets messed up. After 37 days with 8 similar computed columns as the one below.

ALTER TABLE dba.CostCenter DROP COLUMN CcrTotBudgetAY;
ALTER TABLE dba.CostCenter ADD CcrTotBudgetAY AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL;

SQL Server raises the well known 8060 bytes error.

When I add

ALTER TABLE dba.CostCenter REBUILD PARTITION = ALL;

between the DROP and ADD COLUMN the error message doesn't popup and I can easily update the computed columns more than 100 times.

Is there an alternative for the REBUILD option because it is a heavy operation or is this a bug in SQL Server?

1 Answers

causes SQL Server to think the 8060 is reached while it is not

If you are seeing this error message then you are wrong about that. Every time you drop and re-create the computed column the old column is marked as dropped in the metadata but the previous column values still exist in the data page.

When you recreate the column it is treated as a new column at the end of all existing columns. It can reclaim space from dropped columns at the end of the section but not if they are followed by another undropped column. So depending on the approach you take by the time you have done a few iterations of this the data page will be full of rubbish.

Approach 1 (offsets increase)

DROP TABLE IF EXISTS dbo.CostCenter 

go

CREATE TABLE dbo.CostCenter 
(
CcrChildBudgetAY DECIMAL(10,2),
CcrBudgetAY DECIMAL(10,2),
CcrTotBudgetAY AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL,
CcrTotBudgetAY2 AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL
);

INSERT INTO dbo.CostCenter VALUES (1,1);

ALTER TABLE dbo.CostCenter DROP COLUMN CcrTotBudgetAY;
ALTER TABLE dbo.CostCenter ADD CcrTotBudgetAY AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL;
ALTER TABLE dbo.CostCenter DROP COLUMN CcrTotBudgetAY2;
ALTER TABLE dbo.CostCenter ADD CcrTotBudgetAY2 AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL;

SELECT c.name, is_dropped, leaf_offset
FROM sys.system_internals_partition_columns pc
JOIN sys.partitions p on p.partition_id = pc.partition_id
LEFT JOIN sys.columns c on c.object_id = p.object_id and c.column_id = pc.partition_column_id
WHERE p.object_id = OBJECT_ID('dbo.CostCenter')

+------------------+------------+-------------+
|       name       | is_dropped | leaf_offset |
+------------------+------------+-------------+
| CcrChildBudgetAY |          0 |           4 |
| CcrBudgetAY      |          0 |          13 |
| NULL             |          1 |          22 |
| NULL             |          1 |          35 |
| CcrTotBudgetAY   |          0 |          48 |
| CcrTotBudgetAY2  |          0 |          61 |
+------------------+------------+-------------+

Approach 2 (offsets reused)

DROP TABLE IF EXISTS dbo.CostCenter 

go

CREATE TABLE dbo.CostCenter 
(
CcrChildBudgetAY DECIMAL(10,2),
CcrBudgetAY DECIMAL(10,2),
CcrTotBudgetAY AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL,
CcrTotBudgetAY2 AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL
);

INSERT INTO dbo.CostCenter VALUES (1,1);

ALTER TABLE dbo.CostCenter DROP COLUMN CcrTotBudgetAY, CcrTotBudgetAY2;

ALTER TABLE dbo.CostCenter ADD CcrTotBudgetAY AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL, 
                               CcrTotBudgetAY2 AS (CONVERT(DECIMAL(21,5), CcrChildBudgetAY + CcrBudgetAY)) PERSISTED NOT NULL

SELECT c.name, is_dropped, leaf_offset
FROM sys.system_internals_partition_columns pc
JOIN sys.partitions p on p.partition_id = pc.partition_id
LEFT JOIN sys.columns c on c.object_id = p.object_id and c.column_id = pc.partition_column_id
WHERE p.object_id = OBJECT_ID('dbo.CostCenter')

+------------------+------------+-------------+
|       name       | is_dropped | leaf_offset |
+------------------+------------+-------------+
| CcrChildBudgetAY |          0 |           4 |
| CcrBudgetAY      |          0 |          13 |
| NULL             |          1 |          22 |
| NULL             |          1 |          35 |
| CcrTotBudgetAY   |          0 |          22 |
| CcrTotBudgetAY2  |          0 |          35 |
+------------------+------------+-------------+

The best alternative will be to come up with a solution that doesn't involve dropping and re-creating these columns daily as even in the best case this causes logged activity for all rows in the table that need to be rewritten as you add back the persisted computed columns.

Related