4

You have a string like

set @string = 'ddd,1.5,1,eee,2.3,0,fff,1.2,ggg,6.123,1'

I would like to know if there is a way to extract the string values and place them in the first row,the double values and place them in the second row and the int values and place them in the third row.The string logic is like this

"string,double,int,string,double,int..."

but there are cases when there is

"string,double,int,string,double,string,double,int"

and I would like in the third row where the int should be to be 1 by default so the table would look something like this.

First Row   Second Row  Third Row
ddd           1.5         1
eee           2.3         0
fff           1.2         1
ggg           6.123       1

I have a code where u can extract all the values from the string and place them in a row but that is not enough.

declare @string as nvarchar(MAX)

set @string = 'aaa,bbb,ccc,ddd,1.5,1,eee,2.3,1,fff,1.2,ggg,6.123,1'
;with tmp(DataItem, Data) 
as (
select LEFT(@string, CHARINDEX(',',@string+',')-1),
    STUFF(@string, 1, CHARINDEX(',',@string+','), '')
union all
select LEFT(Data, CHARINDEX(',',Data+',')-1),
    STUFF(Data, 1, CHARINDEX(',',Data+','), '')
from tmp
where Data > '')
select DataItem from tmp
option (maxrecursion 0)
John Pietrar
  • 513
  • 7
  • 19
  • This is not a task suited for sql. It's better to perform these kind of things with a programming language. – Zohar Peled Jul 07 '16 at 06:14
  • Still there must be a way to achieve this in sql. Maybe not suited , I realise it's easier to do this in say C# ,but still there must be a way to achieve this using only sql. – John Pietrar Jul 07 '16 at 06:33
  • Of course there is a way, to do it, it just wouldn't be as good or easy as it is in, say, c#. – Zohar Peled Jul 07 '16 at 06:46
  • Well if it would be easy I would just check http://www.w3schools.com/sql/ ,that's why I am asking on stack overflow where I know there are plenty of geniuses and competent people that can help and explain hard ploblems and after a point I could help others with hard problems also. :) – John Pietrar Jul 07 '16 at 06:52
  • How are you supposed to distinguish int = `123` from string = `123`? In your example replace `ggg` with `123`. You'd better explicitly indicate somehow that an integer value is missing. – Vladimir Baranov Jul 09 '16 at 12:17
  • Wish I knew how... – John Pietrar Jul 09 '16 at 16:02

2 Answers2

3

Final version (I hope):

Since sql server 2008 doesn't support order by in the over clause of aggregate functions, I've added another cte to add the row index instead of the sum I've used in the previous version:

;WITH cteAllRows as
(
     SELECT Item, 
            ItemIndex, 
            CASE WHEN ISNUMERIC(Item) = 0 THEN 'String'
            WHEN ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) > 0 THEN 'Double'
            WHEN ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) = 0 THEN 'Integer'
            END As DataType
     FROM dbo.SplitStrings_Numbers(@string, ',')
), cteAll as
(
    SELECT  Item, 
            DataType, 
            ItemIndex, 
            (
                SELECT COUNT(*)
                FROM cteAllRows tInner
                WHERE tInner.DataType = 'String'
                AND tInner.ItemIndex <= tOuter.ItemIndex
            ) As RowIndex
    FROM cteAllRows tOuter
)

All the rest is the same as the previous version.

Update

The first thing I've done is to change the string split function to a function based on a tally table, so that I can easily add the row number to it. So, if you don't already have a tally table, create one. If you are asking your self what is a tally table and why do you need it, read this article by Jeff Moden:

SELECT TOP 10000 IDENTITY(int,1,1) AS Number
    INTO Tally
    FROM sys.objects s1       
    CROSS JOIN sys.objects s2 
ALTER TABLE Tally ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
GO

