How to transform and grouping in SQL - sql

I would like to group bycontract date by transforming segment referring to its contract date like below.
contractdate segment
~2020/2/1 a
2020/2/2~2020/4/1 b
2020/4/2~ c
My desired result is to cut contractdate into segment and countthem into result tables.
If someone has opinion,please let me know.
Thanks
my table is like below.
contractdate status
2020/1/2 A
2020/4/2 B
2020/6/5 C
2020/1/2 C
2020/4/4 B
And here is my desired result.
segment A B C
a 1 0 1
b 0 0 0
c 0 2 1

Replace missing bounds with sentinels (or replace notation with weird tilde character at all), then distribute contractdate of statuses into proper ranges:
with sg (contractdate,segment) as (
select '~2020/2/1' , 'a' from dual union all
select '2020/2/2~2020/4/1', 'b' from dual union all
select '2020/4/2~' , 'c' from dual
), ssg as ( -- sanitized sg
select coalesce(to_date(regexp_replace(contractdate,'([^~]*)~([^~]*)','\1'),'YYYY/MM/DD'), date '-4712-1-1') as lowerbound -- source: https://laurentschneider.com/wordpress/2008/01/what-is-the-lowest-and-highest-possible-date-in-oracle.html
, coalesce(to_date(regexp_replace(contractdate,'([^~]*)~([^~]*)','\2'),'YYYY/MM/DD'), date '9999-01-01') as upperbound
, segment
from sg
), st (contractdate,status) as (
select '2020/1/2', 'A' from dual union all
select '2020/4/2', 'B' from dual union all
select '2020/6/5', 'C' from dual union all
select '2020/1/2', 'C' from dual union all
select '2020/4/4', 'B' from dual
)
select segment
, sum(case when status = 'A' then 1 else 0 end)
, sum(case when status = 'B' then 1 else 0 end)
, sum(case when status = 'C' then 1 else 0 end)
from ssg left join st on to_date(st.contractdate,'YYYY/MM/DD') between ssg.lowerbound and ssg.upperbound
group by segment
order by segment
Please use CTE to specify your input data, not plaintext tables. It helps to concentrate on answer instead of text formatting.

Related

Query Optimization - To repeat a pattern in Oracle SQL

Introduction: I can do this in MS-Excel, it takes me 1 minute, but I m trying to get this in Oracle SQL
Here is my Code:
SELECT A.*, (CASE WHEN A.r = 1 then 'X1' when A.r = 2 then 'X2' when A.r = 3 then 'X3' when A.r = 4
then 'X4' when A.r = 5 then 'X2' when A.r = 6 then 'X6' end) X FROM
(
Select Rownum r
From dual
Connect By Rownum <= 6 ) A
This is the Output:
Now, what if I have to do it for 25000 numbers, meaning when (rownum <= 25000) currently I have it only for 6, Is there a better method to do this with out case statement?
If you want to repeat this pattern of 6 rows for the remaining rows, then you can do:
select t.*,
(case when mod(rownum, 6) = 5 then 'X2'
else 'X' || (mod(rownum - 1, 6) + 1)
end)
from t;

query all of the same id based on multiple true conditions

