Eliminate Multiple Output Oracle PL/SQL - sql

I'm running this query to test my function
select first_name, last_name,get_balance_due(orders.order_id)
from customers, orders, order_lines
Where customers.customer_id=orders.customer_id and orders.order_id = order_lines.order_id and
order_date >= to_date( '30-Nov-1999', 'DD-Mon-YYYY' )
AND order_date< to_date( '01-Dec-1999', 'DD-Mon-YYYY' );
Group By First_name, Last_name, get_balance_due(orders.order_id)
The output is repeated 3 times.
FIRST_NAME LAST_NAME GET_BALANCE_DUE(ORDERS.ORDER_ID)
Jan Busse 602450.56
Jan Busse 602450.56
Jan Busse 602450.56
The function get_balance_due
CREATE OR REPLACE FUNCTION Get_balance_due
(order_id_f NUMBER)
RETURN NUMBER
AS
balance_due_f NUMBER;
BEGIN
SELECT SUM(total_amount) INTO balance_due_f
FROM order_lines
WHERE order_id_f = order_id
RETURN balance_due_f;
end;
How can I have 1 result instead of 3?

Remove the order_lines from your select statement:
select first_name, last_name,get_balance_due(orders.order_id)
from customers INNER JOIN orders
ON customers.customer_id=orders.customer_id
Where
order_date >= to_date( '30-Nov-1999', 'DD-Mon-YYYY' )
AND order_date< to_date( '01-Dec-1999', 'DD-Mon-YYYY' );
(and use INNER JOINs)

Related

Why two of my different sql queries that must perform the same result act different?

I need to get the number of new buyers who came in 1990 year.
The first query says it's 17, but the second says it's 29? So which one is wrong and why?
SELECT DISTINCT COUNT(customer_id) FROM SALES_ORDER WHERE
EXTRACT(YEAR FROM order_date) = 1990
AND
customer_id NOT IN (SELECT customer_id FROM SALES_ORDER WHERE EXTRACT(YEAR FROM order_date) < 1990);
SELECT DISTINCT COUNT(customer_id) FROM SALES_ORDER WHERE
customer_id IN (SELECT customer_id FROM SALES_ORDER WHERE EXTRACT(YEAR FROM order_date) = 1990)
AND
customer_id NOT IN (SELECT customer_id FROM SALES_ORDER WHERE EXTRACT(YEAR FROM order_date) < 1990);
Here is my data schema:
Both queries do not do what you want. You are using SELECT DISTINCT COUNT(customer_id), while you probably want SELECT COUNT(DISTINCT customer_id).
I find that the logic would be simpler expressed with two levels of aggregation:
select count(*)
from (
select customer_id
from sales_order
group by customer_id
having extract(year from min(order_date)) = 1990
) t

SQL query to find the number of customers who shopped for 3 consecutive days in month of January 2020

