web-dev-qa-db-fra.com

nvarchar (max) conversion en varchar et optimisation de table

Je travaille avec une table dont tous les types de caractères sont définis sur nvarchar certains d'entre eux sont nvarchar(max). Nous convertissons tout cela en varchar et spécifions une largeur de caractère basée sur l'utilisation réelle en production. Les données de production utilisent une plage de 2 caractères jusqu'à 900 caractères de la largeur réelle utilisée pour une colonne donnée. Nous allons ajouter un rembourrage de 10% le cas échéant.

-- Insert statements for procedure here
UPDATE Listings WITH (ROWLOCK) 
SET [SubType] = 'S' 
WHERE @idSettings = idSettings AND
    (@idRetsClass = 0 OR idRetsClass = @idRetsClass)
    AND (@idRetsSetting = 0 OR idRetsSetting = @idRetsSetting)
    AND IsNew = 1 AND ([SubType] LIKE '%Single Family Home%' OR [SubType] LIKE '%Modular%' OR [SubType] LIKE '%Mobile Home%' 
    OR [SubType] LIKE '% Story%' OR [SubType] = '' OR [SubType] = 'residential - S' OR [SubType] = '1 House on Lot' OR [SubType] = '2 Houses on Lot' 
    OR [SubType] = 'Detached' OR [SubType] LIKE '%single family%' OR [SubType] = 'ranch' OR [SubType] = 'Semi-Detached' OR [SubType] = 'single' OR [SubType] = 'one family' OR [SubType] = 'Residential' 
    OR [SubType] = 'Ranch Type' OR [SubType] = '2 or More Stories' OR [SubType] = 'Cape Cod' OR [SubType] = 'Split Level' OR [SubType] = 'Bi-Level' OR [SubType] = 'Detached Single' 
    OR [SubType] = 'Single-Family Homes' OR [SubType] = 'house' OR [SubType] = 'detached housing'  OR [SubType] = 'det')

Une grande refonte de cette table qui se compose littéralement de 140 (nvarchar) colonnes, 11 étant MAX. Je supprime 30 index et les recrée ensuite.

Ma question est dans quelles situations varchar(max) est-il préféré?

Uniquement lorsque vous vous attendez à avoir 4 000 caractères ou plus?

Que dois-je apprendre et préparer en faisant cela?

Cela améliorera-t-il les performances lorsqu'une mise à jour d'index cluster qui affecte la clé cluster doit mettre à jour tous les index non cluster?

Nous avons des procédures de mise à jour expirant qui utilisent 75% à 95% du plan d'exécution de la requête & plan affiché pour une mise à jour d'index en cluster.

Lien vers le plan d'exécution réel

7
Keith Beard

{C'est peut-être un peu long, mais vos problèmes réels ne peuvent pas être résolus en consultant les plans d'exécution. Il y a deux problèmes principaux, et les deux sont architecturaux. }

Distractions

Commençons par les éléments qui ne sont pas vos principaux problèmes. Ce sont des choses qui devraient être examinées car cela aide certainement à améliorer les performances pour utiliser les types de données dont vous avez besoin et pas seulement un ajustement général à taille unique -le type de données le plus important. Il y a une très bonne raison pour laquelle les différents types de données existent, et si le stockage de 100 caractères dans NVARCHAR(MAX) n'avait pas d'impact négatif sur les requêtes (ou tout autre aspect du système), alors tout serait stocké sous la forme NVARCHAR(MAX). Cependant, le nettoyage de ces zones ne conduira pas à une véritable scalamabilité.

à MAX, ou pas à MAX

Je travaille avec une table dont tous les types de caractères sont définis sur nvarchar certains d'entre eux sont nvarchar(max).

D'accord. Ce n'est pas nécessairement une mauvaise chose, bien que le plus souvent il y ait au moins un champ de type numérique comme ID. Cependant, il existe certainement des cas valables pour le scénario décrit jusqu'à présent. Et il n'y a rien de intrinsèquement mauvais dans les champs MAX car ils stockeront les données sur la page de données (c'est-à-dire en ligne ) si les données peut y tenir. Et dans cette situation, il devrait fonctionner aussi bien qu'une valeur non MAX de ce même type de données. Mais oui, un tas de champs de type MAX est un signe de négligence dans la modélisation des données et est beaucoup plus susceptible d'avoir la plupart (ou la totalité) de ces données MAX stockées dans des pages de données distinctes ( c'est-à-dire hors ligne ) qui nécessitent une recherche supplémentaire, donc moins efficace.

