SQL Sort Numeric Strings After Split - sql

I currently have char values in a table column which are in the format "IS-" and then 1 to 5 numbers, a possible period with either 2 numbers or a letter following the period.
Examples are, IS-1, IS-12, IS-123, IS-123.11, IS-123.a.
I need to split the string so that I grab only the number part, sort the strings ASC, and the bring the strings back together the way they were.
Explanation. I have the following set of values, IS-1170, IS-1171, IS-1172, IS-1173, IS-1174, IS-870.a, IS-871.a, IS-872.a. As you can see, because IS-1 comes before IS-8 they are sorting out of numerical order.
Any idea where to begin? I was thinking of using CASE, but I'm not really sure how to proceed.
Thanks.

Do string functions in your ORDER BY to remove only the number. Something like this should work:
SELECT col
FROM table
ORDER BY CAST(CASE WHEN ISNUMERIC(SUBSTRING(col,4,20)) = 1
THEN SUBSTRING(col,4,20)
ELSE LEFT(SUBSTRING(col,4,20),CHARINDEX('.',SUBSTRING(col,4,20),0)-1)
END AS NUMERIC)
This will first remove the IS- and check if the rest of the string is a number. If it is, it will leave the decimal digits, otherwise it will remove the . and the following alpha characters.
This is assuming your intended ordering in the case of numeric decimal places would be:
IS-123.A
IS-123.1
IS-123.2
If you don't care about what's after the decimal/period, then simply:
ORDER BY CAST(LEFT(SUBSTRING(col,4,20),CHARINDEX('.',SUBSTRING(col,4,20),0)-1) AS NUMERIC)

If I understand you correctly, this might help you:
DECLARE #mockup TABLE(ID INT IDENTITY,YourExample VARCHAR(100));
INSERT INTO #mockup VALUES
('IS-1, IS-12, IS-123, IS-123.11, IS-123.a.')
,('IS-1170, IS-1171, IS-1172, IS-1173, IS-1174, IS-870.a, IS-871.a, IS-872.a');
WITH Splitted AS
(
SELECT *
,CAST('<x>' + REPLACE(m.YourExample,',','</x><x>') + '</x>' AS XML) AS SplitAtComma
FROM #mockup AS m
)
,NumberExtracted AS
(
SELECT s.ID
,part.value('text()[1]','nvarchar(max)') AS OnePart
,CAST('<y>' + REPLACE(REPLACE(part.value('text()[1]','nvarchar(max)'),'.','-'),'-','</y><y>') + '</y>' AS XML).value('/y[2]/text()[1]','int') AS TheNumber
FROM Splitted AS s
CROSS APPLY s.SplitAtComma.nodes('/x') AS A(part)
)
SELECT *
FROM NumberExtracted
ORDER BY ID,TheNumber;
The first CTE uses a string-split via XML to get all values within the original string (btw: never store comma separated values!).
The second CTE will use the same approach to extract the number, typesafe as INT.
You can use this in an ORDER BY finally.
The result:
+----+-----------+-----------+
| ID | OnePart | TheNumber |
+----+-----------+-----------+
| 1 | IS-1 | 1 |
+----+-----------+-----------+
| 1 | IS-12 | 12 |
+----+-----------+-----------+
| 1 | IS-123 | 123 |
+----+-----------+-----------+
| 1 | IS-123.11 | 123 |
+----+-----------+-----------+
| 1 | IS-123.a. | 123 |
+----+-----------+-----------+
| 2 | IS-870.a | 870 |
+----+-----------+-----------+
| 2 | IS-871.a | 871 |
+----+-----------+-----------+
| 2 | IS-872.a | 872 |
+----+-----------+-----------+
| 2 | IS-1170 | 1170 |
+----+-----------+-----------+
| 2 | IS-1171 | 1171 |
+----+-----------+-----------+
| 2 | IS-1172 | 1172 |
+----+-----------+-----------+
| 2 | IS-1173 | 1173 |
+----+-----------+-----------+
| 2 | IS-1174 | 1174 |
+----+-----------+-----------+