I have below table called orders which has customer id and their order date (Note: there can be multiple orders from same customer on a single day)
create table orders (Id char, order_dt date)
insert into orders values
('A','1/1/2020'),
('B','1/1/2020'),
('C','1/1/2020'),
('D','1/1/2020'),
('A','1/1/2020'),
('B','1/1/2020'),
('A','2/1/2020'),
('B','2/1/2020'),
('C','2/1/2020'),
('B','2/1/2020'),
('A','3/1/2020'),
('B','3/1/2020')
I'm trying to write an SQL query to find the number of customers who shopped for 3 consecutive days in month of January 2020
Based on above order values, the output should be: 2
I referred other similar questions but still wasn't able to come the exact solution
Here is my solution which works fine even there are many orders of one customer in one day;
Some scripts to build test environment:
create table orders (Id varchar2(1), order_dt date);
insert into orders values('A',to_date('01/01/2020','dd/mm/yyyy'));
insert into orders values('B',to_date('01/01/2020','dd/mm/yyyy'));
insert into orders values('C',to_date('01/01/2020','dd/mm/yyyy'));
insert into orders values('D',to_date('01/01/2020','dd/mm/yyyy'));
insert into orders values('A',to_date('01/01/2020','dd/mm/yyyy'));
insert into orders values('B',to_date('01/01/2020','dd/mm/yyyy'));
insert into orders values('A',to_date('02/01/2020','dd/mm/yyyy'));
insert into orders values('B',to_date('02/01/2020','dd/mm/yyyy'));
insert into orders values('C',to_date('02/01/2020','dd/mm/yyyy'));
insert into orders values('B',to_date('02/01/2020','dd/mm/yyyy'));
insert into orders values('A',to_date('03/01/2020','dd/mm/yyyy'));
insert into orders values('B',to_date('03/01/2020','dd/mm/yyyy'));
select distinct id, count_days from (
select id,
order_dt,
count(*) over(partition by id order by order_dt range between 1 preceding and 1 following ) count_days
from orders group by id, order_dt
)
where count_days = 3;
-- Insert for test more days than 3 consecutive
insert into orders values('A',to_date('04/01/2020','dd/mm/yyyy'));
You can use two window functions to calculate difference between consequtive dates and sliding window with ROWS offset to count distinct preceiding consequtive days. Example here:
with gen as (
select 1 as cust_id, (date '2020-01-10') + 1 as q from dual union all
select 1, (date '2020-01-10') + 2 as q from dual union all
select 1, (date '2020-01-10') + 3 as q from dual union all
select 1, (date '2020-01-10') + 3 as q from dual union all
select 1, (date '2020-01-10') + 5 as q from dual union all
select 1, (date '2020-01-10') + 7 as q from dual union all
select 1, (date '2020-01-10') + 8 as q from dual union all
select 1, (date '2020-01-10') + 9 as q from dual
)
, diff as (
select gen.*
, q - lag(q) over(partition by cust_id, trunc(q, 'mm') order by q asc) as datediff
from gen
)
, window as (
select diff.*
, sum(decode(datediff, 1, 1, 0)) over(partition by cust_id, trunc(q, 'mm') order by q asc range between 2 preceding and current row) as cnt
from diff
)
select sum(count(distinct q)) as cnt
from window
where cnt = 2
group by cust_id
why not join twice based on same following two days. As long as you have index on the customer's ID and date, the join should be optimized. Because the joins require match on the same starting date basis, it either finds or it doesn't. If not, it is left out of the result set.
select distinct
o1.id
from
orders o1
JOIN orders o2
on o1.id = o2.id
AND o1.order_dt = o2.order_dt - interval '1' day
JOIN orders o3
on o1.id = o3.id
AND o1.order_dt = o3.order_dt - interval '2' day
Hmmmm . . . one method is to use lead()/lag(). Assuming that you don't have duplicates on a single day, then:
select distinct id
from (select o.*,
lag(order_dt) over (partition by id order by order_dt) as prev_order_dt,
lag(order_dt, 2) over (partition by id order by order_dt) as prev_order_dt2
from orders o
where order_dt >= date '2020-01-01' and
order_dt < date '2020-02-01'
) o
where prev_order_dt = order_dt - interval '1' day and
prev_order_dt2 = order_dt - interval '2' day;
EDIT:
If the table has duplicate records, the above is easily tweaked:
select distinct id
from (select o.*,
lag(order_dt) over (partition by id order by order_dt) as prev_order_dt,
lag(order_dt, 2) over (partition by id order by order_dt) as prev_order_dt2
from (select distinct o.id, trunc(order_dt) as order_dt
from orders o
where order_dt >= date '2020-01-01' and
order_dt < date '2020-02-01'
) o
) o
where prev_order_dt = order_dt - interval '1' day and
prev_order_dt2 = order_dt - interval '2' day;

PL/SQL distinct date for loop

