Convert SQL to PL/SQL Block (ORACLE DB) - sql

I have the following SQL code:
INSERT INTO TIMES (saleDay, dayType)
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');
And I need to convert it to a PL/SQL block. I initially just turned the code above into a create or replace procedure and then called it, but I've been asked to complete the code above in PL/SQL.
Unfortunately, I've really struggled in this area and am still trying to grasp the concept of PL/SQL, especially in situations like this where it doesn't make much sense to conduct the INSERT code via PL/SQL. Any pointers/instruction on what the best way to convert this to PL/SQL is?
Thank you!

I agree, this is better as plain SQL. If this is just for a course, I'd do it with a simple loop and a collection. If you're dealing with a high-volume production environment, a BULK COLLECT .. FORALL approach will have much better performance.
declare
vDayType varchar2(10);
TYPE Holidays is table of varchar2(5);
hd Holidays := Holidays('01.01','15.01','19.01','28.05','04.07','08.10','11.11','22.11','25.12');
begin
for s in (select distinct saleDate from Sales) loop
vDayType := case when TO_CHAR(s.saleDate, 'dd.mm') member of hd then
'Holiday'
when to_char(s.saleDate, 'd') IN (1,7) then
'Weekend'
else
'Weekday'
end;
insert into times (saleDay, dayType) values (s.saleDate, vDayType);
end loop;
end;
/
I feel like PL/SQL procedures like this are a good choice when so much processing needs to be done for each record that it would be awkward, less readable, or impossible to do in SQL. Maybe you're building HTML or modifying a DOCX file in a clob column. I don't know. Honestly, I don't come across use cases for this kind of thing very often in my work.

Thinking I may just go with this...Although I'm not sure if it'll be taken as PL/SQL.
Better solutions are still welcomed!
BEGIN
INSERT INTO TIMES (saleDay, dayType)
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');
END;
/

if TIMES table has only two columns (salesDay and DayType), you can also do it like this,
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;
/

I don't have your tables, but I would do something like I am showing below - creating a table to accept the inserts (similar to yours) and using HIREDATE from the EMP table in the SCOTT schema.
I see that kfinity just posted a similar answer; I am still posting mine because I feel it is cleaner, but it's really the same answer.
Also: keeping the dates in a nested table, as kfinity has done, is the correct approach (declare all the "magic things" at the top of your code, so they are easy to find and modify if/when needed); however, it is possible that in your course you haven't covered collections yet. In my solution I hard-coded the values directly where they were needed.
drop table my_table purge;
create table my_table ( hireday date, daytype varchar2(20) );
begin
for r in ( select hiredate from scott.emp )
loop
insert into my_table ( hireday, daytype ) values
( r.hiredate,
case when to_char(r.hiredate, 'dd.mm') in (
'01.01', '15.01', '19.01', '28.05', '04.07',
'08.10', '11.11', '22.11', '25.12') then 'Holiday'
when to_char(r.hiredate, 'd') in ('1', '7') then 'Weekend'
else 'Weekday' end
);
end loop;
end;
/

Related

Find value that is not a number or a predefined string

