web-dev-qa-db-fra.com

Manipulation d'une colonne contenant des paires clé / valeur

J'accède et crée des rapports d'un fournisseur via une base de données SQL Server répliquée. Ils ont fait des choses absolument folles pour lesquelles j'ai essayé de résoudre, mais celui-ci prend le gâteau.

Ils ont une table qui a de nombreuses colonnes standard. Mais ce tableau a également une colonne appelée "Données". La colonne est un type de données "texte" hérité, et elle contient une liste géante (des centaines) de paires clé/valeur. Chaque paire est séparée par un CRLF, et la clé et la valeur sont séparées par un signe égal. Exemple:

select myTable.[data] from myTable where tblKey = 123

Résultat:

Key 1=Value 1
Key 2=Value 2
Key 3=Value 3
...
Key 500=Value 500

J'essaie de déterminer le moyen le plus efficace de décomposer cette colonne en un tableau de données utilisable. L'objectif final serait de pouvoir interroger la table d'une manière qui renvoie la clé de la table avec les clés/valeurs spécifiées en tant que colonnes/champs en tant que tels:

tblKey | [Key 1] | [Key 3] | [Key 243]
-------|---------|---------|-----------
 123     Value 1   Value 3   Value 243
 124     Value 1   Value 3   Value 243
 125     Value 1   Value 3   Value 243

Existe-t-il un moyen de mouler cette colonne en vue? Je ne peux pas imaginer qu'une fonction serait particulièrement efficace, mais je suis sûr que je pourrais analyser les choses de cette façon en utilisant un string_split ou quelque chose de ce genre. Quelqu'un a-t-il déjà rencontré ce type d'atrocité et trouvé un bon moyen de le transformer en données utilisables?

Modifiez pour ajouter dbfiddle exemple de données.

Les données sont répliquées à partir de la source d'un fournisseur, donc je ne peux pas créer de nouvelles tables. Je peux créer des vues, des procédures et des fonctions. C'est ce que je cherche des conseils pour une façon décente d'accomplir.

6
Andy

[~ # ~] mise à jour [~ # ~]

Si, comme vous l'avez publié dans votre propre réponse, vous pouvez utiliser une FDU pour obtenir des valeurs de clé spécifiques, laissez-moi suggérer celle-ci: (Vous n'avez pas besoin de diviser toutes les clés/valeurs et vous n'avez pas besoin de relire le tableau , vous pouvez l'obtenir en utilisant les fonctions de texte.)

CREATE FUNCTION fnGetKey(@Data text, @Key varchar(20))
RETURNS varchar(100)
AS
BEGIN

  RETURN
  (
  SELECT 
      SUBSTRING (
                  @Data,
                  /* Position of first '=' after key + 1 */
                  CHARINDEX('=', @Data, PATINDEX('%' + @key + '%', @Data)) + 1,
                  /* Lenght, Position of first chr(13) after key less previuos value - 1 */
                  (CHARINDEX(CHAR(13), @Data, PATINDEX('%' + @key + '%', @Data)) 
                  - 
                  CHARINDEX('=', @Data, PATINDEX('%' + @key + '%', @Data))) - 1
                )
  )

END

SELECT
    FruitID, Name, Description,
    dbo.fnGetKey([Data], 'key 2') as [key 2],
    dbo.fnGetKey([Data], 'key 4') as [key 4]
FROM
    [Fruit];
 FruitID | Nom | Description | touche 2 | touche 4 
 ------: | : -- : ---------- | : ------ | : ------ 
 1 | Banane | Délicieux | valeur 2 | valeur 4 
 2 | Poire | Rotton | valeur 2 | valeur 4 
 3 | Kiwi | D'accord | valeur 2 | valeur 4 

