Basic code optimization, exclusion of double grouping - sql

I want to show only one row per column (PN) from result of calculations base on 2 indicators (WK) and (Prio), and I think that doing this in way below using double grouping is... stupid, but I see no other solution. Is there other way to reach same result as query below?
CREATE TABLE #table
(
[PN] varchar(3) null
,[WK] int null
,[Prio] int null
);
INSERT INTO #table
(
[PN]
,[WK]
,[Prio]
)
VALUES
('AAA',37,1)
,('AAA',37,2)
,('AAA',38,3)
,('BBB',39,1)
,('BBB',39,2)
,('BBB',37,3)
,('BBB',38,4)
,('CCC',null,1)
,('CCC',null,2)
,('CCC',37,3)
,('CCC',38,4);
SELECT GTG.[PN]
,GTG.[WK]
,MIN([Prio]) [Prio]
FROM
(
SELECT [PN]
,MIN([WK]) [WK]
FROM #table
GROUP BY [PN]
) GTG
LEFT JOIN #table TMP
ON GTG.[PN] = TMP.[PN]
and GTG.[WK] = TMP.[WK]
GROUP BY GTG.[PN],GTG.[WK];
DROP TABLE #table;

Try using a Common Table Expression (CTE) with ROW_NUMBER():
SQL Fiddle
MS SQL Server 2017 Schema Setup:
CREATE TABLE MyTable
(
[PN] varchar(3) null
,[WK] int null
,[Prio] int null
);
INSERT INTO MyTable
(
[PN]
,[WK]
,[Prio]
)
VALUES
('AAA',37,1)
,('AAA',37,2)
,('AAA',38,3)
,('BBB',39,1)
,('BBB',39,2)
,('BBB',37,3)
,('BBB',38,4)
,('CCC',null,1)
,('CCC',null,2)
,('CCC',37,3)
,('CCC',38,4);
Query 1:
;WITH CTE
AS
( SELECT [PN],
[WK],
[Prio],
ROW_Number() OVER (Partition BY [PN] ORDER BY COALESCE([WK], 999), [Prio]) AS RN
FROM MyTable T1
)
SELECT [PN], [WK], [Prio]
FROM CTE
WHERE RN = 1
Results:
| PN | WK | Prio |
|-----|----|------|
| AAA | 37 | 1 |
| BBB | 37 | 3 |
| CCC | 37 | 3 |

do you find below
SELECT GTG.[PN]
,min(GTG.[WK])
,MIN([Prio]) [Prio]
FROM
#table GTG
LEFT JOIN #table TMP
ON GTG.[PN] = TMP.[PN]
and GTG.[WK] = TMP.[WK]
GROUP BY GTG.[PN]

You seem to want window functions. This should do what you want:
select t.*
from (select t.*, row_number() over (partition by pn order by coalesce(wk, 999), prio) as seqnum
from #table t
) t
where seqnum = 1;
Here is a db<>fiddle.
Note: wk possibly represents a week value, so I replaced it with 999 when it is NULL for the sorting to be correct. You could filter out NULL values or use a CASE expression if COALESCE() does not quite meet your needs.

I think, we can use Row_Number window function to sort result first and then apply where clause. Please try this-
;with cte as (
select
pn,
wk,
prio,
row_number() over (
partition by pn
order by (case when wk is not null then 0 else 1 end), wk, prio
) as rankid
from #table
)
select pn,wk,prio
from cte
where rankid =1;

Related

Frequency of all combinations of values for certain column

