9

Can you assume that the result of the string_split function returns elements in order? In short, is the following code correct?

DECLARE @tags NVARCHAR(400) = 'clothing,road,touring,bike' 
DECLARE @FIRST nvarchar(100);

SELECT TOP 1 @FIRST = value 
FROM STRING_SPLIT(@tags, ',');  
3
  • The function returns a table which is by definition unordered. If you can't find support for a defined order in the documentation, I would not assume that it is. May 19, 2018 at 12:11
  • For the specific example you can quite easily use LEFT(@tags,CHARINDEX(',', @tags + ',') - 1) May 20, 2018 at 10:41
  • Azure SQL's STRING SPLIT now supports an enable_ordinal parameter, according to this article, but I've not tried it myself. Dec 8, 2021 at 15:17

4 Answers 4

17

TOP 1 without ORDER BY could not be guaranteed to return the same results even if the output of STRING_SPLIT() were guaranteed (which it most definitely is not). While in real-world usage you may find it hard to come up with a counter-example where the data is not returned in order, this is a terrible kind of thing to do. Trusting that something always works because you've never seen it break is like assuming that if you put a deer sign on the highway that is the only place you'll ever see a deer.

However, let's look at another way we could solve this. Why don't we locate the position of each individual string in the overall list:

DECLARE 
  @tags nvarchar(400) = N'clothing,road,touring,bike',
  @c nchar(1) = N',';

SELECT value, CHARINDEX(@c + value + @c, @c + @tags + @c) 
  -- we surround the value and the string with leading/trailing ,
  -- so that cloth isn't a false positive for clothing
  FROM STRING_SPLIT(@tags, ',') AS t;

Output here is:

output with charindex

Do you think we could use this to determine the first element in the list? Of course! Let's try again:

DECLARE 
  @tags nvarchar(400) = N'clothing,road,touring,bike',
  @first nvarchar(100),
  @c nchar(1) = N',';

;WITH t AS
(
  SELECT value, idx = CHARINDEX(@c + value + @c, @c + @tags + @c) 
  FROM STRING_SPLIT(@tags, ',')
)
SELECT TOP (1) @first = value FROM t ORDER BY idx;

PRINT @first;

Output:

clothing

You could use the same logic to find the last element, just change the ORDER BY idx to ORDER BY idx DESC. In fact, you could use this logic to return the nth string in the list:

DECLARE 
  @tags nvarchar(400) = N'clothing,road,touring,bike',
  @c nchar(1) = N',',
  @nth tinyint;

SET @nth = 3;

;WITH t AS
(
  SELECT value, idx = ROW_NUMBER() OVER 
    (ORDER BY CHARINDEX(@c + value + @c, @c + @tags + @c))
  FROM STRING_SPLIT(@tags, ',')
)
SELECT * FROM t WHERE idx = @nth;

Result:

touring

As a disclaimer, if you have duplicates, that's going to mess things up, because the index value will always represent the first appearance of that value in the string. You can switch from ROW_NUMBER() to DENSE_RANK() but that won't solve for all cases. You can de-dupe the string first (I talk about some edge cases here).

3
  • 1
    «that's going to mess things up» @AaronBertrand, There is a simple solution for duplicates which allows you to use STRING_SPLIT and guarantee the order, but it is highly not recommended for production -> only for the sake of the discussion please check this post with full explanation and "solution" Feb 13, 2019 at 0:40
  • @RonenAriely Yeah, I saw. While that is an interesting solution from an academic standpoint, you are quite right that it is not suitable for production (and I don't know how realistic it is to have a comma-separated list that is assembled with duplicates). Everything you've gained from the performance and elegance of STRING_SPLIT() you're just throwing away. At that point I'd rather just use the same type of inline table-valued function that I'd have used before 2016. Feb 13, 2019 at 5:42
  • If you don't have duplicates like scenario 1, which I show, and in fact even scenario 2 where there can be only twice or less, then this approach can bring very good performance in some cases. It is when we have multiple duplicates like scenario 3, that "Everything you've gained from the performance and elegance of STRING_SPLIT() you're just throwing away", since we start to make use of loop (recursive) Feb 13, 2019 at 10:11
3

You definitely make a cheating like below:

SELECT  value,
        row_number() OVER (ORDER BY current_timestamp) AS row
FROM    String_Split('your,string,here',',') d
3
  • 3
    Welcome on the DBA SE! Note, code-only answers are typically not very well received, it is better if you also explain, what are you doing and why.
    – peterh
    Mar 29, 2019 at 10:15
  • 2
    How can you know that this forces string_split to guarantee the original order?
    – Tim Lehner
    Mar 2, 2020 at 21:07
  • 1
    @TimLehner: As MS official page, it said "The order is not guaranteed to match the order of the substrings in the input string". Therefore it just a trick and will work for 99% cases I believe (you even can ORDER BY NULL). The func is getting improved in the future, you might to follow the post here: feedback.azure.com/forums/908035-sql-server/suggestions/…
    – Dat Nguyen
    Mar 4, 2020 at 8:28
0

You can use an identity and a temporary table, something like this..

I think it's cleaner:

select  'OrderNum' = identity(int,1,1), * 
into    #tmo
from    STRING_SPLIT ('kjds|kdkjsdhkjshdkj3hjk|3hg43ui46g33754g6325', '|')

select * from #tmo

So, you can obtain this result

|OrderNum    |value
|----------- |----------------------
|1           |kjds
|2           |kdkjsdhkjshdkj3hjk
|3           |3hg43ui46g33754g6325

BUT, microsoft says, the output order of String_SPLIT function is not guaranteed

you can obtain the real order with the "ordinal" parameter, but only works for SQL 2022 and forward

-1

this is my variant with guaranteed right sorting:

DECLARE @string NVARCHAR(256) = N'   a    b x  b  c';
-- creating table with unique identity, for sorting later:
DECLARE @dict TABLE (
    id          INT IDENTITY(1,1),
    value       NVARCHAR(256)
);
DECLARE @result NVARCHAR(256)  = '';
-- split string on parts and insert rows on @dict:
INSERT @dict SELECT value FROM STRING_SPLIT(@string, ' ') WHERE value <> '';
-- got sorted result:
SELECT value FROM @dict ORDER BY id;

Done.

p.s. if you`re wondering, this my function for getting sorted words (like single line):

CREATE FUNCTION [dbo].[getWordsFromString] (@string NVARCHAR(256), @offset TINYINT, @limit TINYINT)
RETURNS NVARCHAR(256)
WITH SCHEMABINDING
AS
BEGIN
    -- creating table with unique identity, which will be used for sorting below
    DECLARE @dict TABLE (
        id          INT IDENTITY(1,1),
        value       NVARCHAR(256)
    );
    DECLARE @result NVARCHAR(256)  = '';
    IF @offset IS null
        SET @offset = 0;
    IF @limit = 0 OR @limit IS null
        SET @limit = 1;
    -- split string into raws and insert them into temp table
    INSERT @dict SELECT value FROM STRING_SPLIT(@string, ' ') WHERE value <> '';
    -- aggregate rows into one row splitted by spaces
    SELECT @result = COALESCE(@result + ' ', '') + value
    FROM @dict
    ORDER BY id-- сортируем по позиции слова в строке
    OFFSET @offset ROWS
    FETCH NEXT @limit ROWS ONLY;
    RETURN LTRIM(@result);
END

Example usage:

SELECT dbo.getWordsFromString(N'   a    b x  b  c', 1, 2);

Result:

b x
1
  • 1
    How can you know that this forces string_split to guarantee the original order?
    – Tim Lehner
    Mar 2, 2020 at 21:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.