Okay so I am trying to a rownum into a variable but I need it to give me only one value, so 2 if it's the second number in the row - oracle

select rownum into v_rownum
from waitlist
where p_callnum=callnum
order by sysdate;
tried doing this but gives too many values.
and if I do p_snum=snum, it will keep returning 1. I need it to return 2 if it's #2 on the waitlist.
select rn into v_rownum
from (select callnum,
row_number() over (order by sysdate) rn
from waitlist)
where p_snum=snum;
Almost got it to work. Running into issues in the first select. I believe I might have to use v_count instead. Also Ordering by Sysdate even if a second apart will order it correctly.
SNU CALLNUM TIME
--- ---------- ---------
101 10125 11-DEC-18
103 10125 11-DEC-18
BTW time is = date which I entered people into waitlist using sysdate. So I suppose ordering by time could work.
create table waitlist(
snum varchar2(3),
callnum number(8),
time date,
constraint fk_waitlist_snum foreign key(snum) references students(snum),
constraint fk_waitlist_callnum foreign key(callnum) references schclasses(callnum),
primary key(snum,callnum)
);
is the waitlist table.

I used Scott's DEPT table to create your WAITLIST; department numbers represent CALLNUM column:
SQL> select * From waitlist;
CALLNUM WAITER
---------- --------------------
10 ACCOUNTING
20 RESEARCH
30 SALES
40 OPERATIONS
How to fetch data you need?
using analytic function (ROW_NUMBER) which orders values by CALLNUMs, you'll know the order
that query will be used as an inline view for the main query that returns number in the waitlist for any CALLNUM
Here's how:
SQL> select rn
2 from (select callnum,
3 row_number() over (order by callnum) rn
4 from waitlist
5 )
6 where callnum = 30;
RN
----------
3
SQL>

rownum in oracle is a generated column, it does not refer to any specific row, it is just the nth row in a set.
With a select into it can only return one row (hence the two many rows error) so rownum will always be 1.
Without more details about your table structure, and how you are uniquely identifying records it is hard to give assist you further with a solution.

Related

How to execute trigger and procedure in oracle

