Distinct Conditional Counting to Avoid Overlap - sql

Consider this table:
[Table1]
------------------------
| Person_ID | Yes | No |
|-----------|-----|----|
| 1 | 1 | 0 |
|-----------|-----|----|
| 1 | 1 | 0 |
|-----------|-----|----|
| 2 | 0 | 1 |
|-----------|-----|----|
| 2 | 0 | 1 |
|-----------|-----|----|
| 3 | 1 | 0 |
|-----------|-----|----|
| 3 | 1 | 0 |
|-----------|-----|----|
| 3 | 0 | 1 |
|-----------|-----|----|
| 3 | 1 | 0 |
------------------------
I need a distinct count on Person_ID to get the number of people that are marked Yes and No. However, if someone has a single instance of No, they should be counted as a No and not be included in the Yes count no matter how many Yes they have.
My first thought was to try something similar to:
select count(distinct (case when Yes = 1 then Person_ID else null end)) Yes_People
, count(distinct (case when No = 1 then Person_ID else null end)) No_People
from Table1
but this will result in 3 being counted in both the Yes and No counts.
My desired output would be:
--------------------------
| Yes_People | No_People |
|------------|-----------|
| 1 | 2 |
--------------------------
I'm hoping to avoid the performance hit from having to evaluate a subquery against each row but if it has to be the way to go I will accept that.

Aggregate first at the person level and then overall:
select sum(yes_only) as yes_only,
sum(1 - yes_only) as no
from (select person_id,
(case when max(yes) = min(yes) and max(yes) = 1
then 1
end) as yes_only
from t
group by person_id
) t

You can first group them by the person.
Then the CASE for the Yes people can have a not No condition.
SELECT
COUNT(CASE WHEN No = 0 AND Yes = 1 THEN Person_ID END) AS Yes_People,
COUNT(CASE WHEN No = 1 THEN Person_ID END) AS No_People
FROM
(
select Person_ID
, MAX(Yes) as Yes
, MAX(No) as No
FROM Table1
GROUP BY Person_ID
) q

You could use a window function to rank the rows for a single person_id to prioritize a 'No' over a 'Yes', but that will require a subquery
select count(case when yes=1 then 1 end) as yes_count,
count(case when no=1 then no_count) as no_count
from (
select person_id, yes, no, row_number() over (order by no desc, yes desc) as rn
from table1
)
where rn = 1
The inner subquery plus the where filter will get you a single row per person_id, giving priority to the 'no' records.
This of course assumes yes/no are mutually exclusive, and if that's true, you should probably change the model to a single field.

Think you need to precheck every person with a window function
with t as (select 1 p_id, 1 yes, 0 no from dual
union all select 1 p_id, 1 yes, 0 no from dual
union all select 2 p_id, 0 yes, 1 no from dual
union all select 2 p_id, 0 yes, 1 no from dual
union all select 3 p_id, 1 yes, 0 no from dual
union all select 3 p_id, 0 yes, 1 no from dual
union all select 3 p_id, 1 yes, 0 no from dual)
, chk as (
select max(no) over (partition by p_id) n
, max(yes) over (partition by p_id) y
, p_id
from t)
-- select * from chk;
select count(distinct decode(y-n,1,p_id,null )) yes_people
, count(distinct decode(n,1,p_id,null )) no_people
from chk
group by 1;

You can use Conditional aggregation as following:
SQL> with table1 as (select 1 PERSON_ID, 1 yes, 0 no from dual
2 union all select 1 PERSON_ID, 1 yes, 0 no from dual
3 union all select 2 PERSON_ID, 0 yes, 1 no from dual
4 union all select 2 PERSON_ID, 0 yes, 1 no from dual
5 union all select 3 PERSON_ID, 1 yes, 0 no from dual
6 union all select 3 PERSON_ID, 0 yes, 1 no from dual
7 union all select 3 PERSON_ID, 1 yes, 0 no from dual)
8 SELECT
9 SUM(CASE WHEN NOS = 0 AND YES > 0 THEN 1 END) YES_PEOPLE,
10 SUM(CASE WHEN NOS > 0 THEN 1 END) NO_PEOPLE
11 FROM
12 (
13 SELECT
14 SUM(NO) NOS,
15 PERSON_ID,
16 SUM(YES) YES
17 FROM TABLE1
18 GROUP BY PERSON_ID
19 );
YES_PEOPLE NO_PEOPLE
---------- ----------
1 2
SQL>
Cheers!!

