PL/SQL Stored Procedure to Populate Fact Table - oracle

I need to populate this fact table using a PL/SQL stored procedure:
CREATE TABLE SALES_FACTS
(saleDay DATE,
vehicleCode INT,
planID INT,
dealerID INT,
vehiclesSold INT,
grossSalesAmt NUMBER(10),
CONSTRAINT SALE_DAY_FK FOREIGN KEY (saleDay) REFERENCES TIMES(saleDay) ON DELETE CASCADE,
CONSTRAINT VEHICLE_CODE_FK FOREIGN KEY (vehicleCode) REFERENCES VEHICLES(vehicleCode) ON DELETE CASCADE,
CONSTRAINT PLAN_ID_FK FOREIGN KEY (planID) REFERENCES FINANCING_PLANS(planID) ON DELETE CASCADE,
CONSTRAINT DEALER_FK FOREIGN KEY (dealerID) REFERENCES DEALERSHIPS(dealerID) ON DELETE CASCADE,
CONSTRAINT SALES_FACTS_PK PRIMARY KEY (saleDay, vehicleCode, planID, dealerID));
I have been asked to do this by using four nested cursor loops to get every possible combination of the dimension tables' primary keys, along with the total vehicles sold and gross sales amount for each combination.
Also, if the values for vehiclesSold and grossSalesAmount are zero, then a row SHOULD NOT be inserted into the SALES_FACTS table.
Only rows for combinations of the four foreign key columns where there were some vehicles sold should be inserted.
I have created the following code that I hoped would accomplish this:
CURSOR factData IS
SELECT vehicleVin,saleDate,sf.planID,sp.dealerID
COUNT (*) AS vehiclesSold
SUM (s.grossSalePrice) AS grossSalesAmount
FROM SALES s, SALES_FINANCINGS sf, SALESPERSONS sp
WHERE s.saleID = sf.saleID
AND s.salespersonID = sp.salespersonID
GROUP BY vehicleVIN, saleDate, sf.planID, sp.dealerID
HAVING COUNT(*) > 0;
BEGIN
FOR record IN factData
LOOP
INSERT INTO SALES_FACTS (saleDay,vehicleCode,planID,dealerID,vehiclesSold, grossSalesAmount
VALUES (record.saleDate,record.vehicleVin,record.planID,record.dealerID,record.vehiclesSold,record.grossSalesAmount);
END LOOP;
END;
/
However the code executes fine, but I do not get any results when I run a
SELECT COUNT(*) FROM SALES_FACTS;
I have created an SQL Fiddle link here http://sqlfiddle.com/#!4/9708d6/1 since the code for the tables and table population was too much to post on this question. Keep in mind that I only INSERTed about 2-3 rows of data for each table to keep the code somewhat short, however the data that has been inserted should suffice to get this working.
Please let me know where I'm going wrong and what the best way to fix it is! Thanks in advance!

This Ended up doing the trick. Thanks for all of the help to those who commented.
DECLARE
CURSOR sales_data
IS
SELECT vehicleVIN, saleDate, SF.planID, SP.dealerID,
COUNT(*) AS vehiclesSold, SUM(S.grossSalePrice) AS grossSalesAmt
FROM SALES S, SALES_FINANCINGS SF, SALESPERSONS SP, VEHICLES V
WHERE S.saleID = SF.saleID AND S.vehicleVIN = V.vehicleCode AND S.salespersonID = SP.salespersonID
GROUP BY vehicleVIN, saleDate, SF.planID, SP.dealerID
HAVING COUNT(*) > 0;
BEGIN
FOR record IN sales_data
LOOP
INSERT INTO SALES_FACTS (saleDay,vehicleCode,planID,dealerID,vehiclesSold, grossSalesAmt)
VALUES (record.saleDate,record.vehicleVIN,record.planID,record.dealerID,record.vehiclesSold,record.grossSalesAmt);
END LOOP;
END;
/

Related

Oracle PLS-00049: bad bind variable

I'm getting this error when I try to create this trigger. I tried everything but I don't know what seems to be the problem.
Here is the code:
CREATE OR REPLACE TRIGGER after_price_update
AFTER UPDATE
ON Item
FOR EACH ROW
DECLARE new_totalprice INT;
BEGIN
IF :OLD.price <> :new.price THEN
new_totalprice := :old.Quantity * :new.price;
INSERT INTO OrderRecord(OrderRecord_Id, Item_Id, Employee_Id, Reservation_Id, Order_Time, Quantity, TotalPrice)
VALUES(old.OrderRecord_Id, old.Item_Id, old.Employee_Id, old.Reservation_Id, old.Order_Time, old.Quantity, new_totalprice);
END IF;
END;
And the error is:
4/22 PLS-00049: bad bind variable 'OLD.QUANTITY'
The tables look like this:
CREATE TABLE Item (
Item_Id int PRIMARY KEY,
Menu_Id int,
Name varchar2(20),
Description varchar2(120),
Price int,
FOREIGN KEY(Menu_Id) REFERENCES Menu(Menu_Id)
);
CREATE TABLE OrderRecord (
OrderRecord_Id int PRIMARY KEY,
Item_Id int,
Employee_Id int,
Reservation_Id int,
Order_Time date,
Quantity int,
TotalPrice int,
FOREIGN KEY(Item_Id)References Item(Item_Id),
FOREIGN KEY(Employee_Id)References Employee(Employee_Id),
FOREIGN KEY(Reservation_Id)References Reservation(Reservation_Id)
);
Table OrderRecord is not the table being updated: you cannot reference an old value for this table; you need to read this value with some SELECT statement.
IF :OLD.price <> :new.price THEN
new_totalprice := <Quantity> * :new.price;
There is no column Quantity in the table ITEM on which you are applying trigger.
If I understand what you are wanting to do correctly, it looks like you want to update the calculated total price of all existing OrderRecord entries when an Item entry has a price change.
CREATE OR REPLACE TRIGGER after_price_update
AFTER UPDATE
ON Item
FOR EACH ROW
BEGIN
IF NVL(:OLD.Price, 0) <> NVL(:new.Price, 0) THEN
-- Update the child table "OrderRecord" for this item using new price
UPDATE OrderRecord SET TotalPrice = Quantity * :new.Price WHERE Item_Id = :new.Item_id;
END IF;
END;
Note that this trigger is on the parent table, Item, and references two values from the modified record: price and Item_Id, to update the child OrderRecord table.
Also note the use of NVL to watch for nulls because the comparison will not succeed if a null is on either side. Using zero for null is debatable; one could use a value that will never occur such as a negative number.

Procedure to remove duplicates in a table

Brief model overview:
I have a student and a course tables. As it's many to many relation there is also a junction table student_course (id_student, id_course), with unique constraint on both columns (composite).
The problem I want to solve:
On account of a mistake, there is no a unique constraint on the code column of the course table. It should as code column should uniquely identify a course. As a result there are two rows in the course table with the same value in the code column. I want to remove that duplicate, check that there is no other duplicates and add a unique constraint on the code column. Without loosing relations with student table.
My approach to solve the issue:
I have create a procedure that should do what I want.
CREATE OR REPLACE PROCEDURE REMOVE_COURSES
(
v_course_code IN VARCHAR2,
v_course_price IN VARCHAR2
)
AS
new_course_id NUMBER;
BEGIN
INSERT INTO course (CODE, PRICE) VALUES (v_course_code, v_course_price)
RETURNING ID INTO new_course_id;
FOR c_course_to_overwrite IN (SELECT *
FROM course
WHERE code = v_course_code AND id != new_course_id) LOOP
UPDATE student_course SET id_course = new_course_id WHERE id_course = c_course_to_overwrite.id;
DELETE FROM course WHERE id = c_course_to_overwrite.id;
END LOOP;
END REMOVE_COURSES;
/
Main problem I want to solve:
The procedure keeps giving me an error about unique constraint violation on student_course table. But I am really not sure how it's possible as I am using new_course_id, so there is no chance that in the junction table there are two rows with the same id_student, id_course. What do I need to fix ?
Miscellaneous:
I want to solve that issue using procedure only for learning purposes
EDITED:
CREATE TABLE student (
id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY,
name VARCHAR2(150) NOT NULL,
PRIMARY KEY (id)
);
ALTER TABLE student MODIFY ID
GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH LIMIT VALUE);
CREATE TABLE course (
id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY,
code VARCHAR2(255) NOT NULL,
PRIMARY KEY (id)
);
ALTER TABLE course MODIFY ID
GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH LIMIT VALUE);
CREATE TABLE student_course (
id_student NUMBER NOT NULL,
id_course NUMBER NOT NULL,
PRIMARY KEY (id_student, id_course),
CONSTRAINT student_fk FOREIGN KEY (id_student) REFERENCES student (id),
CONSTRAINT course_fk FOREIGN KEY (id_course) REFERENCES course (id)
);
insert into student (name) values ('John');
INSERT INTO course (ID, CODE) VALUES (1, 'C_13');
INSERT INTO course (ID, CODE) VALUES (2, 'C_13');
commit;
INSERT INTO STUDENT_COURSE (ID_STUDENT, ID_COURSE) VALUES (1, 1);
INSERT INTO STUDENT_COURSE (ID_STUDENT, ID_COURSE) VALUES (1, 2);
commit;
CALL REMOVE_COURSES('C_13');
[23000][1] ORA-00001: unique constraint (SYS_C0014983) violated ORA-06512: near "REMOVE_COURSES", line 8
Rather than removing one of the duplicate codes, you're creating a third course with the same code, and trying to move all students on either of the old courses onto the new one. The error suggests you have students who are already enrolled on both of the old courses.
Your cursor loop query is:
SELECT *
FROM course
WHERE code = v_course_code AND id != new_course_id
That will find all junction records for both old versions of the code, and the update then sets all of those junction records to the same new ID.
If there are any students listed against both old IDs for the code - which would be allowed by your composite unique key - then they will both be updated to the same new ID.
So say the courses you're looking at are [updated for your example code]:
ID CODE
-- ----
1 C_13
2 C_13
and you have junction records for a student for both courses, like:
ID_STUDENT ID_COURSE
---------- ---------
1 1
1 2
You are creating a new course:
ID CODE
-- ----
3 C_13
Your cursor loop looks for code = 'ABC' and ID != 3, which finds IDs 1 and 2. So in the first iteration of the loop up update the rows with ID 1, so now you have:
ID_STUDENT ID_COURSE
---------- ---------
1 3
1 2
Then in the second iteration you try to update the rows with ID 2, which would attempt to produce:
ID_STUDENT ID_COURSE
---------- ---------
1 3
1 3
which would break the unique constraint - hence the error.
You probably don't want to create a new course at all, but either way, you need to remove duplicate records from student_course - that is, rows which will become duplicates when updated. Basically you need to find students with entries for both existing course IDs, and delete either of them. If you don't care which this would do it:
delete from student_course sc1
where id_course in (
select id
from course
where code = 'C_13'
)
and exists (
select null
from student_course sc2
join course c on c.id = sc.id_course
where sc2.id_student = sc1.id_student
and sc2.id_course > sc1.id_course
and c.code = 'C_13'
);
but there are other (probably better) ways.
You then have the choice of updating all remaining junction records for both old IDs to your new ID; or to consolidate on one of the old IDs and remove the other.
(Your question implies you want to solve the overall task yourself, so I'll refrain from trying to provide a complete solution - this just hopefully helps you understand and resolve your main problem...)

Oracle trigger that update record after insert

I want update book ant set amount-1, when after insert record to sell table.
create table book (
id number(3) not null,
name varchar(20),
author varchar(12),
amount number(3) not null,
constraint book_pk primary key(id)
);
create table sell (
id number(3) not null,
date varchar(20),
book_id number(3),
constraint sell_pk primary key(id)
);
I want after insert to table sell record update book table amount-1;
CREATE OR REPLACE TRIGER changes_amount_trigger
AFTER INSERT ON sell
FOR EACH ROW
BEGIN
UPDATE BOOK SET amount = amount-1 WHERE id = book_id
END;
I not know how to get inserted record book id, to update this record in book table.
Try like this,
CREATE OR REPLACE TRIGGER changes_amount_trigger
AFTER INSERT ON sell
FOR EACH ROW
BEGIN
UPDATE BOOK SET amount = amount-1 WHERE id = :new.book_id;
END;
/
Data Model Assumptions:
I am assuming you will register transactions by changing the data in the SELL table through INSERT DML SQL operations. This is also supported by your set up of a DML trigger on SELL to pass its changes as SALES information to the BOOK table. This is workable.
By accident, I tried setting up the trigger a little differently and I'd like to suggest a different approach:
Consider possibly working in the opposite direction: Change book quantities directly on the BOOK table, so a single purchase of book_id = 5 would handle queries that could:
UPDATE book SET amount = amount -1
WHERE id = 5; COMMIT;
Restocking would mean increasing the quantity of available books
by incrementing the AMOUNT value instead.
There are a few additional changes that might tighten up this two-table design and protect the integrity of the data within them for the longer term:
CREATE TABLE book (
id number(3) not null,
name varchar(20),
author varchar(12),
amount number(3) not null,
CONSTRAINT book_pk PRIMARY KEY(id)
);
ALTER TABLE book
ADD CONSTRAINT book_amt_ck CHECK (amount > 0);
ALTER TABLE book
ENABLE CONSTRAINT book_amt_ck;
To prevent negative book amount (quantity) values, a TABLE CHECK CONSTRAINT would prevent the entry of values by means of arithmetic errors in DML operations such as:
UPDATE book SET amount := amount - 1
In the example above, there is no control over decrementing the book inventory even if the quantity on hand has reached 0. Check out a few references on TABLE CHECK CONSTRAINTS to get a better understanding of what it can do for specific design situations.
Here are some design suggestions for the trigger:
Changes in book quantities should be the only triggering data element that affects the SELL table.
The trigger should account for changes in book quantities > 1.
CREATE OR REPLACE TRIGGER orders_after_update
AFTER UPDATE
ON book
FOR EACH ROW
DECLARE
v_amount number;
BEGIN
IF (:new.amount < :old.amount ) THEN
FOR v_amount in 1 .. (:old.amount - :new.amount)
LOOP
INSERT INTO sell (id, date, book_id)
VALUES (sell_seq.nextval, sysdate, :new.id);
COMMIT;
END LOOP;
END IF;
END;
For more information on triggers and their design, check a few instances to get a better understanding of how they are designed and set up.
CREATE SEQUENCE sell_seq
MINVALUE 1
START WITH 1
INCREMENT BY 1
CACHE 20;
We needed a sequence to populate the primary key/index of the SELL table. Oracle Sequences are useful for this purpose.
By watching the table changes with a trigger on the BOOK table, you can use the built in references which already exist when a table trigger fires. For example, BOOK.ID does not require an additional query because a trigger automatically is made aware of the beginning and ending value of each trigger monitored record.
Some useful discussions on triggers are discussed in more detail through an Internet search.
Setting Up a Foreign Key Relationship
Although the trigger will probably keep this relation clean, a Foreign Key relation between elements BOOK.ID and SELL.BOOK_ID would be good, otherwise queries on Sales transactions may yield book sales without any descriptive production information. The following is a reference on Foreign Keys and their use.
CREATE TABLE sell (
id number(3) not null,
date varchar(20),
book_id number(3)
);
ALTER TABLE table_name
ADD CONSTRAINT sell_fk
FOREIGN KEY (book_id)
REFERENCES book(id);
Do you already have records in sell table and want to update the amount in book table ?
if this is your case you can update your book.amount as following:
update book b
set b.amount = b.amount - (select count(*) from sell s where b.id = s.book_id);

Is there a way similar check constraint to help me to know if there is a duplicate column

table 1
ID - name - main_number - random1 - random2
1* -aaaa-blalablabla*- *** - *
2 -vvvv-blublubluuu*- *** - *
3 -aaaa-blalablabla*- *** - **
ID , name and main number are primary key
My problem that I have noticed coulmn name and main number has duplicate values, i dont want to ADD ANY OTHER DUPLICATE VALUES ( I should keep the old duplicat because in my real table there are a lot of duplicated data and its hard to remove them )
what I want when I TRY ( BEFORE TO COMMIT) to know that this name I am trying to insert is duplicate.
I can do that with in a procedure or triger, but i have heard constraint checking is simpler and easier(if there a simpler way then procedure or triger ill be glad to learn it)
CONSTRAINT check_name
CHECK (name = (A_name))
can the constaraint have more then 1 column in such way?
CONSTRAINT check_name
CHECK (name = (A_name) , main_number=( A_number))
can I a write a constaraint in such way?
CONSTRAINT check_name
CHECK (name = ( select case where there is an column has the same value of column name))
So my question : Is there a way simelar to check constraint to help me to know if there is a duplicate column or I have to use a trigger ?
Since your database is Oracle you could also use NOVALIDATE constraints. Meaning: "doesn't matter how the data is, just validate from now on".
create table tb1
(field1 number);
insert into tb1 values (1);
insert into tb1 values (1);
insert into tb1 values (1);
insert into tb1 values (2);
insert into tb1 values (2);
commit;
-- There should be an non-unique index first
create index idx_t1 on tb1 (field1);
alter table tb1 add constraint pk_t1 primary key(field1) novalidate;
-- If you try to insert another 1 or 2 you would get an error
insert into tb1 values (1);
Yes, you can use constraints on many columns.
But in this case constraint is not applicable, because all table rows must satisfy constraints. Use a trigger.
Constraints cannot contain subqueries.
Alternatively use unique index, that will enforce unique constraint
create unique index index1 on table1
(case when ID <= XXX then null else ID end,
case when ID <= XXX then null else name end);
Replace 'XXX' with your current max(ID).
I assume that you want to prevent duplicate records as defined by the combination of name and main_number.
Then the way to go is to cleanup your database, and create a unique index:
create unique index <index_name> on <table> (name, main_number)
This both checks, and speed's it up.
In theory, if you really wanted to keep the old duplicate records, you could get along by using a trigger, but then you will have a hard time trying to get sense out of this data.
Update
If you used the trigger, you would end up with two partitions of data in one table - one is checked, the other is not. So all of your queries must pay attention to it. You just delay your problem.
So either clean it up (by deleting or merging) or move the old data in a separate table.
You can use SQL select ... group by to find your duplicates, so you can delete/move them in one turn.

Keeping track of all values created by a sequence for multiple inserts

In PL SQL, I'm writing a stored procedure that uses a DB link:
CREATE OR REPLACE PROCEDURE Order_Migration(us_id IN NUMBER, date_id in DATE)
as
begin
INSERT INTO ORDERS(order_id, company_id)
SELECT ORDER_ID_SEQ.nextval, COMPANY_ID
FROM ORDERS#SOURCE
WHERE USER_ID = us_id AND DUE_DATE = date_ID;
end;
It takes all orders done on a certain day, by a certain user and inserts them in the new database. It calls a sequence to makes sure there are no repeat PKs on the orders, and it works well.
However, I want the same procedure to do a second INSERT into another table that has order_id as a foreign key. So I need to add all the order_id's just created, and the data from SOURCE that matches:
INSERT INTO ORDER_COMPLETION(order_id, completion_dt)
SELECT ????, completion_dt
FROM ORDER_COMPLETION#SOURCE
How can I keep track of which order_id that was just created matches up to the one whose data I need to pull from the source database?
I looked into making a temporary table, but you can't create those in a procedure.
Other info: I'll be calling this procedure from a C# app I'm writing
I'm not sure that I follow the question. If there is an ORDERS table and an ORDER_COMPLETION table in the remote database, wouldn't there be some key on the source system that related those two tables? If that key is the ORDER_ID, why would you want to re-assign that key in your procedure? Wouldn't you want to maintain the ORDER_ID from the source system?
If you do want to re-assign the ORDER_ID locally, I would tend to think that you'd want to do something like
CREATE OR REPLACE PROCEDURE order_migration( p_user_id IN orders.user_id%type,
p_due_date IN orders.due_date%type )
AS
TYPE order_rec IS RECORD( new_order_id NUMBER,
old_order_id NUMBER,
company_id NUMBER,
completion_dt DATE );
TYPE order_arr IS TABLE OF order_rec;
l_orders order_arr;
BEGIN
SELECT order_id_seq.nextval,
o.order_id,
o.company_id,
oc.completion_dt
BULK COLLECT INTO l_orders
FROM orders#source o,
order_completion#source oc
WHERE o.order_id = oc.order_id
AND o.user_id = p_user_id
AND o.due_date = p_due_date;
FORALL i IN l_orders.FIRST .. l_orders.LAST
INSERT INTO orders( order_id, company_id )
VALUES( l_orders(i).new_order_id, l_orders(i).company_id );
FORALL i IN l_orders.FIRST .. l_orders.LAST
INSERT INTO order_completion( order_id, completion_dt )
VALUES( l_orders(i).new_order_id, l_orders(i).completion_dt );
END;
You could also do a single FOR loop with two INSERT statements rather than two FORALL loops. And if you're pulling a lot of data each time, you probably want to pull the data in chunks from the remote system by adding a loop and a LIMIT to the BULK COLLECT
There must be some link between the rows in ORDERS#SOURCE and ORDERS, and between ORDERS#SOURCE and ORDER_COMPLETION#SOURCE, so can you not use a join?
Something like:
INSERT INTO ORDER_COMPLETION(order_id, completion_dt)
SELECT o.order_id, ocs.completion_dt
FROM ORDER_COMPLETION#SOURCE ocs
JOIN ORDERS o ON o.xxx = ocs.xxx

Resources