VARCHAR vs NVARCHAR

Nous convertissons tout cela en varchar...

Ok, mais pourquoi exactement (oui, je sais que les informations et les commentaires qui suivent cette déclaration ajoutent de la clarté, mais je vais dans le but de préserver l'aspect conversationnel pour une raison). Chaque type de données a sa place. VARCHAR est de 1 octet par caractère et peut représenter 256 caractères (la plupart du temps) comme défini sur une page de code unique . Alors que les valeurs de caractères 0 - 127 sont les mêmes entre les pages de codes, les valeurs de caractères entre 128 et 255 peuvent changer:

;WITH chars ([SampleCharacters]) AS
(
  SELECT CHAR(42) + ' '   -- *
       + CHAR(65) + ' '   -- A
       + CHAR(126) + ' '  -- 
   -------------------------------
       + CHAR(128) + ' '  -- €
       + CHAR(149) + ' '  -- •
       + CHAR(165) + ' '  -- ¥, Y, ?
       + CHAR(183) + ' '  -- ·, ?
       + CHAR(229) + ' '  -- å, a, ?
)
SELECT chr.SampleCharacters COLLATE SQL_Latin1_General_CP1_CI_AS AS [SQL_Latin1_General_CP1_CI_AS],
       chr.SampleCharacters COLLATE SQL_Latin1_General_CP1255_CI_AS AS [SQL_Latin1_General_CP1255_CI_AS],
       chr.SampleCharacters COLLATE Thai_CI_AS_KS_WS AS [Thai_CI_AS_KS_WS],
       chr.SampleCharacters COLLATE Yakut_100_CS_AS_KS AS [Yakut_100_CS_AS_KS],
       chr.SampleCharacters COLLATE Albanian_CS_AI AS [Albanian_CS_AI]
FROM   chars chr;

Veuillez noter qu'il est possible que les données VARCHAR prennent 2 octets par caractère et représentent plus de 256 caractères. Pour plus d'informations sur les jeux de caractères codés sur deux octets, consultez la réponse suivante: Stockage de caractères japonais dans un tablea .

NVARCHAR est stocké en UTF-16 (Little Endian) et est de 2 ou 4 octets par caractère, ce qui peut représenter le spectre Unicode complet. Donc, si vos données doivent toujours stocker plus de caractères que ce qui peut être représenté par une seule page de codes, le passage à VARCHAR ne vous aidera pas vraiment.

Avant de convertir en VARCHAR, vous devez vous assurer que vous ne stockez aucun caractère Unicode. Essayez la requête suivante pour voir s'il existe des lignes qui ne peuvent pas être converties en VARCHAR sans perdre de données:

SELECT tbl.PKfield, tbl.SubType
FROM   dbo.[Listings] tbl
WHERE  tbl.SubType <> CONVERT(NVARCHAR(MAX), CONVERT(VARCHAR(MAX), tbl.SubType))

Pour clarifier le fonctionnement de NVARCHAR: la longueur maximale d'un champ NVARCHAR est le nombre de caractères de 2 octets . Par conséquent, NVARCHAR(50), permettra un maximum de 100 octets. Le nombre de caractères pouvant tenir dans ces 100 octets dépend du nombre de caractères de 4 octets: aucun ne vous permettra de tenir dans les 50 caractères, tous les caractères de 4 octets ne pourront contenir que 25 caractères et de nombreuses combinaisons entre les deux.

Autre chose à considérer concernant l'espace occupé par VARCHAR vs NVARCHAR: à partir de SQL Server 2008 (éditions Enterprise et Developer uniquement!), Vous pouvez activer la compression de lignes ou de pages sur les tables, les index et les indexés Vues. Pour les situations où une grande partie des données d'un champ NVARCHAR peut réellement tenir dans VARCHAR sans aucune perte de données, la compression permet de stocker les caractères qui tiennent dans VARCHAR comme 1 octet. Et seuls les caractères nécessitant 2 ou 4 octets occuperont cet espace. Cela devrait supprimer l'une des principales raisons pour lesquelles les gens choisissent souvent de s'en tenir à VARCHAR. Pour plus d'informations sur la compression, veuillez consulter la page MSDN pour Création de tables et d'index compressés . Veuillez noter que les données dans les types de données MAX qui sont stockées hors ligne ne sont pas compressibles.

Sujets de préoccupation réels

Les domaines suivants doivent être traités si vous souhaitez que ce tableau soit vraiment évolutif.

Problemo Numero Uno

... et en spécifiant une largeur de caractère basée sur l'utilisation réelle en production. Les données de production utilisent une plage de 2 caractères jusqu'à 900 caractères de la largeur réelle utilisée pour une colonne donnée. Nous allons ajouter un rembourrage de 10% le cas échéant.

Quoi? Avez-vous ajouté toutes ces valeurs? Étant donné le nombre de champs MAX dont vous disposez, il est possible qu'un ou plusieurs de ces champs aient 900 caractères, et même si cela équivaut à 1800 octets, la valeur stockée sur la page de données principale n'est que de 24 octets ( pas toujours 24 car la taille varie en fonction de plusieurs facteurs). Et cela pourrait être la raison pour laquelle il y a tant de champs MAX: ils ne pouvaient pas tenir dans une autre NVARCHAR(100) (prenant jusqu'à 200 octets), mais ils avaient de la place pour 24 octets.

Si l'objectif est d'améliorer les performances, la conversion des chaînes complètes en codes est, à certains niveaux, un pas dans la bonne direction. Vous réduisez considérablement la taille de chaque ligne, ce qui est plus efficace pour le pool de mémoire tampon et les E/S de disque. Et les chaînes plus courtes prennent moins de temps à comparer. C'est bien, mais pas génial.

Si l'objectif est d'améliorer considérablement les performances, la conversion en codes est incorrecte pas dans la bonne direction. Il repose toujours sur des analyses basées sur des chaînes (avec 30 index et 140 colonnes, il devrait y avoir beaucoup d'analyses, sauf si la plupart des champs ne sont pas utilisés pour le filtrage), et je suppose que ce sera le cas - dans les analyses sensibles à celle-ci, qui sont moins efficaces que si elles étaient sensibles à la casse ou binaires (c'est-à-dire en utilisant un classement sensible à la casse ou binaire).

De plus, la conversion en codes basés sur des chaînes ne permet finalement pas d'optimiser correctement un système transactionnel. Ces codes vont-ils être saisis dans un formulaire de recherche? Demander aux gens d'utiliser 'S' Pour [SubType] Est beaucoup moins significatif que de rechercher sur 'Single Family'.

Il existe un moyen de conserver votre texte descriptif complet tout en réduisant l'espace utilisé et en accélérant considérablement les requêtes: créez une table de recherche. Vous devriez avoir une table nommée [SubType] Qui stocke chacun des termes descriptifs distinctement et a un [SubTypeID] Pour chacun. Si les données font partie du système (c'est-à-dire un enum), le champ [SubTypeID] Ne doit pas être un IDENTITY champ car les données doivent être renseignées via un script de version. Si les valeurs sont entrées par les utilisateurs finaux, le champ [SubTypeID] doit être une IDENTITÉ. Dans les deux situations:

  • [SubTypeID] Est la clé primaire.
  • Utilisez très probablement INT pour [SubTypeID].
  • Si les données sont des données internes/système et que vous savez que le nombre maximum de valeurs distinctes sera toujours inférieur à 40 Ko, vous pourriez vous en sortir avec SMALLINT. Si vous commencez à numéroter à 1 (manuellement ou via la graine IDENTITY), vous obtenez un maximum de 32 768. Mais, si vous commencez à la valeur la plus basse, -32 768, vous obtenez les 65 535 valeurs complètes à utiliser.
  • Si vous utilisez Enterprise Edition, activez la compression de lignes ou de pages
  • Le champ de texte descriptif peut être appelé soit [SubType] (Identique au nom de la table), soit peut-être [SubTypeDescription]
  • UNIQUE INDEX Sur [SubTypeDescription]. Gardez à l'esprit que les index ont une taille maximale de 900 octets . Si la longueur maximale de ces données dans Production est de 900 caractères, et si vous avez besoin de NVARCHAR, cela pourrait fonctionner avec la compression activée , OR utilisez VARCHAR uniquement si vous n'avez absolument PAS besoin de stocker des caractères Unicode, ELSE applique l'unicité via un déclencheur AFTER INSERT, UPDATE.
  • La table [Listings] Contient le champ [SubTypeID].
  • Le champ [SubTypeID] Dans la table [Listings] Est Clé étrangère, faisant référence à [SubType].[SubTypeID].
  • Les requêtes peuvent désormais JOIN les tables [SubType] Et [Listings] Et effectuer une recherche sur le texte intégral de [SubTypeDescription] (Insensible à la casse, même, comme la fonctionnalité actuelle), en utilisant cet ID pour effectuer une recherche très efficace sur le champ FK indexé dans [Listings].

Cette approche peut (et devrait) être appliquée à d'autres champs de cette table (et à d'autres tables) qui se comportent de manière similaire.

