Colonne de changement rapide NVARCHAR (4000) à NVARCHAR (260)

12

J'ai un problème de performances avec de très grandes allocations de mémoire pour gérer cette table avec quelques NVARCHAR(4000)colonnes. La chose est que ces colonnes ne sont jamais plus grandes que NVARCHAR(260).

En utilisant

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

entraîne la réécriture de la table entière par SQL Server (et l'utilisation d'une taille de table 2x dans l'espace de journal), qui représente des milliards de lignes, pour ne rien changer, n'est pas une option. L'augmentation de la largeur de colonne n'a pas ce problème, mais sa diminution le fait.

J'ai essayé de créer une contrainte CHECK (DATALENGTH([col]) <= 520)ou CHECK (LEN([col]) <= 260)et SQL Server décide toujours de réécrire la table entière.

Existe-t-il un moyen de modifier le type de données de colonne en tant qu'opération de métadonnées uniquement? Sans les frais de réécriture de la table entière? J'utilise SQL Server 2017 (14.0.2027.2 et 14.0.3192.2).

Voici un exemple de table DDL à utiliser pour reproduire:

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

Et puis lancez le ALTER.

Nick Whaley
la source

Réponses:

16

Je ne connais pas de moyen d'accomplir directement ce que vous cherchez ici. Notez que l'optimiseur de requête n'est pas assez intelligent pour le moment pour prendre en compte les contraintes pour les calculs d'allocation de mémoire, de sorte que la contrainte n'aurait de toute façon pas aidé. Quelques méthodes qui évitent de réécrire les données de la table:

  1. CAST la colonne comme NVARCHAR (260) dans tous les codes qui l'utilisent. L'optimiseur de requêtes calculera l'allocation de mémoire en utilisant le type de données castées au lieu du type brut.
  2. Renommez la table et créez une vue qui effectue la conversion à la place. Cela accomplit la même chose que l'option 1 mais peut limiter la quantité de code que vous devez mettre à jour.
  3. Créez une colonne calculée non persistante avec le bon type de données et faites en sorte que toutes vos requêtes soient sélectionnées dans cette colonne au lieu de la colonne d'origine.
  4. Renommez la colonne existante et ajoutez la colonne calculée avec le nom d'origine. Ajustez ensuite toutes vos requêtes en mettant à jour ou en insérant la colonne d'origine pour utiliser à la place le nouveau nom de colonne.
Joe Obbish
la source
15

Existe-t-il un moyen de modifier le type de données de colonne en tant qu'opération de métadonnées uniquement?

Je ne pense pas, c'est ainsi que fonctionne le produit en ce moment. Il existe de très bonnes solutions de contournement à cette limitation proposée dans la réponse de Joe .

... entraîne la réécriture de SQL Server de la table entière (et l'utilisation d'une taille de table 2x dans l'espace de journal)

Je vais répondre séparément aux deux parties de cette déclaration.

Réécrire la table

Comme je l'ai mentionné précédemment, il n'y a vraiment aucun moyen d'éviter cela. Cela semble être la réalité de la situation, même si cela n'a pas de sens de notre point de vue en tant que clients.

Regarder DBCC PAGEavant et après avoir changé la colonne de 4000 en 260 montre que toutes les données sont dupliquées sur la page de données (ma table de test avait 'A'260 fois de suite):

Capture d'écran de la partie données de la page dbcc avant et après

À ce stade, il existe deux copies des mêmes données exactes sur la page. La "vieille" colonne est essentiellement supprimée (l'ID est changé de id = 2 à id = 67108865), et la "nouvelle" version de la colonne est mise à jour pour pointer vers le nouveau décalage des données sur la page:

Capture d'écran des parties de métadonnées de colonne de la page dbcc avant et après

Utilisation de la taille de table 2x dans l'espace de journal

L'ajout WITH (ONLINE = ON)à la fin de l' ALTERinstruction réduit l'activité de journalisation d'environ la moitié , c'est donc une amélioration que vous pourriez apporter pour réduire la quantité d'écritures sur le disque / l'espace disque nécessaire.

J'ai utilisé ce harnais de test pour l'essayer:

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

J'ai vérifié sys.dm_io_virtual_file_stats(DB_ID(N'248749'), DEFAULT)avant et après l'exécution de l' ALTERinstruction, et voici les différences:

Par défaut (hors ligne) ALTER

  • Écriture du fichier de données / octets écrits: 34,809 / 2,193,801,216
  • Écriture du fichier journal / octets écrits: 40,953 / 1,484,910,080

En ligne ALTER

  • Écriture du fichier de données / octets écrits: 36 874/1 693 745 152 (baisse de 22,8%)
  • Écrits du fichier journal / octets écrits: 24,680 / 866,166,272 (baisse de 41%)

Comme vous pouvez le voir, il y a eu une légère baisse des écritures du fichier de données et une baisse importante des écritures du fichier journal.

Josh Darnell
la source
2

J'ai été dans une situation similaire à plusieurs reprises.

Pas :

Ajouter un nouveau col de largeur souhaitée

Utilisez un curseur, avec quelques milliers d'itérations (peut-être dix ou vingt mille) par validation pour copier les données de l'ancienne colonne vers la nouvelle colonne

Supprimer l'ancienne colonne

Renommer la nouvelle colonne en nom de l'ancienne colonne

Tada!

Jonesome Reinstate Monica
la source
3
Que faire si certains enregistrements que vous avez déjà copiés finissent par être mis à jour ou supprimés?
George.Palacios
1
Il est très facile de faire une finale update table set new_col = old_col where new_col <> old_col;avant d'abandonner old_col.
Colin 't Hart
1
@ Colin'tHart cette approche ne fonctionnera pas avec des millions de lignes ... la transaction devient énorme et elle bloque ....
Jonesome Reinstate Monica
@samsmith Commencez par faire ce que vous décrivez ci-dessus. Ensuite, avant de supprimer la colonne d'origine, s'il y a eu des mises à jour des données d'origine entre-temps, exécutez cette instruction de mise à jour. Cela ne devrait affecter que les quelques lignes qui ont été modifiées. Ou est-ce que je manque quelque chose?
Colin 't Hart
Pour couvrir les lignes mises à jour au cours du processus, en essayant d'éviter l'analyse complète qui where new_col <> old_colsans aucune autre clause de filtrage n'entraînera, vous pouvez ajouter un déclencheur pour reporter ces modifications au fur et à mesure et les supprimer à la fin du processus. Toujours un impact potentiel sur les performances, mais de nombreuses petites quantités sur la durée du processus au lieu d'un énorme hit à la fin, probablement (en fonction du modèle de mise à jour de votre application pour le tableau) totalisant beaucoup moins que cet énorme hit .
David Spillett
1

Eh bien, il existe une alternative en fonction de l'espace disponible dans votre base de données.

  1. Créez une copie exacte de votre tableau (par exemple new_table), à l'exception de la colonne où vous raccourcirez de NVARCHAR(4000)à 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. Dans une fenêtre de maintenance copiez les données de la table "cassée" ( table) vers la table "fixe" ( new_table) avec un simple 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. Renommez la table "cassée" tableen quelque chose d'autre:

    EXEC sp_rename 'table', 'old_table';  
  4. Renommez la table "fixe" new_tableen table:

    EXEC sp_rename 'new_table', 'table';  
  5. Si tout va bien, supprimez le tableau renommé "cassé":

     DROP TABLE [old_table]
     GO

Voilà.

Répondre à vos questions

Existe-t-il un moyen de modifier le type de données de colonne en tant qu'opération de métadonnées uniquement?

Non actuellement impossible

Sans les frais de réécriture de la table entière?

Non.
( Voir ma solution et d'autres. )

John aka hot2use
la source
Votre "insertion dans la sélection de" entraînera, sur une grande table (des millions ou des milliards de lignes) une transaction ENORME, qui peut arrêter la base de données pendant des dizaines ou des centaines de minutes. (En plus de rendre le ldf énorme et éventuellement cassant l'envoi de journaux, s'il est utilisé)
Jonesome Reinstate Monica