Related

Split Columns into two equal number of Rows

I have the table structure below,
I need to merge the CouponNumber to two equal as CouponNumber1 and CouponNumber2 as shown in the figure
SELECT Name, MobileNumber, CouponNumber, IsDispatched, Status
FROM CouponInvoicePrescription
This is my query.
Try this:
WITH
input(ord,name,mobno,couponno,isdispatched,status) AS (
SELECT 0,'amar',8888888888,'CPever901',FALSE,1
UNION ALL SELECT 1,'amar',8888888888,'CP00005' ,FALSE,1
UNION ALL SELECT 2,'pt3' ,7777777777,'cp9090' ,FALSE,1
UNION ALL SELECT 3,'pt3' ,7777777777,'ev2' ,FALSE,1
UNION ALL SELECT 4,'pt3' ,7777777777,'cp9909' ,FALSE,1
UNION ALL SELECT 5,'pt3' ,7777777777,'cp10' ,FALSE,1
)
SELECT
name
, MAX(CASE ord % 2 WHEN 1 THEN couponno END) AS couponno1
, MAX(CASE ord % 2 WHEN 0 THEN couponno END) AS couponno2
, isdispatched
, status
FROM input
GROUP BY
ord / 2
, name
, isdispatched
, status
ORDER BY 1
-- out name | couponno1 | couponno2 | isdispatched | status
-- out ------+-----------+-----------+--------------+--------
-- out amar | CP00005 | CPever901 | f | 1
-- out pt3 | cp10 | cp9909 | f | 1
-- out pt3 | ev2 | cp9090 | f | 1
Try this:
SELECT * FROM
(
SELECT
sub.rn,
sub.Name,
sub.MobileNumber,
sub.CouponNumber as CouponNumber1,
LEAD(sub.CouponNumber,1) OVER (PARTITION BY sub.MobileNumber ORDER BY sub.rn) as CouponNumber2,
sub.IsDispatched,
sub.Status
FROM
(
SELECT
ROW_NUMBER() OVER (PARTITION by MobileNumber ORDER BY Name) as rn,
*
FROM
input
) sub
)
WHERE rn % 2 <> 0

Ranking counts using an SQL query

