SQL Server: ORDER BY parameters in IN statement - sql

I have a SQL statement that is the following:
SELECT A.ID, A.Name
FROM Properties A
WHERE A.ID IN (110, 105, 104, 106)
When I run this SQL statement, the output is ordered according to the IN list by ID automatically and returns
104 West
105 East
106 North
110 South
I want to know if it is possible to order by the order the parameters are listed within the IN clause. so it would return
110 South
105 East
104 West
106 North

I think the easiest way in SQL Server is to use a JOIN with VALUES:
SELECT p.ID, p.Name
FROM Properties p JOIN
(VALUES (110, 1), (105, 2), (104, 3), (106, 4)) ids(id, ordering)
ON p.id = a.id
ORDER BY ids.ordering;

Sure...
just add an Order clause with a case in it
SELECT A.ID, A.Name
FROM Properties A
WHERE A.ID IN (110,105,104,106)
Order By case A.ID
when 110 then 0
when 105 then 1
when 104 then 2
when 106 then 3 end

With the help of a parsing function which returns the sequence as well
SELECT B.Key_PS
, A.ID
, A.Name
FROM Properties A
Join (Select * from [dbo].[udf-Str-Parse]('110,105,104,106',',')) B on A.ID=B.Key_Value
WHERE A.ID IN (110,105,104,106)
Order by Key_PS
The UDF if you need
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimeter varchar(10))
--Usage: Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
-- Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
-- Select * from [dbo].[udf-Str-Parse]('id26,id46|id658,id967','|')
-- Select * from [dbo].[udf-Str-Parse]('hello world. It. is. . raining.today','.')
Returns #ReturnTable Table (Key_PS int IDENTITY(1,1), Key_Value varchar(max))
As
Begin
Declare #XML xml;Set #XML = Cast('<x>' + Replace(#String,#Delimeter,'</x><x>')+'</x>' as XML)
Insert Into #ReturnTable Select Key_Value = ltrim(rtrim(String.value('.', 'varchar(max)'))) FROM #XML.nodes('x') as T(String)
Return
End
The Parser alone would return
Select * from [dbo].[udf-Str-Parse]('110,105,104,106',',')
Key_PS Key_Value
1 110
2 105
3 104
4 106

What you could potentially do is:
Create a TVF that would split string and keep original order.
This questions seems to have this function already written: MS SQL: Select from a split string and retain the original order (keep in mind that there might be other approaches, not only those, covered in this question, I just gave it as an example to understand what function should do)
So now if you'd run this query:
SELECT *
FROM dbo.Split('110,105,104,106', ',') AS T;
It would bring back this table as a result.
items rownum
------------
110 1
105 2
104 3
106 4
Following that, you could simply query your table, join with this TVF passing your IDs as a parameter:
SELECT P.ID, P.Name
FROM Properties AS P
INNER JOIN dbo.Split('110,105,104,106', ',') AS T
ON T.items = P.ID
ORDER BY T.rownum;
This should retain order of parameters.
If you need better performance, I'd advice to put records from TVF into hash table, index it and then join with actual table. See query below:
SELECT T.items AS ID, T.rownum AS SortOrder
INTO #Temporary
FROM dbo.Split('110,105,104,106', ',') AS T;
CREATE CLUSTERED INDEX idx_Temporary_ID
ON #Temporary(ID);
SELECT P.ID, P.Name
FROM Properties AS P
INNER JOIN #Temporary AS T
ON T.ID = P.ID
ORDER BY T.SortOrder;
This should work better on larger data sets and equally well on small ones.

Here is a solution that does not rely on hard codes values or dynamic sql (to eliminate hard coding values).
I would build a table (maybe temp or variable) with OrderByValue and OrderBySort and insert from the application.
OrderByValue OrderBySort
110 1
105 2
104 3
106 4
Then I would join on the value and sort by the sort. The join will be the same as the In clause.
SELECT A.ID, A.Name
FROM Properties A
JOIN TempTable B On A.ID = B.OrderByValue
Order By B.OrderBySort