IF OBJECT_ID(N'tempdb..##table1', N'U') IS NOT NULL
DROP TABLE ##table1;
create table ##table1(col1 varchar(20))
declare #query as nvarchar(max)
declare #var1 as varchar(max)='IS-1, IS-12, IS-123, IS-123.11, IS-123.a.,IS-1170, IS-1171, IS-1172, IS-1173, IS-1174, IS-870.a, IS-871.a, IS-872.a.'
set #var1=replace(#var1,',','''),(''')
set #var1='('''+#var1+''')'
set #var1=replace(#var1,' ','')
set #query='insert into ##table1 values'+#var1
EXEC sp_executesql #query
IF OBJECT_ID(N'tempdb..##table2', N'U') IS NOT NULL
DROP TABLE ##table2;
select * into ##table2 from ##table1 order by cast(replace(replace(replace(col1,'IS-',''),'.a.',''),'.a','') as float)
declare #results varchar(max)
select #results = coalesce(#results + ', ', '') + convert(varchar(12),col1) from ##table2
select #results
DROP TABLE ##table1
DROP TABLE ##table2

Related

How can I remove the spaces from these numbers?

I am using SQL Server 2014 and I have a table (t1) in my database which contain a column called "MealPlan".
This column contains a list of strings (extract below):
MealPlan
Sansrepas315€/pers.=630€pour2pers.Devis/RésaSelectionner
Sansrepas394€/pers.=787€pour2pers.Devis/RésaSelectionner
Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner
Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner
Sansrepas700€/pers.=1 400€pour2pers.Devis/RésaSelectionner
Sansrepas328€/pers.=656€pour2pers.Devis/RésaSelectionner
I need to extract the numbers between the characters = and €
I have the following codes in place which does exactly what I need:
SUBSTRING(MealPlan,LEN(LEFT(MealPlan,CHARINDEX('=', MealPlan)+1)),LEN(MealPlan) - LEN(LEFT(MealPlan,CHARINDEX('=', MealPlan))) - LEN(RIGHT(MealPlan,CHARINDEX('€', (REVERSE(MealPlan)))))) AS [Price]
After running the above my column "Price" appear as follows:
Price
630
787
1 093
1 093
1 400
656
However, I want to get rid of that space in the numbers where a thousand digit is present.
My expected output:
Price
630
787
1093
1093
1400
656
I have tried the following but it is not working:
REPLACE(SUBSTRING(MealPlan,LEN(LEFT(MealPlan,CHARINDEX('=', MealPlan)+1)),LEN(MealPlan) - LEN(LEFT(MealPlan,CHARINDEX('=', MealPlan))) - LEN(RIGHT(MealPlan,CHARINDEX('€', (REVERSE(MealPlan)))))), ' ','') AS [Price2]
Any help would be much appreciated.
I just ran your query in my sample database and it is working fine..
select
REPLACE(SUBSTRING(Description,
LEN(LEFT(Description,CHARINDEX('=', Description)+1)),
LEN(Description) - LEN(LEFT(Description,CHARINDEX('=', Description))) - LEN(RIGHT(Description,CHARINDEX('€', (REVERSE(Description)))))
), ' ','') AS [Description]
from Worker
Table #a1
| MealPlan |
| -------- |
| Sansrepas315€/pers.=630€pour2pers.Devis/RésaSelectionner |
| Sansrepas394€/pers.=787€pour2pers.Devis/RésaSelectionner |
| Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner |
Query
SELECT
REPLACE(
SUBSTRING(MealPlan,CHARINDEX('=', MealPlan)+1, CHARINDEX('=',REVERSE(MealPlan)) - CHARINDEX('€',REVERSE(MealPlan)) -1 )
,' ', ''
)
as value
FROM #a1
results
value
630
787
1093
The solution about "cut and paste" provided by Jiří Baum above did the trick for me.
It is very easy to tokenize a string of characters by using XML and XQuery.
No need to parse string and call multiple functions: SUBSTRING(), CHARINDEX(), PATINDEX(), LEN(), REVERSE(), etc.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY(1,1) PRIMARY KEY, MealPlan NVARCHAR(1000));
INSERT INTO #tbl (MealPlan) VALUES
(N'Sansrepas315€/pers.=630€pour2pers.Devis/RésaSelectionner'),
(N'Sansrepas394€/pers.=787€pour2pers.Devis/RésaSelectionner'),
(N'Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner'),
(N'Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner'),
(N'Sansrepas700€/pers.=1 400€pour2pers.Devis/RésaSelectionner'),
(N'Sansrepas328€/pers.=656€pour2pers.Devis/RésaSelectionner');
-- DDL and sample data population, end
DECLARE #separator CHAR(1) = '='
, #euro CHAR(1) = '€';
SELECT t.*
, REPLACE(c.value('(/root/r[3]/text())[1]', 'VARCHAR(20)'),SPACE(1),'') AS Price
FROM #tbl AS t
CROSS APPLY (SELECT TRY_CAST('<root><r><![CDATA[' +
REPLACE(REPLACE(MealPlan,#euro,#separator), #separator, ']]></r><r><![CDATA[') +
']]></r></root>' AS XML)) AS t1(c);
Output
+----+------------------------------------------------------------+--------+
| ID | MealPlan | Result |
+----+------------------------------------------------------------+--------+
| 1 | Sansrepas315€/pers.=630€pour2pers.Devis/RésaSelectionner | 630 |
| 2 | Sansrepas394€/pers.=787€pour2pers.Devis/RésaSelectionner | 787 |
| 3 | Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner | 1093 |
| 4 | Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner | 1093 |
| 5 | Sansrepas700€/pers.=1 400€pour2pers.Devis/RésaSelectionner | 1400 |
| 6 | Sansrepas328€/pers.=656€pour2pers.Devis/RésaSelectionner | 656 |
+----+------------------------------------------------------------+--------+
Try this:
Declare #MealPlan as varchar(250) = 'Sansrepas547€/pers.=1 093€pour2pers.Devis/RésaSelectionner'
Select Replace(
SUBSTRING(#MealPlan,
CharIndex('=', #MealPlan)+1,
CharIndex(')',Replace(#MealPlan, '€p', ')'))- CharIndex('=', #MealPlan)-1 ),
' ', '')
Output:
Let me know if this doesn't work or you have a problem understanding the query.

SQL Server - Ordering Combined Number Strings Prior To Column Insert

I have 2 string columns (thousands of rows) with ordered numbers in each string (there can be zero to ten numbers in each string). Example:
+------------------+------------+
| ColString1 | ColString2 |
+------------------+------------+
| 1;3;5;12; | 4;6' |
+------------------+------------+
| 1;5;10 | 2;26; |
+------------------+------------+
| 4;7; | 3; |
+------------------+------------+
The end result is to combine these 2 columns, sort the numbers in
ascending order and then put each number into individual columns (smallest, 2nd smallest etc).
e.g. Colstring1 is 1;3;5;12; and ColString2 is 4;6; needs to return 1;3;4;5;6;12; which I then use xml to allocated into columns.
Everthing works fine using xml apart from the step to order the numbers (i.e I'm getting 1;3;5;12;4;6; when I combine the strings i.e. not in ascending order).
I've tried put them into a JSON array first to order, thinking I could do a top[1] etc but that did not work.
Any help on how to combine the 2 columns and order them before inserting into columns:
Steps so far:
Example data:
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, ColString1 VARCHAR(50), ColString2 VARCHAR(50));
INSERT INTO #tbl (ColString1, ColString2)
VALUES
('1;3;5;12;', '4;6;'),
('1;5;10;', '2;26;'),
('14;', '3;8;');
XML Approach (Combines strings and puts into columns but not in the correct order):
;WITH Split_Numbers (xmlname)
AS
(
SELECT
CONVERT(XML,'<Names><name>'
+ REPLACE ( LEFT(ColString1+ColString2,LEN(ColString1+ColString2) - 1),';', '</name><name>') + '</name></Names>') AS xmlname
FROM #tbl
)
SELECT
xmlname.value('/Names[1]/name[1]','int') AS Number1,
xmlname.value('/Names[1]/name[2]','int') AS Number2,
xmlname.value('/Names[1]/name[3]','int') AS Number3,
xmlname.value('/Names[1]/name[4]','int') AS Number4,
xmlname.value('/Names[1]/name[5]','int') AS Number5
--etc for additional columns
FROM Split_Numbers
Current Output: numbers not in correct order,
+---------+---------+---------+---------+---------+
| Number1 | Number2 | Number3 | Number4 | Number5 |
+---------+---------+---------+---------+---------+
| 1 | 3 | 5 | 12 | 4 |
| 1 | 5 | 10 | 2 | 26 |
| 14 | 3 | 8 | NULL | NULL |
+---------+---------+---------+---------+---------+
Desired Output: numbers in ascending order.
+---------+---------+---------+---------+---------+
| Number1 | Number2 | Number3 | Number4 | Number5 |
+---------+---------+---------+---------+---------+
| 1 | 3 | 4 | 5 | 6 |
| 1 | 2 | 5 | 10 | 26 |
| 3 | 8 | 14 | NULL | NULL |
+---------+---------+---------+---------+---------+
JSON Approach: combines the columns into a JSON array but I still can't order correctly when in JSON format.
REPLACE ( CONCAT('[', LEFT(ColString1+ColString2,LEN(ColString1+ColString2) - 1), ']') ,';',',')
Any help will be greatly appreciated whether there is a way to order the xml or JSON string prior to entry. Happy to consider an alternative way if there is an easier solution.
You can use string_agg() and string_split():
select t.*, newstring
from t cross apply
(select string_agg(value, ',') order by (value) as newstring
from (select s1.value
from unnest(colstring1, ',') s1
union all
select s2.value
from unnest(colstring2, ',') s2
) s
) s;
That said, you should probably put your effort into fixing the data model. Storing numbers in strings is bad. Storing multiple values in a string is bad, bad. If the numbers are foreign references to other tables, that is bad, bad, bad, bad, bad.
While waiting for a DDL and sample data population, etc., here is a conceptual example for you. It is using XQuery and its FLWOR expression.
CTE does most of the heavy lifting:
Concatenates both columns values into one string. CONCAT() function protects against NULL values.
Converts it into XML data type.
Sorts XML elements by converting their values to int data type in the FLWOR expression.
Filters out XML elements with no legit values.
The rest is trivial.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, col1 VARCHAR(100), col2 VARCHAR(100));
INSERT INTO #tbl (col1, col2)
VALUES
('1;3;5;12;', '4;6;'),
('1;5;10;', '2;26;');
-- DDL and sample data population, end
DECLARE #separator CHAR(1) = ';';
;WITH rs AS
(
SELECT *
, CAST('<root><r><![CDATA[' +
REPLACE(CONCAT(col1, col2), #separator, ']]></r><r><![CDATA[') +
']]></r></root>' AS XML).query('<root>
{
for $x in /root/r[text()]
order by xs:int($x)
return $x
}
</root>') AS sortedXML
FROM #tbl
)
SELECT ID
, c.value('(r[1]/text())[1]','INT') AS Number1
, c.value('(r[2]/text())[1]','INT') AS Number2
, c.value('(r[3]/text())[1]','INT') AS Number3
-- continue with the rest of the columns
FROM rs CROSS APPLY sortedXML.nodes('/root') AS t(c);
Output
+----+---------+---------+---------+
| ID | Number1 | Number2 | Number3 |
+----+---------+---------+---------+
| 1 | 1 | 3 | 4 |
| 2 | 1 | 2 | 5 |
+----+---------+---------+---------+

Substring output for different cases

I need output with following cases:
+-----------------+--------+
| STRING in table | OUTPUT |
+-----------------+--------+
| NONGL_NONGL | NONGL |
| GL252_GL252 | GL |
| GL400_GL400 | GL |
| NOS_NOS | NOS |
+-----------------+--------+
I tried to use SUBSTRING() but it is not giving me proper output.
So for the NONGL and NOS result you need:
SELECT LEFT([col_name], CHARINDEX('_', [col_name]) - 1)
And for the GL output, since the string is containing numbers you need PATINDEX():
SELECT LEFT([col_name], PATINDEX('%[0-9]%', [col_name]) - 1)
In the end just use a CASE WHEN
DECLARE #tbl Table (string VARCHAR(MAX) );
INSERT INTO #tbl VALUES
('NONGL_NONGL'),
('GL252_GL252'),
('GL400_GL400'),
('NOS_NOS');
SELECT
string
,CASE WHEN
string LIKE '%[0-9]%' then LEFT(string, PATINDEX('%[0-9]%', string) - 1)
ELSE LEFT(string, CHARINDEX('_', string) - 1)
END AS [Output]
from #tbl
This should do what you want:
select (case when col like 'NONGL%' then 'NONGL'
when col like 'GL%' then 'GL'
when col like 'NOS%' then 'NOS'
end) as new_col
Here is another way:
DECLARE #StrTable Table (Str VARCHAR(MAX) );
INSERT INTO #StrTable VALUES
('NONGL_NONGL'),
('GL252_GL252'),
('GL400_GL400'),
('NOS_NOS');
SELECT Str,
IIF(Str LIKE 'NONGL%','NONGL',IIF(Str LIKE 'GL%', 'GL', IIF(Str LIKE 'NOS%', 'NOS', Str))) AS Outputs
FROM #StrTable;
Result:
+-------------+---------+
| Str | Outputs |
+-------------+---------+
| NONGL_NONGL | NONGL |
| GL252_GL252 | GL |
| GL400_GL400 | GL |
| NOS_NOS | NOS |
+-------------+---------+

Row values as column headings

How can I make the following sql result:
Be rendered in a result set like this
| ID | Chain Size | Length | Hook Type | Shortening Grab |
|-----|-----------------|---------|------------|-----------------|
| 163 | 7mm (1.5 tonne) | 1 metre | C Hook | Yes |
| 226 | 7mm (1.5 tonne) | 1 metre | C Hook | No |
| 247 | 7mm (1.5 tonne) | 1 metre | Latch Hook | No |
I know that the values in columns 2,4,6 and 8 (which I want to be headers) will be the same across all rows (but different depending on the initial query).
I believe the approach for what I want is through the use of PIVOT but really struggling to get the desired result.
Thanks
Assuming your source data actually looks like this:
Static Pivot
Select *
From YourTable
Pivot (max(attributeValue) For [attributeName] in ([Chain Size],[Length],[Hook Type],[Shortening Grab]) ) p
Returns
Dynamic Approach
Declare #SQL varchar(max) = Stuff((Select Distinct ',' + QuoteName([attributeName]) From Yourtable Order by 1 For XML Path('')),1,1,'')
Select #SQL = '
Select *
From YourTable
Pivot (max(attributeValue) For [attributeName] in (' + #SQL + ') ) p'
Exec(#SQL);
Returns
Notice, without an item for column sequence, you'll see that they are alphabetical.

SQL Find String

I would like to know if finding a string can be done from another table. It's a bit complicated.
Here's the table: (tbl_dishes)
| dish | Type |
| egg, hotdog & bread | Breakfast |
From the table above, I want to get the individual descriptions of the column dish from another table
2nd Table (tbl_Foods)
| food | Description |
| egg | Fresh |
| hotdog | red |
| bread | toasted |
| steak | meat |
Let's say my query would look like this: (but it's wrong)
SELECT food, description FROM tbl_Foods
WHERE food Exists IN (SELECT dish FROM tbl_Dishes)
My desired results would be:
| food | Description |
| egg | Fresh |
| hotdog | red |
| bread | toasted |
It's like getting all matched word in the dish column. I don't know if it's possible. Please help.
Thank you.
SELECT food, description
FROM tbl_Foods
join tbl_Dishes
on tbl_Dishes.dish like ('%' + tbl_Foods.food +'%')
You will need to split the list
DECLARE #DelimString VARCHAR(100)
SET DelimString = SELECT REPLACE(REPLACE(dish,'&',','),' ', '') FROM tbl_Dishes
DECLARE #Dish TABLE (Dish VARCHAR(50)); INSERT INTO #Dish SELECT CAST(ParamValue AS VARCHAR) FROM MultiValueParams_String(#DelimString)
Use this function.
Create function [dbo].[MultiValueParams_String] (#ParamList varchar(4000))
returns #Values table (RoNum INT,ParamValue varchar(4000))
as
begin
declare #Delim char(1) = ',' -- comma is always the delimiter
declare #Chrind int = 1
declare #Piece nvarchar(50)
declare #RoNum int = 0
while #Chrind>0
begin
select #Chrind=charindex(#Delim,#ParamList)
if #Chrind>0
select #Piece=left(#ParamList,#chrind-1)
else
select #Piece=#ParamList
insert #values(RoNum,ParamValue) values (#RoNum,#Piece)
select #ParamList = right(#ParamList,len(#ParamList)-#chrind)
if len(#ParamList)=0 break
SELECT #RoNum = #RoNum + 1
end
return
end
SELECT food, description
FROM tbl_Foods f
INNER JOIN #Dish d ON f.food = d.dish
Something like this.