I have a dataset in SQL Server 2012 with a column for id and value, like this:
[id] [value]
--------------
A 15
A 11
A 11
B 13
B 15
B 12
C 12
C 13
D 13
D 12
My goal is to get a frequency count of all combinations of [value], with two caveats:
Order doesn't matter, so [11,12,15] is not counted separately from [12,11,15]
Repeated values are counted separately, so [11,11,12,15] is counted separately from [11,12,15]
I'm interested in all combinations, of any length (not just pairs)
So the outcome would look like:
[combo] [frequency]
---------------------
11,11,15 1
12,13,15 1
12,13 2
I've seen answers here involving recursion that answer similar questions but where order counts, and answers here involving self-joins that yield pair-wise combinations. These come close but I'm not quite sure how to adapt for my specific needs.
You can use string_agg():
select vals, count(*) as frequency
from (select string_agg(value, ',') within group (order by value) as vals, id
from t
group by id
) i
group by vals;
SQL Server 2012 doesn't support string_agg() but you can use the XML hack:
select vals, count(*) as frequency
from (select id,
stuff( (select concat(',', value)
from t t2
where t2.id = i.id
for xml path ('')
), 1, 1, ''
) as vals
from (select distinct id from t) i
) i
group by vals;
Your number string is just all the values with the same id in increasing order. So I'm treating the lowest id as a canonical name for the full sequence and all its matches. This spares all the string manipulations though you can expand as necessary.
Just tag each duplicate value with a counter and then look for groups that pair up completely.
with data as (
select id, value,
row_number() over (partition by id, value) as rn
), matches as (
select l.id, r.id as match
from data l full outer join data r on
l.value = r.value and l.rn = r.rn and l.id <= r.id
group by l.id
having count(l.id) = count(*) and count(r.id) = count(*)
)
select id, count(match) as frequency
from matches
group by id;
The logic in the middle query is also easily adaptable for finding subset of values in common.
You can achieve this using CTEs and row_number functions.
DECLARE #table table(id CHAR(1), val int)
insert into #table VALUES
('A',15),
('A',11),
('A',11),
('B',13),
('B',15),
('B',12),
('C',12),
('C',13),
('D',13),
('D',12);
;WITH CTE_rnk as
(
SELECT id,val, row_number() over (partition by id order by val) as rnk
from #table
),
CTE_concat as
(
SELECT id, cast(val as varchar(100)) as val, rnk
from CTE_rnk
where rnk =1
union all
SELECT r.id, cast(concat(c.val,',',r.val) as varchar(100)) as val,r.rnk
from CTE_rnk as r
inner join CTE_concat as c
on r.rnk = c.rnk+1
and r.id = c.id
),
CTE_maxcombo as
(
SELECT id,val, row_number() over(partition by id order by rnk desc) as rnk
from CTE_concat
)
select val as combo, count(*) as frequency
from CTE_maxcombo where rnk = 1
group by val
+----------+-----------+
| combo | frequency |
+----------+-----------+
| 11,11,15 | 1 |
| 12,13 | 2 |
| 12,13,15 | 1 |
+----------+-----------+

SQL Server : SELECT ID having only a single condition