Another solution for this problem is prepare a temporary table for IN clause like
declare #InTable table (ID int, SortOrder int not null identity(1,1));
We can fill this temp table with your data in order you want
insert into #InTable values (110), (105), (104), (106);
At last we need to modify your question to use this temp table like this
select A.ID, A.Name
from Properties A
inner join #InTable as Sort on A.ID = Sort.ID
order by Sort.SortOrder
On the output you can see this
ID Name
110 South
105 East
104 West
106 North
In this solution you don't need to provide order in special way. You just need to insert values in order you want.

Related

Simple CTE recursive query

I am sorry to bother with such a simple question, but I decided to learn CTE recursive queries and I am unable to get my query work even after scoping many sources and threads. So I am humbly asking for pointing out my mistake(s).
Here is a part of table I am querying:
ID ContainerInstanceID ItemID ContentContainerInstanceID
--------- -------------------- ----------- --------------------------
73 40 NULL 41
69 40 23885 NULL
68 40 29683 NULL
67 40 29686 NULL
72 41 27392 NULL
71 41 29235 NULL
70 41 29213 NULL
I assembled this simple CTE query:
;WITH ContainerContent_CTE(InstanceID,ItemID,ContentContainerInstanceID) AS
(
-- ROOT set accordig to input parameter
SELECT ContainerInstanceID,SCA.ItemID,SCA.ContentContainerInstanceID
FROM StockContainerAssignments as SCA
WHERE SCA.ContainerInstanceID = 40 -- input parameter
UNION ALL
-- recursive data
SELECT ContainerInstanceID,SCA2.ItemID,SCA2.ContentContainerInstanceID
FROM ContainerContent_CTE AS CC
JOIN StockContainerAssignments as SCA2 on CC.InstanceID = SCA2.ContentContainerInstanceID
)
SELECT * FROM ContainerContent_CTE;
What I am trying to do is to take a top-level container, in this example it has ID = 40, which is my input parameter. Then, I try to connect other levels by linking ContainerInstanceID with ContentContainerInstanceID. In my example it is not null ar row ID = 73. This should add another 3 rows to my result set (so it should look similar to the example data I presented above), but I still get only top level rows:
InstanceID ItemID ContentContainerInstanceID
----------- ----------- --------------------------
40 29686 NULL
40 29683 NULL
40 23885 NULL
40 NULL 41
I appreciate hints to help me stumble over this subject.
You just had a few little things out of place. This should work for you.
with ContainerContent_CTE as
(
select SCA.ContainerInstanceID
,SCA.ItemID
,SCA.ContentContainerInstanceID
FROM StockContainerAssignments as SCA
WHERE SCA.ContainerInstanceID = 40 -- input parameter
UNION ALL
select SCA.ContainerInstanceID
,SCA.ItemID
,SCA.ContentContainerInstanceID
FROM StockContainerAssignments as SCA
inner join ContainerContent_CTE cte on cte.ContentContainerInstanceID = SCA.ContainerInstanceID
)
select *
from ContainerContent_CTE
This works for me
declare #t table (id int, instance int, container int);
insert into #t values
(73, 40, 41)
, (69, 40, NULL)
, (68, 40, NULL)
, (67, 40, NULL)
, (72, 41, NULL)
, (71, 41, NULL)
, (70, 41, NULL);
select * from #t;
with cte as
( select t.id, t.instance, t.container
from #t t
where t.instance = 40
union all
select t.id, t.instance, t.container
from cte
join #t t
on t.instance = cte.container
)
select * from cte;
As expected, a dumb little mistake got in a way - in the ON clause, I was connecting the parent and child with the oposite pairs of IDs. Since I'm only learning CTE, it was hard to see for me. Here it's fixed (for referrence):
;WITH ContainerContent_CTE(InstanceID,temID,ContentContainerInstanceID) AS
(
-- ROOT set accordig to input parameter
SELECT ContainerInstanceID,SCA.ItemID,SCA.ContentContainerInstanceID
FROM StockContainerAssignments as SCA
WHERE SCA.ContainerInstanceID = 40 -- input parameter
UNION ALL
-- recursive data
SELECT ContainerInstanceID,SCA2.ItemID,SCA2.ContentContainerInstanceID
FROM ContainerContent_CTE AS CC
INNER JOIN StockContainerAssignments as SCA2 on CC.ContentContainerInstanceID = SCA2.ContainerInstanceID)
SELECT * FROM ContainerContent_CTE;
Thank you for suggestions.

