PL/SQL FOR Loop Error when Populating Dimension Table - oracle

I am populating a dimension table named TIMES with data from an OLTP Table called SALES with the following code:
CREATE TABLE TIMES
(saleDay DATE PRIMARY KEY,
dayType VARCHAR(50) NOT NULL);
BEGIN
FOR rec IN
(SELECT saleDate, CASE WHEN h.hd IS NOT NULL THEN 'Holiday'
WHEN to_char(saleDate, 'd') IN (1,7) THEN 'Weekend'
ELSE 'Weekday' END dayType
FROM SALES s LEFT JOIN
(SELECT '01.01' hd FROM DUAL UNION ALL
SELECT '15.01' FROM DUAL UNION ALL
SELECT '19.01' FROM DUAL UNION ALL
SELECT '28.05' FROM DUAL UNION ALL
SELECT '04.07' FROM DUAL UNION ALL
SELECT '08.10' FROM DUAL UNION ALL
SELECT '11.11' FROM DUAL UNION ALL
SELECT '22.11' FROM DUAL UNION ALL
SELECT '25.12' FROM DUAL) h
ON h.hd = TO_CHAR(s.saleDate, 'dd.mm'))
LOOP
INSERT INTO TIMES VALUES rec;
END LOOP;
END;
/
When I run this, I'm getting the errors ORA-00001 (Unique Constraint Violation) and ORA-06512. I believe this is happening because the code is trying to input multiple dates (some of which are the same) into PK for my TIMES Dimension Table (saleDay). How would I implement a clause into this loop so it will only populate one instance of each saleDate into the saleDay PK so there isn't a violation?
For instance, If there are three rows in the SALES table where the saleDate is 2015-10-10, the code should only populate ONE instance of 2015-10-10 into the saleDay PK. I'm thinking the direction I should head is to implement a WHILE clause, however I'm not 100% sure on how that would work since this code is also using CASE to determine whether the saleDay was a Holiday, Weekday, or Weekend and populating the result into the dayType column.

