how to get rows with null values and value - sql

Interviewer asked me the following question:
consider the below table :
tpid data
100 1
100 2
100 NULL
101 6
101 5
101 NULL
102 NULL
103 9
103 65
104 NULL
..
..
If the tpid has got any data then display the data and not null value
but if the tpid has got only null then only display null against the id.
The result set should be like this :
tpid data
100 1
100 2
101 6
101 5
102 NULL
103 9
103 65
104 NULL
I wrote the following query but it doesn't give the desired result :
;with cte as
(select tpid,count(data) as num from a group by TPID)
select a.TPID,
(case when cte.num=0 then NULL else a.DATA end)col
from cte
join A on a.TPID=cte.TPID

DECLARE #temp TABLE(tpid int, data int)
INSERT INTO #temp
SELECT 100, 1 UNION
SELECT 100, 2UNION
SELECT 100, NULL UNION
SELECT 101, 6 UNION
SELECT 101, 5 UNION
SELECT 101, NULL UNION
SELECT 102, NULL UNION
SELECT 103, 9 UNION
SELECT 103, 65 UNION
SELECT 104, NULL
SELECT * FROM #temp a WHERE
data IS not NULL OR (select COUNT(1) FROM #temp WHERE tpid=a.tpid)=1
Ans:
tpid data
100 1
100 2
101 5
101 6
102 NULL
103 9
103 65
104 NULL

You can use windowed functions to get what you want:
SELECT tpid, data
FROM (
SELECT tpid, data,
COUNT(*) OVER (PARTITION BY tpid) AS cnt,
COUNT(CASE WHEN [data] IS NULL THEN 1 END)
OVER (PARTITION BY tpid) AS cntNulls
FROM mytable) AS t
WHERE (data IS NOT NULL) OR (cnt = cntNulls)
NULL values are filtered out, unless there is nothing but NULL values in a tpid slice.
Demo here
Alternatively you can use the following query:
SELECT t1.tpid, t1.data
FROM mytable AS t1
LEFT JOIN (
SELECT tpid
FROM mytable
GROUP BY tpid
HAVING COUNT(*) = COUNT(CASE WHEN data IS NULL THEN 1 END)
) AS t2 ON t1.tpid = t2.tpid
WHERE (t1.data IS NOT NULL) OR (t2.tpid IS NOT NULL)
Demo here

select tpid,data from (SELECT tpid, data,
COUNT(*) OVER (PARTITION BY tpid) AS cnt,
COUNT(CASE WHEN [data] IS NULL THEN 1 END)
OVER (PARTITION BY tpid) AS cntNulls
FROM mytable) t
where t.cntNulls = t.cnt or data is not null

Related

ORACLE get rows with condition value equals something but not equals to anything else

I have rows that look like .
OrderNo OrderStatus SomeOtherColumn
A 1
A 1
A 3
B 1 X
B 1 Y
C 2
C 3
D 2
I want to return all orders that have only one possible value of orderstatus. For e.g Here order B has only order status 1 SO result should be
B 1 X
B 1 Y
Notes:
Rows can be duplicated with same order status. For e.g. B here.
I am interested in the order having a very peculiar status for e.g. 1 here and not having any other status. So if B had a status of 3 at any point of time it is disqualified.
You can use not exists:
select t.*
from t
where not exists (select 1
from t t2
where t.orderno = t2.orderno and t.OrderStatus = t2.OrderStatus
);
If you just want the orders where this is true, you can use group by and having:
select orderno
from t
group by orderno
having min(OrderStatus) = max(OrderStatus);
If you only want a status of 1 then add max(OrderStatus) = 1 to the having clause.
Here is one way to do it. It does not handle the case where the status can be NULL; if that is possible, you will need to explain how you want it handled.
SQL> create table test_data ( orderno, status, othercol ) as (
2 select 'A', 1, null from dual union all
3 select 'A', 1, null from dual union all
4 select 'A', 3, null from dual union all
5 select 'B', 1, 'X' from dual union all
6 select 'B', 1, 'Y' from dual union all
7 select 'C', 2, null from dual union all
8 select 'C', 3, null from dual union all
9 select 'D', 2, null from dual
10 );
Table created.
SQL> variable input_status number
SQL> exec :input_status := 1
PL/SQL procedure successfully completed.
SQL> column orderno format a8
SQL> column othercol format a8
SQL> select orderno, status, othercol
2 from (
3 select t.*, count(distinct status) over (partition by orderno) as cnt
4 from test_data t
5 )
6 where status = :input_status
7 and cnt = 1
8 ;
ORDERNO STATUS OTHERCOL
-------- ---------- --------
B 1 X
B 1 Y
One way to handle NULL status (if that may happen), if in that case the orderno should be rejected (not included in the output), is to define the cnt differently:
count(case when status != :input_status or status is null then 1 end)
over (partition by orderno) as cnt
and in the outer query change the WHERE clause to a single condition,
where cnt = 0
Count distinct OrderStatus partitioned by OrderNo and show only rows where number equals one:
select OrderNo, OrderStatus, SomeOtherColumn
from ( select t.*, count(distinct orderstatus) over (partition by orderno) cnt
from t )
where cnt = 1
SQLFiddle demo
Just wanted to add something to Gordon's answer, using a stats function:
select orderno
from t
group by orderno
having variance(orderstatus) = 0;

SQL query to return data only if ALL necessary columns are present and not NULL

ID | Type | total
1 Purchase 12
1 Return 2
1 Exchange 5
2 Purchase null
2 Return 5
2 Exchange 1
3 Purchase 34
3 Return 4
3 Exchange 2
4 Purchase 12
4 Exchange 2
Above is sample data. What I want to return is:
ID | Type | total
1 Purchase 12
1 Return 2
1 Exchange 5
3 Purchase 34
3 Return 4
3 Exchange 2
So if a field is null in total or the values of Purchase, Return and Exchange are not all present for that ID, ignore that ID completely. How can I go about doing this?
You can use exists. I think you intend:
select t.*
from t
where exists (select 1
from t t2
where t2.id = t.id and t2.type = 'Purchase' and t2.total is not null
) and
exists (select 1
from t t2
where t2.id = t.id and t2.type = 'Exchange' and t2.total is not null
) and
exists (select 1
from t t2
where t2.id = t.id and t2.type = 'Return' and t2.total is not null
);
There are ways to "simplify" this:
select t.*
from t
where 3 = (select count(distinct t2.type)
from t t2
where t2.id = t.id and
t2.type in ('Purchase', 'Exchange', 'Return') and
t2.total is not null
);
I would write this as a join, without subqueries:
SELECT pur.id, pur.total AS Purchase, exc.total AS Exchange, ret.total AS Return
FROM MyTable as pur
INNER JOIN MyTable AS exc ON exc.id=pur.id AND exc.type='Exchange'
INNER JOIN MyTable AS ret ON ret.id=pur.id AND ret.type='Return'
WHERE pur.type='Purchase'
The inner join means that if any of the three rows with different values are not found for a given id, then no row is included in the result.
Analytic functions are a good way to solve this kind of problems. The base table is read just once, and no joins (explicit or implicit, as in EXISTS conditions or correlated subqueries) are needed.
In the solution below, we count distinct values of 'Purchase', 'Exchange' and 'Return' for each id while ignoring other values (assuming that is indeed the requirement), and separately count total nulls in the total column for each id. Then it becomes a trivial matter to select just the "desired" rows in an outer query.
with
test_data ( id, type, total ) as (
select 1, 'Purchase', 12 from dual union all
select 1, 'Return' , 2 from dual union all
select 1, 'Exchange', 5 from dual union all
select 2, 'Purchase', null from dual union all
select 2, 'Return' , 5 from dual union all
select 2, 'Exchange', 1 from dual union all
select 3, 'Purchase', 34 from dual union all
select 3, 'Return' , 4 from dual union all
select 3, 'Exchange', 2 from dual union all
select 4, 'Purchase', 12 from dual union all
select 4, 'Exchange', 2 from dual
)
-- end of test data; actual solution (SQL query) begins below this line
select id, type, total
from ( select id, type, total,
count( distinct case when type in ('Purchase', 'Return', 'Exchange')
then type end
) over (partition by id) as ct_type,
count( case when total is null then 1 end
) over (partition by id) as ct_total
from test_data
)
where ct_type = 3 and ct_total = 0
;
Output:
ID TYPE TOTAL
-- -------- -----
1 Exchange 5
1 Purchase 12
1 Return 2
3 Exchange 2
3 Purchase 34
3 Return 4
This also should work fine even if new values are added to type column
select * from t where
ID not in(select ID from t where
t.total is null or t.[Type] is null)

Oracle sql group sum

I have table With ID,Sub_ID and value coloumns
ID SUB_ID Value
100 1 100
100 2 150
101 1 100
101 2 150
101 3 200
102 1 100
SUB ID can vary from 1..maxvalue( In this example it is 3). I need Sum of values for each Sub_ID. If SUB_ID is less than MAXVALUE for a particlaur ID then it should take MAX(SUB_ID) of each ID As shown below ( In this example for ID=100 for SUB_ID 3 it should take 150 i.e 2<3 so value=150))
SUB_ID SUM(values) Remarks
1 300 (100+100+100)
2 400 (150+150+100)
3 450 (150+200+100)
This can be easily done in PL/SQL . Can we use SQL for the same using Model Clause or any other options
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TableA ( ID, SUB_ID, Value ) AS
SELECT 100, 1, 100 FROM DUAL
UNION ALL SELECT 100, 2, 150 FROM DUAL
UNION ALL SELECT 101, 1, 100 FROM DUAL
UNION ALL SELECT 101, 2, 150 FROM DUAL
UNION ALL SELECT 101, 3, 200 FROM DUAL
UNION ALL SELECT 102, 1, 100 FROM DUAL
Query 1:
WITH sub_ids AS (
SELECT LEVEL AS sub_id
FROM DUAL
CONNECT BY LEVEL <= ( SELECT MAX( SUB_ID ) FROM TableA )
),
max_values AS (
SELECT ID,
MAX( VALUE ) AS max_value
FROM TableA
GROUP BY ID
)
SELECT s.SUB_ID,
SUM( COALESCE( a.VALUE, m.max_value ) ) AS total_value
FROM sub_ids s
CROSS JOIN
max_values m
LEFT OUTER JOIN
TableA a
ON ( s.SUB_ID = a.SUB_ID AND m.ID = a.ID )
GROUP BY
s.SUB_ID
Results:
| SUB_ID | TOTAL_VALUE |
|--------|-------------|
| 1 | 300 |
| 2 | 400 |
| 3 | 450 |
Try this
SELECT SUB_ID,SUM(values),
(SELECT DISTINCT SUBSTRING(
(
SELECT '+'+ CAST(values AS VARCHAR)
FROM table_Name AS T2
WHERE T2.SUB_ID = d.SUB_ID
FOR XML PATH ('')
),2,100000)[values]) as values
FROm table_Name d
GROUP BY SUB_ID
How about something like this:
select max_vals.sub_id, sum(nvl(table_vals.value,max_vals.max_value)) as sum_values
from (
select all_subs.sub_id, t1.id, max(t1.value) as max_value
from your_table t1
cross join (select sub_id from your_table) all_subs
group by all_subs.sub_id, t1.id
) max_vals
left outer join your_table table_vals
on max_vals.id = table_vals.id
and max_vals.sub_id = table_vals.sub_id
group by max_vals.sub_id;
The inner query gets you a list of all sub_id/id combinations and their fall-back values. The out query uses an nvl to use the table value if it exists and the fall-back value if it doesn't.