sql query db2 9.7 [duplicate]

Is there a built in function for comma separated column values in DB2 SQL?
Example: If there are columns with an ID and it has 3 rows with the same ID but have three different roles, the data should be concatenated with a comma.
ID | Role
------------
4555 | 2
4555 | 3
4555 | 4
The output should look like the following, per row:
4555 2,3,4
LISTAGG function is new function in DB2 LUW 9.7
see example:
create table myTable (id int, category int);
insert into myTable values (1, 1);
insert into myTable values (2, 2);
insert into myTable values (5, 1);
insert into myTable values (3, 1);
insert into myTable values (4, 2);
example: select without any order in grouped column
select category, LISTAGG(id, ', ') as ids from myTable group by category;
result:
CATEGORY IDS
--------- -----
1 1, 5, 3
2 2, 4
example: select with order by clause in grouped column
select
category,
LISTAGG(id, ', ') WITHIN GROUP(ORDER BY id ASC) as ids
from myTable
group by category;
result:
CATEGORY IDS
--------- -----
1 1, 3, 5
2 2, 4
I think with this smaller query, you can do what you want.
This is equivalent of MySQL's GROUP_CONCAT in DB2.
SELECT
NUM,
SUBSTR(xmlserialize(xmlagg(xmltext(CONCAT( ', ',ROLES))) as VARCHAR(1024)), 3) as ROLES
FROM mytable
GROUP BY NUM;
This will output something like:
NUM ROLES
---- -------------
1 111, 333, 555
2 222, 444
assumming your original result was something like that:
NUM ROLES
---- ---------
1 111
2 222
1 333
2 444
1 555
Depending of the DB2 version you have, you can use XML functions to achieve this.
Example table with some data
create table myTable (id int, category int);
insert into myTable values (1, 1);
insert into myTable values (2, 2);
insert into myTable values (3, 1);
insert into myTable values (4, 2);
insert into myTable values (5, 1);
Aggregate results using xml functions
select category,
xmlserialize(XMLAGG(XMLELEMENT(NAME "x", id) ) as varchar(1000)) as ids
from myTable
group by category;
results:
CATEGORY IDS
-------- ------------------------
1 <x>1</x><x>3</x><x>5</x>
2 <x>2</x><x>4</x>
Use replace to make the result look better
select category,
replace(
replace(
replace(
xmlserialize(XMLAGG(XMLELEMENT(NAME "x", id) ) as varchar(1000))
, '</x><x>', ',')
, '<x>', '')
, '</x>', '') as ids
from myTable
group by category;
Cleaned result
CATEGORY IDS
-------- -----
1 1,3,5
2 2,4
Just saw a better solution using XMLTEXT instead of XMLELEMENT here.
Since DB2 9.7.5 there is a function for that:
LISTAGG(colname, separator)
check this for more information: Using LISTAGG to Turn Rows of Data into a Comma Separated List
My problem was to transpose row fields(CLOB) to column(VARCHAR) with a CSV and use the transposed table for reporting. Because transposing on report layer slows down the report.
One way to go is to use recursive SQL. You can find many articles about that but its difficult and resource consuming if you want to join all your recursive transposed columns.
I created multiple global temp tables where I stored single transposed columns with one key identifier. Eventually, I had 6 temp tables for joining 6 columns but due to limited resource allocation I wasnt able to bring all columns together. I opted to below 3 formulas and then I just had to run 1 query which gave me output in 10 seconds.
I found various articles on using XML2CLOB functions and have found 3 different ways.
REPLACE(VARCHAR(XML2CLOB(XMLAGG(XMLELEMENT(NAME "A",ALIASNAME.ATTRIBUTENAME)))),'', ',') AS TRANSPOSED_OUTPUT
NVL(TRIM(',' FROM REPLACE(REPLACE(REPLACE(CAST(XML2CLOB(XMLAGG(XMLELEMENT(NAME "E", ALIASNAME.ATTRIBUTENAME))) AS VARCHAR(100)),'',' '),'',','), '', 'Nothing')), 'Nothing') as TRANSPOSED_OUTPUT
RTRIM(REPLACE(REPLACE(REPLACE(VARCHAR(XMLSERIALIZE(XMLAGG(XMLELEMENT(NAME "A",ALIASNAME.ATTRIBUTENAME) ORDER BY ALIASNAME.ATTRIBUTENAME) AS CLOB)), '',','),'',''),'','')) AS TRANSPOSED_OUTPUT
Make sure you are casting your "ATTRIBUTENAME" to varchar in a subquery and then calling it here.
other possibility, with recursive cte
with tablewithrank as (
select id, category, rownumber() over(partition by category order by id) as rangid , (select count(*) from myTable f2 where f1.category=f2.category) nbidbycategory
from myTable f1
),
cte (id, category, rangid, nbidbycategory, rangconcat) as (
select id, category, rangid, nbidbycategory, cast(id as varchar(500)) from tablewithrank where rangid=1
union all
select f2.id, f2.category, f2.rangid, f2.nbidbycategory, cast(f1.rangconcat as varchar(500)) || ',' || cast(f2.id as varchar(500)) from cte f1 inner join tablewithrank f2 on f1.rangid=f2.rangid -1 and f1.category=f2.category
)
select category, rangconcat as IDS from cte
where rangid=nbidbycategory
Try this:
SELECT GROUP_CONCAT( field1, field2, field3 ,field4 SEPARATOR ', ')