I have this table :
id act st_dt end_dt stats
1 a 01/01/20 05/01/20 done
1 b 04/01/20 09/02/20 done
1 c 09/02/20 null not done
1 d 09/02/20 09/02/20 done
2 a 09/03/19 14/05/20 done
2 b 09/02/20 25/06/20 done
2 c 01/03/20 22/03/20 done
2 d 09/02/20 null not done
3 a 11/05/20 13/09/19 done
3 b 09/02/20 04/07/20 done
3 c 01/02/20 30/02/20 done
3 d 11/02/20 24/02/20 done
I want to query all activities 'act' of the same ID having activity a >= 01/01/20 and activity d status is done,
so the result should look like this:
id act st_dt end_dt stats
1 a 01/01/20 05/01/20 done
1 b 04/01/20 09/02/20 done
1 c 09/02/20 null not done
1 d 09/02/20 09/02/20 done
the two conditions are met for this id, i did this :
select * from
(
select
a.* ,
case when (act = 'a' and end_dt > to_date('01/01/20','dd/mm/yy')) and (act = 'd' and status = 'done') then 1 end) flag
from
table a
)
where flag = 1;
but it won't do the required,it'll query only activity 'a' and 'd'
You can use exists:
select a.*
from a
where exists (select 1
from a a2
where a2.id = a.id and a2.act = 'a' and a2.st_dt <= date '2020-01-01'
) and
exists (select 1
from a a2
where a2.id = a.id and a2.act = 'd' and a2.stats = 'done'
) ;
If you like to learn something new:
Creation of your table:
create table tab as
with t (id,act, start_date, end_date, status) as
(
select 1,'a',to_date('01/01/2020','dd/mm/yyyy'), to_date('05/01/2020','dd/mm/yyyy'), 'done' from dual union all
select 1,'b',to_date('04/01/2020','dd/mm/yyyy'), to_date('09/02/2020','dd/mm/yyyy'), 'done' from dual union all
select 1,'c',to_date('09/02/2020','dd/mm/yyyy'), null , 'not done' from dual union all
select 1,'d',to_date('09/02/2020','dd/mm/yyyy'), to_date('09/02/2020','dd/mm/yyyy'), 'done' from dual union all
select 2,'a',to_date('09/03/2019','dd/mm/yyyy'), to_date('14/05/2020','dd/mm/yyyy'), 'done' from dual union all
select 2,'b',to_date('09/02/2020','dd/mm/yyyy'), to_date('25/06/2020','dd/mm/yyyy'), 'done' from dual union all
select 2,'c',to_date('01/03/2020','dd/mm/yyyy'), to_date('22/03/2020','dd/mm/yyyy'), 'done' from dual union all
select 2,'d',to_date('09/02/2020','dd/mm/yyyy'), null , 'not done' from dual union all
select 3,'a',to_date('11/05/2020','dd/mm/yyyy'), to_date('13/09/2019','dd/mm/yyyy'), 'done' from dual union all
select 3,'b',to_date('09/02/2020','dd/mm/yyyy'), to_date('04/07/2020','dd/mm/yyyy'), 'done' from dual union all
select 3,'c',to_date('01/02/2020','dd/mm/yyyy'), to_date('29/02/2020','dd/mm/yyyy'), 'done' from dual union all --End_date is wrong in your input data I changed it to 29
select 3,'d',to_date('11/02/2020','dd/mm/yyyy'), to_date('24/02/2020','dd/mm/yyyy'), 'done' from dual
)
select * from t
Here is the solution that works on 12c and later
select * from tab
match_recognize
(
partition by id
order by start_date
all rows per match
pattern (a random_rows* d)
define a as act = 'a' and a.start_date >= date'2020-01-01',
d as act = 'd' and d.status = 'done'
);
One option would be using aggregated conditionals through an analytic function to determine whether both conditions are satisfied at the same time :
WITH a2 AS
(
SELECT a.*, SUM(CASE WHEN act = 'a' AND end_dt >= date'2020-01-01'
THEN 1
ELSE 0
END)
OVER(PARTITION BY id) *
SUM(CASE WHEN act = 'd' AND stats = 'done'
THEN 1
ELSE 0
END)
OVER(PARTITION BY id) AS satisfies
FROM a
)
SELECT id, act, st_dt, end_dt, stats
FROM a2
WHERE satisfies = 1
Demo
First get in two subqueries the ID of the well started and well done jobs.
INTERSECT the results to get jobs with both conditions and use this IDset in the IN predicate
Query
select *
from tab a
where id in (
select id
from tab a
where (act = 'a' and st_dt >= to_date('01/01/20','dd/mm/yy'))
INTERSECT
select id
from tab a
where (act = 'd' and stats = 'done')
)

COUNT with WHERE clause giving more rows than without WHERE clause