I have to test a column of a sql table for invalid values and for NULL.
Valid values are: Any number and the string 'n.v.' (with and without the dots and in every possible combination as listed in my sql command)
So far, I've tried this:
select count(*)
from table1
where column1 is null
or not REGEXP_LIKE(column1, '^[0-9,nv,Nv,nV,NV,n.v,N.v,n.V,N.V]+$');
The regular expression also matches the single character values 'n','N','v','V' (with and without a following dot). This shouldn't be the case, because I only want the exact character combinations as written in the sql command to be matched. I guess the problem has to do with using REGEXP_LIKE. Any ideas?
I guess this regexp will work:
NOT REGEXP_LIKE(column1, '^([0-9]+|n\.?v\.?)$', 'i')
Note that , is not a separator, . means any character, \. means the dot character itself and 'i' flag could be used to ignore case instead of hard coding all combinations of upper and lower case characters.
No need to use regexp (performance will increase by large data) - plain old TRANSLATE is good enough for your validation.
Note that the first translate(column1,'x0123456789','x') remove all numeric charcters from the string, so if you end with nullthe string is OK.
The second translate(lower(column1),'x.','x') removes all dots from the lowered string so you expect the result nv.
To avoid cases as n.....v.... you also limit the string length.
select
column1,
case when
translate(column1,'x0123456789','x') is null or /* numeric string */
translate(lower(column1),'x.','x') = 'nv' and length(column1) <= 4 then 'OK'
end as status
from table1
COLUMN1 STATUS
--------- ------
1010101 OK
1012828n
1012828nv
n.....v....
n.V OK
Test data
create table table1 as
select '1010101' column1 from dual union all -- OK numbers
select '1012828n' from dual union all -- invalid
select '1012828nv' from dual union all -- invalid
select 'n.....v....' from dual union all -- invalid
select 'n.V' from dual; -- OK nv
You can use:
select count(*)
from table1
WHERE TRANSLATE(column1, ' 0123456789', ' ') IS NULL
OR LOWER(column1) IN ('nv', 'n.v', 'nv.', 'n.v.');
Which, for the sample data:
CREATE TABLE table1 (column1) AS
SELECT '12345' FROM DUAL UNION ALL
SELECT 'nv' FROM DUAL UNION ALL
SELECT 'NV' FROM DUAL UNION ALL
SELECT 'nV' FROM DUAL UNION ALL
SELECT 'n.V.' FROM DUAL UNION ALL
SELECT '...................n.V.....................' FROM DUAL UNION ALL
SELECT '..nV' FROM DUAL UNION ALL
SELECT 'n..V' FROM DUAL UNION ALL
SELECT 'nV..' FROM DUAL UNION ALL
SELECT 'xyz' FROM DUAL UNION ALL
SELECT '123nv' FROM DUAL;
Outputs:
COUNT(*)
5
or, if you want any quantity of . then:
select count(*)
from table1
WHERE TRANSLATE(column1, ' 0123456789', ' ') IS NULL
OR REPLACE(LOWER(column1), '.') = 'nv';
Which outputs:
COUNT(*)
9
db<>fiddle here

How to add a space to an existing string in Oracle character functions without using regular expressions

I have a field as name in a table with names inserted without spaces. Eg: "MarkJones".
Now I want to create a space between the first and lastname of a person within the same column to be displayed as "Mark Jones" using Oracle functions.
I have tried this query
SELECT instr('MarkJones', '%||Upper(*)||%') AS substr1,
SUBSTR('MarkJones', instr('MarkJones', '%lower(*)upper(*)%')) AS substr2,
substr1||' '||substr2
FROM dual
;
However, this query is not working. I want to try it using oracle functions including translate, substr and instr, but no regular expressions.
This approach works for the simple example given, but fails if the name has more than 2 uppercase letters in it. If this is coursework as expected, maybe the requirements are not too difficult for the names to parse as we all know that is fraught with heartache and you can never account for 100% of names from all nationalities.
Anyway my approach was to move through the string looking for uppercase letters and if found replace them with a space followed by the letter. I used the ASCII function to test their ascii value to see if they were an uppercase character. The CONNECT BY construct (needed to loop through each character of the string) returns each character in its own row so LISTAGG() was employed to reassemble back into a string and ltrim to remove the leading space.
I suspect if this is coursework it may be using some features you should not be using yet. At least you should get out of this the importance of receiving and/or giving complete specifications!
SQL> with tbl(name) as (
select 'MarkJones' from dual
)
select ltrim(listagg(case
when ascii(substr(name, level, 1)) >= 65 AND
ascii(substr(name, level, 1)) <= 90 THEN
' ' || substr(name, level, 1)
else substr(name, level, 1)
end, '')
within group (order by level)) fixed
from tbl
connect by level <= length(name);
FIXED
------------------------------------
Mark Jones
When you are ready, here's the regexp_replace version anyway :-)
Find and "remember" the 2nd occurrence of an uppercase character then replace it with a space and the "remembered" uppercase character.
SQL> with tbl(name) as (
select 'MarkJones' from dual
)
select regexp_replace(name, '([A-Z])', ' \1', 1, 2) fixed
from tbl;
FIXED
----------
Mark Jones
Not sure we should go against #Alex Poole advice, but it looks like an homework assignment.
So my idea is to point the second Upper Case. Its doable if you create a set of the upper cases, on which you valuate the position in input string iStr. Then if you're allowed to use length, you can use this position to build firstName too:
SELECT substr(iStr, 1, length(iStr)-length(substr(iStr, instr(iStr, u)))) firstName
, substr(iStr, instr(iStr, u)) lastName
, substr(iStr, 1, length(iStr)-length(substr(iStr, instr(iStr, u)))) ||' '||
substr(iStr, instr(iStr, u)) BINGO
FROM ( select 'MarkJones' iStr from dual
union all select 'SomeOtherNames' from dual -- 2 u-cases gives 2 different results
union all select 'SomeOtherOols' from dual -- only one result
union all select 'AndJim' from dual
union all select 'JohnLenon' from dual
union all select 'LemingWay' from dual
),
( select 'A' U from dual
union all select 'B' from dual
union all select 'C' from dual
union all select 'D' from dual
union all select 'E' from dual
union all select 'F' from dual
union all select 'G' from dual
union all select 'H' from dual
union all select 'I' from dual
union all select 'J' from dual
union all select 'K' from dual
union all select 'L' from dual
union all select 'M' from dual
union all select 'N' from dual
union all select 'O' from dual
union all select 'P' from dual
union all select 'Q' from dual
union all select 'R' from dual
union all select 'S' from dual
union all select 'T' from dual
union all select 'U' from dual
union all select 'V' from dual
union all select 'W' from dual
union all select 'X' from dual
union all select 'Y' from dual
union all select 'Z' from dual
) upper_cases
where instr(iStr, U) > 1
;