I am using DB Browser for SQLite. I have a table in the following format:
+-----------+-------------------------------------+
| search_id | search_town |
+-----------+-------------------------------------+
| 1 | town1,town3 |
| 2 | town2,town4,town5 |
| 3 | town3,town5 |
| 4 | town2,town5 |
| 5 | town2,town3,town4 |
+-----------+-------------------------------------+
I would like to do a COUNT on the number of times town1 through town5 has appeared under search_town, and then rank in descending order the towns based on their respective counts. So far I have the following query:
SELECT SUM(CASE WHEN search_location LIKE '%town01%' THEN 1 ELSE 0 END) AS town01,
SUM(CASE WHEN search_location LIKE '%town02%' THEN 1 ELSE 0 END) AS town02,
SUM(CASE WHEN search_location LIKE '%town03%' THEN 1 ELSE 0 END) AS town03,
SUM(CASE WHEN search_location LIKE '%town04%' THEN 1 ELSE 0 END) AS town04,
SUM(CASE WHEN search_location LIKE '%town05%' THEN 1 ELSE 0 END) AS town05
FROM searches
...but am unable to do an ORDER BY as the towns and their counts are output as columns instead of rows in this format
+-------+-------+-------+-------+-------+
| town1 | town2 | town3 | town4 | town5 |
+-------+-------+-------+-------+-------+
| 12 | 31 | 12 | 24 | 12 |
+-------+-------+-------+-------+-------+
Is there another approach to this? Appreciate any comments.
You are turning your output in a single row using CASE WHEN, to convert it into multiple rows, you can try like following.
;WITH cte
AS (SELECT *
FROM (VALUES ('Town1'),
('Town2'),
('Town3'),
('Town4'),
('Town5')) T(town))
SELECT Count(*) [Count],
C.town
FROM [TABLE_NAME] T
INNER JOIN cte C
ON T.search_location LIKE '%' + C.town + '%'
GROUP BY C.town
ORDER BY Count(*) DESC
Online DEMO
Another approach can be using UNION ALL.
SELECT *
FROM (SELECT Count(*) s,
'Town1' AS Col
FROM tablename
WHERE search_location LIKE '%town1%'
UNION ALL
SELECT Count(*) s,
'Town2' AS Col
FROM tablename
WHERE search_location LIKE '%town2%'
UNION ALL
SELECT Count(*) s,
'Town3' AS Col
FROM tablename
WHERE search_location LIKE '%town3%'
UNION ALL
SELECT Count(*) s,
'Town4' AS Col
FROM tablename
WHERE search_location LIKE '%town4%'
UNION ALL
SELECT Count(*) s,
'Town5' AS Col
FROM tablename
WHERE search_location LIKE '%town5%') t
ORDER BY s DESC
You can use a recursive common-table expression (CTE) to turn the comma-separated list into a set of rows. When the table is normalized, you can group by town and sort by descending count:
WITH rec(town, remain)
AS (
SELECT SUBSTR(search_town, 0, INSTR(search_town, ',')) -- Before ,
, SUBSTR(search_town, INSTR(search_town, ',')+1) || ',' -- After ,
FROM t1
UNION ALL
SELECT SUBSTR(remain, 0, INSTR(remain, ',')) -- Before ,
, SUBSTR(remain, INSTR(remain, ',')+1) -- After ,
FROM rec
WHERE LENGTH(remain) > 0
)
SELECT town
, COUNT(*)
FROM rec
GROUP BY
town
ORDER BY
COUNT(*) DESC
Idea from this blog post. Working example at sqliteonline.

SQL query sum of total corresponding rows