This may not be the right forum to ask this but I want to understand the logical error happening in my query.
I have wrote below query to understand how many users have delivered messages greater than sent messages(possibly an error in data capture, just wanted to assess it).
SELECT COUNT(DISTINCT user_id)
FROM wk_24_trigger
UNION
SELECT COUNT(DISTINCT user_id)
FROM (
SELECT *, (CASE WHEN delivered > 0 THEN 1 ELSE 0 END) as D,
(CASE WHEN sent > 0 THEN 1 ELSE 0 END) as S
FROM wk_24_trigger) t
WHERE t.D > t.s
The result which I got are as belows
_c0
1 1056840
2 1819729
I am not getting why row 2 > row 1.
Ideally even if for every entry Delivered > Sent then row 2 and row 1 should have been same
Are you sure that the first row is the result from the first query and the second one from the second query..??
It always need not be..
Try adding alias name after the count in each query and verify the result..
you can check below example as well..
WITH TEMP
AS(
SELECT 'A' USER_ID , 1 DELIVERED , NULL SENT FROM DUAL
UNION
SELECT 'B' ID , 10 A , 1 B FROM DUAL
UNION
SELECT 'C' ID , NULL A , 1 B FROM DUAL
UNION
SELECT 'D' ID , -1 A , 1 B FROM DUAL
)
SELECT COUNT(DISTINCT USER_ID), 'QUERY_1' QUERY
FROM TEMP
UNION
(SELECT COUNT(DISTINCT USER_ID), 'QUERY_2'
FROM (
SELECT USER_ID,DELIVERED,SENT,
(CASE
WHEN DELIVERED > 0 THEN
1
ELSE
0
END) D,
(CASE
WHEN SENT > 0 THEN
1
ELSE
0
END) S
FROM TEMP) T
WHERE T.D > T.S);
and system output is as below..
COUNT(DISTINCTUSER_ID) QUERY
1 1 QUERY_2
2 4 QUERY_1
the same could be your case as well..

I need help converting T-SQL query to Oracle supported query