I have a patients table with details such as conditions that the patient has. from the below table I want to select Patients, Claims which have ONLY a single condition - 'Hypertension'. Example Patient B is the expected output. Patient A will not be selected because he claimed for multiple conditions.
+----+---------+--------------+
| ID | ClaimID | Condition |
+----+---------+--------------+
| A | 14234 | Hypertension |
| A | 14234 | Diabetes |
| A | 63947 | Diabetes |
| B | 23853 | Hypertension |
+----+---------+--------------+
I tried using the NOT IN condition as below but doesn't seem to help
SELECT ID, ClaimID, Condition
FROM myTable
WHERE Condition IN ('Hypertension')
AND Condition NOT IN ('Diabetes')
One method uses not exists:
select t.*
from mytable t
where t.condition = 'Hypertension' and
not exists (select 1
from mytable t2
where t2.id = t.id and t2.condition <> t.condition
);
Or you can do it like this:
select
id,
claim_id,
condition
from
patient
where
id in
(
select
id
from
patient
group by
id having count (distinct condition) = 1
);
Result:
id claim_id condition
-- ----------- ----------------
B 23853 Hypertension
(1 rows affected)
Setup:
create table patient
(
id varchar(1),
claim_id int,
condition varchar(16)
);
insert into patient (id, claim_id, condition) values ('A', 14234, 'Hypertension');
insert into patient (id, claim_id, condition) values ('A', 14234, 'Diabetes');
insert into patient (id, claim_id, condition) values ('A', 63947, 'Diabetes');
insert into patient (id, claim_id, condition) values ('B', 23853, 'Hypertension');
You can do this with a CTE.
I set up this CTE with two parameters, one being the Condition you seek, and the other being the max number of combined conditions to find (in your case 1).
DECLARE #myTable TABLE (Id VARCHAR(1), ClaimID INT, Condition VARCHAR(100))
INSERT INTO #myTable (Id, ClaimID, Condition)
SELECT 'A',14234,'Hypertension' UNION ALL
SELECT 'A',14234,'Diabetes' UNION ALL
SELECT 'A',63947,'Diabetes' UNION ALL
SELECT 'B',23853,'Hypertension'
DECLARE #Condition VARCHAR(100)
DECLARE #MaxConditions TINYINT
SET #Condition='Hypertension'
SET #MaxConditions=1
; WITH CTE AS
(
SELECT *, COUNT(2) OVER(PARTITION BY ClaimID) AS CN
FROM #myTable T1
WHERE EXISTS (SELECT 1 FROM #myTable T2 WHERE T1.ClaimID=T2.ClaimID AND T2.Condition=#Condition)
)
SELECT *
FROM CTE
WHERE CN<=#MaxConditions
If you don't care about the fluff, and just want all ClaimID's with just ONE condition regardless of which condition it is use this.
DECLARE #myTable TABLE (Id VARCHAR(1), ClaimID INT, Condition VARCHAR(100))
INSERT INTO #myTable (Id, ClaimID, Condition)
SELECT 'A',14234,'Hypertension' UNION ALL
SELECT 'A',14234,'Diabetes' UNION ALL
SELECT 'A',63947,'Diabetes' UNION ALL
SELECT 'B',23853,'Hypertension'
DECLARE #MaxConditions TINYINT
SET #MaxConditions=1
; WITH CTE AS
(
SELECT *, COUNT(2) OVER(PARTITION BY ClaimID) AS CN
FROM #myTable T1
)
SELECT *
FROM CTE
WHERE CN<=#MaxConditions
Here is one method using Having clause
SELECT t.*
FROM mytable t
WHERE EXISTS (SELECT 1
FROM mytable t2
WHERE t2.id = t.id
HAVING Count(CASE WHEN condition = 'Hypertension' THEN 1 END) > 0
AND Count(CASE WHEN condition != 'Hypertension' THEN 1 END) = 0)
And yet a couple of other ways to do this:
declare #TableA table(Id char,
ClaimId int,
Condition varchar(250));
insert into #TableA (id, claimid, condition)
values ('A', 14234, 'Hypertension'),
('A', 14234, 'Diabetes'),
('A', 63947, 'Diabetes'),
('B', 23853, 'Hypertension')
select id, claimid, condition
from #TableA a
where not exists(select id
from #TableA b
where a.id = b.id
group by b.id
having count(b.id) > 1)
OR
;with cte as
(
select id, claimid, condition
from #TableA
)
,
cte2 as
(
Select id, count(Id) as counts
from cte
group by id
having count(id) < 2
)
Select cte.id, claimid, condition
From cte
inner join
cte2
on cte.id = cte2.id
I decided to revise my answer into an appropriate one.
A simple solution to your question is to count the rows instead of the ID values (since it's not an integer).
Here is a simple introduction:
SELECT
ID
FROM
#PatientTable
GROUP BY
ID
HAVING
ID = ID AND COUNT(*) = 1
This will Return the ID B
+----+
| ID |
+----+
| B |
+----+
Surely, this is not enough, as you may work with a large data and need more filtering.
So, we will go and use it as a sub-query.
Using it as a sub-query it's simple :
SELECT
ID,
ClaimID,
Condition
FROM
#PatientTable
WHERE
ID = (SELECT ID AS NumberOfClaims FROM #PatientTable GROUP BY ID HAVING ID = ID AND COUNT(*) = 1)
This will return
+----+---------+--------------+
| ID | ClaimID | Condition |
+----+---------+--------------+
| B | 23853 | Hypertension |
+----+---------+--------------+
So far so good, but there is another issue we may face. Let's say you have a multiple Claims from a multiple patients, using this query as is will only show one patient. To show all patients we need to use IN rather than = under the WHERE clause
WHERE
ID IN (SELECT ID AS NumberOfClaims FROM #PatientTable GROUP BY ID HAVING ID = ID AND COUNT(*) = 1)
This will list all patients that falls under this condition.
If you need more conditions to filter, you just add them to the WHERE clause and you'll be good to go.
SELECT id, sum(ct)
FROM (SELECT customer_id, CASE WHEN category = 'X' THEN 0 else 1
end ct
FROM MASTER_TABLE
) AS t1
GROUP BY id
HAVING sum(ct) = 0
id which will have sum(ct) more than 1, will have multiple conditions
Use joins instead of subquery. Joins are always better in performance. You can use below query.
SELECT T1.id, T1.claimid, T1.Condition
FROM mytable T1
INNER JOIN
(
select id, count(Condition) counter
from mytable
group by id HAVING COUNT(DISTINCT CONDITION)=1
) T2 ON T1.ID=T2.ID
WHERE T2.counter=1

SQL:How do i get the row that has the max value of a column in SQL Server

I have a data a set which is already grouped by Person and Class columns and I use this query for this process:
SELECT Person,Class, MAX(TimeSpent) as MaxTimeSpent
FROM Persons
GROUP BY Person,Class
Output:
Person Class MaxTimeSpent
--------|--------|-------------|
MJ | 0 | 0 |
MJ | 1 | 659 |
MJ | 2 | 515 |
What I want to do is to get the row that has the maximum Class value in this data set (which is the 3rd row for this example).
How can I do this ? Any help would be appreciated.
Try this one
SELECT T.*
FROM
(SELECT Person,
Class,
MAX(TimeSpent) AS MaxTimeSpent
FROM Persons AS P
WHERE Person = 'MJ'
GROUP BY Person, Class) AS T
WHERE T.class = (
SELECT MAX(class) FROM Persons AS P
WHERE P.person = T.person)
You can use cte for that.
declare #Persons table (person nvarchar(10),Class int ,TimeSpent int)
insert into #Persons
select 'MJ',0,0 union all
select 'MJ',1,659 union all
select 'MJ',2,515
;with cte
as(
SELECT Person,Class,TimeSpent , row_number() over(partition by Person order by Class desc ) as RN
FROM #Persons
)
select * from cte where RN=1
Solution 2 :With Out Cte:
SELECT * FROM (
SELECT Person
,Class
,TimeSpent
,row_number() OVER (PARTITION BY Person ORDER BY Class DESC) AS RN FROM #Persons
) t WHERE t.RN = 1
TRY This:
declare #t table (Person varchar (20),Class int ,MaxTimeSpent int )
insert into #t VALUES ('MJ',0,0)
insert into #t VALUES ('MJ',1,659)
insert into #t VALUES ('MJ',2,515)
SELECT TOP 1 * FROM #t ORDER BY 2 DESC
--OR
SELECT * FROM #t WHERE Class = (SELECT max(class) FROM #t)
-- OR
SELECT TOP 1 * FROM (
SELECT *
,ROW_NUMBER() OVER (PARTITION BY person ORDER BY Class DESC) Record_Count FROM #t
) a
SELECT Top 1 Person,Class, MAX(TimeSpent) as MaxTimeSpent
FROM Persons
GROUP BY Person,Class order by Class desc

SQL group by if values are close

Class| Value
-------------
A | 1
A | 2
A | 3
A | 10
B | 1
I am not sure whether it is practical to achieve this using SQL.
If the difference of values are less than 5 (or x), then group the rows (of course with the same Class)
Expected result
Class| ValueMin | ValueMax
---------------------------
A | 1 | 3
A | 10 | 10
B | 1 | 1
For fixed intervals, we can easily use "GROUP BY". But now the grouping is based on nearby row's value. So if the values are consecutive or very close, they will be "chained together".
Thank you very much
Assuming MSSQL
You are trying to group things by gaps between values. The easiest way to do this is to use the lag() function to find the gaps:
select class, min(value) as minvalue, max(value) as maxvalue
from (select class, value,
sum(IsNewGroup) over (partition by class order by value) as GroupId
from (select class, value,
(case when lag(value) over (partition by class order by value) > value - 5
then 0 else 1
end) as IsNewGroup
from t
) t
) t
group by class, groupid;
Note that this assumes SQL Server 2012 for the use of lag() and cumulative sum.
Update:
*This answer is incorrect*
Assuming the table you gave is called sd_test, the following query will give you the output you are expecting
In short, we need a way to find what was the value on the previous row. This is determined using a join on row ids. Then create a group to see if the difference is less than 5. and then it is just regular 'Group By'.
If your version of SQL Server supports windowing functions with partitioning the code would be much more readable.
SELECT
A.CLASS
,MIN(A.VALUE) AS MIN_VALUE
,MAX(A.VALUE) AS MAX_VALUE
FROM
(SELECT
ROW_NUMBER()OVER(PARTITION BY CLASS ORDER BY VALUE) AS ROW_ID
,CLASS
,VALUE
FROM SD_TEST) AS A
LEFT JOIN
(SELECT
ROW_NUMBER()OVER(PARTITION BY CLASS ORDER BY VALUE) AS ROW_ID
,CLASS
,VALUE
FROM SD_TEST) AS B
ON A.CLASS = B.CLASS AND A.ROW_ID=B.ROW_ID+1
GROUP BY A.CLASS,CASE WHEN ABS(COALESCE(B.VALUE,0)-A.VALUE)<5 THEN 1 ELSE 0 END
ORDER BY A.CLASS,cASE WHEN ABS(COALESCE(B.VALUE,0)-A.VALUE)<5 THEN 1 ELSE 0 END DESC
ps: I think the above is ANSI compliant. So should run in most SQL variants. Someone can correct me if it is not.
These give the correct result, using the fact that you must have the same number of group starts as ends and that they will both be in ascending order.
if object_id('tempdb..#temp') is not null drop table #temp
create table #temp (class char(1),Value int);
insert into #temp values ('A',1);
insert into #temp values ('A',2);
insert into #temp values ('A',3);
insert into #temp values ('A',10);
insert into #temp values ('A',13);
insert into #temp values ('A',14);
insert into #temp values ('b',7);
insert into #temp values ('b',8);
insert into #temp values ('b',9);
insert into #temp values ('b',12);
insert into #temp values ('b',22);
insert into #temp values ('b',26);
insert into #temp values ('b',67);
Method 1 Using CTE and row offsets
with cte as
(select distinct class,value,ROW_NUMBER() over ( partition by class order by value ) as R from #temp),
cte2 as
(
select
c1.class
,c1.value
,c2.R as PreviousRec
,c3.r as NextRec
from
cte c1
left join cte c2 on (c1.class = c2.class and c1.R= c2.R+1 and c1.Value < c2.value + 5)
left join cte c3 on (c1.class = c3.class and c1.R= c3.R-1 and c1.Value > c3.value - 5)
)
select
Starts.Class
,Starts.Value as StartValue
,Ends.Value as EndValue
from
(
select
class
,value
,row_number() over ( partition by class order by value ) as GroupNumber
from cte2
where PreviousRec is null) as Starts join
(
select
class
,value
,row_number() over ( partition by class order by value ) as GroupNumber
from cte2
where NextRec is null) as Ends on starts.class=ends.class and starts.GroupNumber = ends.GroupNumber
** Method 2 Inline views using not exists **
select
Starts.Class
,Starts.Value as StartValue
,Ends.Value as EndValue
from
(
select class,Value ,row_number() over ( partition by class order by value ) as GroupNumber
from
(select distinct class,value from #temp) as T
where not exists (select 1 from #temp where class=t.class and Value < t.Value and Value > t.Value -5 )
) Starts join
(
select class,Value ,row_number() over ( partition by class order by value ) as GroupNumber
from
(select distinct class,value from #temp) as T
where not exists (select 1 from #temp where class=t.class and Value > t.Value and Value < t.Value +5 )
) ends on starts.class=ends.class and starts.GroupNumber = ends.GroupNumber
In both methods I use a select distinct to begin because if you have a dulpicate entry at a group start or end things go awry without it.
Here is one way of getting the information you are after:
SELECT Under5.Class,
(
SELECT MIN(m2.Value)
FROM MyTable AS m2
WHERE m2.Value < 5
AND m2.Class = Under5.Class
) AS ValueMin,
(
SELECT MAX(m3.Value)
FROM MyTable AS m3
WHERE m3.Value < 5
AND m3.Class = Under5.Class
) AS ValueMax
FROM
(
SELECT DISTINCT m1.Class
FROM MyTable AS m1
WHERE m1.Value < 5
) AS Under5
UNION
SELECT Over4.Class,
(
SELECT MIN(m4.Value)
FROM MyTable AS m4
WHERE m4.Value >= 5
AND m4.Class = Over4.Class
) AS ValueMin,
(
SELECT Max(m5.Value)
FROM MyTable AS m5
WHERE m5.Value >= 5
AND m5.Class = Over4.Class
) AS ValueMax
FROM
(
SELECT DISTINCT m6.Class
FROM MyTable AS m6
WHERE m6.Value >= 5
) AS Over4

trying to flatten rows into columns

I have a group of rows in a table that have an id. I am trying to flatten it out in rows with multiple column. I am almost certain I have done this with a cte and maybe partition.
I have used cte's to delete duplicate data and I thought I has done something similar to what I am trying to accomplish here. I was able to come up with workable solution (listed below) but still feel like a more elegant solution should be available.
CREATE TABLE #MyTable ( RowID int , field VARCHAR(10), value VARCHAR(10))
INSERT INTO #MyTable ( RowID, field, value ) VALUES ( 1, 'first', 'neil' )
INSERT INTO #MyTable ( RowID, field, value ) VALUES ( 2, 'first', 'bob' )
INSERT INTO #MyTable ( RowID, field, value ) VALUES ( 3, 'first', 'tom' )
INSERT INTO #MyTable ( RowID, field, value ) VALUES ( 1, 'last', 'young' )
INSERT INTO #MyTable ( RowID, field, value ) VALUES ( 2, 'last', 'dylan' )
INSERT INTO #MyTable ( RowID, field, value ) VALUES ( 3, 'last', 'petty' )
SELECT * FROM #mytable
--trying to accomplish this with cte/partition:
SELECT rowid,
[first] = (Select value FROM #mytable where field = 'first' and rowid = t.rowid),
[last] = (Select value FROM #mytable where field = 'last' and rowid = t.rowid)
FROM #mytable t
GROUP BY rowid
This data transformation is known as a PIVOT. In SQL Server 2005+ there is a function that will perform this process. :
select *
from
(
SELECT *
FROM mytable
) src
pivot
(
max(value)
for field in (first, last)
) piv
See SQL Fiddle with Demo.
Or you can use an aggregate function with a CASE expression:
select rowid,
max(case when field = 'first' then value end) first,
max(case when field = 'last' then value end) last
from MyTable
group by rowid
See SQL Fiddle with Demo.
You can also use multiple joins on your table:
select t1.rowid,
t1.value first,
t2.value last
from mytable t1
left join mytable t2
on t1.rowid = t2.rowid
and t2.field = 'last'
where t1.field = 'first'
See SQL Fiddle with Demo
The result for all versions is the same:
| ROWID | FIRST | LAST |
-------------------------
| 1 | neil | young |
| 2 | bob | dylan |
| 3 | tom | petty |