Then, create string split function based on the tally table (taken from Aaron's article but added the row index column):

CREATE FUNCTION dbo.SplitStrings_Numbers
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN
   (
       SELECT   Item = SUBSTRING(@List, Number, CHARINDEX(@Delimiter, @List + @Delimiter, Number) - Number),
                ROW_NUMBER() OVER (ORDER BY Number) As ItemIndex
       FROM dbo.Tally
       WHERE Number <= CONVERT(INT, LEN(@List))
         AND SUBSTRING(@Delimiter + @List, Number, LEN(@Delimiter)) = @Delimiter
   );
GO

Now, The trick I've used is very much like the previous one, only now I've added to the first cte a new column I've called RowIndex, that's basically a running total of the count of strings, based on the row index of all rows:

 SELECT Item, 
        CASE WHEN ISNUMERIC(Item) = 0 THEN 'String'
        WHEN ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) > 0 THEN 'Double'
        WHEN ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) = 0 THEN 'Integer'
        END As DataType,
        SUM(CASE WHEN ISNUMERIC(Item) = 0 THEN 1 END) OVER(ORDER BY ItemIndex) As RowIndex
 FROM dbo.SplitStrings_Numbers(@string, ',')

It gave me this result:

Item       DataType RowIndex
---------- -------- -----------
ddd        String   1
1.5        Double   1
1          Integer  1
eee        String   2
2.3        Double   2
0          Integer  2
fff        String   3
1.2        Double   3
ggg        String   4
6.123      Double   4
1          Integer  4

As you can see, I now have a number for each row, so from now on it's simple:

;WITH cteAll as
(
     SELECT Item, 
            CASE WHEN ISNUMERIC(Item) = 0 THEN 'String'
            WHEN ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) > 0 THEN 'Double'
            WHEN ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) = 0 THEN 'Integer'
            END As DataType,
            SUM(CASE WHEN ISNUMERIC(Item) = 0 THEN 1 END) OVER(ORDER BY ItemIndex) As RowIndex
     FROM dbo.SplitStrings_Numbers(@string, ',')
), cteString AS
(
    SELECT Item, RowIndex
    FROM cteAll
    WHERE DataType = 'String'
), cteDouble AS
(
    SELECT Item, RowIndex
    FROM cteAll
    WHERE DataType = 'Double'
), cteInteger AS
(
    SELECT Item, RowIndex
    FROM cteAll
    WHERE DataType = 'Integer'
)

SELECT  T1.Item As [String],
        T2.Item As [Double],
        T3.Item As [Integer]
FROM dbo.Tally 
LEFT JOIN cteString T1 ON T1.RowIndex = Number 
LEFT JOIN cteDouble T2 ON t2.RowIndex = Number 
LEFT JOIN cteInteger T3 ON t3.RowIndex = Number
WHERE COALESCE(T1.Item, T2.Item, T3.Item) IS NOT NULL

That gave me this result:

String     Double     Integer
---------- ---------- ----------
ddd        1.5        1
eee        2.3        0
fff        1.2        NULL
ggg        6.123      1

As you can see, the items are now sorted by the original order in the string. Thanks for the challenge, It's been a while since I've had a decent one :-)

First attempt

Well, first you have to split that string into a table. To do that you should use a user defined function. You can pick the one best suited for you from Aaron Bertrand's Split strings the right way – or the next best way article.

For this demonstration I've chosen to use the SplitStrings_XML.

So first, create the function:

CREATE FUNCTION dbo.SplitStrings_XML
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN 
   (  
      SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
      FROM 
      ( 
        SELECT x = CONVERT(XML, '<i>' 
          + REPLACE(@List, @Delimiter, '</i><i>') 
          + '</i>').query('.')
      ) AS a CROSS APPLY x.nodes('i') AS y(i)
   );
GO

Now, declare and initialize the variable:

declare @string nvarchar(max) = 'ddd,1.5,1,eee,2.3,0,fff,1.2,ggg,6.123,1'

Then, Create 4 common table expressions - one for all items, one for strings, one for doubles and one for integers. Note the use of the row_number() function - it will be used later to join all the results together:

;WITH AllItems as
(
    SELECT Item, ROW_NUMBER() OVER(ORDER BY (select null)) as rn
    FROM dbo.SplitStrings_XML(@string, ',')
)