Problemo Numero Dos

Une grande refonte de ce tableau qui se compose littéralement de 140 (nvarchar) colonnes, 11 étant MAX. Je supprime 30 index et les recrée ensuite.

S'il s'agit d'un système transactionnel et non d'un entrepôt de données, je dirais que (encore une fois, généralement), 140 colonnes sont trop pour être traitées efficacement. Je doute fortement que les 140 champs soient utilisés en même temps et/ou aient les mêmes cas d'utilisation. Le fait que 11 soient MAX est sans importance s'ils doivent contenir plus de 4000 caractères. MAIS, avoir 30 index sur une table transactionnelle est encore un peu compliqué (comme vous le voyez clairement).

Y a-t-il une raison technique pour laquelle la table doit avoir les 140 champs? Ces champs peuvent-ils être divisés en quelques petits groupes? Considérer ce qui suit:

  • Recherchez les champs "principaux" (les plus importants/les plus fréquemment utilisés) et placez-les dans la table "principale", nommée [Listing] (Je préfère conserver les mots au singulier pour que le champ ID puisse être simplement TableName + "ID").
  • La table [Listing] A ce PK: [ListingID] INT IDENTITY(1, 1) NOT NULL CONSTRAINT [PK_Listing] PRIMARY KEY
  • Les tables "secondaires" sont nommées comme [Listing{GroupName}] (par exemple [ListingPropertyAttribute] - "attributs" comme dans: NumberOfBedrooms, NumberOfBathrooms, etc.).
  • La table [ListingPropertyAttribute] A ce PK: [ListingID] INT NOT NULL CONSTRAINT [PK_ListingPropertyAttribute] PRIMARY KEY, CONSTRAINT [FK_ListingPropertyAttribute_Listing] FOREIGN KEY REFERENCES([Listing].[ListingID])
    • not no IDENTITY here
    • remarquez que le nom de PK est le même entre les tables "principales" et "secondaires"
    • remarquez PK et FK dans la table "core" est le même champ
  • La table "core" [Listing] obtient les champs [CreatedDate] et [LastModifiedDate]
  • Les tables "secondaires" n'obtiennent que le champ [LastModifiedDate]. L'hypothèse est que toutes les tables secondaires obtiennent leurs lignes remplies en même temps que la table "principale" (c'est-à-dire que toutes les lignes doivent toujours être représentées dans toutes les tables "secondaires"). Par conséquent, la valeur [CreatedDate] Dans la table "core" [Listing] Serait toujours la même dans toutes les tables "secondaires", ligne par ligne, donc pas besoin de la dupliquer dans la "secondaire" " les tables. Mais ils peuvent chacun être mis à jour à des moments différents.

Cette structure augmente le nombre de JOIN dont de nombreuses requêtes auront besoin, bien qu'une ou plusieurs vues puissent être créées pour encapsuler les JOIN les plus fréquemment utilisées, pour des raisons de codage. Mais du côté positif:

  • en ce qui concerne les instructions DML, il devrait y avoir beaucoup moins de conflits car la table "core" devrait obtenir la plupart des mises à jour.
  • la plupart des mises à jour prendront moins de temps car elles modifient des lignes plus petites.
  • la maintenance des index sur chacune des nouvelles tables (à la fois les tables "principales" et "secondaires") devrait être plus rapide, au moins sur une base par table.

Résumer

Le modèle actuel est conçu pour être inefficace et il semble atteindre cet objectif de conception (c'est-à-dire qu'il doit être lent). Si vous voulez que le système soit rapide, alors le modèle de données doit être conçu pour être efficace, et pas simplement moins inefficace.

9
Solomon Rutzky

Dans quelles situations varchar (max) est-il préféré

Les commentateurs ont déjà abordé ce point en détail. Je dirais que VARCHAR(MAX) est généralement logique si vous êtes sûr à 100% que la colonne n'aura jamais besoin de caractères non ASCII et que la longueur maximale de la colonne est inconnue ou supérieure à 8 000 caractères. Vous pouvez lire https://stackoverflow.com/questions/7141402/why-not-use-varcharmax pour une question similaire.

Nous avons des procédures de mise à jour qui expirent

Sur la base du plan d'exécution, je pense qu'un facteur majeur affectant les performances de votre mise à jour pourrait être que vous avez un index de texte intégral sur la colonne en cours de mise à jour et que vous utilisez CHANGE_TRACKING = AUTO pour cet index de texte intégral.

Le script au bas de cette réponse montre une simple déclaration de mise à jour sur un nombre modeste de lignes où les performances vont de 19 ms à plus de 500 ms simplement en ajoutant un tel index de texte intégral.

En fonction des besoins de votre recherche de texte intégral, vous pourrez peut-être créer l'index de texte intégral avec CHANGE_TRACKING = OFF (qui n'entraînera aucun de ces frais généraux) et exécutez périodiquement ALTER FULLTEXT INDEX...START FULL POPULATION ou START INCREMENTAL POPULATION afin de synchroniser les données de la table dans l'index de texte intégral. Voir l'article BOL ici: https://msdn.Microsoft.com/en-us/library/ms187317.aspx

