J'essaie d'exécuter un UNPIVOT
sur diverses colonnes contenues dans sys.databases
sur différentes versions de SQL Server, de 2005 à 2012.
UNPIVOT
échoue avec le message d'erreur suivant:
Msg 8167, niveau 16, état 1, ligne 48
Le type de colonne "CompatibilityLevel" entre en conflit avec le type des autres colonnes spécifiées dans la liste UNPIVOT.
Le T-SQL:
DECLARE @dbname SYSNAME;
SET @dbname = DB_NAME();
SELECT [Database] = unpvt.DatabaseName
, [Configuration Item] = unpvt.OptionName
, [Configuration Value] = unpvt.OptionValue
FROM (
SELECT
DatabaseName = name
, RecoveryModel = CONVERT(VARCHAR(50), d.recovery_model_desc)
, CompatibilityLevel = CONVERT(VARCHAR(50), CASE d.[compatibility_level] WHEN 70 THEN 'SQL Server 7' WHEN 80 THEN 'SQL Server 2000' WHEN 90 THEN 'SQL Server 2005' WHEN 100 THEN 'SQL Server 2008' WHEN 110 THEN 'SQL Server 2012' WHEN 120 THEN 'SQL Server 2014' ELSE 'UNKNOWN' END)
, AutoClose = CONVERT(VARCHAR(50), CASE d.is_auto_close_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoCreateStatistics = CONVERT(VARCHAR(50), CASE d.is_auto_create_stats_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoShrink = CONVERT(VARCHAR(50), CASE d.is_auto_shrink_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoUpdateStatistics = CONVERT(VARCHAR(50), CASE d.is_auto_update_stats_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoUpdateStatisticsAsynch = CONVERT(VARCHAR(50), CASE d.is_auto_update_stats_async_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, CloseCursorOnCommit = CONVERT(VARCHAR(50), CASE d.is_cursor_close_on_commit_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DefaultCursor = CONVERT(VARCHAR(50), CASE d.is_local_cursor_default WHEN 1 THEN 'LOCAL' ELSE 'GLOBAL' END)
, ANSINULL_Default = CONVERT(VARCHAR(50), CASE d.is_ansi_null_default_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSINULLS_Enabled = CONVERT(VARCHAR(50), CASE d.is_ansi_nulls_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSIPadding_Enabled = CONVERT(VARCHAR(50), CASE d.is_ansi_padding_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSIWarnings_Enabled = CONVERT(VARCHAR(50), CASE d.is_ansi_warnings_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ArithmeticAbort_Enabled = CONVERT(VARCHAR(50), CASE d.is_arithabort_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ConcatNullYieldsNull = CONVERT(VARCHAR(50), CASE d.is_concat_null_yields_null_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, CrossDBOwnerChain = CONVERT(VARCHAR(50), CASE d.is_db_chaining_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DateCorrelationOptimized = CONVERT(VARCHAR(50), CASE d.is_date_correlation_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, NumericRoundAbort = CONVERT(VARCHAR(50), CASE d.is_numeric_roundabort_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, [Parameterization] = CONVERT(VARCHAR(50), CASE d.is_parameterization_forced WHEN 0 THEN 'SIMPLE' ELSE 'FORCED' END)
, QuotedIdentifiers_Enabled = CONVERT(VARCHAR(50), CASE d.is_quoted_identifier_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, RecursiveTriggers_Enabled = CONVERT(VARCHAR(50), CASE d.is_recursive_triggers_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, [TrustWorthy] = CONVERT(VARCHAR(50), CASE d.is_trustworthy_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, VARDECIMAL_Storage = CONVERT(VARCHAR(50), 'TRUE')
, PageVerify = CONVERT(VARCHAR(50), page_verify_option_desc )
, BrokerEnabled = CONVERT(VARCHAR(50), CASE d.is_broker_enabled WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DatabaseReadOnly = CONVERT(VARCHAR(50), CASE d.is_read_only WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, EncryptionEnabled = CONVERT(VARCHAR(50), CASE d.is_encrypted WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, RestrictedAccess = CONVERT(VARCHAR(50), user_access_desc)
, Collation = CONVERT(VARCHAR(50), d.collation_name)
FROM sys.databases d
WHERE name = @dbname
OR @dbname IS NULL
) src
UNPIVOT
(
OptionValue FOR OptionName IN
(
RecoveryModel
, CompatibilityLevel
, AutoClose
, AutoCreateStatistics
, AutoShrink
, AutoUpdateStatistics
, AutoUpdateStatisticsAsynch
, CloseCursorOnCommit
, DefaultCursor
, ANSINULL_Default
, ANSINULLS_Enabled
, ANSIPadding_Enabled
, ANSIWarnings_Enabled
, ArithmeticAbort_Enabled
, ConcatNullYieldsNull
, CrossDBOwnerChain
, DateCorrelationOptimized
, NumericRoundAbort
, [Parameterization]
, QuotedIdentifiers_Enabled
, RecursiveTriggers_Enabled
, [TrustWorthy]
, VARDECIMAL_Storage
, PageVerify
, BrokerEnabled
, DatabaseReadOnly
, EncryptionEnabled
, RestrictedAccess
, Collation
)
) AS unpvt;
Ceci est conçu pour fournir une liste bien formatée d'options de base de données pour la base de données donnée, semblable à:
+----------+----------------------------+----------------------------+
| Database | Configuration Item | Value in Use |
+----------+----------------------------+----------------------------+
| master | RecoveryModel | SIMPLE |
| master | CompatibilityLevel | SQL Server 2008 |
| master | AutoClose | FALSE |
| master | AutoCreateStatistics | TRUE |
| master | AutoShrink | FALSE |
| master | AutoUpdateStatistics | TRUE |
| master | AutoUpdateStatisticsAsynch | FALSE |
| master | CloseCursorOnCommit | FALSE |
| master | DefaultCursor | GLOBAL |
| master | ANSINULL_Default | FALSE |
| master | ANSINULLS_Enabled | FALSE |
| master | ANSIPadding_Enabled | FALSE |
| master | ANSIWarnings_Enabled | FALSE |
| master | ArithmeticAbort_Enabled | FALSE |
| master | ConcatNullYieldsNull | FALSE |
| master | CrossDBOwnerChain | TRUE |
| master | DateCorrelationOptimized | FALSE |
| master | NumericRoundAbort | FALSE |
| master | Parameterization | SIMPLE |
| master | QuotedIdentifiers_Enabled | FALSE |
| master | RecursiveTriggers_Enabled | FALSE |
| master | TrustWorthy | TRUE |
| master | VARDECIMAL_Storage | TRUE |
| master | PageVerify | CHECKSUM |
| master | BrokerEnabled | FALSE |
| master | DatabaseReadOnly | FALSE |
| master | EncryptionEnabled | FALSE |
| master | RestrictedAccess | MULTI_USER |
| master | Collation | Latin1_General_CI_AS_KS_WS |
+----------+----------------------------+----------------------------+
Lorsque je l'exécute sur un serveur avec Latin1_General_CI_AS_KS_WS
collation, l'instruction réussit. Si je modifie le T-SQL pour que certains champs aient une clause COLLATE
, il fonctionnera sur des serveurs qui ont d'autres classements.
Le code qui fonctionne sur les serveurs avec des classements autres que Latin1_General_CI_AS_KS_WS
est:
DECLARE @dbname SYSNAME;
SET @dbname = DB_NAME();
SELECT [Database] = unpvt.DatabaseName
, [Configuration Item] = unpvt.OptionName
, [Configuration Value] = unpvt.OptionValue
FROM (
SELECT
DatabaseName = name
, RecoveryModel = CONVERT(VARCHAR(50), d.recovery_model_desc) COLLATE SQL_Latin1_General_CP1_CI_AS
, CompatibilityLevel = CONVERT(VARCHAR(50), CASE d.[compatibility_level] WHEN 70 THEN 'SQL Server 7' WHEN 80 THEN 'SQL Server 2000' WHEN 90 THEN 'SQL Server 2005' WHEN 100 THEN 'SQL Server 2008' WHEN 110 THEN 'SQL Server 2012' WHEN 120 THEN 'SQL Server 2014' ELSE 'UNKNOWN' END)
, AutoClose = CONVERT(VARCHAR(50), CASE d.is_auto_close_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoCreateStatistics = CONVERT(VARCHAR(50), CASE d.is_auto_create_stats_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoShrink = CONVERT(VARCHAR(50), CASE d.is_auto_shrink_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoUpdateStatistics = CONVERT(VARCHAR(50), CASE d.is_auto_update_stats_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoUpdateStatisticsAsynch = CONVERT(VARCHAR(50), CASE d.is_auto_update_stats_async_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, CloseCursorOnCommit = CONVERT(VARCHAR(50), CASE d.is_cursor_close_on_commit_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DefaultCursor = CONVERT(VARCHAR(50), CASE d.is_local_cursor_default WHEN 1 THEN 'LOCAL' ELSE 'GLOBAL' END)
, ANSINULL_Default = CONVERT(VARCHAR(50), CASE d.is_ansi_null_default_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSINULLS_Enabled = CONVERT(VARCHAR(50), CASE d.is_ansi_nulls_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSIPadding_Enabled = CONVERT(VARCHAR(50), CASE d.is_ansi_padding_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSIWarnings_Enabled = CONVERT(VARCHAR(50), CASE d.is_ansi_warnings_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ArithmeticAbort_Enabled = CONVERT(VARCHAR(50), CASE d.is_arithabort_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ConcatNullYieldsNull = CONVERT(VARCHAR(50), CASE d.is_concat_null_yields_null_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, CrossDBOwnerChain = CONVERT(VARCHAR(50), CASE d.is_db_chaining_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DateCorrelationOptimized = CONVERT(VARCHAR(50), CASE d.is_date_correlation_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, NumericRoundAbort = CONVERT(VARCHAR(50), CASE d.is_numeric_roundabort_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, [Parameterization] = CONVERT(VARCHAR(50), CASE d.is_parameterization_forced WHEN 0 THEN 'SIMPLE' ELSE 'FORCED' END)
, QuotedIdentifiers_Enabled = CONVERT(VARCHAR(50), CASE d.is_quoted_identifier_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, RecursiveTriggers_Enabled = CONVERT(VARCHAR(50), CASE d.is_recursive_triggers_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, [TrustWorthy] = CONVERT(VARCHAR(50), CASE d.is_trustworthy_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, VARDECIMAL_Storage = CONVERT(VARCHAR(50), 'TRUE')
, PageVerify = CONVERT(VARCHAR(50), page_verify_option_desc ) COLLATE SQL_Latin1_General_CP1_CI_AS
, BrokerEnabled = CONVERT(VARCHAR(50), CASE d.is_broker_enabled WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DatabaseReadOnly = CONVERT(VARCHAR(50), CASE d.is_read_only WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, EncryptionEnabled = CONVERT(VARCHAR(50), CASE d.is_encrypted WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, RestrictedAccess = CONVERT(VARCHAR(50), user_access_desc) COLLATE SQL_Latin1_General_CP1_CI_AS
, Collation = CONVERT(VARCHAR(50), d.collation_name)
FROM sys.databases d
WHERE name = @dbname
OR @dbname IS NULL
) src
UNPIVOT
(
OptionValue FOR OptionName IN
(
RecoveryModel
, CompatibilityLevel
, AutoClose
, AutoCreateStatistics
, AutoShrink
, AutoUpdateStatistics
, AutoUpdateStatisticsAsynch
, CloseCursorOnCommit
, DefaultCursor
, ANSINULL_Default
, ANSINULLS_Enabled
, ANSIPadding_Enabled
, ANSIWarnings_Enabled
, ArithmeticAbort_Enabled
, ConcatNullYieldsNull
, CrossDBOwnerChain
, DateCorrelationOptimized
, NumericRoundAbort
, [Parameterization]
, QuotedIdentifiers_Enabled
, RecursiveTriggers_Enabled
, [TrustWorthy]
, VARDECIMAL_Storage
, PageVerify
, BrokerEnabled
, DatabaseReadOnly
, EncryptionEnabled
, RestrictedAccess
, Collation
)
) AS unpvt;
Le comportement observé est que les champs suivants n'observent ni le classement du serveur, ni le classement de la base de données; ils sont toujours présentés dans Latin1_General_CI_AS_KS_WS
collation.
Sur SQL Server 2012, nous pouvons utiliser sys.sp_describe_first_result_set
pour obtenir facilement des métadonnées sur les colonnes renvoyées par une requête particulière. J'ai utilisé ce qui suit pour déterminer la non-concordance de classement:
DECLARE @cmd NVARCHAR(MAX);
SET @cmd = '
SELECT
DatabaseName = CONVERT(VARCHAR(50), d.name)
, RecoveryModel = CONVERT(VARCHAR(50), d.recovery_model_desc)
, Collation = CONVERT(VARCHAR(50), d.collation_name)
FROM sys.databases d
WHERE name = DB_NAME();
';
EXEC sp_describe_first_result_set @command = @cmd;
Les resultats:
Pourquoi le classement de ces colonnes est-il défini statiquement?
Le mot officiel de Microsoft:
Certaines des colonnes qui contiennent des chaînes prédéfinies (comme les types, les descriptions système et les constantes) sont toujours fixées sur un classement spécifique -
Latin1_General_CI_AS_KS_WS
. Ceci est indépendant du classement d'instance/base de données. La raison en est qu'il s'agit de métadonnées système (et non de métadonnées utilisateur) et, fondamentalement, ces chaînes sont traitées indépendamment de la casse (comme les mots clés, donc toujours en latin).
D'autres colonnes dans les tables système qui contiennent des métadonnées utilisateur comme les noms d'objets, les noms de colonnes, les noms d'index, les noms de connexion, etc. prennent le classement d'instance ou de base de données. Les colonnes sont regroupées pour un classement correct au moment de l'installation de SQL Server en cas de classement d'instance et au moment de la création de la base de données en cas de classement de la base de données.
Vous avez demandé (soulignement le mien):
Pourquoi le classement de ces colonnes est-il statiquement défini?
La raison pour laquelle certaines colonnes sont statiquement définies est que les requêtes n'ont pas à se soucier du classement du serveur ou de la base de données (plus important encore: CaSe SenSiTIviTy) pour fonctionner correctement. Cette requête fonctionnera toujours quel que soit le classement:
SELECT * FROM sys.databases WHERE state_desc = N'ONLine';
Alors que si le classement du serveur était sensible à la casse, la requête ci-dessus retournerait 0 lignes, tout comme ceci:
SELECT * FROM sys.databases
WHERE state_desc COLLATE Albanian_BIN = N'ONLine';
Par exemple, si vous installez une instance de SQL Server avec SQL_Estonian_CP1257_CS_AS
classement, puis exécutez ce qui suit:
SELECT name, collation_name
FROM master.sys.all_columns
WHERE collation_name IS NOT NULL
AND [object_id] = OBJECT_ID(N'sys.databases');
Vous verrez ces résultats (ou quelque chose de similaire, selon votre version de SQL Server):
name SQL_Estonian_CP1257_CS_AS
collation_name SQL_Estonian_CP1257_CS_AS
user_access_desc Latin1_General_CI_AS_KS_WS
state_desc Latin1_General_CI_AS_KS_WS
snapshot_isolation_state_desc Latin1_General_CI_AS_KS_WS
recovery_model_desc Latin1_General_CI_AS_KS_WS
page_verify_option_desc Latin1_General_CI_AS_KS_WS
log_reuse_wait_desc Latin1_General_CI_AS_KS_WS
default_language_name SQL_Estonian_CP1257_CS_AS
default_fulltext_language_name SQL_Estonian_CP1257_CS_AS
containment_desc Latin1_General_CI_AS_KS_WS
delayed_durability_desc SQL_Estonian_CP1257_CS_AS
Maintenant, pour illustrer les vues de métadonnées qui héritent du classement de la base de données, plutôt que d'hériter du classement du serveur de la base de données master:
CREATE DATABASE server_collation;
GO
CREATE DATABASE albanian COLLATE Albanian_BIN;
GO
CREATE DATABASE hungarian COLLATE Hungarian_Technical_100_CS_AI;
GO
SELECT name, collation_name
FROM server_collation.sys.all_columns
WHERE collation_name IS NOT NULL
AND object_id = -391; -- sys.columns
SELECT name, collation_name
FROM albanian.sys.all_columns
WHERE collation_name IS NOT NULL
AND object_id = -391; -- sys.columns
SELECT name, collation_name
FROM hungarian.sys.all_columns
WHERE collation_name IS NOT NULL
AND object_id = -391; -- sys.columns
Résultats:
server_collation
----------------
name SQL_Estonian_CP1257_CS_AS
collation_name SQL_Estonian_CP1257_CS_AS
generated_always_type_desc Latin1_General_CI_AS_KS_WS
encryption_type_desc Latin1_General_CI_AS_KS_WS
encryption_algorithm_name Latin1_General_CI_AS_KS_WS
column_encryption_key_database_name SQL_Estonian_CP1257_CS_AS
albanian
----------------
name Albanian_BIN
collation_name Albanian_BIN
generated_always_type_desc Latin1_General_CI_AS_KS_WS
encryption_type_desc Latin1_General_CI_AS_KS_WS
encryption_algorithm_name Latin1_General_CI_AS_KS_WS
column_encryption_key_database_name Albanian_BIN
hungarian
----------------
name Hungarian_Technical_100_CS_AI
collation_name Hungarian_Technical_100_CS_AI
generated_always_type_desc Latin1_General_CI_AS_KS_WS
encryption_type_desc Latin1_General_CI_AS_KS_WS
encryption_algorithm_name Latin1_General_CI_AS_KS_WS
column_encryption_key_database_name Hungarian_Technical_100_CS_AI
Ainsi, vous pouvez voir que dans ce cas, plusieurs colonnes héritent du classement de la base de données, tandis que d'autres sont fixées sur ce classement Latin1 "générique", ce qui signifie qu'il est utilisé pour isoler certains noms et propriétés des problèmes de respect de la casse comme décrit ci-dessus.
Si vous essayez d'effectuer un UNION
, par exemple:
SELECT name FROM albanian.sys.columns
UNION ALL
SELECT name FROM server_collation.sys.columns;
Vous obtenez cette erreur:
Msg 451, niveau 16, état 1
Impossible de résoudre le conflit de classement entre "Albanian_BIN" et "SQL_Estonian_CP1257_CS_AS" dans l'opérateur UNION ALL survenant dans la colonne 1 de l'instruction SELECT.
De même, si vous essayez d'effectuer un PIVOT
ou UNPIVOT
, les règles sont encore plus strictes (les types de sortie doivent tous correspondre exactement plutôt que d'être simplement compatible), mais le message d'erreur est beaucoup moins utile, voire trompeur:
Msg 8167, niveau 16, état 1
Le type de colonne "nom de colonne" est en conflit avec le type des autres colonnes spécifiées dans la liste UNPIVOT.
Vous devez contourner ces erreurs à l'aide de clauses COLLATE
explicites dans vos requêtes. Par exemple, l'union ci-dessus pourrait être:
SELECT name COLLATE Latin1_General_CI_AS_KS_WS
FROM albanian.sys.columns
UNION ALL
SELECT name COLLATE Latin1_General_CI_AS_KS_WS
FROM server_collation.sys.columns;
La seule fois où cela peut provoquer des problèmes est que la sortie sera confuse si un classement est forcé mais ne contient pas la même représentation de caractères, ou si le tri est utilisé et que le classement forcé utilise un ordre de tri différent de la source.
Le comportement que vous voyez en ce qui concerne le classement de divers champs dans les vues de catalogue système est le résultat de la façon dont chaque champ est défini et de la priorité du classement.
Lorsque vous regardez sys.databases
, Il est important de garder à l'esprit qu'il ne s'agit pas d'une table. Alors que dans le passé (je pense que cela se terminait à SQL Server 2000), il s'agissait du catalogue système tables, ils sont maintenant du catalogue système vues. Par conséquent, la source des informations qu'ils contiennent ne provient pas nécessairement du contexte actuel de la base de données (ou du contexte de la base de données spécifiée lorsqu'il s'agit d'un objet pleinement qualifié tel que master.sys.databases
).
En ce qui concerne spécifiquement sys.databases
, Certains des champs proviennent de la base de données [master]
(Qui a été créée avec un classement basé sur le classement par défaut de l'instance - c'est-à-dire le classement au niveau du serveur), certains des les champs sont des expressions (c'est-à-dire des instructions CASE
), et certains proviennent d'une source "cachée": la base de données [mssqlsystemresource]
. Et la base de données [mssqlsystemresource]
A un classement de: Latin1_General_CI_AS_KS_WS
.
Le champ name
provient du champ name
dans master.sys.sysdbreg
. Ce champ doit donc toujours être dans le classement de la base de données [master]
, Qui correspondra à nouveau au classement du serveur.
MAIS, les champs suivants dans sys.databases
Proviennent du champ [name]
Dans [mssqlsystemresource].[sys].[syspalvalues]
:
Ces champs doivent toujours avoir un classement de Latin1_General_CI_AS_KS_WS
.
Le champ collation_name
Provient cependant de l'expression suivante:
CONVERT(nvarchar(128),
CASE
WHEN serverproperty('EngineEdition')=5
AND [master].[sys].[sysdbreg].[id] as [d].[id]=(1)
THEN serverproperty('collation')
ELSE collationpropertyfromid(
CONVERT(int,
isnull([master].[sys].[sysobjvalues].[value] as [coll].[value],
CONVERT_IMPLICIT(sql_variant,DBPROP.[cid],0)
),
0),'name')
END,
0)
C'est là que Priorité de classement commence à entrer. Les deux options de sortie ici sont des fonctions système: serverproperty()
et collationpropertyfromid()
. Le classement de cette expression est considéré comme un "Coercible-default":
Toute variable, paramètre, littéral ou chaîne de caractères Transact-SQL ou la sortie d'une fonction intégrée de catalogue, ou une fonction intégrée qui ne prend pas les entrées de chaîne mais produit une sortie de chaîne.
Si l'objet est déclaré dans une fonction, une procédure stockée ou un déclencheur défini par l'utilisateur, l'objet est affecté au classement par défaut de la base de données dans laquelle la fonction, la procédure stockée ou le déclencheur est créé. Si l'objet est déclaré dans un lot, l'objet est affecté au classement par défaut de la base de données actuelle pour la connexion.
À la lumière de ce 2e paragraphe, étant donné que sys.databases
Est une vue qui existe dans la base de données master
, il prend le classement de la base de données master
(pas la base de données actuelle).
Le champ state_desc
Est également une expression:
CASE
WHEN serverproperty('EngineEdition')=5
AND [Expr1081]=(1)
THEN N'RESTORING'
ELSE
CASE
WHEN serverproperty('EngineEdition')=5
AND CONVERT(bit,
[master].[sys].[sysdbreg].[status] as [d].[status]&(128),
0)=(1)
THEN N'COPYING'
ELSE
CASE
WHEN serverproperty('EngineEdition')=5
AND CONVERT(bit,
[master].[sys].[sysdbreg].[status] as [d].[status]&(256),
0)=(1)
THEN N'SUSPECT'
ELSE [mssqlsystemresource].[sys].[syspalvalues].[name] as [st].[name]
END
END
END
Mais, le classement sur cette expression est Latin1_General_CI_AS_KS_WS
. Pourquoi? Eh bien, quelque chose de nouveau est introduit dans cette expression: une référence à un champ réel: [mssqlsystemresource].[sys].[syspalvalues].[name]
Dans cette dernière clause ELSE
. Les références de colonne sont considérées comme "implicites":
Une référence de colonne. Le classement de l'expression est tiré du classement défini pour la colonne dans la table ou la vue.
Bien sûr, cela ouvre une question intéressante: est-il possible que cette expression retourne un classement différent selon la façon dont le CASE
est évalué? Les littéraux seront dans le classement de la base de données où cet objet est défini, mais la condition ELSE
renvoie une valeur de champ qui doit conserver son classement d'origine. Heureusement, nous pouvons simuler un test en utilisant la fonction de gestion dynamique sys.dm_exec_describe_first_result_set :
-- Force ELSE condition
SELECT system_type_name, max_length, collation_name
FROM sys.dm_exec_describe_first_result_set(N'
DECLARE @A INT;
SET @A = -1;
SELECT CASE WHEN @A = 100 THEN N''All About the Benjamins''
ELSE [name]
END AS [Stuff]
FROM msdb.dbo.sysjobs
', NULL, NULL) rs
-- Force WHEN condition
SELECT system_type_name, max_length, collation_name
FROM sys.dm_exec_describe_first_result_set(N'
DECLARE @A INT;
SET @A = 100;
SELECT CASE WHEN @A = 100 THEN N''All About the Benjamins''
ELSE [name]
END AS [Stuff]
FROM msdb.dbo.sysjobs
', NULL, NULL) rs
-- Control test
SELECT system_type_name, max_length, collation_name
FROM sys.dm_exec_describe_first_result_set(N'
DECLARE @A INT;
SET @A = 100;
SELECT CASE WHEN @A = 100 THEN N''All About the Benjamins''
ELSE N''Whazzup, yo?!?!?''
END AS [Stuff]
', NULL, NULL) rs
Renvoie (sur une instance configurée avec un classement de SQL_Latin1_General_CP1_CI_AS
Mais fonctionnant dans une base de données avec un classement de Japanese_Unicode_CI_AS
):
system_type_name max_length collation_name
---------------- ---------- --------------
nvarchar(128) 256 SQL_Latin1_General_CP1_CI_AS
nvarchar(128) 256 SQL_Latin1_General_CP1_CI_AS
nvarchar(23) 46 Japanese_Unicode_CI_AS
Nous voyons ici que les deux requêtes qui référencent le champ dans [msdb]
Prennent le classement de la base de données [msdb]
(Qui, étant une base de données système, a été déterminée par le classement du serveur).
Le comportement observé est que les champs suivants n'observent ni le classement du serveur, ni le classement de la base de données; ils sont toujours présentés dans le classement
Latin1_General_CI_AS_KS_WS
.
Votre observation est précise: ces champs ont toujours un classement de Latin1_General_CI_AS_KS_WS
, Indépendamment du classement du serveur ou de la base de données. Et la raison en est: Priorité de classement. Ces champs proviennent d'une table de la base de données [mssqlsystemresource]
Et conserveront ce classement initial à moins qu'ils ne soient remplacés par une clause COLLATE
explicite, car celle-ci a la priorité la plus élevée:
Explicit = Expression qui est explicitement convertie en un classement spécifique à l'aide d'une clause COLLATE dans l'expression.
L'explicite prime sur l'implicite. Implicite a priorité sur Coercible-default:
Explicite> Implicite> Coercible par défaut
Et la question connexe:
Pourquoi le classement de ces colonnes est-il défini statiquement?
Ce n'est pas qu'ils soient statiquement définis, ni que les autres champs soient en quelque sorte dynamiques. Tous les champs de toutes ces vues de catalogue système fonctionnent selon les mêmes règles de priorité de classement. La raison pour laquelle ils semblent être plus "statiques" que les autres champs (c'est-à-dire qu'ils ne changent pas même si vous installez SQL Server avec un classement par défaut différent, qui à son tour crée les bases de données système avec ce classement par défaut) est que le [mssqlsystemresource]
La base de données a toujours un classement de Latin1_General_CI_AS_KS_WS
Sur toute installation de SQL Server (ou du moins cela apparaît certainement). Et cela a du sens car sinon il serait difficile pour SQL Server de se gérer en interne (c'est-à-dire si les règles de tri et de comparaison utilisées pour la logique interne changent en fonction de l'installation).
Si vous souhaitez voir la source de tout champ dans l'une de ces vues de catalogue système, procédez comme suit:
Exécutez une requête en sélectionnant un champ dans l'une des vues du catalogue système (je recommande de sélectionner un seul champ à la fois, car le plan d'exécution est ridiculement grand/complexe, même pour un seul champ, et inclura des références à de nombreux champs que vous n'êtes pas '' t sélection):
SELECT recovery_model_desc FROM sys.databases;
Execution plan.xml
Execution plan.xml
<OutputList>
(Elle doit être comprise entre les lignes 10 et 20 en général)<ColumnReference>
. Les attributs de cette balise doivent soit pointer vers un champ spécifique dans une table, soit pointer vers une expression définie plus loin dans le plan.Si les attributs pointent vers un champ réel, vous avez terminé car il contient toutes les informations. Voici ce qui apparaît pour le champ recovery_model_desc
:
<ColumnReference Database="[mssqlsystemresource]" Schema="[sys]"
Table="[syspalvalues]" Alias="[ro]" Column="name" />
Si les attributs pointent vers une expression, comme si vous avez plutôt sélectionné le champ state_desc
, Vous trouverez d'abord:
<ColumnReference Column="Expr1024" />
Dans ce cas, vous devez parcourir le reste du plan pour la définition de Expr1024
Ou tout autre élément qui en découle. Gardez juste à l'esprit qu'il pourrait y avoir plusieurs de ces références, mais la définition ne sera pas dans un bloc <OutputList>
. Cependant, il aura un élément frère <ScalarOperator>
Qui contient la définition. Voici ce qui apparaît pour le champ state_desc
:
<ScalarOperator ScalarString="CASE WHEN serverproperty('EngineEdition')=5 AND [Expr1081]=(1) THEN N'RESTORING' ELSE CASE WHEN serverproperty('EngineEdition')=5 AND CONVERT(bit,[master].[sys].[sysdbreg].[status] as [d].[status]&(128),0)=(1) THEN N'COPYING' ELSE CASE WHEN serverproperty('EngineEdition')=5 AND CONVERT(bit,[master].[sys].[sysdbreg].[status] as [d].[status]&(256),0)=(1) THEN N'SUSPECT' ELSE [mssqlsystemresource].[sys].[syspalvalues].[name] as [st].[name] END END END">
La même chose peut être effectuée pour vérifier la source des vues de catalogue au niveau de la base de données. Faire cela pour un objet comme sys.tables
Montrera que le champ name
vient de [current_db].[sys].[sysschobjs]
(C'est pourquoi il a un classement correspondant au classement de la base de données), tandis que le lock_escalation_desc
Provient de [mssqlsystemresource].[sys].[syspalvalues]
(C'est pourquoi il a un classement de Latin1_General_CI_AS_KS_WS
).
Maintenant que nous savons pourquoi est la priorité de classement et comment cela fonctionne, appliquons ces connaissances à la requête UNPIVOT.
Pour une opération UNPIVOT
, SQL Server semble vraiment préférer (ce qui signifie: nécessite) que chaque champ source soit du exactement le même type. Habituellement, "type" fait référence au type de base (c'est-à-dire VARCHAR
/NVARCHAR
/INT
/etc) mais ici SQL Server inclut également la COLLATION. Cela ne doit pas être considéré comme déraisonnable compte tenu de ce que les classements contrôlent: le jeu de caractères (c'est-à-dire la page de codes) pour VARCHAR, et les règles linguistiques qui déterminent l'équivalence des caractères et les combinaisons de caractères (c'est-à-dire la normalisation). Ce qui suit est un mimi-primer sur ce qu'est la "normalisation" Unicode:
PRINT '---';
IF (N'aa' COLLATE Danish_Greenlandic_100_CI_AI = N'å' COLLATE Danish_Greenlandic_100_CI_AI)
PRINT 'Danish_Greenlandic_100_CI_AI';
IF (N'aa' COLLATE SQL_Latin1_General_CP1_CI_AI = N'å' COLLATE SQL_Latin1_General_CP1_CI_AI)
PRINT 'SQL_Latin1_General_CP1_CI_AI';
PRINT '---';
IF (N'of' COLLATE Upper_Sorbian_100_CI_AI = N'öf' COLLATE Upper_Sorbian_100_CI_AI)
PRINT 'Upper_Sorbian_100_CI_AI';
IF (N'of' COLLATE German_PhoneBook_CI_AI = N'öf' COLLATE German_PhoneBook_CI_AI)
PRINT 'German_PhoneBook_CI_AI';
PRINT '---';
Retour:
---
Danish_Greenlandic_100_CI_AI
---
Upper_Sorbian_100_CI_AI
---
Alors maintenant, commençons votre requête d'origine. Nous ferons quelques tests pour voir comment divers changements modifient le résultat, puis nous verrons comment quelques changements peuvent le corriger.
La première erreur concerne le champ CompatibilityLevel
, qui est le deuxième champ à ne pas pivoter, et se trouve juste être une expression contenant tous les littéraux de chaîne. Sans référence de champ, le classement résultant est considéré comme un "défaut coercitif"). Les valeurs par défaut coercitives prennent en charge le classement de la base de données actuelle, disons SQL_Latin1_General_CP1_CI_AS
. Les 20 champs suivants sont également uniquement des littéraux de chaîne et sont donc également des valeurs par défaut coercitives, donc ils ne devraient pas être en conflit. Mais si nous regardons le premier champ, recovery_model_desc
, Qui provient directement d'un champ de sys.databases
, Ce qui en fait un classement "implicite", et cela ne fait pas prendre le classement de la base de données locale, mais conserve à la place son classement d'origine, qui est Latin1_General_CI_AS_KS_WS
(car il provient vraiment de la base de données [mssqlsystemresource]
).
Donc, si le champ 1 (RecoveryModel) est Latin1_General_CI_AS_KS_WS
, Et le champ 2 (CompatibilityLevel) est SQL_Latin1_General_CP1_CI_AS
, Alors nous devrions être en mesure de forcer le champ 2 à être Latin1_General_CI_AS_KS_WS
Pour correspondre au champ 1, puis l'erreur doit apparaître pour le champ 3 (fermeture automatique).
Ajoutez ce qui suit à la fin de la ligne CompatibilityLevel
:COLLATE Latin1_General_CI_AS_KS_WS
Et puis exécutez la requête. Effectivement, l'erreur indique maintenant que c'est le champ AutoClose
qui a le conflit.
Pour notre deuxième test, nous devons annuler la modification que nous venons d'apporter (c'est-à-dire supprimer la clause COLLATE
de la fin de la ligne CompatibilityLevel
.
Maintenant, si SQL Server évalue vraiment dans l'ordre dans lequel les champs sont spécifiés, nous devrions pouvoir supprimer le champ 1 (RecoveryModel), ce qui fera que le champ actuel 2 (CompatibilityLevel) sera le champ qui définit le classement principal de UNPIVOT résultant. Et le champ CompatibilityLevel
est un défaut coercitif qui prend le classement de la base de données, donc la première erreur doit être le champ PageVerify
, qui est une référence de champ, qui est un classement implicite conservant l'original classement, qui dans ce cas est Latin1_General_CI_AS_KS_WS
et qui n'est pas le classement du DB actuel.
Alors allez-y et commentez la ligne commençant par , RecoveryModel
Dans la SELECT
(vers le haut), puis commentez la ligne RecoveryModel
dans la clause UNPIVOT
ci-dessous et supprimez la virgule de début de la ligne suivante pour CompatibilityLevel
afin de ne pas obtenir d'erreur de syntaxe.
Exécutez cette requête. Effectivement, l'erreur indique maintenant que c'est le champ PageVerify
qui a le conflit.
Pour notre troisième test, nous devons annuler les modifications que nous venons de faire pour supprimer le champ RecoveryModel
. Alors allez-y et remettez la virgule, et décommentez ces deux autres lignes.
Maintenant, nous pouvons aller dans l'autre sens en forçant un classement. Plutôt que de changer le classement des champs de classement coercible par défaut (qui est la plupart d'entre eux), nous devrions pouvoir changer les champs de classement implicites en celui de la base de données actuelle, non?
Ainsi, tout comme notre premier test, nous devrions être en mesure de forcer le classement du champ 1 (RecoveryModel) avec une clause COLLATE
explicite. Mais si nous spécifions un classement particulier, puis exécutons la requête dans une base de données avec un classement différent, les champs de classement coercible par défaut prendront le nouveau classement qui entrera alors en conflit avec ce à quoi nous définissons ce premier champ. Cela semble être une douleur. Heureusement, il existe un moyen dynamique de résoudre ce problème. Il existe un pseudo classement appelé DATABASE_DEFAULT
Qui récupère le classement actuel des bases de données (comme le font les champs coercitifs par défaut).
Allez-y et ajoutez ce qui suit à la fin de la ligne, vers le haut, qui commence par , RecoveryModel
: COLLATE DATABASE_DEFAULT
Exécutez cette requête. Effectivement, l'erreur indique, encore une fois, que c'est le champ PageVerify
qui a le conflit.
Pour le test final, nous n'avons pas besoin d'annuler les modifications précédentes.
Tout ce que nous devons faire maintenant pour résoudre cette requête UNPIVOT
est d'ajouter le COLLATE DATABASE_DEFAULT
À la fin des champs de classement implicite restants: PageVerify
et RestrictedAccess
. Bien que le champ Collation
soit également un classement implicite, ce champ provient de la base de données master
, qui est généralement conforme à la base de données "actuelle". Mais, si vous voulez être sûr pour que cela fonctionne toujours, alors continuez d'ajouter le COLLATE DATABASE_DEFAULT
À la fin de ce champ.
Exécutez cette requête. Effectivement, pas d'erreurs. Tout ce qu'il a fallu pour corriger cette requête était d'ajouter COLLATE DATABASE_DEFAULT
À la fin de 3 champs (requis) et peut-être 1 de plus (facultatif).
Test facultatif: Maintenant que la requête UNPIVOT fonctionne correctement, modifiez une seule des définitions de champ commençant par CONVERT(VARCHAR(50),
pour qu'elle soit à la place 51
, Comme dans: CONVERT(VARCHAR(51),
.
Exécutez la requête. Vous devriez obtenir la même erreur The type of column "X" conflicts with the type of other columns specified in the UNPIVOT list.
Que vous avez obtenue alors que seul le classement ne correspondait pas.
Obtenir la même erreur pour les incompatibilités de type de données et de classement n'est pas suffisamment spécifique pour être vraiment utile. Il y a donc certainement place à amélioration :).
Remarque relative à la requête plus qu'à la question spécifique sur le classement:
Étant donné que tous les champs source sont du type de données NVARCHAR
, il serait plus sûr de CONVERT
tous les champs de sortie de NVARCHAR
au lieu de VARCHAR
. Il se peut que vous ne soyez pas en train de traiter des données contenant actuellement des caractères ASCII non standard, mais les métadonnées du système permettent leur conversion en NVARCHAR(128)
- qui est la plus grande longueur maximale de tout de ces champs - garantit au moins qu'il n'y aura pas de problème pour vous à l'avenir, ou pour toute autre personne copiant ce code qui pourrait déjà avoir certains de ces caractères dans leur système.
Il s'agit d'une solution de contournement pour le problème spécifique plutôt que d'une réponse complète à la question. Vous pouvez éviter l'erreur en convertissant en sql_variant
Plutôt qu'en varchar(50)
:
DECLARE @dbname SYSNAME;
SET @dbname = DB_NAME();
SELECT [Database] = unpvt.DatabaseName
, [Configuration Item] = unpvt.OptionName
, [Configuration Value] = unpvt.OptionValue
, [BaseType] = SQL_VARIANT_PROPERTY(unpvt.OptionValue, 'BaseType')
, [MaxLength] = SQL_VARIANT_PROPERTY(unpvt.OptionValue, 'MaxLength')
, [Collation] = SQL_VARIANT_PROPERTY(unpvt.OptionValue, 'Collation')
FROM (
SELECT
DatabaseName = name
, RecoveryModel = CONVERT(sql_variant, d.recovery_model_desc)
, CompatibilityLevel = CONVERT(sql_variant, CASE d.[compatibility_level] WHEN 70 THEN 'SQL Server 7' WHEN 80 THEN 'SQL Server 2000' WHEN 90 THEN 'SQL Server 2005' WHEN 100 THEN 'SQL Server 2008' WHEN 110 THEN 'SQL Server 2012' WHEN 120 THEN 'SQL Server 2014' ELSE 'UNKNOWN' END)
, AutoClose = CONVERT(sql_variant, CASE d.is_auto_close_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoCreateStatistics = CONVERT(sql_variant, CASE d.is_auto_create_stats_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoShrink = CONVERT(sql_variant, CASE d.is_auto_shrink_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoUpdateStatistics = CONVERT(sql_variant, CASE d.is_auto_update_stats_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, AutoUpdateStatisticsAsynch = CONVERT(sql_variant, CASE d.is_auto_update_stats_async_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, CloseCursorOnCommit = CONVERT(sql_variant, CASE d.is_cursor_close_on_commit_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DefaultCursor = CONVERT(sql_variant, CASE d.is_local_cursor_default WHEN 1 THEN 'LOCAL' ELSE 'GLOBAL' END)
, ANSINULL_Default = CONVERT(sql_variant, CASE d.is_ansi_null_default_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSINULLS_Enabled = CONVERT(sql_variant, CASE d.is_ansi_nulls_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSIPadding_Enabled = CONVERT(sql_variant, CASE d.is_ansi_padding_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ANSIWarnings_Enabled = CONVERT(sql_variant, CASE d.is_ansi_warnings_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ArithmeticAbort_Enabled = CONVERT(sql_variant, CASE d.is_arithabort_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, ConcatNullYieldsNull = CONVERT(sql_variant, CASE d.is_concat_null_yields_null_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, CrossDBOwnerChain = CONVERT(sql_variant, CASE d.is_db_chaining_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DateCorrelationOptimized = CONVERT(sql_variant, CASE d.is_date_correlation_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, NumericRoundAbort = CONVERT(sql_variant, CASE d.is_numeric_roundabort_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, [Parameterization] = CONVERT(sql_variant, CASE d.is_parameterization_forced WHEN 0 THEN 'SIMPLE' ELSE 'FORCED' END)
, QuotedIdentifiers_Enabled = CONVERT(sql_variant, CASE d.is_quoted_identifier_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, RecursiveTriggers_Enabled = CONVERT(sql_variant, CASE d.is_recursive_triggers_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, [TrustWorthy] = CONVERT(sql_variant, CASE d.is_trustworthy_on WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, VARDECIMAL_Storage = CONVERT(sql_variant, 'TRUE')
, PageVerify = CONVERT(sql_variant, page_verify_option_desc )
, BrokerEnabled = CONVERT(sql_variant, CASE d.is_broker_enabled WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, DatabaseReadOnly = CONVERT(sql_variant, CASE d.is_read_only WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, EncryptionEnabled = CONVERT(sql_variant, CASE d.is_encrypted WHEN 0 THEN 'FALSE' ELSE 'TRUE' END)
, RestrictedAccess = CONVERT(sql_variant, user_access_desc)
, Collation = CONVERT(sql_variant, d.collation_name)
FROM sys.databases d
WHERE name = @dbname
OR @dbname IS NULL
) src
UNPIVOT
(
OptionValue FOR OptionName IN
(
RecoveryModel
, CompatibilityLevel
, AutoClose
, AutoCreateStatistics
, AutoShrink
, AutoUpdateStatistics
, AutoUpdateStatisticsAsynch
, CloseCursorOnCommit
, DefaultCursor
, ANSINULL_Default
, ANSINULLS_Enabled
, ANSIPadding_Enabled
, ANSIWarnings_Enabled
, ArithmeticAbort_Enabled
, ConcatNullYieldsNull
, CrossDBOwnerChain
, DateCorrelationOptimized
, NumericRoundAbort
, [Parameterization]
, QuotedIdentifiers_Enabled
, RecursiveTriggers_Enabled
, [TrustWorthy]
, VARDECIMAL_Storage
, PageVerify
, BrokerEnabled
, DatabaseReadOnly
, EncryptionEnabled
, RestrictedAccess
, Collation
)
) AS unpvt;
J'ai ajouté trois colonnes pour plus d'informations sur le type sous-jacent de la colonne OptionValue
.
Si le client ne peut pas gérer les données sql_variant
, Effectuez une conversion finale (niveau supérieur) dans la colonne unpvt.OptionValue
, Par exemple nvarchar(256)
.
Ok, donc j'ai jeté un coup d'œil à
sp_helptext [sys.databases]
puis est tombé en panne d'où provenaient les colonnes. Ceux avec le Latin1_General_CI_AS_KS_WS
le classement provient tous de la table système sys.syspalvalues
qui semble être une table de recherche générique (c'est une table système, vous devrez donc vous connecter via le DAC pour la voir.).
Je suppose qu'il est défini sur Latin1_General_CI_AS_KS_WS
pour gérer toutes les valeurs de recherche possibles. Je peux voir à quel point ce serait ennuyeux.
Une autre façon de voir la définition (initialement fournie par Max dans un commentaire) est:
SELECT ObjectSchema = s.name
, ObjectName = o.name
, ObjectDefinition = sm.definition
FROM master.sys.all_sql_modules sm
INNER JOIN master.sys.system_objects o ON sm.object_id = o.object_id
INNER JOIN master.sys.schemas s ON o.schema_id = s.schema_id
WHERE s.name = 'sys'
AND o.name = 'databases';`