I am working on my Project 'Supermarket Billing Management System' since I am a beginner I m facing a lot of issues while making project. Here I've already created a trigger and a procedure but I don't know how I can execute it, I've created a trigger for a total price of a single Product i.e ProdTotal = ProdPrice * ProdQuantity;.
That means whenever a user enters some data in the Products table, then this trigger must get executed, but I don't know how to execute it, similarly, I've created a procedure to calculate the total price of all the products purchased by a single customer. Just like when you go to the supermarket or any store then after purchasing the items, you get a bill and there is the final total amount. I m not even exactly sure whether my procedure code is right or wrong, though it was created successfully, I m not sure whether it will give me the exact output which I want, So if you can help me then, please let me know, I've researched a lot from different websites and also from many youtube videos but seriously I am not getting how to solve it, so please help me!
Code:
Products table
create table Products
( ProdId number primary key,
ProdNum number not null unique,
ProdName varchar2(15),
ProdPrice int,
ProdQuantity int,
ProdCustId int references Customers,
ProdOrdId int references Orders,
ProdStoreId int references Stores
);
Payments table
create table Payments
( PayId int primary key,
PayDate date,
ProdTotal int,
FinalTotal int,
PayOrdId int references orders,
PayProdId int references Products,
PayCustId int references Customers
);
Trigger code
create trigger PROD_TOTAL
AFTER INSERT ON Products
BEGIN
UPDATE Payments
SET ProdTotal = (SELECT Products.ProdPrice * Products.ProdQuantity FROM Products);
END;
/
Procedure code
create procedure FINAL_TOTAL(C IN NUMBER, T OUT NUMBER)
IS
BEGIN
UPDATE Payments
SET FinalTotal = FinalTotal + ProdTotal
WHERE PayCustId = C;
Commit;
SELECT FinalTotal into T FROM Payments WHERE PayCustId = C;
END;
/
Insert statement in Product table:
insert into Products values(1,1001,'Syrup',30,4,1,1,1);
Insert statements in Payments table:
insert into Payments(PayId, PayDate, PayOrdID, PayProdId, PayCustId)
values(1,date'2020-10-07',1,1,1);
Output:
select * from products;
PRODID PRODNUM PRODNAME PRODPRICE PRODQUANTITY PRODCUSTID
---------- ---------- --------------- ---------- ------------ ----------
PRODORDID PRODSTOREID
---------- -----------
1 1001 Syrup 30 4 1
1 1
select * from Payments;
PAYID PAYDATE PRODTOTAL FINALTOTAL PAYORDID PAYPRODID PAYCUSTID
---------- --------- ---------- ---------- ---------- ---------- ----------
1 07-OCT-20 1 1 1
Now here, as you can see PRODTOTAL and FINALTOTAL column is blank, I know why it is blank because I didn't enter any value. And the reason why I didn't enter any value in these two columns is that I want, the system should automatically calculate that calculation with the help of trigger and procedure and, I can't even remove trigger and procedure because it's mandatory in our project to use both of these concepts. So please help me!!!
As already proposed, first try to get the design right with respect to your requirements. You can implement many constraints just by designing correctly your database schema.
Stay away from triggers and PL/SQL for as long as possible. It will force you to better design in the end and will pay off.
Before using triggers for business logic, try to use views for things that can be selected. That's what the database is for.
When you are "done", test for the performance and if it's suboptimal, improve your schema. If nothing helps, start using triggers for business logic.
I've put together a sample with views I am talking about. I hope it can get you started.
create table Products (
ProdId number generated always as identity primary key
, ProdName varchar2(20) not null
);
create table Stores (
StoreId number generated always as identity primary key
, StoreName varchar2(20) not null
);
create table Customers (
CustomerId number generated always as identity primary key
, CustomerName varchar2(20) not null
);
create table Prices (
PriceId number generated always as identity primary key
, ProdId number not null
, Price number
, ValidFrom date default on null sysdate
, constraint fk_Prices_Product foreign key (ProdId) references Products (ProdId)
);
create unique index uniq_prices_product_price on Prices (ProdId, ValidFrom);
create table Orders (
OrderId number generated always as identity primary key
, CustomerId number not null
, StoreId number not null
, OrderedAt date default on null sysdate
, constraint fk_Orders_Customer foreign key (CustomerId) references Customers (CustomerId)
, constraint fk_Orders_Store foreign key (StoreId) references Stores (StoreId)
);
create table OrderLines (
OrderLineId number generated always as identity primary key
, OrderId number not null
, ProdId number not null
, ProdQuantity number not null
, constraint fk_OrderLines_Order foreign key (OrderId) references Orders (OrderId)
, constraint fk_OrderLines_Prod foreign key (ProdId) references Products (ProdId)
);
create table Payments (
PaymentId number generated always as identity primary key
, OrderId number not null
, PaidAt date default on null sysdate
, PaidAmount number not null
, constraint fk_Payments_Order foreign key (OrderId) references Orders (OrderId)
);
create view Prices_V as
select
p.*
, coalesce(
lead(p.ValidFrom) over (partition by p.ProdId order by p.ValidFrom)
, to_date('9999', 'YYYY')
) ValidTo
from Prices p;
create view Orders_V as
select
o.*
, (
select sum(ol.ProdQuantity * p.Price)
from OrderLines ol
join Prices_V p on (p.ProdId = ol.ProdId and o.OrderedAt between p.ValidFrom and p.ValidTo)
where o.OrderId = ol.OrderId
) Total
, (
select sum(PaidAmount)
from Payments p
where p.OrderId = o.OrderId
) TotalPaid
from Orders o;
insert into Products(ProdName)
select 'Prod A' from dual union all
select 'Prod B' from dual;
insert into Stores(StoreName) values ('Store A');
insert into Customers(CustomerName)
select 'Customer A' from dual union all
select 'Customer B' from dual;
insert into Prices(ProdId, Price, ValidFrom)
select 1, 10, sysdate - 10 from dual union all
select 1, 12, sysdate - 2 from dual union all
select 1, 14, sysdate + 3 from dual union all
select 2, 100, sysdate - 10 from dual union all
select 2, 90, sysdate - 2 from dual union all
select 2, null, sysdate + 5 from dual;
insert into Orders(CustomerId, StoreId, OrderedAt)
select 1 cid, 1 stoid, sysdate - 5 from dual union all
select 2, 1, sysdate - 5 from dual union all
select 2, 1, sysdate - 1 from dual;
insert into OrderLines(OrderId, ProdId, ProdQuantity)
select 1 ordid, 1 prodid, 3 prodquant from dual union all
select 1, 2, 2 from dual union all
select 2, 2, 10 from dual union all
select 3, 2, 10 from dual;
insert into Payments(OrderId, PaidAmount) values (2, 500);
select * from Prices_V order by ProdId, ValidFrom;
select * from OrderLines order by OrderId, ProdId;
select * from Orders_v order by OrderId;
Some of the ideas in there:
Prices are stored in separate table, reference the product and have validity so that product price can change over time. Price view have ValidTo column added so it's easier to work with
There is a unique index on Prices so that we cannot have 2 prices for the same product at the same time
You can have many items in order, so that's why there is Orders and OrderLines tables in 1-to-many relationship
In Order_V the total paid is shown (using a subquery on Payments) and the total order values is shown (using a subquery on OrderLines and Prices, date of order is used to get prices form the correct period)
Based on the schema you will se what things you can represent and which you cannot. It's your job to make it match your requirements :)
And now I've come to the point you say triggers and procedures are mandatory in your project. Hence I have a proposal:
Create a procedure that will allow users to create new price for a product. It should definitely check that the validity does not start in the past. Then implement another one that allows for changing the valid to date (also cannot end in the past). You can than revoke any insert/update privileges on Products table and force users to use your procedures that will contain this business logic.
Create a table PricesLog and trigger on Prices that will insert the PriceId, old.Price, new.Price, sysdate and User to the log on any inserts/updates to the prices table.