db <> violon --- (ici

Réponse originale

La seule solution que je peux comprendre est de diviser les clés/valeurs, puis de les faire pivoter pour obtenir le résultat souhaité.

Malheureusement, il y a quelques inconvénients:

  • STRING_SPLIT ne fonctionne pas avec les colonnes text. Par conséquent, vous devez le convertir en varchar avant de pouvoir le manipuler.
  • STRING_SPLIT nécessite une nchar(1) ou nvarchar(1), ergo vous devez remplacer CHAR(3)+CHAR(10) par un seul caractère.
  • La fonction d'agrégation sur PIVOT fonctionne mieux avec les valeurs numériques, alors vous devez convertir Value en un certain type de données numériques.
  • PIVOT a besoin d'un nombre bien connu de colonnes, dans mon exemple, j'en ai utilisé quelques-unes, mais vous devez écrire la séquence entière, sauf si vous préférez traiter des requêtes dynamiques.

Voici ce que j'ai en utilisant vos exemples de données:

WITH KP AS
(
    SELECT FruitID, Name, Description, value as KPair
    FROM   Fruit
    CROSS APPLY STRING_SPLIT(REPLACE(CAST(Data AS varchar(max)), CHAR(13)+CHAR(10), ','), ',') /* STRING_SPLIT only allows nchar(1),  varchar(1) */
)
, KP1 AS
(
  SELECT
      FruitID,  
      SUBSTRING(KPair, 5, CHARINDEX('=', KPair) - 5) AS [Key],
      SUBSTRING(KPair, CHARINDEX('=', KPair) + 7, LEN(KPair) - CHARINDEX('=', KPair) - 6) AS [Value]
  FROM
      KP
)
SELECT [FruitID], [1],[2],[3],[4],[5]
FROM   KP1
PIVOT (MAX([Value]) FOR [Key] IN ([1],[2],[3],[4],[5])) AS PVT;

Premier CTE divisé tous les Key X=Value Y. Le second coupe cette valeur pour obtenir chaque [Clé] et [Valeur]. Et le PIVOT final compose le résultat final en colonnes.

 FruitID | 1 | 2 | 3 | 4 | 5 
 ------: | :  : - | : - | : - | : - 
 1 | 1 | 2 | 3 | 4 | 5 
 2 | 1 | 2 | 3 | 4 | 5 
 3 | 1 | 2 | 3 | 4 | 5 

db <> violon --- (ici

REMARQUE: je ne sais pas si je dois conserver [Clé 1] & [Valeur 1] ou il doit être converti en une colonne nommée [Clé] & [Valeur].

Une approche différente

Lorsque je travaille avec des bases de données tierces, j'ajoute généralement une nouvelle base de données, sur le même serveur/instance si possible, puis je l'utilise à mes propres fins, juste pour éviter les conflits avec les propriétaires de bases de données.

Dans ce cas, vous pouvez ajouter une nouvelle table et lancer périodiquement un processus pour la mettre à jour avec les nouvelles valeurs.

Vous pouvez utiliser un tableau avec toutes les colonnes:

CREATE TABLE [FruitKeys]
(
    [FruitID] int NOT NULL PRIMARY KEY,
    [V1]      int NULL,
    [V2]      int NULL,
    [V3]      int NULL,
    [V4]      int NULL,
    [V5]      int NULL
);

ou un tableau avec des paires clé/valeur et utilisez un pivot pour obtenir le résultat final:

CREATE TABLE [FruitKeys]
(
    [FruitID] int NOT NULL,
    [Key]     int NOT NULL,
    [Value]   int NOT NULL,
    CONSTRAINT [PK_FruitKeys] PRIMARY KEY ([FruitID], [Key])
);
5
McNets

Il me semble que les données source ne sont pas si éloignées du format JSON.

Vous pouvez le convertir assez directement puis utiliser OPENJSON pour produire une sortie relationnelle:

SELECT
    F.FruitID,
    F.[Name],
    OJ.[key 1],
    OJ.[key 2],
    OJ.[key 3],
    OJ.[key 4],
    OJ.[key 5],
    F.[Description]
FROM dbo.Fruit AS F
CROSS APPLY OPENJSON
(
    -- Convert source data to JSON format
    '{' + 
        CHAR(34) + 
        REPLACE
        (
            REPLACE
            (
                CONVERT(varchar(max), F.Data), 
                '=', CHAR(34) + ':' + CHAR(34)
            ), 
            CHAR(13) + CHAR(10), 
            CHAR(34) + ',' + CHAR(34)
        ) + 
        CHAR(34) + 
    '}'
) 
WITH
(
    [key 1] varchar(100),
    [key 2] varchar(100),
    [key 3] varchar(100),
    [key 4] varchar(100),
    [key 5] varchar(100)
) AS OJ;

Production:

 FruitID | Nom | touche 1 | touche 2 | touche 3 | touche 4 | touche 5 | Description 
 ------: | : -- : ------ | : ------ | : ------ | : ------ | : ------ | : ---------- 
 1 | Banane | valeur 1 | valeur 2 | valeur 3 | valeur 4 | valeur 5 | Délicieux 
 2 | Poire | valeur 1 | valeur 2 | valeur 3 | valeur 4 | valeur 5 | Rotton 
 3 | Kiwi | valeur 1 | valeur 2 | valeur 3 | valeur 4 | valeur 5 | D'accord       

--- (démo db <> violon

5
Paul White 9

McNets a fourni une approche raisonnable, mais le fractionnement pour chaque paire, bien qu'évidemment nécessaire, est un processus assez long. Avec plus de 500 paires clé/valeur pour chaque enregistrement de la table, je ne suis pas sûr que cela fonctionnera pour mes besoins. C'est probablement une approche décente s'il y a moins de paires et un petit nombre de lignes dans la table affectée.

Étant donné que je travaille avec des centaines de paires clé/valeur et également des milliers d'enregistrements dans la table elle-même, je pense à implémenter une fonction définie par l'utilisateur (ci-dessous) à utiliser selon les besoins dans les rapports et les requêtes où une clé/valeur spécifique une paire est nécessaire (et connue).

CREATE FUNCTION udfsv_GetFruitDataValue(
    @FruitID int, 
    @DataId varchar(100)
)
RETURNS varchar(100)
AS BEGIN
  DECLARE @DataVal varchar(100)

  set @DataVal = (
    select 
      replace(replace(split1, @DataId + '=', ''), char(13), '') as DataValue
    from Fruit
    left outer join (
      select
        FruitID,
           value as split1
      from Fruit 
      cross apply string_split(cast([data] as varchar(max)), char(10))
    ) line1 on line1.FruitID = Fruit.FruitID         
    where Fruit.FruitID = @FruitID
    and split1 like @DataId + '=%'
  )

  RETURN @DataVal
END

Avec cela, je serais en mesure d'effectuer des requêtes pour inclure des clés/valeurs spécifiées, mais pas toutes les clés/valeurs.

SELECT
  FruitID,
  Name,
  Description,
  udfsv_GetFruitDataValue(FruitID, 'Key 1') as [Key 1],
  udfsv_GetFruitDataValue(FruitID, 'Key 4') as [Key 4]
FROM
  Fruit
WHERE FruitID = 123
1
Andy