I want to use for loop for date in my table which only cares years and months, not days.
CURSOR ret_cur is SELECT orderdate FROM Orders WHERE status
= 'DELAYED';
ret_rec ret_cur%ROWTYPE;
I currently have
insert into Orders(OrderId, CustomerId, RetailerId, ProductId, Count,
UnitPrice, OrderDate, Status) values (2,2,1,10,45,60,
to_date('20180102','YYYYMMDD'),'DELIVERED');
this data type in my orders table. (its an example for format)
I want to use DISTINCT to iterate through orderdate based on YYYY-MM. (dont care Day)
I have tried select distinct to_char(orderdate, 'YYYY-MM') but I seems to not work.
for example, if i have 20180103, 20180104, 20180105 , it should be one iteration since they all have same years and months.
To select days without time you could tunc(sysdate). For months we have to group by a char-value:
select to_char(mydatecol,'yyyymm'), count(*) from
(
select sysdate mydatecol from dual UNION ALL -- Fake-Table with some dates
select sysdate - 1 mydatecol from dual UNION ALL
select sysdate - 2 mydatecol from dual UNION ALL
select sysdate - 3 mydatecol from dual UNION ALL
select sysdate - 4 mydatecol from dual UNION ALL
select sysdate - 30 mydatecol from dual UNION ALL
select sysdate - 31 mydatecol from dual UNION ALL
select sysdate - 32 mydatecol from dual UNION ALL
select sysdate - 33 mydatecol from dual
)
group by to_char(mydatecol,'yyyymm')
Result:
201809 3
201810 6
I think you'd like to have such a collation as below :
with Orders
(
OrderId, CustomerId, RetailerId, ProductId,
Count, UnitPrice, OrderDate, Status
) as
(
select 2,2,1,10,45,60, to_date('20180102','YYYYMMDD'),'DELIVERED' from dual
)
select o.*
from Orders o
where to_char(OrderDate,'yyyy-mm')
= to_char(to_date('&myDate','yyyymmdd'),'yyyy-mm');
-- for "myDate" substitution variable use 20180103 or 20180104 or 20180105 .. etc.
The best way todo this kind of query is to truncate the date value:
SELECT CustomerId, trunc(OrderDate,'MM') OrderMonth
, sum(Count) totalCount
, sum(Count*UnitPrice) totalPrice
FROM Orders
GROUP BY CustomerId, trunc(OrderDate,'MM')
for example...

Avg date of birth year from single table (oracle sql)