I have two tables as below. Caseid from first table is referenced in second table along with accidents. What I am trying to get total different accidents for a case type. Below two tables I documented sample data and expected result.
Table case:
caseId CaseType
1 AB
2 AB
3 AB
4 CD
5 CD
6 DE
Table CaseAccidents:
AccidentId caseID AccidentRating
1 1 High
2 1 High
3 1 Medium
4 1 LOW
5 2 High
6 2 Medium
7 2 LOW
8 5 High
9 5 High
10 5 Medium
11 5 LOW
Result should look like:
CaseType TotalHIghrating TotalMediumRating TotalLOWRating
AB 3 2 2
CD 2 1 1
DE 0 0 0
To get the sum of every rating, you can Use a SUM(CASE WHEN) clause, adding 1 by every record that match the rating.
In your question, you have pointed out that you want to see all distinct CaseType, you can get it by using a RIGHT JOIN, this will include all records of case table.
select case.CaseType,
sum(case when caseAccidents.AccidentRating = 'High' then 1 else 0 end) as TotalHighRating,
sum(case when caseAccidents.AccidentRating = 'Medium' then 1 else 0 end) as TotalMediumRating,
sum(case when caseAccidents.AccidentRating = 'LOW' then 1 else 0 end) as TotalLowRating
from caseAccidents
right join case on case.caseId = caseAccidents.caseID
group by case.CaseType;
+----------+-----------------+-------------------+----------------+
| CaseType | TotalHighRating | TotalMediumRating | TotalLowRating |
+----------+-----------------+-------------------+----------------+
| AB | 3 | 2 | 2 |
+----------+-----------------+-------------------+----------------+
| CD | 2 | 1 | 1 |
+----------+-----------------+-------------------+----------------+
| DE | 0 | 0 | 0 |
+----------+-----------------+-------------------+----------------+
Check it: http://rextester.com/MCGJA9193
Have you use case in a select clause before?
select C.CaseType,
sum(case when CA.AccidentRating = 'High' then 1 else 0 end)
from Case C join CaseAccidents CA on C.CaseId = CA.CaseId
group by C.CaseType
Please see this. Sample query of the table and also that result
create table #case(caseid int,casetype varchar(5))
insert into #case (caseid,casetype)
select 1,'AB' union all
select 2,'AB' union all
select 3,'AB' union all
select 4,'CD' union all
select 5,'CD' union all
select 6,'DE'
create table #CaseAccidents(AccidentId int, CaseId int,AccidentRating varchar(10))
insert into #CaseAccidents(AccidentId, CaseId, AccidentRating)
select 1,1,'High' union all
select 2,1,'High' union all
select 3,1,'Medium' union all
select 4,1,'Low' union all
select 5,2,'High' union all
select 6,2,'Medium' union all
select 7,2,'Low' union all
select 8,5,'High' union all
select 9,5,'High' union all
select 10,5,'Medium' union all
select 11,5,'Low'
My script
select c.casetype,
sum(case when ca.AccidentRating='High' then 1 else 0 end) as TotalHighRating,
sum(case when ca.AccidentRating='Medium' then 1 else 0 end) as TotalMediumRating,
sum(case when ca.AccidentRating='Low' then 1 else 0 end) as TotalLowRating
from #case c
Left join #CaseAccidents ca
on c.Caseid=ca.Caseid
group by c.casetype
Hope This could help!
Another approach using Pivot operator
SELECT casetype,
[High],
[Medium],
[Low]
FROM (SELECT c.casetype,
AccidentRating
FROM case c
LEFT JOIN CaseAccidents ca
ON ca.CaseId = c.caseid)a
PIVOT (Count(AccidentRating)
FOR AccidentRating IN ([High],
[Medium],
[Low]) ) p
Try This code once.
select casetype,
sum(case when ca.AccidentRating='High' then 1 else 0 end ) as TotalHIghrating,
sum(case when ca.AccidentRating='Medium' then 1 else 0 end ) as TotalMediumRating ,
sum(case when ca.AccidentRating='Low' then 1 else 0 end ) as TotalLOWRating
from #case c
left join #CaseAccidents ca on c.caseid=ca.CaseId
group by casetype

How do I combine 3 select statements into one? (SQL Server 2008)

I have 4 sql statements, each returns 2 columns
select count(id) open, sum(amount) from A where status = 1 group by est_id
result: 25 | 4400
select count(id) close, sum(amount) from A where status = 2 group by est_id
result: 0 | 0
select count(id) stop, sum(amount) from A where status = 3 group by est_id
result: 20 | 4000
I need to return the result of the 4 sql sql statement in one, for example:
25 | 4400| active
0 | 0 | inactive
20 | 4000| close
regards
As mentioned by #bluefeet you're looking for the UNION ALL operator.
select count(id) as c1, sum(amount) as c2, 'active' as c3 from A where status = 1 group by est_id
union all
select count(id), sum(amount), 'inactive' from A where status = 2 group by est_id
union all
select count(id), sum(amount), 'close' from A where status = 3 group by est_id
The column names in the combined resultset will be taken from the first component resultset.

SQL: Get multiple line entries linked to one item?