SQL Merge table rows based on IN criteria

I have a table result like following
Code Counts1 Counts2 TotalCounts
1 10 20 30
4 15 18 33
5 5 14 19
... ... ... ...
What I am trying to achieve is merging counts for all rows where Code (the column counts are grouped on) belongs IN (1,4). However, within all my research, all I found was methods to merge rows based on a common value for each row (same id, etc.)
Is there a way to merge rows based on IN criteria, so I know if I should research it further?
How about a union?
select
1 as Code,
sum(Counts1) as Counts1,
sum(Counts2) as Counts2,
sum(TotalCount) as TotalCounts
from
YourTable
where
code in (1,4)
union
select *
from
YourTable
where
code not in(1,4)
Just assuming you will have numerous groupings (See the #Groupings mapping table)
You can have dynamic groupings via a LEFT JOIN
Example
Declare #YourTable Table ([Code] varchar(50),[Counts1] int,[Counts2] int,[TotalCounts] int)
Insert Into #YourTable Values
(1,10,20,30)
,(4,15,18,33)
,(5,5,14,19)
Declare #Groupings Table (Code varchar(50),Grp int)
Insert Into #Groupings values
(1,1)
,(4,1)
select code = Isnull(B.NewCode,A.Code)
,Counts1 = sum(Counts1)
,Counts2 = sum(Counts2)
,TotalCounts = sum(TotalCounts)
From #YourTable A
Left Join (
Select *
,NewCode = (Select Stuff((Select ',' + Code From #Groupings Where Grp=B1.Grp For XML Path ('')),1,1,'') )
From #Groupings B1
) B on (A.Code=B.Code)
Group By Isnull(B.NewCode,A.Code)
Returns
code Counts1 Counts2 TotalCounts
1,4 25 38 63
5 5 14 19
If it helps with the Visualization, the subquery generates
Code Grp NewCode
1 1 1,4
4 1 1,4
sum the count, remove code from the select statement. Add a new column to group 1 and 4 using case statement lets name this groupN. then in SQL group it by groupN.
You are correct, grouping has to be based on common value. so by creating a new column, you are making that happen.

Improving performance of Union All query

I have a pretty extended Union All query with Union All used 34 times as follows:
INSERT INTO #Temp2
(RowNumber,ValFromUser,ColumnName,ValFromFunc,FuncWeight,percentage)
SELECT RowNumber,#firstname,'firstname',
PercentMatch,fw1.FunctionWeight,
PercentMatch * fw1.FunctionWeight
FROM
dbo.MatchFirstName(#firstname, #checkbool) mfn
CROSS JOIN
dbo.FunctionWeights fw1
WHERE
#firstname is not null and fw1.FunctionId = 1
UNION ALL
SELECT RowNumber, #MiddleName,'Middlename',
PercentMatch, fw2.FunctionWeight,
PercentMatch * fw2.FunctionWeight
FROM
dbo.MatchMiddleName(#MiddleName, #checkbool) mmn
CROSS JOIN
dbo.FunctionWeights fw2
WHERE
#MiddleName IS NOT NULL AND
fw2.FunctionId = 2
UNION ALL
SELECT RowNumber, #LastName,'Lastname',
PercentMatch, fw3.FunctionWeight,
PercentMatch * fw3.FunctionWeight
FROM
dbo.MatchLastName(#LastName, #checkbool) mln
CROSS JOIN
dbo.FunctionWeights fw3
WHERE
#LastName IS NOT NULL AND
fw3.FunctionId = 3
UNION ALL...........
and so on.
dbo.MatchFirstName is an inline TVF which looks like this :
CREATE FUNCTION MatchFirstName (#FirstNameFromUser nvarchar(20) , #checkbool int)
RETURNS TABLE AS RETURN
select RowId As RowNumber, PercentMatch from dbo.MATCH(#FirstNameFromUser, 'firstname' , #checkbool )
GO
It is calling a dbo.Match function which is a multiline UDF and has some logic contained in it for calculating the percentage match based on field names passed as parameters to it.
34 Union All's means collaboration of results from 35 Inline TVF's using select query based on 35 different columns individually in them.All these belong to one table.
Now since I can not afford to have 35 indexes but yet I want to avoid table scans as this query is taking pretty long.
Is there a work around to improve performance here ?

Simple Query to Grab Max Value for each ID

OK I have a table like this:
ID Signal Station OwnerID
111 -120 Home 1
111 -130 Car 1
111 -135 Work 2
222 -98 Home 2
222 -95 Work 1
222 -103 Work 2
This is all for the same day. I just need the Query to return the max signal for each ID:
ID Signal Station OwnerID
111 -120 Home 1
222 -95 Work 1
I tried using MAX() and the aggregation messes up with the Station and OwnerID being different for each record. Do I need to do a JOIN?
Something like this? Join your table with itself, and exclude the rows for which a higher signal was found.
select cur.id, cur.signal, cur.station, cur.ownerid
from yourtable cur
where not exists (
select *
from yourtable high
where high.id = cur.id
and high.signal > cur.signal
)
This would list one row for each highest signal, so there might be multiple rows per id.
You are doing a group-wise maximum/minimum operation. This is a common trap: it feels like something that should be easy to do, but in SQL it aggravatingly isn't.
There are a number of approaches (both standard ANSI and vendor-specific) to this problem, most of which are sub-optimal in many situations. Some will give you multiple rows when more than one row shares the same maximum/minimum value; some won't. Some work well on tables with a small number of groups; others are more efficient for a larger number of groups with smaller rows per group.
Here's a discussion of some of the common ones (MySQL-biased but generally applicable). Personally, if I know there are no multiple maxima (or don't care about getting them) I often tend towards the null-left-self-join method, which I'll post as no-one else has yet:
SELECT reading.ID, reading.Signal, reading.Station, reading.OwnerID
FROM readings AS reading
LEFT JOIN readings AS highersignal
ON highersignal.ID=reading.ID AND highersignal.Signal>reading.Signal
WHERE highersignal.ID IS NULL;
In classic SQL-92 (not using the OLAP operations used by Quassnoi), then you can use:
SELECT g.ID, g.MaxSignal, t.Station, t.OwnerID
FROM (SELECT id, MAX(Signal) AS MaxSignal
FROM t
GROUP BY id) AS g
JOIN t ON g.id = t.id AND g.MaxSignal = t.Signal;
(Unchecked syntax; assumes your table is 't'.)
The sub-query in the FROM clause identifies the maximum signal value for each id; the join combines that with the corresponding data row from the main table.
NB: if there are several entries for a specific ID that all have the same signal strength and that strength is the MAX(), then you will get several output rows for that ID.
Tested against IBM Informix Dynamic Server 11.50.FC3 running on Solaris 10:
+ CREATE TEMP TABLE signal_info
(
id INTEGER NOT NULL,
signal INTEGER NOT NULL,
station CHAR(5) NOT NULL,
ownerid INTEGER NOT NULL
);
+ INSERT INTO signal_info VALUES(111, -120, 'Home', 1);
+ INSERT INTO signal_info VALUES(111, -130, 'Car' , 1);
+ INSERT INTO signal_info VALUES(111, -135, 'Work', 2);
+ INSERT INTO signal_info VALUES(222, -98 , 'Home', 2);
+ INSERT INTO signal_info VALUES(222, -95 , 'Work', 1);
+ INSERT INTO signal_info VALUES(222, -103, 'Work', 2);
+ SELECT g.ID, g.MaxSignal, t.Station, t.OwnerID
FROM (SELECT id, MAX(Signal) AS MaxSignal
FROM signal_info
GROUP BY id) AS g
JOIN signal_info AS t ON g.id = t.id AND g.MaxSignal = t.Signal;
111 -120 Home 1
222 -95 Work 1
I named the table Signal_Info for this test - but it seems to produce the right answer.
This only shows that there is at least one DBMS that supports the notation. However, I am a little surprised that MS SQL Server does not - which version are you using?
It never ceases to surprise me how often SQL questions are submitted without table names.
WITH q AS
(
SELECT c.*, ROW_NUMBER() OVER (PARTITION BY id ORDER BY signal DESC) rn
FROM mytable
)
SELECT *
FROM q
WHERE rn = 1
This will return one row even if there are duplicates of MAX(signal) for a given ID.
Having an index on (id, signal) will greatly improve this query.
with tab(id, sig, sta, oid) as
(
select 111 as id, -120 as signal, 'Home' as station, 1 as ownerId union all
select 111, -130, 'Car', 1 union all
select 111, -135, 'Work', 2 union all
select 222, -98, 'Home', 2 union all
select 222, -95, 'Work', 1 union all
select 222, -103, 'Work', 2
) ,
tabG(id, maxS) as
(
select id, max(sig) as sig from tab group by id
)
select g.*, p.* from tabG g
cross apply ( select top(1) * from tab t where t.id=g.id order by t.sig desc ) p
We can do using self join
SELECT T1.ID,T1.Signal,T2.Station,T2.OwnerID
FROM (select ID,max(Signal) as Signal from mytable group by ID) T1
LEFT JOIN mytable T2
ON T1.ID=T2.ID and T1.Signal=T2.Signal;
Or you can also use the following query
SELECT t0.ID,t0.Signal,t0.Station,t0.OwnerID
FROM mytable t0
LEFT JOIN mytable t1 ON t0.ID=t1.ID AND t1.Signal>t0.Signal
WHERE t1.ID IS NULL;
select a.id, b.signal, a.station, a.owner from
mytable a
join
(SELECT ID, MAX(Signal) as Signal FROM mytable GROUP BY ID) b
on a.id = b.id AND a.Signal = b.Signal
SELECT * FROM StatusTable
WHERE Signal IN (
SELECT A.maxSignal FROM
(
SELECT ID, MAX(Signal) AS maxSignal
FROM StatusTable
GROUP BY ID
) AS A
);
select
id,
max_signal,
owner,
ownerId
FROM (
select * , rank() over(partition by id order by signal desc) as max_signal from table
)
where max_signal = 1;