web-dev-qa-db-fra.com

Comment interroger des valeurs et des attributs XML à partir d'une table dans SQL Server?

J'ai une table qui contient une colonne Xml:

SELECT * 
FROM Sqm

enter image description here

Un échantillon des données xml d'une ligne serait:

<Sqm version="1.2">
  <Metrics>
    <Metric id="TransactionCleanupThread.RecordUsedTransactionShift" type="timer" unit="µs" count="1" sum="21490"   average="21490"   minValue="73701"    maxValue="73701"                               >73701</Metric>
    <Metric id="TransactionCleanupThread.RefundOldTrans"             type="timer" unit="µs" count="1" sum="184487"  average="184487"  minValue="632704"   maxValue="632704"                              >632704</Metric>
    <Metric id="Database.CreateConnection_SaveContextUserGUID"       type="timer" unit="µs" count="2" sum="7562"    average="3781"    minValue="12928"    maxValue="13006"    standardDeviation="16"     >12967</Metric>
    <Metric id="Global.CurrentUser"                                  type="timer" unit="µs" count="6" sum="4022464" average="670411"  minValue="15"       maxValue="13794345" standardDeviation="1642047">2299194</Metric>
    <Metric id="Global.CurrentUser_FetchIdentityFromDatabase"        type="timer" unit="µs" count="1" sum="4010057" average="4010057" minValue="13752614" maxValue="13752614"                            >13752614</Metric>
  </Metrics>
</Sqm>

Dans le cas de ces données, je voudrais:

SqmId  id                                                   type   unit  count  sum      minValue  maxValue  standardDeviation  Value
=====  ===================================================  =====  ====  =====  ======   ========  ========  =================  ======
1      TransactionCleanupThread.RecordUsedTransactionShift  timer  µs    1      21490    73701     73701     NULL               73701
1      TransactionCleanupThread.RefundOldTrans              timer  µs    1      184487   632704    632704    NULL               632704
1      Database.CreateConnection_SaveContextUserGUID        timer  µs    2      7562     12928     13006     16                 12967
1      Global.CurrentUser                                   timer  µs    6      4022464  15        13794345  1642047            2299194
1      Global.CurrentUser_FetchIdentityFromDatabase         timer  µs    1      4010057  13752614  13752614  NULL               13752614
2      ...

En fin de compte, je vais effectuer une agrégation de SUM(), MIN(), MAX(). Mais pour l'instant, j'essaie simplement de interroger une colonne xml.

En pseudo-code, j'essaierais quelque chose comme:

SELECT
    SqmId,
    Data.query('/Sqm/Metrics/Metric/@id') AS id,
    Data.query('/Sqm/Metrics/Metric/@type') AS type,
    Data.query('/Sqm/Metrics/Metric/@unit') AS unit,
    Data.query('/Sqm/Metrics/Metric/@sum') AS sum,
    Data.query('/Sqm/Metrics/Metric/@count') AS count,
    Data.query('/Sqm/Metrics/Metric/@minValue') AS minValue,
    Data.query('/Sqm/Metrics/Metric/@maxValue') AS maxValue,
    Data.query('/Sqm/Metrics/Metric/@standardDeviation') AS standardDeviation,
    Data.query('/Sqm/Metrics/Metric') AS value
FROM Sqm

Mais cette requête SQL ne fonctionne pas:

Msg 2396, Niveau 16, Etat 1, Ligne 2
XQuery [Sqm.data.query ()]: l'attribut peut ne pas apparaître en dehors d'un élément

J'ai chassé, et il est étonnant de constater à quel point l'interrogation XML est mal documentée ou analysée. La plupart des ressources plutôt que d'interroger une table , interrogent une variable ; ce que je ne fais pas. La plupart des ressources utilisent uniquement des requêtes XML pour le filtrage et la sélection, plutôt que la lecture de valeurs. La plupart des ressources lisent les nœuds enfants codés en dur (par index), plutôt que les valeurs réelles.

Ressources associées que j'ai lues

Mise à jour: .value plutôt que .query

J'ai essayé d'utiliser au hasard .value à la place de .query:

SELECT
    Sqm.SqmId,
    Data.value('/Sqm/Metrics/Metric/@id', 'varchar(max)') AS id,
    Data.value('/Sqm/Metrics/Metric/@type', 'varchar(max)') AS type,
    Data.value('/Sqm/Metrics/Metric/@unit', 'varchar(max)') AS unit,
    Data.value('/Sqm/Metrics/Metric/@sum', 'varchar(max)') AS sum,
    Data.value('/Sqm/Metrics/Metric/@count', 'varchar(max)') AS count,
    Data.value('/Sqm/Metrics/Metric/@minValue', 'varchar(max)') AS minValue,
    Data.value('/Sqm/Metrics/Metric/@maxValue', 'varchar(max)') AS maxValue,
    Data.value('/Sqm/Metrics/Metric/@standardDeviation', 'varchar(max)') AS standardDeviation,
    Data.value('/Sqm/Metrics/Metric', 'varchar(max)') AS value
FROM Sqm

Mais cela ne fonctionne pas non plus:

