Sort an array of strings in SQL - sql

I have a column of strings in SQL Server 2019 that I want to sort
Select * from ID
[7235, 6784]
[3235, 2334]
[9245, 2784]
[6235, 1284]
Trying to get the result below:
[6784, 7235]
[2334, 3235]
[2784, 9245]
[1284, 6235]

Given this sample data:
CREATE TABLE dbo.ID(ID int IDENTITY(1,1), SomeCol varchar(64));
INSERT dbo.ID(SomeCol) VALUES
('[7235, 6784]'),
('[3235, 2334]'),
('[9245, 2784]'),
('[6235, 1284]');
You can run this query:
;WITH cte AS
(
SELECT ID, SomeCol,
i = TRY_CONVERT(int, value),
s = LTRIM(value)
FROM dbo.ID CROSS APPLY
STRING_SPLIT(PARSENAME(SomeCol, 1), ',') AS s
)
SELECT ID, SomeCol,
Result = QUOTENAME(STRING_AGG(s, ', ')
WITHIN GROUP (ORDER BY i))
FROM cte
GROUP BY ID, SomeCol
ORDER BY ID;
Output:
ID
SomeCol
Result
1
[7235, 6784]
[6784, 7235]
2
[3235, 2334]
[2334, 3235]
3
[9245, 2784]
[2784, 9245]
4
[6235, 1284]
[1284, 6235]
Example db<>fiddle

The source table has a column with a JSON array.
That's why it is a perfect case to handle it via SQL Server JSON API.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID int IDENTITY PRIMARY KEY, jArray NVARCHAR(100));
INSERT #tbl (jArray) VALUES
('[7235, 6784]'),
('[3235, 2334]'),
('[9245, 2784]'),
('[6235, 1284]');
-- DDL and sample data population, end
SELECT t.*
, Result = QUOTENAME(STRING_AGG(j.value, ', ')
WITHIN GROUP (ORDER BY j.value ASC))
FROM #tbl AS t
CROSS APPLY OPENJSON(t.jArray) AS j
GROUP BY t.ID, t.jArray
ORDER BY t.ID;
Output
+----+--------------+--------------+
| ID | jArray | Result |
+----+--------------+--------------+
| 1 | [7235, 6784] | [6784, 7235] |
| 2 | [3235, 2334] | [2334, 3235] |
| 3 | [9245, 2784] | [2784, 9245] |
| 4 | [6235, 1284] | [1284, 6235] |
+----+--------------+--------------+

Related

SQL query to get list of all records that are placed higher in hierarchy

Table:
+-----+------------+-------------+
| Id | DocumentNo | ParentCCID |
+-----+------------+-------------+
| 10 | CC001 | NULL |
| 20 | CC002 | CC001 |
| 33 | CC003 | CC002 |
+-----+-------------+-------------+
Value passed to the query: CC003
Expected Output:
CC003
CC002
CC001
Failed Attempt:
select b2.documentno,b2.ParentCCID from basicdetails b1
inner join basicdetails b2 on b1.documentno = b2.ParentCCID
where b2.documentno='CC003'
Note: DocumentNo is unique primary key. ParentCCID could have null values if there is no parent record.
EDIT:
create table basicdetails2
(
id int identity,
documentno varchar(30),
parentccid varchar(30)
)
insert into basicdetails2 values('CC001', null)
insert into basicdetails2 values('CC002', 'CC001')
insert into basicdetails2 values('CC003', 'CC002')
insert into basicdetails2 values('CC004', 'CC003')
You want a recursive cte:
with cte as (
select bd.documentno, bd.ParentCCID
from basicdetails bd
where bd.documentno = 'CC003'
union all
select cte.documentno, cte.ParentCCID
from cte join
basicdetails bd
on bd.documentno = cte.ParentCCID
)
select bd.documentno
from cte;
Just a minor twist on Gordon's answer (already +1).
I like to track the level and see the parents for each record
Example
Declare #Fetch varchar(25) = 'CC003'
;with cte as (
Select DocumentNo
,ParentCCDocumentNo
,Lvl=1
From YourTable
Where DocumentNo=#Fetch
Union All
Select R.DocumentNo
,R.ParentCCDocumentNo
,P.Lvl+1
From YourTable R
Join cte P on P.ParentCCDocumentNo = R.DocumentNo)
Select Lvl = Row_Number() over (Order By Lvl Desc)
,DocumentNo
,ParentCCDocumentNo
From cte
Order By 1 desc
Returns
Lvl DocumentNo ParentCCDocumentNo
3 CC003 CC002
2 CC002 CC001
1 CC001 NULL

