-2

I have a field in my database that contains strings, separated by comma:

select [MyField] from dbo.[MyDatabase];

MyField
-------
A, B, C
C, B, D, A
A, C, H, Z, J
K, A

What I need is to return these strings, but ordered alphabetically:

MyField
-------
A, B, C
A, B, C, D
A, C, H, J, Z
A, K

How can I achieve this in the same select query?

Pr0no
  • 3,910
  • 21
  • 74
  • 121
  • 1
    You really need to fix your design. Stop storing delimited data and you don't have a problem here. – Thom A Apr 05 '20 at 14:24
  • 1
    Looking at your previous questions, this isn't the first time you've been given this advice. Rather than ignoring it, you'll find things so much easier if you do fix the design. Yes, it might take a little bit, but solutions in the long run will be much easier, and your database will run that much better when it can use indexes, contains trusted foreign keys, etc etc. – Thom A Apr 05 '20 at 14:34
  • 1
    What have you tried and what specific problems did you encounter? "I have"/"I need" is not a suitable question for SO. – HABO Apr 05 '20 at 15:01
  • @Larnu I have no control over the database design, or the values it contains. The only option would be to re-create all tables myself in a database that I do have appropriate priviliges to, but then I run the risk on working on outdated data. – Pr0no Apr 05 '20 at 17:19
  • Then it time to talk to someone that does, @Pr0no . From the couple of questions I've seen it's heavily denormalised, with repeating columns and delimited data, it ***needs*** fixing, – Thom A Apr 05 '20 at 17:20

1 Answers1

1

As pointed out by Larnu in the comments, you really should fix your broken data model, and change that column into a table, with a one-to-many relationship to the original table.

Having said that, I realize that sometimes this simply can't be done, for a variety of reasons - and because of that, I can suggest a workaround in case you can't fix the database structure.

That workaround includes breaking that string of comma-delimited values to a table, and select from that table in an alphabetical order, concatenating the values from the different rows as you go.
Note: This will probably have poor performance, but it is a direct result of the broken data model.

First, create and populate sample table(Please save us this step in your future questions):

DECLARE @T AS TABLE (
    Col1 varchar(13)
);

INSERT INTO @T (Col1) VALUES
('A, B, C'),
('C, B, D, A'),
('A, C, H, Z, J'),
('K, A');

Then, use a UDF to break the comma delimited string into a table. I'm using Jeff Moden's splitter, but you can choose your own.

CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
        (@pString VARCHAR(8000), @pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE!  IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
     -- enough to cover VARCHAR(8000)
  WITH E1(N) AS (
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
                ),                          --10E+1 or 10 rows
       E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
       E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
 cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
                     -- for both a performance gain and prevention of accidental "overruns"
                 SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
                ),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
                 SELECT 1 UNION ALL
                 SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter
                ),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
                 SELECT s.N1,
                        ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1,8000)
                   FROM cteStart s
                )
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
 SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
        Item       = SUBSTRING(@pString, l.N1, l.L1)
   FROM cteLen l
;

Once you have a string splitter, use the stuff + for xml path technique to reconstruct the final string from the rows returned by the string splitter:

SELECT Col1, STUFF((
    SELECT ', '+ RTRIM(LTRIM(Item)) 
    FROM [dbo].[DelimitedSplit8K](Col1, ', ')   
    ORDER BY RTRIM(LTRIM(Item)) 
    FOR XML PATH('')
    ), 1, 2, '') As Ordered
FROM @T

Results:

Col1            Ordered
A, B, C         A, B, C
C, B, D, A      A, B, C, D
A, C, H, Z, J   A, C, H, J, Z
K, A            A, K

You can see a live demo on rextester.

Zohar Peled
  • 79,642
  • 10
  • 69
  • 121
  • Considering the OP is using SQL Server 2012, then they might has well make use the `DelimitedSplit8k_LEAD`. – Thom A Apr 05 '20 at 17:21
  • @Larnu good point, but what string splitter function to use is not the main point of this answer... – Zohar Peled Apr 05 '20 at 17:48
  • I agree, it's not, but you might as well make use of the more performant option for the answer if the OP can make use of it. :) – Thom A Apr 05 '20 at 18:01