Search code examples
sqlsql-servert-sqlsqlxml

SQLXML without XML encoding?


I'm using a generic system for reporting which takes data from a database view (SQL Server 2005). In this view I had to merge data from one-to-many relations in one row and used the solution described by priyanka.sarkar in this thread: Combine multiple results in a subquery into a single comma-separated value. The solution uses SQLXML for merging the data (subquery):

SELECT STUFF(
    (    SELECT ', ' + Name 
         FROM MyTable _in 
         WHERE _in.ID = _out.ID 
         FOR XML PATH('')),        -- Output multiple rows as one xml type value,
                                   -- without xml tags
    1, 2, '')      -- STUFF: Replace the comma at the beginning with empty string
FROM MyTable _out 
GROUP BY ID        -- Removes duplicates

That works perfectly (it's not even that heavy in performance) except my data now gets XML encoded (& => & etc.) by SQLXML -I didn't want XML data after all, I just used this as a trick- and because of the generic system I can't code around this to clean it up so the encoded data goes straight to the report. I can't use stored procedures with the generic system so CURSOR-merging or COALESCE-ing is not an option here...

So what I'm looking for is a manner in T-SQL that lets me decode the XML again, or even better: avoids SQLXML from encoding it. Obviously I could write a stored function that does this, but I'd prefer a built-in, more safe manner...

Thanks for your help...


Solution

  • If you specify type as an option to for xml, you can use an XPath query to convert the XML type back to a varchar. With an example table variable:

    declare @MyTable table (id int, name varchar(50))
    
    insert @MyTable (id, name) select 1, 'Joel & Jeff'
    union all select 1, '<<BIN LADEN>>'
    union all select 2, '&&BUSH&&'
    

    One possible solution is:

    select  b.txt.query('root').value('.', 'varchar(max)')
    from    (
            select  distinct id
            from    @MyTable
            ) a
    cross apply
            (
                select  CASE ROW_NUMBER() OVER(ORDER BY id) WHEN 1 THEN '' 
                            ELSE ', ' END + name
            from    @MyTable
            where   id = a.id
            order by 
                    id
            for xml path(''), root('root'), type
            ) b(txt)
    

    This will print:

    Joel & Jeff, <<BIN LADEN>>
    &&BUSH&&
    

    Here's an alternative without XML conversions. It does have a recursive query, so performance mileage may vary. It's from Quassnoi's blog:

    ;WITH   with_stats(id, name, rn, cnt) AS
            (
            SELECT  id, name,
                    ROW_NUMBER() OVER (PARTITION BY id ORDER BY name),
                    COUNT(*) OVER (PARTITION BY id)
            FROM    @MyTable
            ),
            with_concat (id, name, gc, rn, cnt) AS
            (
            SELECT  id, name,
                    CAST(name AS VARCHAR(MAX)), rn, cnt
            FROM    with_stats
            WHERE   rn = 1
            UNION ALL
            SELECT  with_stats.id, with_stats.name,
                    CAST(with_concat.gc + ', ' + with_stats.name AS VARCHAR(MAX)),
                    with_stats.rn, with_stats.cnt
            FROM    with_concat
            JOIN    with_stats
            ON      with_stats.id = with_concat.id
                    AND with_stats.rn = with_concat.rn + 1
            )
    SELECT  id, gc
    FROM    with_concat
    WHERE   rn = cnt
    OPTION  (MAXRECURSION 0)