I have a table:
ID | ITEMID | STATUS | TYPE
1 | 123 | 5 | 1
2 | 123 | 4 | 2
3 | 123 | 5 | 3
4 | 125 | 3 | 1
5 | 125 | 5 | 3
Any item can have 0 to many entries in this table. I need a query that will tell me if an ITEM has all it's entries in either a state of 5 or 4. For example, in the above example, I would like to end up with the result:
ITEMID | REQUIREMENTS_MET
123 | TRUE --> true because all statuses are either 5 or 4
125 | FALSE --> false because it has a status of 3 and a status of 5.
If the 3 was a 4 or 5, then this would be true
What would be even better is something like this:
ITEMID | MET_REQUIREMENTS | NOT_MET_REQUIREMENTS
123 | 3 | 0
125 | 1 | 1
Any idea how to write a query for that?
Fast, short, simple:
SELECT itemid
,count(status = 4 OR status = 5 OR NULL) AS met_requirements
,count(status < 4 OR status > 5 OR NULL) AS not_met_requirements
FROM tbl
GROUP BY itemid
ORDER BY itemid;
Assuming all columns to be integer NOT NULL.
Builds on basic boolean logic:
TRUE OR NULL yields TRUE
FALSE OR NULL yields NULL
And NULL is not counted by count().
->SQLfiddle demo.
SELECT a.ID FROM (SELECT ID, MIN(STATUS) AS MINSTATUS, MAX(STATUS) AS MAXSTATUS FROM TABLE_NAME AS a GROUP BY ID)
WHERE a.MINSTATUS >= 4 AND a.MAXSTATUS <= 5
One way of doing this would be
SELECT t1.itemid, NOT EXISTS(SELECT 1
FROM mytable t2
WHERE itemid=t1.itemid
AND status NOT IN (4, 5)) AS requirements_met
FROM mytable t1
GROUP BY t1.itemid
UPDATE: for your updated requirement, you can have something like:
SELECT itemid,
sum(CASE WHEN status IN (4, 5) THEN 1 ELSE 0 END) as met_requirements,
sum(CASE WHEN status IN (4, 5) THEN 0 ELSE 1 END) as not_met_requirements
FROM mytable
GROUP BY itemid
simple one:
select
"ITEMID",
case
when min("STATUS") in (4, 5) and max("STATUS") in (4, 5) then 'True'
else 'False'
end as requirements_met
from table1
group by "ITEMID"
better one:
select
"ITEMID",
sum(case when "STATUS" in (4, 5) then 1 else 0 end) as MET_REQUIREMENTS,
sum(case when "STATUS" in (4, 5) then 0 else 1 end) as NOT_MET_REQUIREMENTS
from table1
group by "ITEMID";
sql fiddle demo
WITH dom AS (
SELECT DISTINCT item_id FROM items
)
, yes AS ( SELECT item_id, COUNT(*) AS good_count FROM items WHERE status IN (4,5) GROUP BY item_id
)
, no AS ( SELECT item_id, COUNT(*) AS bad_count FROM items WHERE status NOT IN (4,5) GROUP BY item_id
)
SELECT d.item_id
, COALESCE(y.good_count,0) AS good_count
, COALESCE(n.bad_count,0) AS bad_count
FROM dom d
LEFT JOIN yes y ON y.item_id = d.item_id
LEFT JOIN no n ON n.item_id = d.item_id
;
Can be done with an outer join, too:
WITH yes AS ( SELECT item_id, COUNT(*) AS good_count FROM items WHERE status IN (4,5) GROUP BY item_id)
, no AS ( SELECT item_id, COUNT(*) AS bad_count FROM items WHERE status NOT IN (4,5) GROUP BY item_id)
SELECT COALESCE(y.item_id, n.item_id) AS item_id
, COALESCE(y.good_count,0) AS good_count
, COALESCE(n.bad_count,0) AS bad_count
FROM yes y
FULL JOIN no n ON n.item_id = y.item_id
;
Nevermind, it was actually easy to do:
select ITEM_ID ,
sum (case when STATUS >= 3 then 1 else 0 end ) as met_requirements,
sum (case when STATUS < 3 then 1 else 0 end ) as not_met_requirements
from TABLE as d
group by ITEM_ID