Problem with MINUS and sub queries with ORDER BYs

select salary
from (
(select salary
from employees
where rownum<=10
order by salary desc)
minus
(select salary
from employees
where rownum<=4
order by salary desc)
);
You cannot use ORDER BY there.
Try this instead:
select salary from (
select salary, row_number() over ( order by salary desc ) rn
from employees )
where rn between 5 and 10;
On Oracle 12c or later, you can also do this:
select salary from employees
order by salary desc
offset 4 rows fetch next 6 rows only;
You've got several issues in what you've written. The immediate problem is that you'll get an error from having an order by in the first branch of your union, but just removing that won't help you much.
You're making a (fairly common) mistake with ordering and rownum; looking just at the first subquery you have:
select salary
from employees
where rownum<=10
order by salary desc
The rownum filter will be applied before the order-by, so what this will actually produce is 10 indeterminate rows from the table, which are then ordered. If I run that I get:
SALARY
----------
24000
13000
12000
10000
8300
6500
6000
4400
2600
2600
but you'll see different values, even from the same sample schema. If you look at the whole table you'll see higher values than those; and even running the second query will show something isn't as you expect - for me that gets:
SALARY
----------
13000
4400
2600
2600
which are not the first four rows from the previous query. (Again, you'll see different results, but hopefully the same effect; if not, look at the whole table ordered by salary.)
You need to order the whole table - in a subquery - and then filter:
select salary
from (
select salary
from employees
order by salary desc
)
where rownum<=10
which gives a much more sensible - and consistent - result. You can then minus the two queries:
select salary
from (
select salary
from employees
order by salary desc
)
where rownum<=10
minus
select salary
from (
select salary
from employees
order by salary desc
)
where rownum<=4
order by salary desc;
SALARY
----------
13500
13000
12000
11500
You may be expecting to see six values there, but there are three employees with a salary of 12000, and minus eliminates duplicates so that is only reported once. #Matthew's approach (or #Jeff's!) will give you all six, including duplicates, if that is what you want. It also stops you having to hit the table multiple times.
A further problem is with ties - if the 4th highest was the same as the 5th highest, what would you expect to happen? Using minus would exclude that value; #Matthew's approach would preserve it.
You need to define what you actually want to get - the 5th to 10th highest salary values? The salaries of the 5th to 10th highest-paid people (a subtle but important difference)? Do you really only want the numbers, or who those employees are - in which case how you deal with ties is even more important? Etc. Once you know what you actually need to find you can decide the best way to get that result.
It doesn't make sense to order rows in two sets that are subsequently operated upon because sets don't have order. If you need a solution that can execute on older versions and you want to return the bottom 6 ranked out of the top 10 ranked, then this will work. If you can use newer features, then you may want to because it's possible they'll require fewer machine instruction executions.
After making the obvious changes that escaped me in my haste...
select salary
from (
select rownum rn, salary
from (
select salary
from employees
order by salary desc
)
)
where rn between 5 and 10

In oracle SQL DB same primary Id is present more then once with different batch_id. How can I know the batch ID just before the current batch ID

I am working on oracle database.
We load customer data in source table which eventually migrates to target table.
Every time customer data is loaded in source table it is having a unique batch_id.
If we want to update some field in customer table, then we again load the same customer in source table but this time with different batch_id.
Now I want to know batch_id of the customer just before the latest batch_id.
Batch_id we take is usually the current date.
Use ROW_NUMBER analytic function
your sample data
select * from tab
order by 1,2
CUSTOMER_ID BATCH_ID
----------- -------------------
1 09.12.2019 00:00:00
1 10.12.2019 00:00:00
2 10.12.2019 00:00:00
Row_number assihns sequence number starting from 1 for each customer order descending on BATCH_ID - you are interested on one before the latest, i.e. the rows with the number 2.
with cust as (
select
customer_id, batch_id,
row_number() over (partition by customer_id order by batch_id desc) rn
from tab)
select CUSTOMER_ID, BATCH_ID
from cust
where rn = 2;
CUSTOMER_ID BATCH_ID
----------- -------------------
1 09.12.2019 00:00:00
It seems that you're basically looking for the second biggest value in the SOURCE table.
In this example code the SOURCE_TABLE represents the table containing same CUSTOMER_NO with different BATCH_NO:
create table source_table (customer_no integer, batch_no date);
insert into source_table values ('1', SYSDATE-2);
insert into source_table values ('1', SYSDATE-1);
insert into source_table values ('1', SYSDATE);
SELECT batch_no
FROM (
SELECT batch_no, row_number() over (order by batch_no desc) as row_num
FROM source_table
) t
WHERE row_num = 2
Where row_num = 2 represents the second biggest value in the table.
The query returns SYSDATE-1.

Use a sub-select in the PIVOT's FOR clause?

The standard PIVOT syntax uses a static FOR list:
SELECT *
FROM (
SELECT log_id, event_id, event_time
FROM patient_events
WHERE event_id IN (10,20,30,40,50)
) v
PIVOT (
max(event_time) event_time
FOR event_id IN( 10,20,30,40,50 )
)
Is there a way to make this dynamic?
I know the sub-select in the WHERE clause will work, but can I use one in the FOR?
SELECT *
FROM (
SELECT log_id, event_id, event_time
FROM patient_events
WHERE event_id IN ( sub-select to generate list of IDs )
) v
PIVOT (
max(event_time) event_time
FOR event_id IN( sub-select to generate list of IDs )
)
You can't in pure SQL, but I don't think quite because of the reason suggested - it's not that the IN clause needs to be ordered, it's that it has to be constant.
When given a query, the database needs to know the shape of the result set and the shape needs to be consistent across queries (assuming no other DDL operations have taken place that might affect it). For a PIVOT query, the shape of the result is defined by the IN clause - each entry becomes a column, with a data type corresponding to the aggregation clause.
Hypothetically if you were to allow a sub-select for the IN clause then you could alter the shape of the result set just by performing DML operations. Imagine your sub-select worked and got you a list of all event_ids known to the system - by inserting a new record into whatever drives that sub-select, your query returns a different number of columns even though no DDL has occurred.
Now we're stuck - any view built on that query is invalid because its shape wouldn't match that of the query, but Oracle couldn't know that it's invalid because none of the objects it depends on have been changed by DDL.
Depending on where you're consuming the result, dynamic SQL's your only option - either at the application level (build the IN list yourself) or via a ref cursor in a database function or procedure.
Interesting question.
On the face of it, it shouldn't work, since the list of values (which will become column names) must be ordered. This is not the case for an "IN" list in the WHERE clause. But perhaps it would work with an ORDER BY condition in the sub-SELECT?
Unfortunately, no. This is easy to test. Got the same error message with or without ORDER BY. (And the query works fine if the IN list is just 10, 20, 30, 40 - the actual department numbers from the DEPT table.) Using tables from the standard SCOTT schema.
SQL> select deptno from scott.dept;
DEPTNO
----------
10
20
30
40
4 rows selected.
SQL> select * from (
2 select sal, deptno
3 from scott.emp
4 )
5 pivot (sum(sal) as total_sal
6 for deptno in (10, 20, 30, 40))
7 ;
10_TOTAL_SAL 20_TOTAL_SAL 30_TOTAL_SAL 40_TOTAL_SAL
------------ ------------ ------------ ------------
8750 10875 9400
1 row selected.
SQL> select * from (
2 select sal, deptno
3 from scott.emp
4 )
5 pivot (sum(sal) as total_sal
6 for deptno in (select deptno from scott.dept order by deptno))
7 ;
for deptno in (select deptno from scott.dept order by deptno))
*
ERROR at line 6:
ORA-00936: missing expression

How to find records in one table which aren't in another

I'm very new to oracle sql and programming and I need some help with one of my first projects. I'm working with this table schema:
Column Data Type Length Precision Scale Nullable
EMPLOYEE_ID NUMBER 22 6 0 No
START_DATE DATE 7 - - No
END_DATE DATE 7 - - No
JOB_ID VARCHAR2 10 - - No
DEPARTMENT_ID NUMBER 22 4 0 Yes
I want to display all employees who have never changed their jobs, not even once(employees not listed in the above table) This table is labeled job_history. How would I go about doing this? I'm not sure on how to get this started.
select * from employees
where employee_id not in ( select employee_id from job_history)
/
You can use a left join and check for a null employee_id on the job_history table.
select * from employees
left join job_history
on job_history.employee_id = employees.employee_id
where job_history.employee_id is NULL
Fairly often an execution plan is better for
select * from employees e where not exists
(select 1 from job_history jh.employee_id = e.employee_id)
than "not in ()".
And if the tables have the same structure the best result will with
select * from employees
minus
select * from job_history

Resources