use SUM on certain conditions

I have a script that extracts transactions and their details from a database. But my users complain that the file size being generated is too large, and so they asked for certain transactions to be just summed up/consolidated instead if they are of a certain classification, say Checking Accounts. That means there should only be one line in the result set named "Checking" which contains the sum of all transactions under Checking Accounts. Is there a way for an SQL script to go like:
CASE
WHEN Acct_class = 'Checking'
then sum(tran_amount)
ELSE tran_amount
END
I already have the proper GROUP BY and ORDER BY statements, but I can't seem to get my desired output. There is still more than one "Checking" line in the result set. Any ideas would be very much appreciated.
Try This,
Select sum(tran_amount) From tran_amount Where Acct_class = 'Checking'
You can try to achieve this using UNION ALL
SELECT tran_amount, .... FROM table WHERE NOT Acct_class = 'Checking'
UNION ALL
SELECT SUM(tran_amount), .... FROM table WHERE Acct_class = 'Checking' GROUP BY Acct_class, ...;
hi you can try below sql
select account_class,
case when account_class = 'saving' then listagg(trans_detail, ',') within group (order by emp_name) -- will give you all details transactions
when account_class = 'checking' then to_char(sum(trans_detail)) -- will give you only sum of transactions
end as trans_det from emp group by account_class;
Or, if your desired output is getting either the sum, either the actual column value based on another column value, the solution would be to use an analytical function to get the sum together with the actual value:
select
decode(acct_class, 'Checking', tran_amount_sum, tran_amount)
from (
select
sum(tran_amount) over (partition by acct_class) as tran_amount_sum,
tran_amount,
acct_class
from
YOUR_TABLE
)
You can try something like the following, by keeping single rows for some classes, and aggregating for some others:
with test (id, class, amount) as
(
select 1, 'a' , 100 from dual union all
select 2, 'a' , 100 from dual union all
select 3, 'Checking', 100 from dual union all
select 4, 'Checking', 100 from dual union all
select 5, 'c' , 100 from dual union all
select 6, 'c' , 100 from dual union all
select 7, 'c' , 100 from dual union all
select 8, 'd' , 100 from dual
)
select sum(amount), class
from test
group by case
when class = 'Checking' then null /* aggregates elements of class 'b' */
else id /* keeps elements of other classes not aggregated */
end,
class

select statement with subqueries against two databases

I have the below code to show what I am "trying" to accomplish in a stored procedure:
select * from
(
select to_char(sum(aa.amount))
from additional_amount aa, status st
where aa.int_tran_id = st.int_tran_id
and st.stage in ('ACHPayment_Confirmed')
and aa.entry_timestamp > (
select to_date(trunc(last_day(add_months(sysdate,-1))+1), 'DD-MON-RR') AS "day 1"
from dual
)
)
UNION ALL
(
select distinct it.debit_acct as "debit_accounts"
from internal_transactions it
where it.debit_acct IN ( select texe_cnasupro
from service.kndtexe, service.kndtctc
where texe_cncclipu = tctc_cncclipu
and tctc_cntipcli = 'C'
)
)
union all
(select distinct it.credit_acct as "credit_account"
from internal_transactions it
where it.credit_acct IN (select texe_cnasupro
from service.kndtexe, service.kndtctc
where texe_cncclipu = tctc_cncclipu
and tctc_cntipcli = 'C'
)
)
;
Output:
TO_CHAR(SUM(AA.AMOUNT))
----------------------------------------
130250292.22
6710654504
0000050334
2535814905
0007049560
5 rows selected
The top row of the output is what I need in the SP as output based on the below two queries which I am guessing needs to be sub-queried against the top select statement.
The top select is to select the sum of the amount a table with a join against another table for filtering (output:130250292.22).
The second and third selects is actually to check that the accounts in the internal_transactions table are signed up for the corresponding two tables in the service db which is a different db on the same server(owned by the same application).
The tables in the "service" db do not have the same common primary keys as in the first select which is against the same database.
Thank you for your help!
I don't understand your question, but I do know you can simplify this bit:
to_date(trunc(last_day(add_months(sysdate,-1))+1), 'DD-MON-RR') AS "day 1"
to this
trunc (sysdate, 'mm')
and you don't need a SELECT from DUAL to do that either.
and aa.entry_timestamp > trunc (sysdate, 'mm')

