2

I'm using SQL Server and I'm trying to find results but I would like to get the results in the same order as I had input the conditions.

My code:

SELECT 
    AccountNumber, EndDate
FROM 
    Accounts
WHERE 
    AccountNumber IN (212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689)   -- I would like the results to be in the same order as these numbers.
halfer
  • 19,824
  • 17
  • 99
  • 186
user1777929
  • 777
  • 1
  • 10
  • 28
  • 1
    https://stackoverflow.com/a/19209883/61305 - `SELECT a.AccountNumber, a.EndDate FROM dbo.Accounts AS a INNER JOIN dbo.SplitStrings_Ordered('212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689') AS s ORDER BY s.[Index];` – Aaron Bertrand Jun 25 '18 at 19:39
  • 2
    I'm not sure this is something that is possible without creating some kind of horrible string / XML conversion mess. An IN statement is essentially a "WHERE column = 'value1' OR column = 'value2') - sorting the results is completely up to SQL server unless you specify a sort order. Taking you back a step - is there something driving the order of that list? Or is it user input? – fritterfatboy Jun 25 '18 at 19:44

6 Answers6

4

Here is an in-line approach

Example

Declare @List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689'

Select A.AccountNumber 
      ,A.EndDate
 From  Accounts A
 Join (
        Select RetSeq = Row_Number() over (Order By (Select null))
              ,RetVal = v.value('(./text())[1]', 'int')
        From  (values (convert(xml,'<x>' + replace(@List,',','</x><x>')+'</x>'))) x(n)
        Cross Apply n.nodes('x') node(v)
      ) B on A.AccountNumber = B.RetVal
 Order By B.RetSeq

EDIT - the subquery Returns

RetSeq  RetVal
1       212345
2       312345
3       145687
4       658975
5       256987
6       365874
7       568974
8       124578
9       125689
John Cappelletti
  • 79,615
  • 7
  • 44
  • 66
  • John, I just posted one more answer into this thread with a new approach I've just stumbled over using `FROM OPENJSON`. Might be useful... – Shnugo Jul 18 '18 at 11:54
3

You can replace IN with a JOIN, and set a field for ordering, like this:

SELECT AccountNumber , EndDate
FROM Accounts a
JOIN (
    SELECT 212345 AS Number, 1 AS SeqOrder
UNION ALL
    SELECT 312345 AS Number, 2 AS SeqOrder
UNION ALL
    SELECT 145687 AS Number, 3 AS SeqOrder
UNION ALL
    ... -- and so on
) AS inlist ON inlist.Number = a.AccountNumber
ORDER BY inlist.SeqOrder
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
3

I will offer one more approach I just found out, but this needs v2016. Regrettfully the developers forgot to include the index into the resultset of STRING_SPLIT(), but this would work and is documented:

A solution via FROM OPENJSON():

DECLARE @str VARCHAR(100) = 'val1,val2,val3';

SELECT *
FROM OPENJSON('["' +  REPLACE(@str,',','","') + '"]');

The result

key value   type
0   val1    1
1   val2    1
2   val3    1

The documentation tells clearly:

When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.

Shnugo
  • 66,100
  • 9
  • 53
  • 114
  • Very clean and elegant. Should be the new standard on 2016+ Like you I never understood why they omitted the sequence in split_string(). Have you performed any stress tests / benchmarks. – John Cappelletti Jul 18 '18 at 14:01
  • @JohnCappelletti, yes, very promising... https://stackoverflow.com/a/51401153/5089204 – Shnugo Jul 18 '18 at 14:02
  • @JohnCappelletti I just added a comprehensive test scenario there. – Shnugo Jul 18 '18 at 20:21
1

This is not an answer, just some test-code to check John Cappelletti's approach.

DECLARE @tbl TABLE(ID INT IDENTITY,SomeGuid UNIQUEIDENTIFIER);


--Create more than 6 mio rows with an running number and a changing Guid
WITH tally AS (SELECT ROW_NUMBER()OVER(ORDER BY (SELECT NULL)) AS Nmbr 
               FROM master..spt_values v1 
               CROSS JOIN master..spt_values v2)
INSERT INTO @tbl 
SELECT NEWID() from tally;

SELECT COUNT(*) FROM @tbl; --6.325.225 on my machine