Msg 2389, niveau 16, état 1, ligne 3 XQuery [Sqm.data.value ()]:
'valeur ()' nécessite un singleton (ou une séquence vide), opérande trouvé de type 'xdt: untypedAtomic *'

70
Ian Boyd

En fait, vous êtes proche de votre objectif, il vous suffit d'utiliser la méthode nodes () pour scinder vos lignes, puis obtenir les valeurs suivantes:

select
    s.SqmId,
    m.c.value('@id', 'varchar(max)') as id,
    m.c.value('@type', 'varchar(max)') as type,
    m.c.value('@unit', 'varchar(max)') as unit,
    m.c.value('@sum', 'varchar(max)') as [sum],
    m.c.value('@count', 'varchar(max)') as [count],
    m.c.value('@minValue', 'varchar(max)') as minValue,
    m.c.value('@maxValue', 'varchar(max)') as maxValue,
    m.c.value('.', 'nvarchar(max)') as Value,
    m.c.value('(text())[1]', 'nvarchar(max)') as Value2
from sqm as s
    outer apply s.data.nodes('Sqm/Metrics/Metric') as m(c)

sql fiddle demo

93
Roman Pekar

utilisez value au lieu de query (vous devez spécifier l'index du noeud à renvoyer dans XQuery et transmettre le type de données SQL à renvoyer comme second paramètre):

select
    xt.Id
    , x.m.value( '@id[1]', 'varchar(max)' ) MetricId
from
    XmlTest xt
    cross apply xt.XmlData.nodes( '/Sqm/Metrics/Metric' ) x(m)
9
Moho

J'ai essayé de faire quelque chose de très similaire mais sans utiliser les nœuds. Cependant, ma structure XML est un peu différente.

Vous l'avez comme ça:

<Metrics>
    <Metric id="TransactionCleanupThread.RefundOldTrans" type="timer" ...>

Si c'était comme ça à la place:

<Metrics>
    <Metric>
        <id>TransactionCleanupThread.RefundOldTrans</id>
        <type>timer</type>
        .
        .
        .

Ensuite, vous pouvez simplement utiliser cette instruction SQL.

SELECT
    Sqm.SqmId,
    Data.value('/Sqm/Metrics/Metric/id)[1]', 'varchar(max)') as id,
    Data.value('/Sqm/Metrics/Metric/type)[1]', 'varchar(max)') AS type,
    Data.value('/Sqm/Metrics/Metric/unit)[1]', 'varchar(max)') AS unit,
    Data.value('/Sqm/Metrics/Metric/sum)[1]', 'varchar(max)') AS sum,
    Data.value('/Sqm/Metrics/Metric/count)[1]', 'varchar(max)') AS count,
    Data.value('/Sqm/Metrics/Metric/minValue)[1]', 'varchar(max)') AS minValue,
    Data.value('/Sqm/Metrics/Metric/maxValue)[1]', 'varchar(max)') AS maxValue,
    Data.value('/Sqm/Metrics/Metric/stdDeviation)[1]', 'varchar(max)') AS stdDeviation,
FROM Sqm

Pour moi, cela est beaucoup moins déroutant que d'utiliser l'application externe ou l'application croisée.

J'espère que cela aidera quelqu'un d'autre à la recherche d'une solution plus simple!

7
Ryan Dorendorf

Je ne comprends pas pourquoi certaines personnes suggèrent d'utiliser cross apply ou outer apply pour convertir le xml en une table de valeurs. Pour moi, cela a ramené beaucoup trop de données.

Voici mon exemple de création d'un objet xml, puis de le transformer en tableau.

(J'ai ajouté des espaces dans ma chaîne xml, juste pour le rendre plus facile à lire.)

DECLARE @str nvarchar(2000)

SET @str = ''
SET @str = @str + '<users>'
SET @str = @str + '  <user>'
SET @str = @str + '     <firstName>Mike</firstName>'
SET @str = @str + '     <lastName>Gledhill</lastName>'
SET @str = @str + '     <age>31</age>'
SET @str = @str + '  </user>'
SET @str = @str + '  <user>'
SET @str = @str + '     <firstName>Mark</firstName>'
SET @str = @str + '     <lastName>Stevens</lastName>'
SET @str = @str + '     <age>42</age>'
SET @str = @str + '  </user>'
SET @str = @str + '  <user>'
SET @str = @str + '     <firstName>Sarah</firstName>'
SET @str = @str + '     <lastName>Brown</lastName>'
SET @str = @str + '     <age>23</age>'
SET @str = @str + '  </user>'
SET @str = @str + '</users>'

DECLARE @xml xml
SELECT @xml = CAST(CAST(@str AS VARBINARY(MAX)) AS XML) 

--  Iterate through each of the "users\user" records in our XML
SELECT 
    x.Rec.query('./firstName').value('.', 'nvarchar(2000)') AS 'FirstName',
    x.Rec.query('./lastName').value('.', 'nvarchar(2000)') AS 'LastName',
    x.Rec.query('./age').value('.', 'int') AS 'Age'
FROM @xml.nodes('/users/user') as x(Rec)

Et voici la sortie:

enter image description here

6
Mike Gledhill