Сжатие SQL Server 2014 и максимальный размер строки

8

Мне нужно создать широкую денормализованную таблицу с большим количеством десятичных (26,8) столбцов (ограничение не более 1024 столбцов, большинство столбцов будет нулевым или нулевым). Я знаю около 8060 байт на ограничение строки, поэтому я попытался создать таблицу со сжатием страницы. Код ниже создает таблицу, вставляет одну строку и запрашивает размер строки. Размер строки намного ниже предела, но если я попытаюсь добавить еще один десятичный (26,8) столбец в таблицу, операция завершится неудачно с ошибкой «Создание или изменение таблицы« t1 »завершилось неудачно, поскольку минимальный размер строки составил бы 8074, включая 1256» байты внутренней служебной информации. Есть ли способ создать одну таблицу с таким количеством столбцов?

drop table t1
GO
create table t1(c1 decimal(26, 8) null)
with (data_compression = page)
GO

declare @i int = 2;
declare @sql varchar(100);
while @i <= 486
begin
    set @sql = 'alter table t1 add c' + convert(varchar, @i) + ' decimal(26, 8) null';
    execute (@sql);
    set @i += 1;
end;
GO


insert into t1(c1) select 0
GO
declare @i int = 2;
declare @sql varchar(100);
while @i <= 486
begin
    set @sql = 'update t1 set c' + convert(varchar, @i) + ' = 0';
    execute (@sql);
    set @i += 1;
end;
GO

select max_record_size_in_bytes from sys.dm_db_index_physical_stats (db_id(), object_id('t1'), NULL, NULL, 'DETAILED')
GO
Alex
источник
1
Кстати, я могу получить 613 DECIMAL(26, 8) NULLполей в таблицу, без сжатия страниц или десятичного сжатия. При включении vardecimal, но не сжатии страниц, накладные расходы превышают 1 K. Существует внешняя вероятность, что вы сможете хранить больше полей на странице без vardecimal, в зависимости от ваших значений.
Джон на все

Ответы:

4

Ограничение, с которым вы сталкиваетесь, не имеет ничего общего с данными, хранящимися на странице. Расчет производится на основе типов данных столбцов. Вот почему вы столкнулись с ошибкой без каких-либо данных в таблице. Сжатие делает этот предел хуже. Вы можете прочитать о технических деталях за накладными расходами здесь .

Вы можете обойти эту проблему, используя столбцы SPARSE . Это означает, что для вставок может произойти сбой в зависимости от того, что вы вставляете, но вы можете обойти ограничение в 8060 байт. Следующий код показывает, что вы можете просто создать 1023 столбца:

drop table t1
GO
create table t1(c1 decimal(26, 8) null)
GO

declare @i int = 2;
declare @sql varchar(100);
while @i <= 1023
begin
    set @sql = 'alter table t1 add c' + convert(varchar, @i) + ' decimal(26, 8) SPARSE null';
    execute (@sql);
    set @i += 1;
end;
GO

Тем не менее, все ограничения (см. Связанную статью) могут сделать это не подходящим для вашего варианта использования. В частности, только NULLзначения (не 0) оптимизированы, чтобы занять очень мало места. Если вы попытаетесь вставить слишком много 0s в одну строку, вы получите ошибку. Вот что я вижу, когда пытаюсь вставить 1023 0значения:

Сообщение 511, уровень 16, состояние 1, строка 1 Невозможно создать строку размером 17402, которая превышает максимально допустимый размер строки 8060.

Я полагаю, что если вы действительно впали в отчаяние, вы можете создать столбцы VARCHAR(27)вместо этого. Столбцы переменной длины можно переместить за пределы страницы, чтобы вы могли превысить ограничение в 8060 байт в определении таблицы, но вставить определенные комбинации значений не удастся. SQL Server предупреждает вас об этом при создании таблицы:

Предупреждение: таблица "t1" была создана, но ее максимальный размер строки превышает допустимый максимум в 8060 байтов. INSERT или UPDATE этой таблицы завершатся неудачно, если результирующая строка превысит ограничение размера.

Сжатие страниц или строк может быть полезно, если вы придерживаетесь этого VARCHAR(27)подхода. Это минимизирует пространство, используемое обоими 0и NULL. С помощью VARCHAR(27)я могу вставить 1023 0значения просто отлично.