Adding DISTINCT as suggested in a Comment below your question is one way to solve the problem.
The following approach may be more efficient:
for rec in (select distinct saledate from sales)
loop
insert into times (saleday, daytype) values
(rec.saledate, CASE .......);
end loop;
That is: put the CASE expression in the INSERT statement, not in the definition of the (implicit) cursor. There is no reason to compute the CASE expression multiple times for the same date, which may appear many times in the SALES table. There is no reason for the CASE expression to be part of the cursor, either. The CASE expression can use an IN condition (case when to_char(rec.saledate, 'dd.mm') in ('01.01', '15.01', ....) then 'Holiday' when .......)
Unless, of course, the homework problem specifically instructs you to use a left outer join....... :-(

Adding DISTINCT resolved this. Originally thought DISTINCT would negatively impact the CASE but it doesn't. Thanks to I3rutt for pointing this out.
BEGIN
FOR rec IN
(SELECT DISTINCT saleDate, CASE WHEN h.hd IS NOT NULL THEN 'Holiday'
WHEN to_char(saleDate, 'd') IN (1,7) THEN 'Weekend'
ELSE 'Weekday' END dayType
FROM SALES s LEFT JOIN
(SELECT '01.01' hd FROM DUAL UNION ALL
SELECT '15.01' FROM DUAL UNION ALL
SELECT '19.01' FROM DUAL UNION ALL
SELECT '28.05' FROM DUAL UNION ALL
SELECT '04.07' FROM DUAL UNION ALL
SELECT '08.10' FROM DUAL UNION ALL
SELECT '11.11' FROM DUAL UNION ALL
SELECT '22.11' FROM DUAL UNION ALL
SELECT '25.12' FROM DUAL) h
ON h.hd = TO_CHAR(s.saleDate, 'dd.mm'))
LOOP
INSERT INTO TIMES VALUES rec;
END LOOP;
END;
/

Related

Inserting from a SELECT but changing one column in the middle?

Wondering if there is a way to insert a row into a table from another, with exception of one column in the middle without specifying all the column name? I have 128 columns in the table.
I created a view to store the original records.
CREATE VIEW V_TXN_STG AS
SELECT * FROM TXN_STG;
In table TXN_STG, only one column BRN_CODE is changing.
Something like this doesn't work, because the column is not on the last, but somewhere middle of table structure.
INSERT INTO TXN_STG
SELECT v.*, 'BRN-001' AS BRN_CODE
FROM V_TXN_STG v;
I don't believe that this is possible without explicitly specifying the columns in your select.
first you have to get the columns:
SELECT listagg(column_name, ',') within group (order by column_name) columns
FROM all_tab_columns
WHERE table_name = 'AAA' --Table to insert too
and column_name <> 'B' -- column name you want to exclude
GROUP BY table_name;
Then insert that result on the first row:
insert into aaa(A,C) -- A,C is my result from above,I have excluded column B
select *
from (select 'a' A,'q' AMOUNT,'c' C from dual union all -- my sample data
select 'a','a','c' from dual union all
select 'a','b','c' from dual union all
select 'a','c','c' from dual union all
select 'a','d','c' from dual )
pivot
(
max(1)
for (AMOUNT) -- the column you want to remove from the sample data
IN ()
)
where 1=1
order by A;

Reduce overload on pl/sql

I have a requirement to do matching of few attributes one by one. I'm looking to avoid multiple select statements. Below is the example.
Table1
Col1|Price|Brand|size
-----------------------
A|10$|BRAND1|SIZE1
B|10$|BRAND1|SIZE1
C|30$|BRAND2|SIZE2
D|40$|BRAND2|SIZE4
Table2
Col1|Col2|Col3
--------------
B|XYZ|PQR
C|ZZZ|YYY
Table3
Col1|COL2|COL3|LIKECOL1|Price|brand|size
-----------------------------------------
B|XYZ|PQR|A|10$|BRAND1|SIZE1
C|ZZZ|YYY|D|NULL|BRAND2|NULL
In table3, I need to insert data from table2 by checking below conditions.
Find a match for record in table2, if Brand and size, Price match
If no match found, then try just Brand, Size
still no match found, try brand only
In the above example, for the first record in table2, found match with all the 3 attributes and so inserted into table3 and second record, record 'D' is matching but only 'Brand'.
All I can think of is writing 3 different insert statements like below into an oracle pl/sql block.
insert into table3
select from tab2
where all 3 attributes are matching;
insert into table3
select from tab2
where brand and price are matching
and not exists in table3 (not exists is to avoid
inserting the same record which was already
inserted with all 3 attributes matched);
insert into table3
select from tab2
where Brand is matching and not exists in table3;
Can anyone please suggest a better way to achieve it in any better way avoiding multiple times selecting from table2.
This is a case for OUTER APPLY.
OUTER APPLY is a type of lateral join that allows you join on dynamic views that refer to tables appearing earlier in your FROM clause. With that ability, you can define a dynamic view that finds all the matches, sorts them by the pecking order you've specified, and then use FETCH FIRST 1 ROW ONLY to only include the 1st one in the results.
Using OUTER APPLY means that if there is no match, you will still get the table B record -- just with all the match columns null. If you don't want that, you can change OUTER APPLY to CROSS APPLY.
Here is a working example (with step by step comments), shamelessly stealing the table creation scripts from Michael Piankov's answer:
create table Table1 (Col1,Price,Brand,size1)
as select 'A','10','BRAND1','SIZE1' from dual union all
select 'B','10','BRAND1','SIZE1' from dual union all
select 'C','30','BRAND2','SIZE2' from dual union all
select 'D','40','BRAND2','SIZE4'from dual
create table Table2(Col1,Col2,Col3)
as select 'B','XYZ','PQR' from dual union all
select'C','ZZZ','YYY' from dual;
-- INSERT INTO table3
SELECT t2.col1, t2.col2, t2.col3,
t1.col1 likecol1,
decode(t1.price,t1_template.price,t1_template.price, null) price,
decode(t1.brand,t1_template.brand,t1_template.brand, null) brand,
decode(t1.size1,t1_template.size1,t1_template.size1, null) size1
FROM
-- Start with table2
table2 t2
-- Get the row from table1 matching on col1... this is our search template
inner join table1 t1_template on
t1_template.col1 = t2.col1
-- Get the best match from table1 for our search
-- template, excluding the search template itself
outer apply (
SELECT * FROM table1 t1
WHERE 1=1
-- Exclude search template itself
and t1.col1 != t2.col1
-- All matches include BRAND
and t1.brand = t1_template.brand
-- order by match strength based on price and size
order by case when t1.price = t1_template.price and t1.size1 = t1_template.size1 THEN 1
when t1.size1 = t1_template.size1 THEN 2
else 3 END
-- Only get the best match for each row in T2
FETCH FIRST 1 ROW ONLY) t1;
Unfortunately is not clear what do you mean when say match. What is you expectation if there is more then one match?
Should it be only first matching or it will generate all available pairs?
Regarding you question how to avoid multiple inserts there is more then one way:
You could use multitable insert with INSERT first and condition.
You could join table1 to self and get all pairs and filter results in where condition
You could use analytical function
I suppose there is another ways. But why you would like to avoid 3 simple inserts. Its easy to read and maintain. And may be
There is example with analytical function next:
create table Table1 (Col1,Price,Brand,size1)
as select 'A','10','BRAND1','SIZE1' from dual union all
select 'B','10','BRAND1','SIZE1' from dual union all
select 'C','30','BRAND2','SIZE2' from dual union all
select 'D','40','BRAND2','SIZE4'from dual
create table Table2(Col1,Col2,Col3)
as select 'B','XYZ','PQR' from dual union all
select'C','ZZZ','YYY' from dual
with s as (
select Col1,Price,Brand,size1,
count(*) over(partition by Price,Brand,size1 ) as match3,
count(*) over(partition by Price,Brand ) as match2,
count(*) over(partition by Brand ) as match1,
lead(Col1) over(partition by Price,Brand,size1 order by Col1) as like3,
lead(Col1) over(partition by Price,Brand order by Col1) as like2,
lead(Col1) over(partition by Brand order by Col1) as like1,
lag(Col1) over(partition by Price,Brand,size1 order by Col1) as like_desc3,
lag(Col1) over(partition by Price,Brand order by Col1) as like_desc2,
lag(Col1) over(partition by Brand order by Col1) as like_desc1
from Table1 t )
select t.Col1,t.Col2,t.Col3, coalesce(s.like3, like_desc3, s.like1, like_desc1, s.like1, like_desc1),
case when match3 > 1 then size1 end as size1,
case when match1 > 1 then Brand end as Brand,
case when match2 > 1 then Price end as Price
from table2 t
left join s on s.Col1 = t.Col1
COL1 COL2 COL3 LIKE_COL SIZE1 BRAND PRICE
B XYZ PQR A SIZE1 BRAND1 10
C ZZZ YYY D - BRAND2 -

How can I update a column with PL\SQL by using a calculated value

I created a dummy database for learning purposes, and I purposefully created some duplicated records in one of the tables. In every case I want to flag one of the duplicated records as Latest='Y', and the other record as 'N', and for every single record the Latest flag would be 'Y'.
I tried to use PlSQL to go through all of my records, but when I try to use the previously calculated value (which would tell that its a duplicated record) it says that:
ORA-06550: line 20, column 17:
PLS-00201: identifier 'COUNTER' must be declared
Here is the statement I try to use:
DECLARE
CURSOR cur
IS
SELECT order_id, order_date, person_id,
amount, successfull_order, country_id, latest, ROWCOUNT AS COUNTER
FROM (SELECT order_id,
order_date,
person_id,
amount,
successfull_order,
country_id,
latest,
ROW_NUMBER () OVER (PARTITION BY order_id, order_date,
person_id, amount, successfull_order, country_id
ORDER BY order_id, order_date,
person_id, amount, successfull_order, country_id) ROWCOUNT
FROM orders) orders
FOR UPDATE OF orders.latest;
rec cur%ROWTYPE;
BEGIN
FOR rec IN cur
LOOP
IF MOD (COUNTER, 2) = 0
THEN
UPDATE orders
SET latest = 'N'
WHERE CURRENT OF cur;
ELSE
UPDATE orders
SET latest = 'Y'
WHERE CURRENT OF cur;
END IF;
END LOOP;
END;
I am new to PlSQL so I tried to modify the statements I found here:
http://www.adp-gmbh.ch/ora/plsql/cursors/for_update.html
What should I change in my statement, or should I use a different approach?
Thanks for your answers in advance!
Botond
Your refer the ROWNUM as COUNTER in your cursor.
While fetching, you should be accessing it from the cursor reference like MOD (rec.COUNTER, 2)
You need to declare the variable COUNTER and then you need to maintain (ie increment) it in your loop.
I suspect that you example is just for learning PL/SQL. However be aware that it's often much more performant to do things with a single SQL statement, as opposed to using cursor loops.
Your issue is that COUNTER is an attribute of the cursor record rec and not a PL/SQL variable. So:
IF MOD (COUNTER, 2) = 0
Should be:
IF MOD (rec.COUNTER, 2) = 0
However, you do not need to use PL/SQL or cursors, it can be done in a single MERGE statement:
Oracle Setup:
CREATE TABLE orders ( order_id, order_date, latest ) AS
SELECT 1, DATE '2017-01-01', CAST( NULL AS CHAR(1) ) FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-02', NULL FROM DUAL UNION ALL
SELECT 1, DATE '2017-01-03', NULL FROM DUAL UNION ALL
SELECT 2, DATE '2017-01-04', NULL FROM DUAL UNION ALL
SELECT 2, DATE '2017-01-01', NULL FROM DUAL UNION ALL
SELECT 3, DATE '2017-01-06', NULL FROM DUAL;
Update Statement:
MERGE INTO orders dst
USING ( SELECT ROW_NUMBER() OVER ( PARTITION BY order_id
ORDER BY order_date DESC ) AS rn
FROM orders
) src
ON ( src.ROWID = dst.ROWID )
WHEN MATCHED THEN
UPDATE SET latest = CASE src.rn WHEN 1 THEN 'Y' ELSE 'N' END;
Output:
SELECT * FROM orders;
ORDER_ID ORDER_DATE LATEST
-------- ---------- ------
1 2017-01-01 N
1 2017-01-02 N
1 2017-01-03 Y
2 2017-01-04 Y
2 2017-01-01 N
3 2017-01-06 Y

Using a single select statment to get the next row from a table or return the first row if the end of table is reached

I have a table say STAFF like below:
STAFF_NAME
============
ALEX
BERNARD
CARL
DOMINIC
EMMA
Now, I want to write a stored function with a single argument. E.g. GET_NEXT_STAFF(CURRENT_STAFF).
The input and output should be like:
Input | Output
=====================
NULL | ALEX
ALEX | BERNARD
BERNARD | CARL
EMMA | ALEX (Start from the beginning of the table again)
I know how to handle this problem using PL/SQL, but is it possible to deal with this problem with a single select statement?
In the solution below, I assume the rows are ordered alphabetically by names. They may be ordered by another column in the same table (for example by hire date, or by salary, etc. - it doesn't matter) - then the name of that column should be used in the ORDER BY clause of the two analytic functions.
The input name is passed in as a bind variable, :input_staff_name. The solution uses pure SQL, with no need for functions (PL/SQL), but if you must make it into a function, you can adapt it easily.
Edit: In my original answer I missed the required behavior when the input is null. The last line of code (excluding the semicolon) takes care of that. As written currently, the query returns ALEX (or in general the first value in the table) when the input is null, and it returns no rows when the input is not null and not in the table. If instead the requirement is to return the first name when the input is null or not found in the table, then it can be accommodated easily by removing and :input_staff_name is null from the last line.
with
tbl ( staff_name ) as (
select 'ALEX' from dual union all
select 'BERNARD' from dual union all
select 'CARL' from dual union all
select 'DOMINIC' from dual union all
select 'EMMA' from dual
),
prep ( staff_name, next_name, first_name ) as (
select staff_name,
lead(staff_name) over (order by staff_name),
first_value (staff_name) over (order by staff_name)
from tbl
)
select nvl(next_name, first_name) as next_staff_name
from prep
where staff_name = :input_staff_name
or (next_name is null and :input_staff_name is null)
;
Based on the answer from #mathguy I have made a few changes that seem to work. I have added the follow
UNION ALL
SELECT NULL
FROM DUAL
and
WHERE NVL (staff_name, 'X') = NVL (NULL, 'X');
The full code
WITH tbl (staff_name) AS
(SELECT 'ALEX' FROM DUAL
UNION ALL
SELECT 'BERNARD' FROM DUAL
UNION ALL
SELECT 'CARL' FROM DUAL
UNION ALL
SELECT 'DOMINIC' FROM DUAL
UNION ALL
SELECT 'EMMA' FROM DUAL
UNION ALL
SELECT NULL
FROM DUAL),
prep (staff_name,
next_name,
first_name,
last_name) AS
(SELECT staff_name,
LEAD (staff_name) OVER (ORDER BY staff_name),
FIRST_VALUE (staff_name) OVER (ORDER BY staff_name),
LAG (staff_name) OVER (ORDER BY staff_name)
FROM tbl)
SELECT NVL (next_name, first_name) AS next_staff_name
FROM prep
WHERE NVL (staff_name, 'X') = NVL (:input_staff_name, 'X');

Oracle Drop Table with YYYMMDD and YYYMMDD_HHMMSS

I have some tables end with date (YYYYMMDD), some end with HHMMSS:
INVENLEVEL_20160419
INVENLEVEL_20160419_120232 <-optional to exist
INVENLEVEL_20160425
INVENLEVEL_20160426
INVENLEVEL_20160426_032112 <-optional to exist
I need to keep tables within 7 days and drop other INVENLEVEL TABLES.
Expected Results, the following 2 tables deleted:
INVENLEVEL_20160419
INVENLEVEL_20160419_120232
Im able to drop for tables with date, but not the one with HHMMSS.
FOR x IN ( SELECT TABLE_NAME
FROM USER_TABLES
WHERE REGEXP_LIKE(TABLE_NAME, 'INVENLEVEL_[[:digit:]]{8}')
AND TO_DATE(SUBSTR(TABLE_NAME, -8), 'yyyymmdd') <= TRUNC(SYSDATE) - 7
) LOOP
EXECUTE IMMEDIATE 'DROP TABLE ' ||
x.TABLE_NAME ||
' PURGE';
How can i also drop for the table with HHMMSS also? Please note that tables with HHMMSS is optional to exist, means, sometimes we have it, sometime not.
Something like this, perhaps:
with sample_data as (select 'INVENLEVEL_20160419' table_name from dual union all
select 'INVENLEVEL_20160419_120232' table_name from dual union all
select 'INVENLEVEL_20160425' table_name from dual union all
select 'INVENLEVEL_20160426' table_name from dual union all
select 'INVENLEVEL_20160426_032112' table_name from dual union all
select 'NEW_20160426_032112' table_name from dual union all
select 'FRED' table_name from dual)
---- end of mimicking your data; see SQL below
select table_name,
to_date(substr(table_name, 12, 8), 'yyyymmdd') dt
from sample_data
where REGEXP_LIKE(TABLE_NAME, '^INVENLEVEL_[[:digit:]]{8}($|_[[:digit:]]{6})')
and to_date(substr(table_name, 12, 8), 'yyyymmdd') <= trunc(sysdate -7);
TABLE_NAME DT
-------------------------- ----------
INVENLEVEL_20160419 19/04/2016
INVENLEVEL_20160419_120232 19/04/2016
Obviously, you wouldn't need the sample_data subquery - I just used that in order to have data for the SQL to work against. You'd query your user_tables instead.
I amended your regexp to additionally check that it had either reached the end of the string after the 8 digits or there was another underscore followed by 6 digits.
Then I amended your substr to check for the 8 characters from the 12th position, in order to get the date - you have to do it like this, since if you use the end of the string as you had been doing, the date is not necessarily 8 characters from the end.

Resources