I am executing this query in SQL Server and it is working fine but when I try to execute it in Oracle, it is not giving the same results.
You can see in my attached photo the data of one customer, which have got the code 1, 2,4, 8 and he should get 0.70 value for having code 1,2,4 and then for having code 8 he should get 0.75 so after multiplication it should return 0.52 as value. I tried it in Oracle by replacing is null by nvl but it returned 1 instead of 0.52. Please help me convert this query in an oracle supported query which will return the same results.
Here is my query
SELECT [id] ,[name],r = isnull(nullif(
max(CASE WHEN [code] IN (1,2,4) then 0.70 else 0 end)
,0),1)
* isnull(nullif(
min(CASE WHEN [code] IN (1,2) then 0 else 1 end)
* max(CASE WHEN [code] IN (4) then 0.20 else 0 end)
,0),1)
* isnull(nullif(
max(CASE WHEN [code] IN (8) then 0.75 else 0 end)
,0),1)
FROM (values (1, 'ali',4)
,(1, 'ali',1)
,(1, 'ali',8)
,(1, 'ali',2)
,(2, 'sunny',1)
,(4, 'arslan',4)) as t(id, name,code)
GROUP BY id, name;
Since now you are multiplying scores, first we need to decide, what is the score if non of codes is matched. I suppose, it should be 0.
Next, we should break all possible codes into independent groups, that is which results do not depend on other groups members. Here they are (1,2,4) and (8). And define the rule for every group.
So
SELECT [id] ,[name],r =
-- At least one of values needed to get score > 0
MAX(CASE WHEN code IN (1,2,4, 8) THEN 1.0 ELSE 0.0 END) *
-- Now rules for every independent set of codes. Rule should return score if matched or 1.0 if not matched
-- (1,2,4)
coalesce(MAX(CASE WHEN [code] IN (1,2,4) THEN 0.70 END), 1.0 ) *
-- (8)
coalesce(MAX(CASE WHEN [code] IN (8) THEN 0.75 END), 1.0)
-- more ?
FROM (values (1, 'ali',4)
,(1, 'ali',1)
,(1, 'ali',8)
,(1, 'ali',2)
,(2, 'sunny',1)
,(4, 'arslan',4)) as t(id, name,code)
GROUP BY id, name;
There are some SQL Server things in the query that are not standard SQL:
[] around column names - remove them; you don't need them here (otherwise you would use standard SQL quotes "")
r = expression - for an alias name. Change this to standard SQL expression AS r
ISNULL(expression, value) - Change this to standard SQL COALESCE(expression, value) or Oracle's NVL(expression, value)
NULLIF(expression, value) - this you can keep; Oracle supports it, too
values (), (), ... - replace with a SELECT FROM DUAL UNION ALL subquery
You get:
select
id,
name,
coalesce(nullif( max(case when code in (1,2,4) then 0.70 else 0 end), 0), 1) *
coalesce(nullif( min(case when code in (1,2) then 0 else 1 end) *
max(case when code in (4) then 0.20 else 0 end) , 0), 1) *
coalesce(nullif( max(case when code in (8) then 0.75 else 0 end), 0), 1) as r
from
(
select 1 as id, 'ali' as name, 4 as code from dual
union all
select 1 as id, 'ali' as name, 8 as code from dual
union all
select 1 as id, 'ali' as name, 2 as code from dual
union all
select 2 as id, 'sunny' as name, 1 as code from dual
union all
select 4 as id, 'arslan' as name, 4 as code from dual
)
group by id, name;
The calculation, however, is unnecessarily complicated:
coalesce(nullif( max(case when code in (1,2,4) then 0.70 else 0 end), 0), 1)
means if there is at least one match then 0.70 else 0 which is turned to null which is turned to 1. So it is the same as
min(case when code in (1,2,4) then 0.70 else 1 end)
So if I am not mistaken, the whole calcultion becomes:
case when max(case when code in (1,2) then 1 end) = 1
then 0.7 else max(case when code = 4 then 0.14 else 1 end) end *
min(case when code = 8 then 0.75 else 1 end) as r
or
case when max(case when code in (1,2) then 1 end) = 1 then 0.7
when max(case when code = 4 then 1 end) = 1 then 0.14
else 1
end *
min(case when code = 8 then 0.75 else 1 end) as r
Well, there are many ways to write this.
The code below should give you the answer you expect;
CREATE TABLE #TestData (ID int, Name varchar(10), Code int)
INSERT INTO #TestData (ID, Name, Code)
VALUES
(1,'ali',4)
,(1,'ali',1)
,(1,'ali',8)
,(1,'ali',2)
,(2,'sunny',1)
,(4,'arslan',4)
SELECT DISTINCT
a.id
,a.Name
,COALESCE(b.HasCode1, b.HasCode2, b.HasCode4,1) * COALESCE(b.HasCode8,1) Result
FROM (SELECT ID, Name FROM #TestData GROUP BY ID, Name) a
LEFT JOIN
(
SELECT
ID
,Name
,SUM(CASE WHEN CODE = 1 THEN 0.7 END) HasCode1
,SUM(CASE WHEN CODE = 2 THEN 0.7 END) HasCode2
,SUM(CASE WHEN CODE = 4 THEN 0.7 END) HasCode4
,SUM(CASE WHEN CODE = 8 THEN 0.75 END) HasCode8
FROM #TestData
GROUP BY
ID
,Name
) b
ON a.ID = b.ID
AND a.Name = b.Name
DROP TABLE #TestData
If I understand what you're after (ie. for each of the cases, the id/name combination needs to have all the codes specified), then this will probably do what you're after. You may want to add some sort of trunc/floor/round function on the val column if you're after the answer to 2 decimal places, though:
with t as (select 1 id, 'ali' name, 4 code from dual union all
select 1 id, 'ali' name, 1 code from dual union all
select 1 id, 'ali' name, 8 code from dual union all
select 1 id, 'ali' name, 2 code from dual union all
select 2 id, 'ali' name, 4 code from dual union all
select 2 id, 'ali' name, 8 code from dual union all
select 3 id, 'bob' name, 1 code from dual union all
select 3 id, 'bob' name, 2 code from dual union all
select 3 id, 'bob' name, 8 code from dual),
res as (select id,
name,
case when count(distinct case when code in (1, 2, 4) then code end) = 3 then 0.7
when count(distinct case when code in (1, 2) then code end) = 2 then 0.5
else 1
end case_1_2_and_poss_4,
case when count(distinct case when code = 8 then code end) = 1 then 0.75 else 1 end case_8
from t
group by id, name)
select id,
name,
case_1_2_and_poss_4 * case_8 val
from res;
ID NAME VAL
---------- ---- ----------
1 ali 0.525
2 ali 0.75
3 bob 0.375

SQL query - sum of values by status for date interval

I get crazy because of one query. I have a table like following and I want to get a data - Summa of Values by Status For every Date in interval.
Table
Id Name Value Date Status
1 pro1 2 01.04.14 0
2 pro1 8 02.04.14 1
3 pro2 6 02.04.14 1
4 pro3 0 03.04.14 0
5 pro4 7 03.04.14 0
6 pro4 2 03.04.14 0
7 pro4 4 03.04.14 1
8 pro4 6 04.04.14 1
9 pro4 1 04.04.14 1
For example,
Input: Name = pro4, minDate = 01.02.14, maxDate = 04.09.14
Output:
Date Values sum for 0 Status Values sum for 1 Status
01.04.14 0 0
02.04.14 0 0
03.04.14 9 (=7+2) 4 (only 4 exist)
04.04.14 0 7 (6+1)
In 01.02.14 and 02.04.14 dates, pro4 has not values by status, but I want to show that rows, because I need all dates in that interval. Can anyone help me to create this query?
Edit:
I can not change structure, I have already that table with data. Every day exist in table many times (minimum 1 time)
Thanks in advance.
Assuming you have a row for each date in the table, use conditional aggregation:
select date,
sum(Case when name = 'pro4' and status = 0 then Value else 0 end) as values_0,
sum(case when name = 'pro4' and status = 1 then Value else 0 end) as values_1
from Table t
where date >= '2014-04-01' and date <= '2014-04-09'
group by date
order by date;
If you don't have this list of dates, you can take this approach instead:
with dates as (
select cast('2014-04-01' as date) as thedate
union all
select dateadd(day, 1, thedate)
from dates
where thedate < '2014-04-09'
)
select dates.thedate,
sum(Case when status = 0 then Value else 0 end) as values_0,
sum(case when status = 1 then Value else 0 end) as values_1
from dates left outer join
table t
on t.date = dates.thedate and t.name = 'pro4'
group by dates.thedate;
just an assumption query :
select Distinct date ,case when status = 0 and MAX(date) then SUM(value) ELSE 0 END Status0 ,
case when status = 1 and MAX(date) then SUM(value) ELSE 0 END Status1 from table
To expand my comment the complete query is
WITH [counter](N) AS
(SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1)
, days(N) AS (
SELECT row_number() over (ORDER BY (SELECT NULL)) FROM [counter])
, months (N) AS (
SELECT N - 1 FROM days WHERE N < 13)
, calendar ([date]) AS (
SELECT DISTINCT cast(dateadd(DAY, days.n
, dateadd(MONTH, months.n, '20131231')) AS date)
FROM months
CROSS JOIN days
)
SELECT a.Name
, c.Date
, [Sum of 0] = SUM(CASE Status WHEN 0 THEN Value ELSE 0 END)
, [Sum of 1] = SUM(CASE Status WHEN 1 THEN Value ELSE 0 END)
FROM Calendar c
LEFT JOIN myTable a ON c.Date = a.Date AND a.name = 'pro4'
WHERE c.date BETWEEN '20140201' AND '20140904'
GROUP BY c.Date, a.Name
ORDER BY c.Date
Note that the condition on the name need to be in the JOIN, otherwise you'll get only the date of your table.
If you need multiple years just add another CTE for the count and a dateadd(YEAR,...) in the CTE calendar
This is not really the exact query, but I think you can get that by having a query that looks like:
select date, status, sum(value) from table
where (date between mindate and maxdate) and name = product_name
group by date, status;
this page gives more info.
EDIT
So the above query only gives a part of the answer required by the OP. A LEFT OUTER JOIN of the original table and the result of the above query on thedate and status fields will give the missing info.
e.g.
select x.date, x.status, x.sum_of_values from table as y
left outer join
(select date, status, sum(value) as sum_of_values
from table
where (date between mindate and maxdate) and name = product_name
group by date, status) as x
on y.date= x.date and y.status = x.status
order by x.date;