SQL - Group by to get sum but also return a row if the sum is 0

I have the following table:
ID BuyOrSell Total
4 B 10
4 B 11
4 S 13
4 S 29
8 B 20
9 S 23
What I am trying to do is to have sum of B and S columns for each ID and if there is not a B or S for an ID have a row with 0, so expected output would be
ID BuyOrSell Total
4 B 21
4 S 42
8 B 20
8 S 0
9 S 23
9 B 0
I have tried this and it is kind of doing what I am after but not exactly:
DECLARE #Temp Table (ID int, BuyOrSell VARCHAR(1), charge Decimal)
INSERT INTO #Temp
SELECT 4, 'B', 10 UNION ALL
SELECT 4, 'B', 11 UNION ALL
SELECT 4, 'S', 13 UNION ALL
SELECT 4, 'S', 29 UNION ALL
SELECT 8, 'B', 20 UNION ALL
SELECT 9, 'S', 23
;With Results AS
(
SELECT ID,
BuyOrSell,
SUM(charge) AS TOTAL
FROM #Temp
Group by ID, BuyOrSell
)
Select t.*,max(
case when BuyOrSell = 'B' then 'Bfound'
end) over (partition by ID) as ref
,max(
case when BuyOrSell = 'S' then 'Sfound'
end) over (partition by ID) as ref
FROM Results t;
Thanks
Try this:
;WITH CTE(ID, BuyOrSell) AS(
SELECT
ID, T.BuyOrSell
FROM #Temp
CROSS JOIN(
SELECT 'B' UNION ALL SELECT 'S'
)T(BuyOrSell)
GROUP BY ID, T.BuyOrSell
)
SELECT
C.ID,
C.BuyOrSell,
Total = ISNULL(SUM(T.charge), 0)
FROM CTE C
LEFT JOIN #Temp T
ON T.ID = C.ID
AND T.BuyOrSell = C.BuyOrSell
GROUP BY C.ID, C.BuyOrSell
ORDER BY C.ID, C.BuyOrSell
#03Usr, despite that your question has been answered, please try this:
SELECT two.ID,
two.BuyOrSell,
ISNULL (one.Total, 0) Total
FROM
(SELECT ID,
BuyOrSell,
SUM (Total) Total
FROM #Temp
GROUP BY ID, BuyOrSell) one
LEFT OUTER JOIN
(SELECT ID,
BuyOrSell
FROM #Temp
GROUP BY ID,
BuyOrSell) two
ON one.ID = two.ID
AND one.BuyOrSell = two.BuyOrSell;
Here is a solution with tricky join:
SELECT t1.ID,
v.l as BuyOrSell,
SUM(CASE WHEN t1.BuyOrSell = v.l THEN t1.charge ELSE 0 END) AS Total
FROM #Temp t1
JOIN (VALUES('B'),('S')) v(l)
ON t1.BuyOrSell = CASE WHEN EXISTS(SELECT * FROM #Temp t2
WHERE t2.ID = t1.ID AND t2.BuyOrSell <> t1.BuyOrSell)
THEN v.l ELSE t1.BuyOrSell END
GROUP BY t1.ID, v.l
ORDER BY t1.ID, v.l
Output:
ID l Total
4 B 21
4 S 42
8 B 20
8 S 0
9 B 0
9 S 23