Extract the maximum value from the last number in a hierarchyID?

I have a column with hierarchy IDs converted to strings in SQL Server. I need to add new hierarcyIDs for the new lines, but first I have to find the last child of the current ID. The hierarchyIDs are look like these:
/1/1/1/6/1/
/1/1/1/6/7/
/1/1/1/6/3/
/1/1/1/6/13/
/1/1/1/6/4/
As you can see, the maximum number is not equal with the count of the lines, so I can not use count()+1 unfortunately.
What I need to extract from this list is:
13
I only have experience in PL SQL, where it was easy to do this with regexp functions, but I can not find the solution in SQL Server.
You can use some STRING operation as below to get your desired output-
DEMO HERE
WITH your_table(your_column)
AS
(
SELECT '/1/1/1/6/1/' UNION ALL
SELECT '/1/1/1/6/7/' UNION ALL
SELECT '/1/1/1/6/3/' UNION ALL
SELECT '/1/1/1/6/13/' UNION ALL
SELECT '/1/1/1/6/4/'
)
SELECT
MAX(
CAST(
REVERSE(
LEFT(
REVERSE(LEFT(your_column,LEN(your_column)-1)),
CHARINDEX('/',REVERSE(LEFT(your_column,LEN(your_column)-1)) ,0) - 1
)
)
AS INT
)
)
FROM your_table
Note: Data has to be as your sample data
The creators of hierarchyid have anticipated your needs and have an officially supported solution for this.
declare #h table (h HIERARCHYID);
insert into #h (h)
values
('/1/1/1/6/1/'),
('/1/1/1/6/7/'),
('/1/1/1/6/3/'),
('/1/1/1/6/13/'),
('/1/1/1/6/4/');
declare #parent HIERARCHYID = '/1/1/1/6/';
declare #maxChild HIERARCHYID = (
select max(h)
from #h
where h.IsDescendantOf(#parent) = 1
);
-- ToString() added here for readability in the output;
-- it's not needed to be used as data.
select #parent.GetDescendant(#maxChild, null).ToString();
You can read more about this here.
Another way around this is to specify your own components to the hierarchyid yourself. I like to use the primary key values. For example, let's say that this data represents a company's org chart. If EmployeeID 1 is the CEO, 42 is the CFO (who reports to the CEO) and 306 is Accounting Manager (who reports to the CFO), the latter's hierarchyid would be /1/42/306/. Because the PK values are unique, the generated hierarchid is also unique.
To give some ideas, 2 solutions.
The first one is for Sql Server 2017 and beyond.
The second for most versions below that.
create table YourTable
(
ID int identity(101,1) primary key,
hierarcyIDs varchar(100)
)
GO
✓
insert into YourTable
(hierarcyIDs) values
('/1/2/3/4/5/')
,('/1/2/3/4/15/')
,('/1/2/3/4/10/')
,('/11/12/13/14/15/')
,('/11/12/13/14/42/')
;
GO
5 rows affected
SELECT
[1] as id1,
[2] as id2,
[3] as id3,
[4] as id4,
max([5]) as id5,
concat_ws('/','',[1],[2],[3],[4],max([5])+1,'') as nextHierarcyIDs
FROM YourTable
OUTER APPLY
(
select *
from
( select try_cast(value as int) as id
, row_number() over (order by (select 0)) rn
from string_split(trim('/' from hierarcyIDs),'/') s
) src
pivot (max(id)
for rn in ([1],[2],[3],[4],[5])
) pvt
) anarchy
group by [1],[2],[3],[4]
GO
id1 | id2 | id3 | id4 | id5 | nextHierarcyIDs
--: | --: | --: | --: | --: | :---------------
1 | 2 | 3 | 4 | 15 | /1/2/3/4/16/
11 | 12 | 13 | 14 | 42 | /11/12/13/14/43/
SELECT hierarcyIdLvl1to4
, MAX(hierarcyIDLvl5) AS maxHierarcyIDLvl5
FROM
(
SELECT id, hierarcyIDs
, substring(hierarcyIDs, 0, len(hierarcyIDs)-charindex('/',
reverse(hierarcyIDs),2)+2) AS hierarcyIdLvl1to4
, reverse(substring(reverse(hierarcyIDs),2,charindex('/',
reverse(hierarcyIDs),2)-2)) AS hierarcyIDLvl5
FROM YourTable
) q
GROUP BY hierarcyIdLvl1to4
GO
hierarcyIdLvl1to4 | maxHierarcyIDLvl5
:---------------- | :----------------
/1/2/3/4/ | 5
/11/12/13/14/ | 42
db<>fiddle here