convert a function into SQL statement

I have a below function for rounding the value of an amount based on some logic -
Currently I use this round function in my SQL to get the rounded value of amount as
f_round_value(nom_ccy_cd,tran_amt) -
However, my current requirement is to not use this function. Instead I am trying to achieve the same in SQL directly. Should I use case statements, or any other way to achieve the below logic..
I am using oracle 10i
Function f_round_value ( in_t_ccy_cd IN CCY.ccy_cd%TYPE, in_n_amt IN NUMBER)
RETURN VARCHAR2 AS
ln_dec_place_cnt CCY.decimal_place_cnt%TYPE;
ln_out_amt NUMBER;
lv_out_amt_str VARCHAR2(30);
lb_decimal_reqd BOOLEAN :=TRUE;
lb_neg_val BOOLEAN :=FALSE;
BEGIN
IF in_n_amt IS NULL THEN
lv_out_amt_str:=NULL;
ELSE IF in_n_amt < 0 THEN
lb_neg_val:=TRUE;
END IF;
IF in_t_ccy_cd IN (C_CCY_CD_JP, C_CCY_CD_IT, C_CCY_CD_IR, C_CCY_CD_KR) THEN
ln_dec_place_cnt :=0;
lb_decimal_reqd:=FALSE;
ELSE
ln_dec_place_cnt :=2; lb_decimal_reqd:=TRUE;
END IF;
ln_out_amt:=ROUND(in_n_amt,ln_dec_place_cnt);
IF lb_decimal_reqd THEN
lv_out_amt_str:=TRIM(TO_CHAR(ln_out_amt,'S999,999,999,999,990.99'));
ELSE
lv_out_amt_str:=TRIM(TO_CHAR(ln_out_amt,'S999,999,999,999,999'));
END IF;
IF lb_neg_val THEN
lv_out_amt_str:='('||SUBSTR(lv_out_amt_str,2)||')';
ELSE
lv_out_amt_str:= SUBSTR(lv_out_amt_str,2);
END IF;
END
Any help will be appreciated.
You do realize there are currencies with 3 decimal places don't you? Anyway, you don't show the complete contents of the CCY table, but if it should contain the decimal places of each currency, you're in luck. You have everything you need. Here is a sample CCY table with 4 currencies and a list with a value for each currency.
WITH
CCY AS(
SELECT 'BGN' ccy_cd, '975' ccy_id, 2 DecPlaces, 'Bulgarian lev' CCY_Name, 'Bulgaria' Cntry FROM dual UNION ALL
SELECT 'BHD', '048', 3, 'Bahraini dinar', 'Bahrain' FROM dual UNION ALL
SELECT 'BIF', '108', 0, 'Burundian franc', 'Burundi' FROM dual UNION ALL
SELECT 'BMD', '060', 2, 'Bermudian dollar', 'Bermuda' FROM dual
),
CcyValues as(
SELECT 'BGN' ccy_cd, 15.852 amt FROM dual UNION ALL
SELECT 'BHD', -15.852 FROM dual UNION ALL
SELECT 'BIF', 15.852 FROM dual UNION ALL
SELECT 'BMD', -15.852 FROM dual
)
SELECT v.ccy_cd, v.amt, y.DecPlaces,
translate( to_char( round( v.amt, y.DecPlaces ),
CASE y.DecPlaces
WHEN 2 THEN 'FM999,999,999,999,990.99PR'
WHEN 3 THEN 'FM999,999,999,999,990.999PR'
ELSE 'FM999,999,999,999,990PR'
END ), '<>', '()' ) Amt_Str
FROM CcyValues v
JOIN CCY y
on y.ccy_cd = v.ccy_cd;