Average date of birth from a single table where the column's date format is '01-JAN-2001'. Looking to return total count of users, and the average DOB YEAR (oracle sql)
mock table:
|User_ID|birth_date|
|123|01-JAN-2001|
|123|01-JAN-2001|
|123|01-JAN-2001|
Assuming your intention is to just average the year, you can parse it to a date, extract the year and average it as a number:
SELECT COUNT(*), AVG(EXTRACT(YEAR FROM TO_DATE(birth_date, 'dd-mon-yyyy'))
FROM users
(Oracle 12c:) Test table and data:
create table dt (
userid_ number generated always as identity ( start with 123 )
, d_ date
);
begin
for i in 1 .. 15
loop
insert into dt ( d_ ) values (
to_date ( trunc( dbms_random.value( 2446067, 2458100 ) ), 'J' )
) ;
end loop;
end;
/
SQL> select userid_, to_char( d_, 'DD-MON-YYYY' ) dob from dt;
USERID_ DOB
---------- --------------------
123 19-JUN-2000
124 06-OCT-2005
125 27-JAN-2012
126 09-JUL-2003
127 23-JUL-2010
128 07-FEB-1992
129 20-DEC-2002
130 19-MAY-2002
131 23-FEB-1990
132 26-DEC-1990
133 19-JUN-1999
134 16-DEC-1994
135 13-APR-2017
136 31-MAR-2000
137 23-MAY-1987
Query and result:
select
count(*) user_count
, trunc( avg( extract( year from d_ ) ) ) avg_dob_year
from dt ;
USER_COUNT AVG_DOB_YEAR
---------- ------------
15 2000
(Not taking "days" into account, just "years").
When using the method suggested by #mathguy
"... It can be calculated by subtracting a fixed date, like
2000/01/01, from all dates, taking the average of the differences, and
adding it to the fixed date."
... this may be a query we could start with (I'm sure it can be refined):
select
count(*) usercount
, to_date( '2000/01/01', 'YYYY/MM/DD' ) fixeddate
, avg( differences ) difftofixed
, to_date('2000/01/01', 'YYYY/MM/DD' ) + avg( differences ) fixedplusdiff
, extract( year from ( to_date('2000/01/01', 'YYYY/MM/DD' ) + avg( differences ) ) ) dob_year
from (
select
d_ - to_date( '2000/01/01', 'YYYY/MM/DD' ) differences
from dt
);
-- result
USERCOUNT FIXEDDATE DIFFTOFIXED FIXEDPLUSDIFF DOB_YEAR
15 01-JAN-00 250.6 07-SEP-00 2000
I don’t know why you’re trying to get the average date but I think it would go something like this :
“SELECT COUNT(user_id), AVG(EXTRACT(YEAR FROM TO_DATE(birth_date, 'dd-MM-yyyy'))
FROM users”

PL/SQL code to find leave detail of employee

I Have three tables :-
per_Absences_table have start_date, end_date.
per_absence_type have Privelege, Casual etc
per_people_table have Employee number, Employee name
I Want to find out number of leaves applied by an employee between two dates.
Eg :- If i pass '1-JAN-2013' as start date and '20-JUN-2013' as end date then all the leaves applied by the employee
will come as output
I Have written code for it which is working when i pas any parameter. But suppose there is an employee who applies a leave
from 10-May-2013 to 20-May-2013 and i pass the starting date as 11-May-2013 i.e. the employees who have taken leave after 11-may-2013 should come.
i.e. the leave applied from 10-May-2013 to 20-May-2013 should appear. Also if i want to find out the employees who have applied leave till 19-May-2103 even
then this leave should appear. All the other cases have been taken care of.
per_people_table :-
EMP_NUM EMP_NAME
P101 XYZ
PER_ABSENCE_TABLE
EMP_NUM START_DATE END_DATE TYPE_ID
101 10-May-2013 15-May-2013 1
per_absence_type
type_id leave_type
1 casual
Now, if i pass the parameter as 11-May-2013 (p_start_date) even then this record should appear.
and if i pass the parameter as 19-May-2013 (before the actual end date) even then this record should appear.
i.e.
declare
l_r varchar2(10) :=NULL;
L_E VARCHAR2(10) := NULL;
p_person_id VARCHAR2(10) :='P101',
p_start_date VARCHAR2(10);
p_end_date VARCHAR2(10);
leave_type VARCHAR2(10);
BEGIN
Leave_detail_packa.Leave_detail_packA(NULL,NULL,'P101','11-MAY-2013',NULL,NULL);
END;
Leave_detail_packa body
procedure Leave_details( errbuff out varchar2,
retcode out varchar2,
p_person_id VARCHAR2,
p_start_date varchar2,
p_end_date varchar2,
leave_type varchar2
)
as
l_st_date :=to_date(trunc(fnd_conc_date.string_to_date(p_start_date)));
l_end_date :=to_date(trunc(fnd_conc_date.string_to_date(p_end_date)));
/****************Cursor for start date ****************************/
Cursor c_var_st_date
is select
to_char(paa.date_start,'DD-MON-RRRR') Start_date
from per_Absences_table paa,
per_absence_type paat
where ( nvl(l_st_date,paa.date_start) between paa.date_start and paa.date_end
or paa.date_start >= nvl(l_st_date,paa.date_start))
paa.type_id =paat.type_id
and paat.name = nvl(leave_type,paat.name);
/****************Cursor for end date ****************************/
Cursor c_var_nd_date
is select
to_char(paa.date_end,'DD-MON-RRRR') End_date
from per_Absences_table paa,
per_absence_type paat
where ( nvl(l_st_date,paa.date_end) between paa.date_start and paa.date_end
or paa.date_end <= nvl(l_st_date,paa.date_end))
paa.type_id =paat.type_id
and paat.name = nvl(leave_type,paat.name);
/********** Cursor to give all the leave details ***************/
Cursor c_var(l_start_date varchar2,
l_end_date1 varchar2)
is
select
emp_number employee_number,
emp_name employee_name,
leave_type Leave_type,
to_char(paa.date_end,'DD-MON-RRRR') End_date,
to_char(paa.date_start,'DD-MON-RRRR') Start_date,
sum(No_of_days) Leave_days
from
per_Absences_table paa,
per_absence_type paat,
per_people_table
where
paa.date_start>= ( nvl(l_start_date),to_char(paa.date_start,'DD-MON-RRRR'))
and paa.date_end <= ( nvl(l_end_date1),to_char(paa.date_end,'DD-MON-RRRR'))
AND EMP_NUM=P_PERSON_ID;
/**** Begin looping******************************************************/
for ( c_var_number in c_var_st_date)
loop
for ( c_var_number1 in c_var_nd_date)
loop
for ( c_var_number3 in c_var(c_var_st_date.start_end,c_var_nd_date.end_date)
loop
dbms.output_put.line( c_var_number3.end_date,c_var_number3.start_date);
end loop;
end loop;
end loop;
Firstly, there's no need to use PL/SQL to do this at all. It can be done in a single SQL statement.
Secondly, you haven't provided the DDL for your tables so I'm going to assume they're set us as follows (also in a SQL Fiddle with additional cases):
create table per_people (
emp_num number not null
, emp_name varchar2(100) not null
, constraint pk_per_people primary key (emp_num)
);
create table per_absence_type (
type_id number not null
, leave_type varchar2(100) not null
, constraint pk_absence_type primary key (type_id)
);
create table per_absence (
emp_num number not null
, start_date date not null
, end_date date not null
, type_id number not null
, constraint pk_per_abs primary key (emp_num, start_date)
, constraint fk_per_abs_emp foreign key (emp_num)
references per_people(emp_num)
, constraint fk_per_abs_typ foreign key (type_id)
references per_absence_type (type_id)
, constraint chk_per_abs_dates check (start_date <= end_date )
);
These are the minimal constraints I'd have on these tables. If you're missing one of them I'd recommend adding it.
This query will give you all the information from all tables for all absences:
select pp.emp_num
, pp.emp_name
, pa.start_date
, pa.end_date
, pat.leave_type
from per_people pp
join per_absence pa
on pp.emp_num = pa.emp_num
join per_absence_type pat
on pa.type_id = pat.type_id
You want to find all employees who will be on leave between two dates. For this you need to add a WHERE clause to the above query. Let's assume that employees are taking the following absences:
insert all
into per_absence values( 101, date '2013-05-10', date '2013-05-15', 1)
into per_absence values( 101, date '2013-09-10', date '2013-09-10', 1)
into per_absence values( 102, date '2013-05-15', date '2013-05-20', 1)
into per_absence values( 103, date '2013-05-05', date '2013-05-20', 1)
select * from dual;
If you pass a start date of 2013-05-15 and an end date of 2013-05-20, you would expect three rows to be returned. If you pass a start date of 2013-05-16 you would only expect two.
You have to look at how you want to construct your WHERE clause. Looking at the date you want where your start date is between an absence start and end date or your end date is between an absence start and end date. Don't forget that an absence can only be one day so you want to include the start and end dates in the comparison.
This changes the query to:
select pp.emp_num
, pp.emp_name
, pa.start_date
, pa.end_date
, pat.leave_type
from per_people pp
join per_absence pa
on pp.emp_num = pa.emp_num
join per_absence_type pat
on pa.type_id = pat.type_id
where ( :start_date between pa.start_date and pa.end_date
or :end_date between pa.start_date and pa.end_date
)
Now, you want to find the number of leaves for a specific employee between the given dates. As your tables are properly normalised you can just GROUP BY the EMP_NUM to get it for every employee.
Assuming you want this for EMP_NUM 1 (single employee so no need for the GROUP BY) between 2013-05-15 and 2013-05-20 the query would look like this:
select count(*) as no_leaves
from per_people pp
join per_absence pa
on pp.emp_num = pa.emp_num
join per_absence_type pat
on pa.type_id = pat.type_id
where emp_num = 101
and ( date '2013-05-15' between pa.start_date and pa.end_date
or date '2013-05-20' between pa.start_date and pa.end_date
)
If I were faced with this problem, I would create a calendar table, JOIN using BETWEEN paa.date_start AND paa.date_end; that way regardless of when their leave started or ended I know whether they were on leave for any given day. If from there you are interested in finding out how many leaves they took, defining consecutive days as an instance of a leave, then you'd just have to find gaps between leave days.