SQL query for statistic info - sql

So let's assume I have two tables, cars and engines. Every car has an id column and sold_date column which points to a date when the car was sold. Every engine has car_id and type columns, where the first one is a foreign key to the cars table and the second is an engine's type name, which can be anything from V1 to V999.
So what I want to get is a list of dates from let's say August 1st to August 3rd with every type of engine and a number of sold cars like this:
sold_date engine_type number_of_sold_cards
08.01.2015 V8 6
08.01.2015 V6 8
08.01.2015 V4 9
08.02.2015 V8 15
08.02.2015 V6 0
08.02.2015 V4 5
08.03.2015 V8 4
08.03.2015 V6 6
08.03.2015 V4 0
The example assumes that for these 3 days were sold only cars with engines' types of V8, V6 and V4. What it means is that if there had been sold 5 types of engines for the period (V8, V6, V4, V2, V0) instead, I'd need 5 rows for every date

Partition outer join to the rescue!
with cars as (select 1 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 2 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 3 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 4 id, to_date('03/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 5 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 6 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 7 id, to_date('03/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 8 id, to_date('03/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 9 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 10 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 11 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 12 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 13 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 14 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 14 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual),
engines as (select 1 car_id, 'V8' engine_type from dual union all
select 2 car_id, 'V8' engine_type from dual union all
select 3 car_id, 'V8' engine_type from dual union all
select 4 car_id, 'V8' engine_type from dual union all
select 5 car_id, 'V4' engine_type from dual union all
select 6 car_id, 'V4' engine_type from dual union all
select 7 car_id, 'V4' engine_type from dual union all
select 8 car_id, 'V4' engine_type from dual union all
select 9 car_id, 'V6' engine_type from dual union all
select 10 car_id, 'V6' engine_type from dual union all
select 11 car_id, 'V6' engine_type from dual union all
select 12 car_id, 'V6' engine_type from dual union all
select 13 car_id, 'V2' engine_type from dual union all
select 14 car_id, 'V2' engine_type from dual union all
select 15 car_id, 'V0' engine_type from dual),
engine_types as (select distinct engine_type from engines),
res as (select c.id car_id,
e.engine_type,
c.sold_date
from engines e
inner join cars c on (e.car_id = c.id)),
final_res as (select et.engine_type,
res.sold_date
from engine_types et
left outer join res on (et.engine_type = res.engine_type)),
dates as (select to_date('01/08/2015', 'dd/mm/yyyy') - 1 + level dt -- paramaterise the start date
from dual
connect by level <= to_date('03/08/2015', 'dd/mm/yyyy') - to_date('01/08/2015', 'dd/mm/yyyy') + 1 -- paramaterise the start and end dates
)
select dts.dt,
fr.engine_type,
count(fr.sold_date) cnt
from dates dts
left outer join final_res fr partition by (fr.engine_type) on (dts.dt = fr.sold_date)
group by dts.dt,
fr.engine_type
order by dts.dt,
fr.engine_type;
DT ENGINE_TYPE CNT
---------- ----------- ----------
01/08/2015 V0 0
01/08/2015 V2 0
01/08/2015 V4 2
01/08/2015 V6 0
01/08/2015 V8 2
02/08/2015 V0 0
02/08/2015 V2 0
02/08/2015 V4 0
02/08/2015 V6 3
02/08/2015 V8 1
03/08/2015 V0 0
03/08/2015 V2 0
03/08/2015 V4 2
03/08/2015 V6 0
03/08/2015 V8 1
The first and second subqueries ("cars" and "engines") are just mimicking your tables; you would not need to include them in your query.
The "engine_types" subquery is just getting the distinct list of engine_types used in the engines table. If you have some other table that lists the available engine_types, then use that instead.
The "dates" subquery is just generating a list of dates between a given date range - in an ideal world, the start and end dates would be parameterised (assuming this is being run in PL/SQL or some such).
The "res" subquery does the join on the cars and engines table, to get the type of engine sold on each date.
The "final_res" subquery outer joins the res and engine_types subqueries, so that every engine type is listed, along with the sold_date, if it was sold.
Once you have that, then it's easy to do a partition outer join on the final_res subquery to the dates subquery.

You need to join cars and engine tables. Need to count the sold cars and group by sold_date and engine_type:
select c.sold_date
,e.engine_type
,count(*) as number_of_sold_cars
from cars c
inner join engines e on c.id = e.car_id
where c.sold_date between date '2015-08-01' and date '2015-08-03'
group by c.sold_date,e.engine_type
Update:
With this query you will have all engine types sold in the selected period. You will have 0 for number_of_sold_cars if this engine type is not sold on the particular date:
with engine_type as (
select distinct e.engine_type
from cars c
inner join engines e on c.id = e.car_id
where c.sold_date between date '2015-08-01' and date '2015-08-03'
)
select c.sold_date
,t.engine_type
,count(*) as number_of_sold_cars
from cars c
inner join engines e on c.id = e.car_id
left join enginte_type t on t.engine_type = e.engine_type
where c.sold_date between date '2015-08-01' and date '2015-08-03'
group by c.sold_date,t.engine_type

Related

Return Month wise count if no data for month return 0 as count in oracle sql

I have a table having data for January to March (till current month) and I am able to take the month wise count.But user required is to display zero for rest of the month.Kindly suggest.
For example:
select count(a.emp_id) as cnt ,to_char(a.due_date,'MONTH') as Process_Month from EMP_Request a
where a.due_date is not null
group by to_char(a.due_date,'MONTH')
Output:
cnt Process_month
20 JANUARY
35 FEBUARY
26 March
Desired output:
cnt Process_month
20 JANUARY
35 FEBUARY
26 March
0 APRIL
0 MAY
…….
….
….
0 DECEMBER
Please assist.
use WWV_FLOW_MONTHS_MONTH to get all the month and left join with your query to get the month name from the date column and join with it
with cte
(
SELECT month_display as month FROM WWV_FLOW_MONTHS_MONTH
) , cnt as
(
select count(a.emp_id) as cnt ,
to_char(a.due_date,'MONTH') as Process_Month from EMP_Request a
where a.due_date is not null
group by to_char(a.due_date,'MONTH')
) select coalesce(Process_Month,month), cnt from cte left join cnt on cte.month=cnt.to_char(to_date(Process_Month, 'DD-MM-YYYY'), 'Month')
Right join months generator with your query:
select to_char(to_date(mth_num, 'MM'), 'MONTH') month, nvl(cnt, 0) cnt
from (
select count(emp_id) as cnt, to_char(due_date, 'mm') mth_num
from emp_request where due_date is not null
group by to_char(due_date, 'mm')) e
right join (
select to_char(level, 'fm00') mth_num
from dual connect by level <= 12) m using (mth_num)
order by mth_num
dbfiddle demo
Months generator is a simple hierarchical query which gives us 12 values 01, 02... 12:
select to_char(level, 'fm00') mth_num from dual connect by level <= 12
You can also use system views to get these numbers:
select to_char(rownum, 'fm00') mth_num from all_objects where rownum <= 12
or this syntax:
select to_char(column_value, 'fm00') mth_num
from table(sys.odcivarchar2list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
It's better to work on numbers which you can sort properly and convert to month names in the last step. This way you have natural months order.
If you want to be sure that month names are always in english, not dependent from local settings then use to_date with third parameter, like here:
select to_char(sysdate, 'month', 'nls_date_language=english') from dual
This is a general problem which is not really a sql problem. SQL doesn't really know about what months you are interested in. So the solution is to tell it in a sub query.
Here is a solution that doesn't use external tables. You simply select all months of the year and outer join your data.
select TO_CHAR(TO_DATE(available_months.m,'MM'),'MONTH') , NVL(sum(data.cnt),0) from
(select to_number(to_char(sysdate,'MM')) m, 7 cnt from dual) data,
(select 1 m from dual union select 2 from dual union select 3 from dual union select 4 from dual
union select 5 from dual union select 6 from dual union select 7 from dual
union select 8 from dual union select 9 from dual union select 10 from dual
union select 11 from dual union select 12 from dual) available_months
where
data.m (+) = available_months.m
group by available_months.m
order by available_months.m;
Or with your data query included is should look like (not tested):
select TO_CHAR(TO_DATE(available_months.m,'MM'),'MONTH') , NVL(sum(data.cnt),0) from
(select count(a.emp_id) as cnt ,to_char(a.due_date,'MONTH') as Process_Month from EMP_Request a where a.due_date is not null) data
(select 1 m from dual union select 2 from dual union select 3 from dual union select 4 from dual
union select 5 from dual union select 6 from dual union select 7 from dual
union select 8 from dual union select 9 from dual union select 10 from dual
union select 11 from dual union select 12 from dual) available_months
where
data.due_date (+) = available_months.m
group by available_months.m
order by available_months.m;

Month counts between dates

I have the below table. I need to count how many ids were active in a given month. So thinking I'll need to create a row for each id that was active during that month so that id can be counted each month. A row should be generated for a term_dt during that month.
active_dt term_dt id
1/1/2018 101
1/1/2018 5/15/2018 102
3/1/2018 6/1/2018 103
1/1/2018 4/25/18 104
Apparently this is a "count number of overlapping intervals" problem. The algorithm goes like this:
Create a sorted list of all start and end points
Calculate a running sum over this list, add one when you encounter a start and subtract one when you encounter an end
If two points are same then perform subtractions first
You will end up with list of all points where the sum changed
Here is a rough outline of the query. It is for SQL Server but could be ported to any RDBMS that supports window functions:
WITH cte1(date, val) AS (
SELECT active_dt, 1 FROM #t AS t
UNION ALL
SELECT COALESCE(term_dt, '2099-01-01'), -1 FROM #t AS t
-- if end date is null then assume the row is valid indefinitely
), cte2 AS (
SELECT date, SUM(val) OVER(ORDER BY date, val) AS rs
FROM cte1
)
SELECT YEAR(date) AS YY, MONTH(date) AS MM, MAX(rs) AS MaxActiveThisYearMonth
FROM cte2
GROUP BY YEAR(date), MONTH(date)
DB Fiddle
I was toying with a simpler query, that seemed to do the trick, for Oracle:
with candidates (month_start) as (
select to_date ('2018-' || column_value || '-01','YYYY-MM-DD')
from
table
(sys.odcivarchar2list('01','02','03','04','05',
'06','07','08','09','10','11','12'))
), sample_data (active_dt, term_dt, id) as (
select to_date('01/01/2018', 'MM/DD/YYYY'), null, 101 from dual
union select to_date('01/01/2018', 'MM/DD/YYYY'),
to_date('05/15/2018', 'MM/DD/YYYY'), 102 from dual
union select to_date('03/01/2018', 'MM/DD/YYYY'),
to_date('06/01/2018', 'MM/DD/YYYY'), 103 from dual
union select to_date('01/01/2018', 'MM/DD/YYYY'),
to_date('04/25/2018', 'MM/DD/YYYY'), 104 from dual
)
select c.month_start, count(1)
from candidates c
join sample_data d
on c.month_start between d.active_dt and nvl(d.term_dt,current_date)
group by c.month_start
order by c.month_start
An alternative solution would be to use a hierarchical query, e.g.:
WITH your_table AS (SELECT to_date('01/01/2018', 'dd/mm/yyyy') active_dt, NULL term_dt, 101 ID FROM dual UNION ALL
SELECT to_date('01/01/2018', 'dd/mm/yyyy') active_dt, to_date('15/05/2018', 'dd/mm/yyyy') term_dt, 102 ID FROM dual UNION ALL
SELECT to_date('01/03/2018', 'dd/mm/yyyy') active_dt, to_date('01/06/2018', 'dd/mm/yyyy') term_dt, 103 ID FROM dual UNION ALL
SELECT to_date('01/01/2018', 'dd/mm/yyyy') active_dt, to_date('25/04/2018', 'dd/mm/yyyy') term_dt, 104 ID FROM dual)
SELECT active_month,
COUNT(*) num_active_ids
FROM (SELECT add_months(TRUNC(active_dt, 'mm'), -1 + LEVEL) active_month,
ID
FROM your_table
CONNECT BY PRIOR ID = ID
AND PRIOR sys_guid() IS NOT NULL
AND LEVEL <= FLOOR(months_between(coalesce(term_dt, SYSDATE), active_dt)) + 1)
GROUP BY active_month
ORDER BY active_month;
ACTIVE_MONTH NUM_ACTIVE_IDS
------------ --------------
01/01/2018 3
01/02/2018 3
01/03/2018 4
01/04/2018 4
01/05/2018 3
01/06/2018 2
01/07/2018 1
01/08/2018 1
01/09/2018 1
01/10/2018 1
Whether this is more or less performant than the other answers is up to you to test.

SQL Oracle Query self query

I am trying to figure out how to populate the below NULL values with 1.245 for dates from 07-OCT-14 to 29-SEP-14 then from 26-SEP-14 to 28-JUL-14 it will be 1.447.
This means if the date is less than or equal to the given date then use the value of max effective date which is less than the given date
We could select the last available index_ratio value for given security_alias and effective date <=p.effective_date , so in other words we will need to modify the sql to return from the subquery the index ratio value identified for the maximum available effective date assuming that this effective date is less or equal position effective date
How to populate the value ?
select ab.security_alias,
ab.index_ratio,
ab.effective_date
from securitydbo.security_analytics_fi ab
where ab.security_alias = 123627
order by ab.effective_date desc
Below should be the output
Assuming I understand your requirements correctly, I think the analytic function LAST_VALUE() is what you're after. E.g.:
with sample_data as (select 1 id, 10 val, to_date('01/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, null val, to_date('02/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, null val, to_date('03/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, null val, to_date('04/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, 20 val, to_date('05/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, 21 val, to_date('06/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, null val, to_date('07/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, null val, to_date('08/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, 31 val, to_date('09/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, null val, to_date('10/08/2015', 'dd/mm/yyyy') dt from dual union all
select 1 id, 42 val, to_date('11/08/2015', 'dd/mm/yyyy') dt from dual)
select id,
last_value(val ignore nulls) over (partition by id order by dt) val,
dt
from sample_data
order by id, dt desc;
ID VAL DT
---------- ---------- ----------
1 42 11/08/2015
1 31 10/08/2015
1 31 09/08/2015
1 21 08/08/2015
1 21 07/08/2015
1 21 06/08/2015
1 20 05/08/2015
1 10 04/08/2015
1 10 03/08/2015
1 10 02/08/2015
1 10 01/08/2015

Transposing rows into columns

I have following data
with sample_data as (select to_date('05/01/2015', 'dd/mm/yyyy') dt, '1' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '2' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '3' Period, 'P' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '4' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '5' Period, 'P' code from dual
)
select * from sample_data
That gives me simple query results as below
DT PERIOD CODE
2015-01-05 00:00:00 1 A
2015-01-05 00:00:00 2 A
2015-01-05 00:00:00 3 P
2015-01-05 00:00:00 4 A
2015-01-09 00:00:00 5 P
I would like to transpose the results like this -
DATE ATTENDANCE
2015-01-05 12345
AAPAP
How can I do that?
Thanks a bunch!
This is an Oracle SQL question and not PL/SQL!
That could be something like:
select to_char(dt,'YYYY-MM-DD') date,listagg(period) within group (order by period)||chr(13)||chr(10)||listagg(code) within group (order by period) attendance
from (
with sample_data as (select to_date('05/01/2015', 'dd/mm/yyyy') dt, '1' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '2' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '3' Period, 'P' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '4' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '5' Period, 'P' code from dual
) select * from sample_data
) group by to_char(dt,'YYYY-MM-DD')
Note that the function listagg only can be used from Oracle 11.2. If you are on an earlier version you can use xmlagg, but thats a bit more clotted.

How do I write an SQL to get a cumulative value and a monthly total in one row?

Say, I have the following data:
select 1 id, date '2007-01-16' date_created, 5 sales, 'Bob' name from dual union all
select 2 id, date '2007-04-16' date_created, 2 sales, 'Bob' name from dual union all
select 3 id, date '2007-05-16' date_created, 6 sales, 'Bob' name from dual union all
select 4 id, date '2007-05-21' date_created, 4 sales, 'Bob' name from dual union all
select 5 id, date '2013-07-16' date_created, 24 sales, 'Bob' name from dual union all
select 6 id, date '2007-01-17' date_created, 15 sales, 'Ann' name from dual union all
select 7 id, date '2007-04-17' date_created, 12 sales, 'Ann' name from dual union all
select 8 id, date '2007-05-17' date_created, 16 sales, 'Ann' name from dual union all
select 9 id, date '2007-05-22' date_created, 14 sales, 'Ann' name from dual union all
select 10 id, date '2013-07-17' date_created, 34 sales, 'Ann' name from dual
I want to get results like the following:
Name Total_cumulative_sales Total_sales_current_month
Bob 41 24
Ann 91 34
In this table, for Bob, his total sales is 41 starting from the beginning. And for this month which is July, his sales for this entire month is 24. Same goes for Ann.
How do I write an SQL to get this result?
Try this way:
select name, sum(sales) as Total_cumulative_sales ,
sum(
case trunc(to_date(date_created), 'MM')
when trunc(sysdate, 'MM') then sales
else 0
end
) as Total_sales_current_month
from tab
group by name
SQL Fiddle Demo
More information
Trunc
Case Statement
SELECT Name,
SUM(Sales) Total_sales,
SUM(CASE WHEN MONTH(date_created) = MONTH(GetDate()) AND YEAR(date_created) = YEAR(GetDate()) THEN Sales END) Total_sales_current_month
GROUP BY Name
Should work, but there's probably a more elegant way to specify "in the current month".
This should work for sales over a number of years. It will get the cumulative sales over any number of years. It won't produce a record if there are no sales in the latest month.
WITH sales AS
(select 1 id, date '2007-01-16' date_created, 5 sales, 'Bob' sales_name from dual union all
select 2 id, date '2007-04-16' date_created, 2 sales, 'Bob' sales_name from dual union all
select 3 id, date '2007-05-16' date_created, 6 sales, 'Bob' sales_name from dual union all
select 4 id, date '2007-05-21' date_created, 4 sales, 'Bob' sales_name from dual union all
select 5 id, date '2013-07-16' date_created, 24 sales, 'Bob' sales_name from dual union all
select 6 id, date '2007-01-17' date_created, 15 sales, 'Ann' sales_name from dual union all
select 7 id, date '2007-04-17' date_created, 12 sales, 'Ann' sales_name from dual union all
select 8 id, date '2007-05-17' date_created, 16 sales, 'Ann' sales_name from dual union all
select 9 id, date '2007-05-22' date_created, 14 sales, 'Ann' sales_name from dual union all
select 10 id, date '2013-07-17' date_created, 34 sales, 'Ann' sales_name from dual)
SELECT sales_name
,total_sales
,monthly_sales
,mon
FROM (SELECT sales_name
,SUM(sales) OVER (PARTITION BY sales_name ORDER BY mon) total_sales
,SUM(sales) OVER (PARTITION BY sales_name,mon ORDER BY mon) monthly_sales
,mon
,max_mon
FROM ( SELECT sales_name
,sum(sales) sales
,mon
,max_mon
FROM (SELECT sales_name
,to_number(to_char(date_created,'YYYYMM')) mon
,sales
,MAX(to_number(to_char(date_created,'YYYYMM'))) OVER (PARTITION BY sales_name) max_mon
FROM sales
ORDER BY 2)
GROUP BY sales_name
,max_mon
,mon
)
)
WHERE max_mon = mon
;