, Strings as
(
    SELECT Item as StringItem, ROW_NUMBER() OVER(ORDER BY (select null))  as rn
    FROM dbo.SplitStrings_XML(@string, ',')
    WHERE ISNUMERIC(Item) = 0
), Doubles as 
(
    SELECT Item as DoubleItem, ROW_NUMBER() OVER(ORDER BY (select null))  as rn
    FROM dbo.SplitStrings_XML(@string, ',')
    WHERE ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) > 0
), Integers as
(
    SELECT Item as IntegerItem, ROW_NUMBER() OVER(ORDER BY (select null))  as rn
    FROM dbo.SplitStrings_XML(@string, ',')
    WHERE ISNUMERIC(Item) = 1 AND CHARINDEX('.', Item) = 0 
)

Then, select from joining all these common table expressions. Note the use of the COALESCE built in function to only return rows where at least one value is present:

SELECT StringItem,  DoubleItem, IntegerItem
FROM AllItems A
LEFT JOIN Strings S ON A.rn = S.rn
LEFT JOIN Doubles D ON A.rn = D.rn
LEFT JOIN Integers I ON A.rn = I.rn
WHERE COALESCE(StringItem,  DoubleItem, IntegerItem) IS NOT NULL

Results:

StringItem  DoubleItem  IntegerItem
----------  ----------  -----------
ddd         1.5         1
eee         2.3         0
fff         1.2         1
ggg         6.123       NULL
Community
  • 1
  • 1
Zohar Peled
  • 79,642
  • 10
  • 69
  • 121
  • and the IntegerItem will not consider the rule `"aaa,1.3,1,bbb,1.5,ccc,2,1"` and it will result in `StringItem :aaa|bbb|ccc 'DoubleItem:1.3|1.5|NULL' and 'IntegerItem:1|2|1'` while it should be `StringItem :aaa|bbb|ccc 'DoubleItem:1.3|1.5|2' and 'IntegerItem:1|NULL|1'` – John Pietrar Jul 07 '16 at 07:44
  • I'm sorry, I don't understand this comment at all. – Zohar Peled Jul 07 '16 at 08:14
  • You have a `@string = "aaa,1.3,1,bbb,1.5,ccc,2,1"` that should result in 3 rows `StringItem DoubleItem IntegerItem` in the `StringItem` should result `aaa``bbb``ccc` wich happens,but in the `DoubleItem` row it should result `1.3``1.5``2` while it results `1.3``1.5``NULL` and in the `IntegerItem` row it should result `1``NULL``1` while it results `1``2``1` – John Pietrar Jul 07 '16 at 08:20
  • so the null in the integer item column is in the wrong place? in that case, you need to add a row_number to the split string function so that you can tell the original item "index" in the string. – Zohar Peled Jul 07 '16 at 08:22
  • Not only is the NULL in the wrong place but the rule applied on the double values is kind of not ok, because you can have something like `double x=2` but you cannot have `int x=1.2` so the `DoubleItem` row could have values that are integers ,but the integer can't have items that are doubles . – John Pietrar Jul 07 '16 at 08:27
  • 2
    Oh, I missed that, I was getting the data type based on the data. In that case, there a lot more you will need. If there is any way you can change your input from a delimited string into table, you should do it. otherwise you are going to face a lot of problems getting it right. – Zohar Peled Jul 07 '16 at 08:31
  • 1
    @ZoharPeled, How can you be sure that `ROW_NUMBER() OVER(ORDER BY (select null))` will always generate numbers in the correct order? `SplitStrings_XML` returns an unordered table... `ORDER BY (select null)` tells optimiser that you don't care about the order of generated numbers, but you actually do care... Do you have any link that documents the behaviour of `ROW_NUMBER` in this case? – Vladimir Baranov Jul 09 '16 at 12:22
  • 1
    All tables are unordered, and I can't be sure about the order of the row_number. It's basically whatever order I don't care about. If the order matters, the string splitter must return an ordibal column as well. – Zohar Peled Jul 09 '16 at 13:56
  • I was thinking, what if you consider each value that is followed by a "character" + "," and the integer value from the integer row column – John Pietrar Jul 10 '16 at 10:26
  • So it would be `"string"`,`"value followed by a string" +","`,and the `"integer value"` – John Pietrar Jul 10 '16 at 10:35
  • 1
    I hope to get to it tomorrow morning, today it's impossible for me. – Zohar Peled Jul 10 '16 at 13:25
  • @ZoharPeled strange ,when I try the 3rd piece of code I get Incorrect syntax near order – John Pietrar Jul 11 '16 at 05:43
  • apparently sql 2008 R2 doesn't support `OVER (ORDER BY)` only `OVER (PARTITION BY)` – John Pietrar Jul 11 '16 at 05:58
  • That Shouldn't be correct. The over clause was introduced in 2008 version. [Check your compatibility level.](https://msdn.microsoft.com/en-us/library/bb933794(v=sql.105).aspx) Make sure it's set to 100. Also [check the documentation](https://msdn.microsoft.com/en-us/library/ms189461(v=sql.105).aspx) - it clearly demonstrate the use of SUM with OVER clause. – Zohar Peled Jul 11 '16 at 06:09
  • Sorry, my mistake, you are correct. back to the drawing board :-) – Zohar Peled Jul 11 '16 at 06:12
  • Works like a charm but sadly it still won't recognize for example `double x = 2` as an actual double it will see it as an int but I will try to figure out how to resolve that .Thank you for the amazing answers and I learned a lot from you my friend,if you have a hint on how I could resolve that problem I'm open for solutions :-) – John Pietrar Jul 11 '16 at 06:46
  • Sorry, the only idea I have about that problem is to compare the location of the item inside it's row (since it should always come after a string), unless your source string can be (string, double, int, string, int, string, double, string double int) - in that case I can't think of any way to recognize 2 as a double instead of an int. Anyway, I'm glad I could help. It was in interesting challenge :-) – Zohar Peled Jul 11 '16 at 06:54
  • It always comes after a string – John Pietrar Jul 11 '16 at 06:55
  • @ZoharPeled I want to ask one last question if I may,how could I make the NULL in the result to 0? I see you can use something like `ISNULL(Result,0)` but not quite sure how. – John Pietrar Jul 11 '16 at 12:56
  • Yes, that's correct. you can use the `ISNULL` function on every column of the final select statement. – Zohar Peled Jul 11 '16 at 12:58
  • something like this? `T3.Item As [Result] ISNULL(Result,0)` – John Pietrar Jul 11 '16 at 13:21
  • 1
    No, like this: `ISNULL(T3.Item,0) As [Result]` – Zohar Peled Jul 11 '16 at 13:24
  • Thank you a lot for all that you taught me you are amazing :D – John Pietrar Jul 11 '16 at 13:28