Джо Оббиш
источник
2

Помимо технических аспектов и предлагаемого обходного пути (с использованием VARCHAR(27)столбцов), обсуждаемых в ответе @ Joe , я подвергаю сомнению « необходимость создания широкой денормализованной таблицы», как это выражено в OP, если не существует какого-то странного технического требования, что все эти столбцы должно быть в одной таблице, я бы посоветовал / рекомендовал распределить их по столько раз, сколько необходимо. Родственные таблицы - это таблицы, которые:

  • иметь отношения 1 к 1 друг с другом,
  • все имеют один и тот же первичный ключ,
  • только один имеет IDENTITY колонка (и нет ФК для других)
  • остальные имеют внешний ключ (в столбце PK), указывающий на PK таблицы, которая имеет IDENTITY

Здесь вы разбиваете логическую строку на две или более физических таблицы. Но это по сути то, что нормализация в любом случае, и какие реляционные базы данных предназначены для обработки.

В этом сценарии вам потребуется некоторое дополнительное пространство, используемое при дублировании PK, и некоторая дополнительная сложность запросов из-за необходимости INNER JOINобъединения таблиц (часто, но не всегда, если все SELECTзапросы не используют все столбцы, но обычно это не происходит) или создать явную транзакцию INSERTили UPDATEих вместе (DELETE может быть обработано с помощьюON DELETE CASCADE набор на FK).

ОДНАКО вы получаете преимущества от наличия надлежащей модели данных с правильными родными типами данных и без хитрости, которая может привести к непредвиденным последствиям в дальнейшем. Даже если использоватьVARCHAR(27) позволяет работать на техническом уровне, прагматически я не думаю, что хранение десятичных дробей как строк отвечает интересам вашего проекта.

Итак, если вам «нужна» только одна таблица из-за того, что вы не понимаете, что один логический объект не должен быть физически представлен в одном контейнере, то не пытайтесь заставить все это объединить в одну таблицу, когда она будет работать изящно через несколько столов.

Пример ниже иллюстрирует основную концепцию:

НАСТРОИТЬ

CREATE TABLE tempdb.dbo.T1
(
  [ID] INT NOT NULL IDENTITY(11, 2) PRIMARY KEY,
  [Col1] VARCHAR(25),
  [Col2] DATETIME NOT NULL DEFAULT (GETDATE())
);

CREATE TABLE tempdb.dbo.T2
(
  [ID] INT NOT NULL PRIMARY KEY
                    FOREIGN KEY REFERENCES tempdb.dbo.T1([ID]) ON DELETE CASCADE,
  [Col3] UNIQUEIDENTIFIER,
  [Col4] BIGINT
);

GO
CREATE PROCEDURE #TestInsert
(
  @Val1 VARCHAR(25),
  @Val4 BIGINT
)
AS
SET NOCOUNT ON;

BEGIN TRY
  BEGIN TRAN;

  DECLARE @InsertedID INT;

  INSERT INTO tempdb.dbo.T1 ([Col1])
  VALUES (@Val1);

  SET @InsertedID = SCOPE_IDENTITY();

  INSERT INTO tempdb.dbo.T2 ([ID], [Col3], [Col4])
  VALUES (@InsertedID, NEWID(), @Val4);

  COMMIT TRAN;
END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK TRAN;
  END;

  THROW;
END CATCH;

SELECT @InsertedID AS [ID];
GO

ТЕСТОВОЕ ЗАДАНИЕ

EXEC #TestInsert 'aa', 454567678989;

EXEC #TestInsert 'bb', 12312312312234;

SELECT *
FROM   tempdb.dbo.T1
INNER JOIN tempdb.dbo.T2
        ON T2.[ID] = T1.[ID];

Возвращает:

ID  Col1  Col2                     ID  Col3                                  Col4
11  aa    2017-07-04 10:39:32.660  11  44465676-E8A1-4F38-B5B8-F50C63A947A4  454567678989
13  bb    2017-07-04 10:41:38.180  13  BFE43379-559F-4DAD-880B-B09D7ECA4914  12312312312234
Соломон Руцкий
источник