How to write a query to produce counts for arbitrary value bands?

My table had 3 fields: id and unit. I want to count how many ids have <10, 10-49, 50-100 etc units. The final result should look like:
Category | countIds
<10 | 1516
10 - 49 | 710
50 - 99 | 632
etc.
This is the query that returns each id and how many units it has:
select id, count(unit) as numUnits
from myTable
group by id
How can I build on that query to give me the category, countIds result?
create temporary table ranges (
seq int primary key,
range_label varchar(10),
lower int,
upper int
);
insert into ranges values
(1, '<10', 0, 9),
(2, '10 - 49', 10, 49),
(3, '50 - 99', 50, 99)
etc.
select r.range_label, count(c.numUnits) as countIds
from ranges as r
join (
select id, count(unit) as numUnits
from myTable
group by id) as c
on c.numUnits between r.lower and r.upper
group by r.range_label
order by r.seq;
edit: changed sum() to count() above.
select category_bucket, count(*)
from (select case when category < 10 then "<10"
when category >= 10 and category <= 49 then "10 - 49"
when category >= 50 and category <= 99 then "50 - 99"
else "100+"
end category_bucket, num_units
from my_table)
group by category_bucket
A dynamically grouped solution is much harder.
SELECT id, countIds
FROM (
SELECT id
, 'LESS_THAN_TEN' CATEGORY
, COUNT(unit) countIds
FROM table1
GROUP BY ID
HAVING COUNT(UNIT) < 10
UNION ALL
SELECT id
, 'BETWEEN_10_AND_49' category
, COUNT(unit) countIds
FROM table1
GROUP BY ID
HAVING COUNT(UNIT) BETWEEN 10 AND 49
UNION ALL
SELECT id
, 'BETWEEN_50_AND_99' category
, COUNT(unit) countIds
FROM table1
GROUP BY id
HAVING COUNT(UNIT) BETWEEN 50 AND 99
) x
Giving an example for one range: (10 - 49)
select count(id) from
(select id, count(unit) as numUnits from myTable group by id)
where numUnits >= '10' && numUnits <= '49'
It's not precisely what you want, but you could use fixed ranges, like so:
select ' < ' || floor(id / 50) * 50, count(unit) as numUnits
from myTable
group by floor(id / 50) * 50
order by 1
Try this working sample in SQL Server TSQL
SET NOCOUNT ON
GO
WITH MyTable AS
(
SELECT 00 as Id, 1 Value UNION ALL
SELECT 05 , 2 UNION ALL
SELECT 10 , 3 UNION ALL
SELECT 15 , 1 UNION ALL
SELECT 20 , 2 UNION ALL
SELECT 25 , 3 UNION ALL
SELECT 30 , 1 UNION ALL
SELECT 35 , 2 UNION ALL
SELECT 40 , 3 UNION ALL
SELECT 45 , 1 UNION ALL
SELECT 40 , 3 UNION ALL
SELECT 45 , 1 UNION ALL
SELECT 50 , 3 UNION ALL
SELECT 55 , 1 UNION ALL
SELECT 60 , 3 UNION ALL
SELECT 65 , 1 UNION ALL
SELECT 70 , 3 UNION ALL
SELECT 75 , 1 UNION ALL
SELECT 80 , 3 UNION ALL
SELECT 85 , 1 UNION ALL
SELECT 90 , 3 UNION ALL
SELECT 95 , 1 UNION ALL
SELECT 100 , 3 UNION ALL
SELECT 105 , 1 UNION ALL
SELECT 110 , 3 UNION ALL
SELECT 115 , 1 Value
)
SELECT Category, COUNT (*) CountIds
FROM
(
SELECT
CASE
WHEN Id BETWEEN 0 and 9 then '<10'
WHEN Id BETWEEN 10 and 49 then '10-49'
WHEN Id BETWEEN 50 and 99 then '50-99'
WHEN Id > 99 then '>99'
ELSE '0' END as Category
FROM MyTable
) as A
GROUP BY Category
This will give you the following result
Category CountIds
-------- -----------
<10 2
>99 4
10-49 10
50-99 10