问题
I know how to join an XML variable to other tables, but in this case, I am trying to select each row from a table plus the structure of the XML from each respective table row, alongside that row. I cannot find any examples online to help with this, as most examples deal with a single XML value (apologies if there are, I was unable to locate them in amongst the myriad of other XML examples).
The table structure is this:
CREATE TABLE tbl_QuizHistory (
HistoryId int PRIMARY KEY,
QuizData xml NOT NULL
);
Each QuizData row value is similar to this:
<quizresult>
<question>
<questionText>Which fire extinguisher is most suitable for a waste paper basket fire?</questionText>
<answer number="0" value="0" chosen="0" imageURL="">Powder</answer>
<answer number="1" value="0" chosen="0" imageURL="">Carbon Dioxide (CO2)</answer>
<answer number="2" value="1" chosen="1" imageURL="">Water (H2O)</answer>
<answer number="3" value="0" chosen="0" imageURL="">Foam</answer>
<result>Correct</result>
</question>
<question>
<questionText>Should you use lifts during a fire?</questionText>
<answer number="0" value="0" chosen="0" imageURL="">Yes</answer>
<answer number="1" value="1" chosen="1" imageURL="">No</answer>
<result>Correct</result>
</question>
</quizresult>
In an earlier question I was shown how to display the XML data hierarchically (@xml ==> questions ==> answer(s)), but only for a single XML value, which I adapted to migrate the question/answer hierarchy into a table:
-- Works for a single XML value/variable...
;WITH q AS (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
n.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
n.q.value('(./result)[1]', 'nvarchar(50)') AS result,
n.q.query('answer') AS answers
FROM
@xml.nodes('/quizresult/question') AS n (q)
),
qa AS (
SELECT
qID,
questionText,
result,
answer.query('.') AS answer
FROM
q CROSS APPLY
answers.nodes('answer') AS a(answer)
)
SELECT
qa.qID,
q.questionText,
q.result,
qa.answer.value('answer[1]', 'nvarchar(max)') AS answer,
qa.answer.value('answer[1]/@number', 'int') AS number,
qa.answer.value('answer[1]/@value', 'int') AS val,
qa.answer.value('answer[1]/@chosen', 'bit') AS chosen
FROM
qa INNER JOIN
q ON qa.qID = q.qID;
How can this logic be applied to every XML value, in every table row? I need to display
- The quiz HistoryId
- Each question from that quiz (with optional ID for clarity, although this was generated by the SQL statement, and doesn't exist in the XML)
- All the answers for each question
The end result I am trying to achieve would produce something like this:
HistoryId qID questionText result answer number val chosen
--------- ---- --------------------------------------------------------------------------------------- ---------- ---------------------------------------------------------------------------------------- ------- ---- ------
100 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Powder 0 0 0
100 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Carbon Dioxide (CO2) 1 0 0
100 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Water (H2O) 2 1 1
100 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Foam 3 0 0
100 2 What should your immediate action be on hearing a fire alarm? Correct Find all of your colleagues before making a speedy exit together 0 0 0
100 2 What should your immediate action be on hearing a fire alarm? Correct Collect all your valuables before making a speedy exit 1 0 0
100 2 What should your immediate action be on hearing a fire alarm? Correct Check the weather to see if you need your coat before leaving 2 0 0
100 2 What should your immediate action be on hearing a fire alarm? Correct Leave the building by the nearest exit, closing doors behind you if the rooms are empty 3 1 1
101 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Powder 0 0 0
101 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Carbon Dioxide (CO2) 1 0 0
101 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Water (H2O) 2 1 1
101 1 Which fire extinguisher is most suitable for a waste paper basket fire? Correct Foam 3 0 0
101 2 Should you use lifts during a fire? Correct Yes 0 0 0
101 2 Should you use lifts during a fire? Correct No 1 1 1
101 3 Which part of a Carbon Dioxide (CO2) extinguisher should you not touch when operating? Incorrect The body of the extinguisher 0 0 1
101 3 Which part of a Carbon Dioxide (CO2) extinguisher should you not touch when operating? Incorrect The release trigger and the bottom of the extinguisher 1 0 0
101 3 Which part of a Carbon Dioxide (CO2) extinguisher should you not touch when operating? Incorrect The horn of the extinguisher 2 1 0
I appreciate that this creates a large number of duplication (as the questions are repeated for each answer), but that's okay.
I have a SQL Fiddle which I've been working from, with sample data set up.
回答1:
It can be a bit shorter with a series of 3 CROSS APPLY, level by level
SELECT HistoryId,
t.qID,
t.questionText,
t.result,
a.aId,
a.answerNbr,
a.answerChosen,
a.answerTxt
FROM
tbl_QuizHistory
CROSS APPLY QuizData.nodes('quizresult') AS n(q)
CROSS APPLY (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
t.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
t.q.value('(./result)[1]', 'nvarchar(50)') AS result,
t.q.query('.') queryXml
FROM
n.q.nodes('./question') t(q)
) t
CROSS APPLY (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS aID,
q.a.value('(./@number)[1]', 'int') as answerNbr,
q.a.value('(./@chosen)[1]', 'bit') as answerChosen,
q.a.value('.','nvarchar(max)') as answerTxt
FROM
t.queryXml.nodes('question/answer') q(a)
) a;
If no level specific calculations (e.g. row_number()) are need:
SELECT HistoryId,
t.qID,
t.questionText,
t.result,
q.a.value('(./@number)[1]', 'int') as answerNbr,
q.a.value('(./@chosen)[1]', 'bit') as answerChosen,
q.a.value('.','nvarchar(max)') as answerTxt
FROM
tbl_QuizHistory
CROSS APPLY QuizData.nodes('quizresult') AS n(q)
CROSS APPLY (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
t.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
t.q.value('(./result)[1]', 'nvarchar(50)') AS result,
t.q.query('.') queryXml
FROM n.q.nodes('./question') t(q)
) t
CROSS APPLY t.queryXml.nodes('question/answer') q(a)
Demo
回答2:
If I understand you correctly you want:
;WITH q AS (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
n.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
n.q.value('(./result)[1]', 'nvarchar(50)') AS result,
n.q.query('answer') AS answers
FROM tbl_QuizHistory t
CROSS APPLY t.QuizData.nodes('/quizresult/question') AS n (q)
),
qa AS (
SELECT
qID,
questionText,
result,
answer.query('.') AS answer
FROM q
CROSS APPLY answers.nodes('answer') AS a(answer)
)
SELECT
qa.qID,
q.questionText,
q.result,
qa.answer.value('answer[1]', 'nvarchar(max)') AS answer,
qa.answer.value('answer[1]/@number', 'int') AS number,
qa.answer.value('answer[1]/@value', 'int') AS val,
qa.answer.value('answer[1]/@chosen', 'bit') AS chosen
FROM qa
JOIN q ON qa.qID = q.qID;
Rextester Demo
Or even shorter:
;WITH q AS (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
n.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
n.q.value('(./result)[1]', 'nvarchar(50)') AS result,
n.q.query('answer') AS answers,
answer.query('.') AS answer
FROM tbl_QuizHistory t
CROSS APPLY t.QuizData.nodes('/quizresult/question') AS n (q)
CROSS APPLY n.q.nodes('answer') AS a(answer)
)
SELECT
q.qID,
q.questionText,
q.result,
answer.value('answer[1]', 'nvarchar(max)') AS answer,
answer.value('answer[1]/@number', 'int') AS number,
answer.value('answer[1]/@value', 'int') AS val,
answer.value('answer[1]/@chosen', 'bit') AS chosen
FROM q;
Rextester Demo 2
EDIT:
;WITH q AS (
SELECT
t.HistoryId,
ROW_NUMBER() OVER(PARTITION BY t.HistoryId ORDER BY(SELECT NULL)) AS qID,
n.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
n.q.value('(./result)[1]', 'nvarchar(50)') AS result,
n.q.query('answer') AS answers,
answer.query('.') AS answer
FROM tbl_QuizHistory t
CROSS APPLY t.QuizData.nodes('/quizresult/question') AS n (q)
CROSS APPLY n.q.nodes('answer') AS a(answer)
)
SELECT
q.HistoryId,
q.qID,
q.questionText,
q.result,
answer.value('answer[1]', 'nvarchar(max)') AS answer,
answer.value('answer[1]/@number', 'int') AS number,
answer.value('answer[1]/@value', 'int') AS val,
answer.value('answer[1]/@chosen', 'bit') AS chosen
FROM q;
回答3:
I think that the most straight-forward method to achieve it is to wrap your code that works for a given variable into a table-valued function. You can achieve the same result by in-lining your query, but with a complex code like yours it is much more readable if you use a function. Performance would stay the same, because it is an "inline" table-valued function, not a multi-statement function.
See for example, When would you use a table-valued function?
Function
CREATE FUNCTION [dbo].[GetQuizData]
(
@ParamQuizData xml
)
RETURNS TABLE
AS
RETURN
(
WITH q AS
(
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
n.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
n.q.value('(./result)[1]', 'nvarchar(50)') AS result,
n.q.query('answer') AS answers
FROM
@ParamQuizData.nodes('/quizresult/question') AS n (q)
),
qa AS
(
SELECT
qID,
questionText,
result,
answer.query('.') AS answer
FROM
q CROSS APPLY
answers.nodes('answer') AS a(answer)
)
SELECT
qa.qID,
q.questionText,
q.result,
qa.answer.value('answer[1]', 'nvarchar(max)') AS answer,
qa.answer.value('answer[1]/@number', 'int') AS number,
qa.answer.value('answer[1]/@value', 'int') AS val,
qa.answer.value('answer[1]/@chosen', 'bit') AS chosen
FROM
qa INNER JOIN
q ON qa.qID = q.qID
)
Main query
SELECT
tbl_QuizHistory.HistoryID
,Q.*
FROM
tbl_QuizHistory
CROSS APPLY [dbo].[GetQuizData](tbl_QuizHistory.QuizData) AS Q
;
See SQL Fiddle
Disclaimer: I didn't analyse your code from the question for correctness. I simply wrapped it into the function, assuming that it works as you need it to work.
You can inline the long query from the TVF into the CROSS APPLY manually. You'll have to inline CTE as well and it will look ugly. You can compare execution plans of this variant and variant with TVF. They should be the same.
Here is SQL Fiddle.
Inlined query
SELECT
tbl_QuizHistory.HistoryID
,CA.*
FROM
tbl_QuizHistory
CROSS APPLY
(
SELECT
qa.qID,
q.questionText,
q.result,
qa.answer.value('answer[1]', 'nvarchar(max)') AS answer,
qa.answer.value('answer[1]/@number', 'int') AS number,
qa.answer.value('answer[1]/@value', 'int') AS val,
qa.answer.value('answer[1]/@chosen', 'bit') AS chosen
FROM
(
SELECT
qID,
questionText,
result,
answer.query('.') AS answer
FROM
(
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
n.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
n.q.value('(./result)[1]', 'nvarchar(50)') AS result,
n.q.query('answer') AS answers
FROM
tbl_QuizHistory.QuizData.nodes('/quizresult/question') AS n (q)
) AS q0
CROSS APPLY
answers.nodes('answer') AS a(answer)
) AS qa
INNER JOIN
(
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS qID,
n.q.value('(./questionText)[1]', 'nvarchar(max)') AS questionText,
n.q.value('(./result)[1]', 'nvarchar(50)') AS result,
n.q.query('answer') AS answers
FROM
tbl_QuizHistory.QuizData.nodes('/quizresult/question') AS n (q)
) AS q
ON qa.qID = q.qID
) AS CA
;
This long query may be simplified, but I didn't analyse what it does and how it does it. I simply inlined the given working query.
来源:https://stackoverflow.com/questions/45938720/select-row-data-in-addition-to-structured-xml-data