1

this is your solution, just a bit more completed:

declare @string as nvarchar(MAX)
declare @id int=0
set @string = 'aaa,bbb,ccc,ddd,1.5,1,eee,2.3,1,fff,1.2,ggg,6.123,1'
;with tmp( id,[type],DataItem, Data) 
as (
select 
id=row_number() over(order by @string), 'string',
LEFT(@string, CHARINDEX(',',@string+',')-1),
    STUFF(@string, 1, CHARINDEX(',',@string+','), '')
union all
select 
        case when LEFT(Data, CHARINDEX(',',Data+',')-1) like '%[a-Z]%'  then id+1
             when LEFT(Data, CHARINDEX(',',Data+',')-1) like '%[0-9]%'  then id 
        end,
        case when LEFT(Data, CHARINDEX(',',Data+',')-1) like '%[a-Z]%'  then 'string'
             when LEFT(Data, CHARINDEX(',',Data+',')-1) like '%[0-9]%' and LEFT(Data, CHARINDEX(',',Data+',')-1) not like '%.%' then 'int' 
             when LEFT(Data, CHARINDEX(',',Data+',')-1) like '%[0-9]%' and LEFT(Data, CHARINDEX(',',Data+',')-1) like '%.%' then 'double' 
        end,
        LEFT(Data, CHARINDEX(',',Data+',')-1) as dataItem,
        STUFF(Data, 1, CHARINDEX(',',Data+','), '')
from tmp
where Data > ''
)
select  
        id,
        min(case [type]  when 'string' then DataItem end) as 'String',
        min(case [type]  when 'int' then DataItem end) as 'Int',
        min(case [type]  when 'double' then DataItem end) as 'Double'
from tmp 
group by id
option (maxrecursion 0)
sqlandmore.com
  • 163
  • 1
  • 8