You can use something like:
SELECT
N.C.value('let $i := . return count(//xml_line[. << $i]) + 1', 'int') as LineNumber,
Item.Node.value('local-name(.)', 'varchar(max)') name,
Item.Node.value('.', 'varchar(max)') value
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
CROSS APPLY
N.C.nodes('*') Item(Node)
To get:
LineNumber |
name |
value |
1 |
col1 |
1 |
1 |
col2 |
foo 1 |
2 |
col1 |
2 |
2 |
col2 |
foo 2 |
See this db<>fiddle.
However, to spread columns horizontally, you will need to generate dynamic SQL after querying for distinct element names.
ADDENDUM: Here is an updated db<>fiddle that also shows a dynamic SQL example.
The above maps all values as VARCHAR(MAX)
. If you have NVARCHAR
data you can make the appropriate changes. If you have a need to map specific columns to specific types, you will need to explicitly define and populate a name-to-type mapping table and incorporate that into the dynamic SQL logic. The same may be necessary if you prefer that the result columns be in a specific order.
ADDENDUM 2: This updated db<>fiddle now includes column type and ordering logic.
--------------------------------------------------
-- Extract column names
--------------------------------------------------
DECLARE @Names TABLE (name VARCHAR(100))
INSERT @Names
SELECT DISTINCT Item.Node.value('local-name(.)', 'varchar(max)')
FROM table1
CROSS APPLY data_xml.nodes('//xml_line/*') Item(Node)
--SELECT * FROM @Names
--------------------------------------------------
-- Define column-to-type mapping
--------------------------------------------------
DECLARE @ColumnTypeMap TABLE ( ColumnName SYSNAME, ColumnType SYSNAME, ColumnOrder INT)
INSERT @ColumnTypeMap
VALUES
('col1', 'int', 1),
('col2', 'varchar(10)', 2)
DECLARE @ColumnTypeDefault SYSNAME = 'varchar(max)'
--------------------------------------------------
-- Define SQL Templates
--------------------------------------------------
DECLARE @SelectItemTemplate VARCHAR(MAX) =
' , N.C.value(<colpath>, <coltype>) <colname>
'
DECLARE @SqlTemplate VARCHAR(MAX) =
'SELECT
N.C.value(''let $i := . return count(//xml_line[. << $i]) + 1'', ''int'') as LineNumber
<SelectItems>
FROM
table1
CROSS APPLY
data_xml.nodes(''//xml_line'') N(C)
'
--------------------------------------------------
-- Expand SQL templates into SQL
--------------------------------------------------
DECLARE @SelectItems VARCHAR(MAX) = (
SELECT STRING_AGG(SI.SelectItem, '')
WITHIN GROUP(ORDER BY ISNULL(T.ColumnOrder, 999), N.Name)
FROM @Names N
LEFT JOIN @ColumnTypeMap T ON T.ColumnName = N.name
CROSS APPLY (
SELECT SelectItem = REPLACE(REPLACE(REPLACE(
@SelectItemTemplate
, '<colpath>', QUOTENAME(N.name + '[1]', ''''))
, '<colname>', QUOTENAME(N.name))
, '<coltype>', QUOTENAME(ISNULL(T.ColumnType, @ColumnTypeDefault), ''''))
) SI(SelectItem)
)
DECLARE @Sql VARCHAR(MAX) = REPLACE(@SqlTemplate, '<SelectItems>', @SelectItems)
--------------------------------------------------
-- Execute
--------------------------------------------------
SELECT DynamicSql = @Sql
EXEC (@Sql)
Result (with some additional data):
LineNumber |
col1 |
col2 |
bar |
foo |
1 |
1 |
foo 1 |
null |
More |
2 |
2 |
foo 2 |
Stuff |
null |