SQL Pivot 2nd step -- Combine rows / Remove nulls

I am trying to pivot data. We start with two columns: ListNum and Value
The row ?integrity? doesn't matter, I just want all the values to collapse upwards and remove the nulls.
In this case ListNum is like an enum, the values are limited to List1, List2, or List3. Notice that they are not in order (1,3,3,1,2 rather than 1,2,3,1,2,3 etc.).
It would be nice to have a solution that uses standard sql so it would work across many databases.
Starting Point:
+---------+------------+
| ListNum | Value |
+---------+------------+
| List1 | A |
| List3 | 123 |
| List3 | CDE |
| List1 | Somestring |
| List2 | randString |
+---------+------------+
I was able to separate the Lists into columns with:
select
case when ListNum = "List1" then Value end as List1,
case when ListNum = "List2" then Value end as List2,
case when ListNum = "List3" then Value end as List3
from Table;
Midpoint:
+------------+------------+-------+
| List1 | List2 | List3 |
+------------+------------+-------+
| A | NULL | NULL |
| NULL | NULL | 123 |
| NULL | NULL | CDE |
| Somestring | NULL | NULL |
| NULL | randString | NULL |
+------------+------------+-------+
but now I need to collapse upwards/remove the nulls to get -
Desired Output:
+------------+------------+-------+
| List1 | List2 | List3 |
+------------+------------+-------+
| A | randString | 123 |
| Somestring | NULL | CDE |
+------------+------------+-------+
Aren't you missing some kind of grouping criterion? How do you determine, that A belongs to 123 and not to CDE? And why is randString in the first line and not in the second?
This is easy, with such a grouping key:
DECLARE #tbl TABLE(GroupingKey INT, ListNum VARCHAR(100),[Value] VARCHAR(100));
INSERT INTO #tbl VALUES
(1,'List1','A')
,(1,'List3','123')
,(2,'List3','CDE')
,(2,'List1','Somestring')
,(1,'List2','randString');
SELECT p.*
FROM #tbl
PIVOT
(
MAX([Value]) FOR ListNum IN(List1,List2,List3)
) p;
But with your data this seems rather random...
UPDATE: A random approach...
The following approach will sort the values into their columns rather randomly:
DECLARE #tbl TABLE(ListNum VARCHAR(100),[Value] VARCHAR(100));
INSERT INTO #tbl VALUES
('List1','A')
,('List3','123')
,('List3','CDE')
,('List1','Somestring')
,('List2','randString');
--This will use three independant, but numbered sets and join them:
WITH All1 AS (SELECT [Value],ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS RandomNumber FROM #tbl WHERE ListNum='List1')
,All2 AS (SELECT [Value],ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS RandomNumber FROM #tbl WHERE ListNum='List2')
,All3 AS (SELECT [Value],ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS RandomNumber FROM #tbl WHERE ListNum='List3')
SELECT All1.[Value] AS List1
,All2.[Value] AS List2
,All3.[Value] AS List3
FROM All1
FULL OUTER JOIN All2 ON All1.RandomNumber=All2.RandomNumber
FULL OUTER JOIN All3 ON All1.RandomNumber=All3.RandomNumber ;
Hint: There is no implicit sort order in your table!
From your comment:
It’s simply the index / instance number. randString is the first non-null row.
Without a specific ORDER BY the same SELECT may return your data in any random order. So there is no first non-null row, at least not in the meaning of first comes before second...
Something with recursive CTE may work:
DECLARE #tbl TABLE( ListNum VARCHAR(100),[Value] VARCHAR(100));
INSERT INTO #tbl VALUES
( 'List1','A')
,( 'List3','123')
,( 'List3','CDE')
,( 'List1','Somestring')
,( 'List2','randString');
DECLARE #mmax int;
SELECT #mmax = cnt from (SELECT TOP 1 count(*) cnt from #tbl group by ListNum ORDER BY count(*) DESC) t;
With rec AS (
SELECT 1 AS num
UNION ALL
SELECT num+1 FROM rec WHERE num+1<=#mmax
)
SELECT t1.List1, t2.List2, t3.List3 FROM rec
FULL JOIN (
select Value as List1, row_number() over(order by ListNum) rn from #tbl where ListNum = 'List1'
) t1
ON rec.num = t1.rn
FULL JOIN
(
select Value as List2, row_number() over(order by ListNum) rn from #tbl where ListNum = 'List2'
) t2
ON rec.num = t2.rn
FULL JOIN
(
select Value as List3, row_number() over(order by ListNum) rn from #tbl where ListNum = 'List3'
) t3
ON rec.num = t3.rn;
DEMO
Ordinarily you'd use an aggregating operation such as MAX because it hides nulls (a null can never be the max unless there is no other valid value in the group).. but your query is a bit odd because you don't seem to have a solid pivot anchor, and you're allowing any of your data to become associated with anything else. In the real world this probably wouldn't happen because it isn't particularly useful
Better example data:
Person, Attribute, Value
1, Name, John
1, Age, 10
2, Name, Sarah
3 Age, 39
Pivoting query:
SELECT
Person,
MAX(case when attribute = 'name' then value end) as name,
MAX(case when attribute = 'age' then value end) as age
FROM
data
GROUP BY person
Result:
Person, Name, Age
1, John, 10
2, Sarah, NULL
3, NULL, 39

SQL SELECT Convert Min/Max into Separate Rows

I have a table that has a min and max value that I'd like create a row for each valid number in a SELECT statement.
Original table:
| Foobar_ID | Min_Period | Max_Period |
---------------------------------------
| 1 | 0 | 2 |
| 2 | 1 | 4 |
I'd like to turn that into:
| Foobar_ID | Period_Num |
--------------------------
| 1 | 0 |
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 2 | 2 |
| 2 | 3 |
| 2 | 4 |
The SELECT results need to come out as one result-set, so I'm not sure if a WHILE loop would work in my case.
If you expect just a handful of rows per foobar, then this is a good opportunity to learn about recursive CTEs:
with cte as (
select foobar_id, min_period as period_num, max_period
from original t
union all
select foobar_id, min_period + 1 as period_num, max_period
from cte
where period_num < max_period
)
select foobar_id, period_num
from cte
order by foobar_id, period_num;
You can extend this to any number of periods by setting the MAXRECURSION option to 0.
One method would be to use a Tally table, ther's plenty of examples out there, but I'm going to create a very small one in this example. Then you can JOIN onto that and return your result set.
--Create the Tally Table
CREATE TABLE #Tally (I int);
WITH ints AS(
SELECT 0 AS i
UNION ALL
SELECT i + 1
FROM ints
WHERE i + 1 <= 10)
--And in the numbers go!
INSERT INTO #Tally
SELECT i
FROM ints;
GO
--Create the sample table
CREATE TABLE #Sample (ID int IDENTITY(1,1),
MinP int,
MaxP int);
--Sample data
INSERT INTO #Sample (Minp, MaxP)
VALUES (0,2),
(1,4);
GO
--And the solution
SELECT S.ID,
T.I AS P
FROM #Sample S
JOIN #Tally T ON T.I BETWEEN S.MinP AND S.MaxP
ORDER BY S.ID, T.I;
GO
--Clean up
DROP TABLE #Sample;
DROP TABLE #Tally;
Depending on the size of the data and the range of the period, the easiest way to do this is to use a dynamic number fact table, as follows:
WITH rn AS (SELECT ROW_NUMBER() OVER (ORDER BY object_id) -1 as period_num FROM sys.objects)
SELECT f.foobar_id, rn.period_num
FROM foobar f
INNER JOIN rn ON rn.period_num BETWEEN f.min_period AND f.max_period
However, if you're working with a larger volume of data, it will be worth creating a number fact table with an index. You can even use a TVV for this:
-- Declare the number fact table
DECLARE #rn TABLE (period_num INT IDENTITY(0, 1) primary key, dummy int)
-- Populate the fact table so that all periods are covered
WHILE (SELECT COUNT(1) FROM #rn) < (SELECT MAX(max_period) FROM foobar)
INSERT #rn select 1 from sys.objects
-- Select using a join to the fact table
SELECT f.foo_id, rn.period_num
FROM foobar f
inner join #rn rn on rn.period_num between f.min_period and f.max_period
Just Create a function sample date and use it
CREATE FUNCTION [dbo].[Ufn_GetMInToMaxVal] (#Min_Period INT,#Max_Period INT )
RETURNS #OutTable TABLE
(
DATA INT
)
AS
BEGIN
;WIth cte
AS
(
SELECT #Min_Period As Min_Period
UNION ALL
SELECT Min_Period+1 FRom
cte
WHERE Min_Period < #Max_Period
)
INSERT INTO #OutTable
SELECT * FROM cte
RETURN
END
Get the result by executing sql statement
DECLARE #Temp AS TABLE(
Foobar_ID INT,
Min_Period INT,
Max_Period INT
)
INSERT INTO #Temp
SELECT 1, 0,2 UNION ALL
SELECT 2, 1,4
SELECT Foobar_ID ,
DATA
FROM #Temp
CROSS APPLY
[dbo].[Ufn_GetMInToMaxVal] (Min_Period,Max_Period)
Result
Foobar_ID DATA
----------------
1 0
1 1
1 2
2 1
2 2
2 3
2 4

How do I Pivot Vertical Data to Horizontal Data SQL with Variable Row Lengths?

Okay I have the following table.
Name ID Website
Aaron | 2305 | CoolSave1
Aaron | 8464 | DiscoWorld1
Adriana | 2956 | NewCin1
Adriana | 5991 | NewCin2
Adriana | 4563 NewCin3
I would like to transform it into the following way.
Adriana | 2956 | NewCin1 | 5991 | NewCin2 | 4563 | NewCin3
Aaron | 2305 | CoolSave1 | 8464 | DiscoWorld | NULL | NULL
As you can see i am trying to take the first name from the first table and make a single row with all the IDs / Websites associated with that name. The problem is, there is a variable amount of websites that may be associated with each name. To handle this i'd like to just make a table with with the number of fields sequal to the max line item, and then for the subsequent lineitems, plug in a NULL where there are not enough data.
In order to get the result, you will need to apply both the UNPIVOT and the PIVOT functions to the data. The UNPIVOT will take the columns (ID, website) and convert them to rows, once this is done, then you can PIVOT the data back into columns.
The UNPIVOT code will be similar to the following:
select name,
col+'_'+cast(col_num as varchar(10)) col,
value
from
(
select name,
cast(id as varchar(11)) id,
website,
row_number() over(partition by name order by id) col_num
from yt
) src
unpivot
(
value
for col in (id, website)
) unpiv;
See SQL Fiddle with Demo. This gives a result:
| NAME | COL | VALUE |
-------------------------------------
| Aaron | id_1 | 2305 |
| Aaron | website_1 | CoolSave1 |
| Aaron | id_2 | 8464 |
| Aaron | website_2 | DiscoWorld1 |
As you can see I applied a row_number() to the data prior to the unpivot, the row number is used to generate the new column names. The columns in the UNPIVOT must also be of the same datatype, I applied a cast to the id column in the subquery to convert the data to a varchar prior to the pivot.
The col values are then used in the PIVOT. Once the data has been unpivoted, you apply the PIVOT function:
select *
from
(
select name,
col+'_'+cast(col_num as varchar(10)) col,
value
from
(
select name,
cast(id as varchar(11)) id,
website,
row_number() over(partition by name order by id) col_num
from yt
) src
unpivot
(
value
for col in (id, website)
) unpiv
) d
pivot
(
max(value)
for col in (id_1, website_1, id_2, website_2, id_3, website_3)
) piv;
See SQL Fiddle with Demo.
The above version works great if you have a limited or known number of values. But if the number of rows is unknown, then you will need to use dynamic SQL to generate the result:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME( col+'_'+cast(col_num as varchar(10)))
from
(
select row_number() over(partition by name order by id) col_num
from yt
) t
cross apply
(
select 'id' col union all
select 'website'
) c
group by col, col_num
order by col_num, col
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT name,' + #cols + '
from
(
select name,
col+''_''+cast(col_num as varchar(10)) col,
value
from
(
select name,
cast(id as varchar(11)) id,
website,
row_number() over(partition by name order by id) col_num
from yt
) src
unpivot
(
value
for col in (id, website)
) unpiv
) x
pivot
(
max(value)
for col in (' + #cols + ')
) p '
execute(#query);
See SQL Fiddle with Demo. Both versions give the result:
| NAME | ID_1 | WEBSITE_1 | ID_2 | WEBSITE_2 | ID_3 | WEBSITE_3 |
------------------------------------------------------------------------
| Aaron | 2305 | CoolSave1 | 8464 | DiscoWorld1 | (null) | (null) |
| Adriana | 2956 | NewCin1 | 4563 | NewCin3 | 5991 | NewCin2 |