-- Create test data on a new database
CREATE DATABASE TestFullTextUpdate
GO
USE TestFullTextUpdate
GO
CREATE TABLE dbo.fulltextUpdateTest (
    id INT NOT NULL IDENTITY(1,1)
        CONSTRAINT PK_fulltextUpdateTest PRIMARY KEY,
    object_counter_name NVARCHAR(512) NOT NULL
)
GO

--13660 row(s) affected
INSERT INTO dbo.fulltextUpdateTest (object_counter_name)
SELECT object_name + ': ' + counter_name
FROM sys.dm_os_performance_counters
CROSS JOIN (SELECT TOP 10 * FROM master..spt_values) x10
GO

-- ~19ms per update
DECLARE @startDate DATETIME2 = GETDATE()
SET NOCOUNT ON
UPDATE dbo.fulltextUpdateTest SET object_counter_name = object_counter_name
SET NOCOUNT OFF
DECLARE @endDate DATETIME2 = GETDATE()
PRINT(DATEDIFF(ms, @startDate, @endDate))
GO 10

-- Add a fulltext index with the default change-tracking behavior
CREATE FULLTEXT CATALOG DefaultFulltextCatalog AS DEFAULT AUTHORIZATION dbo
GO
CREATE FULLTEXT INDEX ON dbo.fulltextUpdateTest (object_counter_name)
KEY INDEX PK_fulltextUpdateTest
WITH CHANGE_TRACKING = AUTO
GO

-- ~522ms per update
-- Execution plan, like the plan in your question, shows that we must
-- touch the fulltext_index_docidstatus for each row that is updated
DECLARE @startDate DATETIME2 = GETDATE()
SET NOCOUNT ON
UPDATE dbo.fulltextUpdateTest SET object_counter_name = object_counter_name
SET NOCOUNT OFF
DECLARE @endDate DATETIME2 = GETDATE()
PRINT(DATEDIFF(ms, @startDate, @endDate))
GO 10

-- Cleanup
USE master
GO
DROP DATABASE TestFullTextUpdate
GO
4
Geoff Patterson