--Create an XML with nothing more than a list of GUIDs in the order of the table's ID
DECLARE @xml XML=
(SELECT SomeGuid FRom @tbl ORDER BY ID FOR XML PATH(''),ROOT('root'),TYPE);

--Create one invalid entry
UPDATE @tbl SET SomeGuid = NEWID() WHERE ID=10000;

--Read all GUIDs out of the XML and number them
DECLARE @tbl2 TABLE(Position INT,TheGuid UNIQUEIDENTIFIER);
INSERT INTO @tbl2(Position,TheGuid)
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
      ,g.value(N'text()[1]',N'uniqueidentifier')
FROM @xml.nodes(N'/root/SomeGuid') AS A(g);

--then JOIN them via "Position" and check, 
--if there are rows, where not the same values get into the same row.
SELECT *
FROM @tbl t
INNER JOIN @tbl2 t2 ON t2.Position=t.ID
WHERE t.SomeGuid<>t2.TheGuid;

At least in this simple case I always get exactly only the one record back which was invalidated...

Shnugo
  • 66,100
  • 9
  • 53
  • 114
1

Okay, after some re-thinking I'll offer the ultimative XML based type-safe and sort-safe splitter:

Declare @List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689';
DECLARE @delimiter VARCHAR(10)=', ';

WITH Casted AS
(
    SELECT (LEN(@List)-LEN(REPLACE(@List,@delimiter,'')))/LEN(REPLACE(@delimiter,' ','.')) + 1 AS ElementCount
           ,CAST('<x>' + REPLACE((SELECT @List AS [*] FOR XML PATH('')),@delimiter,'</x><x>')+'</x>' AS XML) AS ListXml
)
,Tally(Nmbr) As
(
    SELECT TOP((SELECT ElementCount FROM Casted)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values v1 CROSS JOIN master..spt_values v2
)
SELECT Tally.Nmbr AS Position
      ,(SELECT ListXml.value('(/x[sql:column("Tally.Nmbr")])[1]','int') FROM Casted) AS Item 
FROM Tally;

The trick is to create a list of running numbers with the fitting number of element (a number's table was even better) and to pick the elements according to their position.

Hint: This is rather slow...

UPDATE: even better:

WITH Casted AS
(
    SELECT (LEN(@List)-LEN(REPLACE(@List,@delimiter,'')))/LEN(REPLACE(@delimiter,' ','.')) + 1 AS ElementCount
           ,CAST('<x>' + REPLACE((SELECT @List AS [*] FOR XML PATH('')),@delimiter,'</x><x>')+'</x>' AS XML)
           .query('
                   for $x in /x
                   return <x p="{count(/x[. << $x])}">{$x/text()[1]}</x>
                  ') AS ListXml
)
SELECT x.value('@p','int') AS Position
      ,x.value('text()[1]','int') AS Item 
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);

Elements are create as

<x p="99">TheValue</x>

Regrettfully the XQuery function position() is not available to retrieve the value. But you can use the trick to count all elements before a given node. this is scaling badly, as this count must be performed over and over. The more elements the worse it goes...

UPDATE2: With a known count of elements one might use this (much better performance)

Use XQuery to iterate a literally given list:

WITH Casted AS
(
    SELECT (LEN(@List)-LEN(REPLACE(@List,@delimiter,'')))/LEN(REPLACE(@delimiter,' ','.')) + 1 AS ElementCount
           ,CAST('<x>' + REPLACE((SELECT @List AS [*] FOR XML PATH('')),@delimiter,'</x><x>')+'</x>' AS XML)
           .query('
                   for $i in (1,2,3,4,5,6,7,8,9)
                   return <x p="{$i}">{/x[$i]/text()[1]}</x>
                  ') AS ListXml
)
SELECT x.value('@p','int') AS Position
      ,x.value('text()[1]','int') AS Item 
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);
Community
  • 1
  • 1
Shnugo
  • 66,100
  • 9
  • 53
  • 114
0

In Azure SQL, there is now extended version of STRING_SPLIT which also can return the order of items if the third optional argument enable_ordinal is set to 1.

Then this simple task is finally easy:

DECLARE @string AS varchar(200) = 'a/b/c/d/e'
DECLARE @position AS int = 3

SELECT value FROM STRING_SPLIT(@string, '/', 1) WHERE ordinal = @position

Unfortunately not available in SQL Server 2019, only in Azure for now, lets hope it will be in SQL Server 2022.

Lukas.Navratil
  • 590
  • 4
  • 12