Altere rapidamente a coluna NVARCHAR (4000) para NVARCHAR (260)

12

Eu tenho um problema de desempenho com concessões de memória muito grandes que manipulam esta tabela com algumas NVARCHAR(4000)colunas. O problema é que essas colunas nunca são maiores que NVARCHAR(260).

Usando

ALTER TABLE [table] ALTER COLUMN [col] NVARCHAR(260) NULL

resulta no SQL Server reescrevendo a tabela inteira (e usando o tamanho da tabela 2x no espaço do log), que é bilhões de linhas, apenas para alterar nada, não é uma opção. Aumentar a largura da coluna não tem esse problema, mas diminuir.

Tentei criar uma restrição CHECK (DATALENGTH([col]) <= 520)ou o CHECK (LEN([col]) <= 260)SQL Server ainda decide reescrever a tabela inteira.

Existe alguma maneira de alterar o tipo de dados da coluna como uma operação somente de metadados? Sem a despesa de reescrever a tabela inteira? Estou usando o SQL Server 2017 (14.0.2027.2 e 14.0.3192.2).

Aqui está uma tabela DDL de amostra a ser usada para reproduzir:

CREATE TABLE [table](
    id INT IDENTITY(1,1) NOT NULL,
    [col] NVARCHAR(4000) NULL,
    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

E então execute o ALTER.

Nick Whaley
fonte

Respostas:

15

Existe alguma maneira de alterar o tipo de dados da coluna como uma operação somente de metadados?

Acho que não, é assim que o produto funciona agora. Existem algumas soluções realmente ótimas para essa limitação proposta na resposta de Joe .

... resulta no SQL Server reescrevendo a tabela inteira (e usando o tamanho da tabela 2x no espaço de log)

Vou responder às duas partes dessa declaração separadamente.

Reescrevendo a Tabela

Como mencionei antes, não há realmente nenhuma maneira de evitar isso. Essa parece ser a realidade da situação, mesmo que não faça todo o sentido da nossa perspectiva como clientes.

Observar DBCC PAGEantes e depois de alterar a coluna de 4000 para 260 mostra que todos os dados são duplicados na página de dados (minha tabela de teste teve 'A'260 vezes na linha):

Captura de tela da parte de dados da página dbcc antes e depois

Neste ponto, há duas cópias exatamente dos mesmos dados na página. A coluna "antiga" é essencialmente excluída (a identificação é alterada de id = 2 para id = 67108865) e a versão "nova" da coluna é atualizada para apontar para o novo deslocamento dos dados na página:

Captura de tela de partes de metadados da coluna da página dbcc antes e depois

Usando 2x o tamanho da tabela no espaço de log

A adição WITH (ONLINE = ON)ao final da ALTERinstrução reduz a atividade de registro em cerca de metade . Portanto, esse é um aprimoramento que você pode fazer para reduzir a quantidade de gravações necessárias no espaço em disco / disco.

Eu usei este equipamento de teste para testá-lo:

USE [master];
GO
DROP DATABASE IF EXISTS [248749];
GO
CREATE DATABASE [248749] 
ON PRIMARY 
(
    NAME = N'248749', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749.mdf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
)
LOG ON 
(
    NAME = N'248749_log', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749_log.ldf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
);
GO
USE [248749];
GO

CREATE TABLE dbo.[table]
(
    id int IDENTITY(1,1) NOT NULL,
    [col] nvarchar (4000) NULL,

    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

INSERT INTO dbo.[table]
SELECT TOP (1000000)
    REPLICATE(N'A', 260)
FROM master.dbo.spt_values v1
    CROSS JOIN master.dbo.spt_values v2
    CROSS JOIN master.dbo.spt_values v3;
GO

Eu verifiquei sys.dm_io_virtual_file_stats(DB_ID(N'248749'), DEFAULT)antes e depois de executar a ALTERinstrução, e aqui estão as diferenças:

Padrão (offline) ALTER

  • Gravações no arquivo de dados / bytes gravados: 34.809 / 2.193.801.216
  • Gravações no arquivo de log / bytes gravados: 40.953 / 1.484.910.080

Conectados ALTER

  • Gravações no arquivo de dados / bytes gravados: 36.874 / 1.693.745.152 (queda de 22,8%)
  • Gravações do arquivo de log / bytes gravados: 24.680 / 866.166.272 (queda de 41%)

Como você pode ver, houve uma pequena queda nas gravações do arquivo de dados e uma grande queda nas gravações do arquivo de log.

Josh Darnell
fonte
15

Não sei como realizar diretamente o que você está procurando aqui. Observe que, no momento, o otimizador de consulta não é inteligente o suficiente para levar em consideração as restrições dos cálculos de concessão de memória; portanto, a restrição não teria ajudado. Alguns métodos que evitam reescrever os dados da tabela:

  1. CAST a coluna como NVARCHAR (260) em todos os códigos que a utilizam. O otimizador de consulta calculará a concessão de memória usando o tipo de dados convertidos em vez do bruto.
  2. Renomeie a tabela e crie uma exibição que faça a conversão. Isso realiza o mesmo que a opção 1, mas pode limitar a quantidade de código que você precisa atualizar.
  3. Crie uma coluna computada não persistente com o tipo de dados correto e faça com que todas as suas consultas sejam selecionadas nessa coluna, em vez da coluna original.
  4. Renomeie a coluna existente e adicione a coluna computada com o nome original. Em seguida, ajuste todas as suas consultas fazendo atualizações ou inserções na coluna original para usar o novo nome da coluna.
Joe Obbish
fonte
2

Eu já estive em uma situação semelhante muitas vezes.

Passos :

Adicione uma nova coluna da largura desejada

Use um cursor, com alguns milhares de iterações (talvez dez ou vinte mil) por confirmação para copiar dados da coluna antiga para a nova coluna

Soltar coluna antiga

Renomeie a nova coluna para o nome da coluna antiga

Tada!

Jonesome Restabelecer Monica
fonte
3
E se alguns registros que você já copiou acabam sendo atualizados ou excluídos?
George.Palacios
1
É muito fácil fazer uma final update table set new_col = old_col where new_col <> old_col;antes de largar old_col.
Colin 't Hart
1
@ Colin'tHart essa abordagem não vai funcionar com milhões de linhas ... a transação fica enorme, e ele bloqueia ....
Jonesome Reintegrar Monica
@samsmith Primeiro você faz o que descreve acima. Antes de descartar a coluna original, se houver alguma atualização nos dados originais, execute a instrução de atualização. Isso deve afetar apenas as poucas linhas que foram modificadas. Ou eu estou esquecendo de alguma coisa?
Colin 't Hart
Para cobrir as linhas atualizadas durante o processo, tentando evitar a varredura completa em que where new_col <> old_colnão resultará nenhuma outra cláusula de filtragem, você pode adicionar um gatilho para transportar essas alterações à medida que elas acontecem e removê-las no final do processo. Ainda é um sucesso em potencial, mas muitas pequenas quantidades ao longo do processo, em vez de um grande sucesso no final, provavelmente (dependendo do padrão de atualização do seu aplicativo para a tabela) resultando em muito menos menos no total do que aquele grande sucesso .
David Spillett
1

Bem, há uma alternativa dependendo do espaço disponível no seu banco de dados.

  1. Crie uma cópia exata da sua tabela (por exemplo new_table), exceto a coluna na qual você reduzirá de NVARCHAR(4000)para NVARCHAR(260):

    CREATE TABLE [new_table](
        id INT IDENTITY(1,1) NOT NULL,
        [col] NVARCHAR(260) NULL,
        CONSTRAINT [PK_test_new] PRIMARY KEY CLUSTERED (id ASC)
    );
  2. Em uma janela de manutenção, copie os dados da tabela "quebrada" ( table) para a tabela "fixa" ( new_table) com um simples INSERT ... INTO ... SELECT ....:

    SET IDENTITY_INSERT [new_table] ON
    GO
    INSERT id, col INTO [new_table] SELECT id, col from [table]
    GO
    SET IDENTITY_INSERT [new_table] OFF
    GO
  3. Renomeie a tabela "quebrada" tablepara outra coisa:

    EXEC sp_rename 'table', 'old_table';  
  4. Renomeie a tabela "fixa" new_tablepara table:

    EXEC sp_rename 'new_table', 'table';  
  5. Se tudo estiver bem, descarte a tabela renomeada "quebrada":

     DROP TABLE [old_table]
     GO

Ai está.

Respondendo as suas perguntas

Existe alguma maneira de alterar o tipo de dados da coluna como uma operação somente de metadados?

Não. Atualmente não é possível

Sem a despesa de reescrever a tabela inteira?

Não.
( Veja minha solução e outras. )

John aka hot2use
fonte
Seu "inserir na seleção de" resultará, em uma tabela grande (milhões ou bilhões de linhas) em uma transação ENORMOUS, que poderá interromper o banco de dados por dezenas ou centenas de minutos. (Bem como fazer a ldf enorme e possivelmente quebrar o envio de log, se estiver em uso)
Jonesome Reintegrar Monica