I am using a procedure to get data from a table called Datastream which has 1000 rows and I have to read 100 records in a cursor once. And for each record match the primary key with foreign key in table Masterdata and move all the matching records in in multiple tables. Then the function should get the next 100 records from the table and do the same.
P.S: I need to use 2 loops as a condition.
I'm stuck with this error :
ORA-01722: invalid number
EDIT 1: Solved the above error, table datatype for 'product_id' was different then what it was supposed to be.
New Error: Everything seems fine, the procedure runs, but my tables aren't populated by the insert query. The output line prints SupID and PName.
I'm using the below code:
create or replace procedure newting is
s integer := 1;
e integer := s+99;
total_S integer := 1;
cursor endCount is
select datastream_id, ROW_NUMBER() OVER ( ORDER BY datastream_id )
from datastream
where datastream_id <= (select (count(*)/100) from datastream);
cursor transC is
SELECT datastream_id, product_id, customer_id, customer_name, outlet_id, outlet_name, quantity_sold, d_date
from datastream
where datastream_id between s and e
order by datastream_id;
TYPE val1 IS TABLE OF datastream.datastream_id%type;
v1 val1;
TYPE val2 IS TABLE OF datastream.product_id%type;
v2 val2;
TYPE val3 IS TABLE OF datastream.customer_id%type;
v3 val3;
TYPE val8 IS TABLE OF datastream.d_date%type;
v8 val8;
PName masterdata.product_name%TYPE;
SupID masterdata.supplier_id%TYPE;
SName masterdata.supplier_name%TYPE;
PPrice masterdata.sale_price%TYPE;
begin
open endCount;
open transC;
fetch transC bulk collect into v1,v2,v3,v4,v5,v6,v7,v8;
close endCount;
close transC;
for x in endCount
loop
for y in v1.first .. v1.last
loop
select product_name, supplier_id, supplier_name, sale_price into PName, SupID, SName, PPrice
from masterdata m
where m.product_id=v2(y);
Dbms_output.put_line(SupID);
insert into product (product_id, product_name) select v2(y), PName from dual --error in this line
where not exists (select * from product where product_id=v2(y));
insert into customer (customer_id, customer_name) select v3(y), v4(y) from dual
where not exists (select * from customer
where customer_id=v3(y));
insert into d_time (d_date, d_year, d_month, d_day)
select v8(y), to_char(v8(y),'YY'), to_char(v8(y),'MM'), to_char(v8(y),'DD')
from dual
where not exists (select * from d_time
where d_date=v8(y));
total_S := v7(y) * PPrice;
insert into sales_fact (transaction_id, product_id, supplier_id, outlet_id, customer_id, d_date, quantity, price, total_sales)
select v1(y), v2(y), SupID, v5(y), v3(y), v8(y), v7(y), PPrice, total_S
from dual
where not exists (select * from sales_fact
where transaction_id = v1(y));
end loop;
s:=s+99;
e:=e+99;
end loop;
end;
Any leads on this would be highly appreciated.
Thanks.
When you try to convert a character string into a number you could get An ORA-01722 error, basically the message says string cannot be converted into a number. Check all your DML(INSERT) specially the ones for dates. my advice is comment all (INSERT) but one, try to run it and if there is not issue, so proceed with the second one and keep going till you manage to find the problematic one and fix it(divide and conquer). Something else(just an observation), I believe the 'COMMIT' is out of this proc scope right?
Hope this could help.
Related
I have 2 tables (TABLE_A & TABLE_B) where I'm using the MINUS command to see if there are differences in the tables.
In my example below you can see that TABLE_A has an additional row.
Is there a way to capture the numeric difference between the two tables, in this case 1 row.
If there is a difference >0 then display the value. Although my example is small it could contain many rows. Therefore I would only like to do the MINUS command once if possible. I'm also also amenable to alternative solutions and not tied to the MINUS command or if this can be done with SQL only that will work too.
Thanks in advance for your expertise and all who answer.
CREATE TABLE TABLE_A(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
nm VARCHAR(30)
);
/
CREATE TABLE TABLE_B(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
nm VARCHAR(30)
);
/
BEGIN
FOR I IN 1..4 LOOP
INSERT INTO TABLE_A (nm) VALUES('Name '||I);
end loop;
FOR I IN 1..3 LOOP
INSERT INTO TABLE_B (nm) VALUES('Name '||I);
end loop;
END;
-- MINUS operation
SELECT nm FROM TABLE_A
MINUS
SELECT nm FROM TABLE_B;
Output:
NM
Name 4
Pseudo code
Do minus command
If difference >0 then display rows
There are many ways for this, you can try 1 as below -
SELECT COUNT(*)
FROM (SELECT nm FROM TABLE_A
MINUS
SELECT nm FROM TABLE_B);
Another method maybe -
SELECT COUNT(*)
FROM TABLE_A A
WHERE NOT EXISTS (SELECT NULL
FROM TABLE_B B
WHERE A.nm = B.nm)
If I understood the question correctly you can do it using analytic count:
select *
from (
select v.*,count(*)over() cnt
from (
SELECT nm FROM TABLE_A
MINUS
SELECT nm FROM TABLE_B
) v
)
where cnt>=4;
DBFiddle: https://dbfiddle.uk/?rdbms=oracle_21&fiddle=0ac62f3d1ea835f60427a1da8efb965e
Let me start my question by setting up my scenario.
I have a test2 table which contains only 2 fields: productid and productlife, I would like to explicitly list all the years along with the products
for example,
With product A, I would like to list
Y15|Y16|Y17|Y18|Y19, A
and for product B, using the same rule, I should get
Y18|Y19, B
My sql does not produce the result I am looking for:
SELECT
(
SELECT listagg("Year",'|') within GROUP (
ORDER BY "Year") "Prefix"
FROM
(
SELECT 'Y'
||(TO_CHAR(SYSDATE,'yy')-LEVEL) "Year"
FROM dual
CONNECT BY level<=r.productlife
)
) "Prefix", productid
FROM TEST2 r
How should it be corrected? I would think the field productlife in each record will control the level in the statement but it does not seem to do so..
Please advise.
Below is the script to create my example for your convenience.
CREATE TABLE "TEST2"
( productid VARCHAR2(20 BYTE),
productlife NUMBER
) ;
Insert into TEST2 (productid,productlife) values ('A',5);
Insert into TEST2 (productid,productlife) values ('B',2);
Thanks!
If it doesn't absolutely have to use connect by, another approach might be to write it explicitly as a function:
with
function list_years(n number) return varchar2 as
end_year date := trunc(sysdate,'YYYY');
start_year date := add_months(end_year, (n*-12));
y date;
years_list varchar(200);
begin
for i in reverse 1..n loop
y := add_months(sysdate, -12 * i);
years_list := years_list || to_char(y,'"Y"YY"|"');
end loop;
return rtrim(years_list,'|');
end list_years;
select productid
, productlife
, list_years(productlife) as prefix
from test2
/
DBFiddle
This query will do it, but I feel like there must be a better way:
SELECT t.productid, s.yr
FROM test2 t
INNER JOIN (SELECT ROWNUM RN, TO_CHAR(SYSDATE, 'YY')-ROWNUM+1 YR
FROM dual d
CONNECT BY level <= (SELECT max(productlife)
FROM test2)) s ON t.productlife >= s.rn
ORDER BY t.productid, s.yr;
Here is a SQLFiddle for you: SQLFiddle
I have the following loop (simplified for an example):
DO $$
DECLARE
l record;
BEGIN
FOR l IN SELECT id, country_id FROM logo LOOP
WITH cs AS (
INSERT INTO logo_settings (targeted) VALUES (true)
RETURNING id
)
INSERT INTO logo_settings_targeted (logo_settings_id, country_id)
VALUES
( (SELECT id FROM cs),
logo.country_id,
);
END LOOP;
END;
END $$;
The body of a loop works fine. But if I wrap it into a loop (I would like to make records for all records from logo table) - it shows an error saying:
ERROR: syntax error at or near "END"
Position: 712
END;
^
meaning the last EnND before END $$; which does not give much sense to me. I do not know what to fix to make it running. Any hints?
There is one end to many. end loop closes the loop body:
DO $$
DECLARE
l record;
BEGIN
FOR l IN SELECT id, country_id FROM logo LOOP
WITH cs AS (
INSERT INTO logo_settings (targeted) VALUES (true)
RETURNING id
)
INSERT INTO logo_settings_targeted (logo_settings_id, country_id)
VALUES
( (SELECT id FROM cs),
logo.country_id,
);
END LOOP;
END $$;
Additionally to answer of #Andronicus, this is one possible way to do this without PL/pgsql:
Click: demo:db<>fiddle
WITH countries AS (
SELECT id, country_id,
row_number() OVER () -- 1
FROM logo
), ins_settings AS (
INSERT INTO logo_settings(targeted)
SELECT true FROM countries c
RETURNING id
)
INSERT INTO logo_settings_targeted (logo_settings_id, country_id)
SELECT
ins.id, c.country_id
FROM
(SELECT id, row_number() OVER () FROM ins_settings) ins -- 2
JOIN countries c ON c.row_number = ins.row_number
Put the first SELECT query (which you used for the loop) into a second CTE at the top.
The trick is, that you can join the outputs from the logo SELECT statement and the first insert. You can use the fact, that both outputs have the same number of rows. Simply add a column with a row count to both outputs. This can be done, for example, using the row_number() window function. As you can see, for the logo SELECT I already did this directly in the CTE (1), for the INSERT output I added this in a subquery (2). Now there are the same identifiers on both tables, which can be used for the join.
The join is the basis for the second INSERT.
I created a procedure from querying other tables including transaction tbl to settle all transaction records with a reference number and date automatically stamped on it.
What I try to do is my settle_transaction procedure needs to generate my query from the selected statement and insert them into the settlement table. Meanwhile, I also need to update the ref_num and processed date as a "stamp" to the transaction table so that I don't have duplicated settlement when calling the procedure again. Otherwise, I don't know how to stop showing the same settlement data twice
Here is the procedure to output a settlement tbl and structure similar below:
BEGIN
for r_client in
(
select clientid,
client_name, sum(transaction) total_amount
from transaction_tbl tran join terminal_tbl term
on tran.terminalid = term.terminalid join client_tbl c on c.clientid = term.clientid
where refnr is null
)
loop
v_refnr := get_refnr;
insert into settlement_tbl
(
Ref_Num,
Total,
CLIENTID,
TITLE,
processeddate
)
values (v_refnr, total_amount, clientid,
name,sysdate);
update_refnr(v_refnr, sysdate)
end loop;
END
Output:
| reference_num | total amount | client id | client name | processed_date |
|---------------|--------------|-----------|-------------|----------------|
When I execute the above procedure, it populates all the result from the select query. However, if I execute again, it will duplicate the same result especially the total amount.
I'm seeking a solution to put another procedure/function inside this settlement procedure to prevents duplicate records from the selected query in this procedure.
I use the ref. no# and process_date to update the existing reference num and date to the transaction tbl show below.
| transaction_num | transaction amount | reference_num | processed_date |
|-----------------|--------------------|---------------|----------------|
Here is the attempted code I put inside the settlement procedure but still shows duplicated records and can not update to the transaction tbl.
procedure update_refnr(
p_refnr in number,
p_processeddate in date
)
is
begin
UPDATE TRANSACTION t
SET t.refnr = p_refnr
WHERE EXISTS (SELECT p_processeddate
FROM terminal_tbl
WHERE t.TERMINALID= term.TERMINALID
AND t.processeddate = p_processeddate
AND t.refnr IS NULL);
--exception handling below
end update_refnr;
I also tried other SQL reference but cannot compile.
Ideally, I don't have duplicated records in my settlement tbl when I retrieve each record from my stored procedure.
You want to insert new data into your table only when it doesn't already exist. As others have said, you can use MERGE to do that:
BEGIN
for r_client in (select clientid,
client_name,
sum(transaction) total_amount
from transaction_tbl tran
join terminal_tbl term
on tran.terminalid = term.terminalid
join client_tbl c
on c.clientid = term.clientid
where refnr is null)
loop
v_refnr := get_refnr;
MERGE INTO settlement_tbl s
USING (SELECT v_refnr AS REF_NUM,
total_amount AS TOTAL,
clientid AS CLIENTID,
name AS TITLE,
SYSDATE AS PROCESSEDDATE
FROM DUAL) d
ON (s.REF_NUM = d.REF_NUM)
WHEN NOT MATCHED THEN
INSERT (Ref_Num, Total, CLIENTID, TITLE, processeddate)
VALUES (d.REF_NUM, d.TOTAL, d.CLIENTID, d.TITLE, d.PROCESSEDDATE);
update_refnr(v_refnr, sysdate);
END LOOP;
END;
WHEN NOT MATCHED inserts new data when v_refnr does not already exist in your table.
Best of luck.
I get the error "subquery must return only one column" but i tried to use differents away to return the first record when i'm selecting the curProd.
I'm using this function, but i get the the errror as far as i know in:
curProd := (
SELECT "KeysForSale".*
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
The all function is:
CREATE FUNCTION "paymentRun"(buyer_id integer, payment_date DATE, payMethod paymentMethod, paid_amount double precision, payDetails text) RETURNS VOID AS
$$
DECLARE
row_STab "SearchTable"%rowtype;
curProd "KeysForSale"%rowtype;
totalPrice double precision;
returnedPID integer;
BEGIN
--For each entry in the search table
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
--We retrieve the associated product info, together with an available key
curProd := (
SELECT "KeysForSale".*
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
--Either there is no such product, or no keys for it
IF curProd IS NULL THEN
RAISE EXCEPTION 'Product is not available for purchase.';
END IF;
--Product's seller is the buyer - we can't let that pass
IF curProd.user_id = buyer_id THEN
RAISE EXCEPTION 'A Seller cannot purchase their own product.';
END IF;
--Fill in the rest of the data to prepare the purchase
UPDATE "SearchTable"
SET "SearchTable".price = (
CASE curProd.discounted_price IS NOT NULL -- if there was a discounted price, use it
WHEN TRUE THEN curProd.discounted_price
ELSE curProd.price
END
), "SearchTable".sk_id = curProd.sk_id
WHERE "SearchTable".product_id = curProd.product_id;
END LOOP;
--Get total cost
totalPrice := (
SELECT SUM("SearchTable".price)
FROM "SearchTable"
);
--The given price does not match the actual cost?
IF totalPrice <> paid_amount THEN
RAISE EXCEPTION 'Payment does not match cost!';
END IF;
--Create a purchase while keeping it's ID for register
INSERT INTO "Purchases" (purchase_id, final_price, user_id, paid_date, payment_method, details)
VALUES (DEFAULT, totalPrice, buyer_id, payment_date, payMethod, payDetails)
RETURNING purchase_id INTO returnedPID;
--For each product we wish to purchase
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
VALUES (row_STab.sk_id, returnedPID, row_STab.price);
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
WHERE row_STab.sk_id = "SerialKeys".sk_id;
END LOOP;
END
$$
LANGUAGE 'plpgsql' ;
Thank you in advance
Because the question has an incorrect answer, I'm providing an answer beyond the comment. The code that you want is:
curProd := (
SELECT "KeysForSale"
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
The difference is the lack of .*. Your version is returning a bunch of columns -- which is the error you are getting. You want to return a single record. The table name provides this.
I also think that parentheses will have the same effect:
SELECT ("KeysForSale".*)
For this case you should not to use syntax:
var := (SELECT ..).
Preferred should be SELECT INTO:
SELECT * INTO curProd FROM ...
The syntax SELECT tabname FROM tabname is PostgreSQL's proprietary, and although it is works well, better to not use, due unreadability for all without deeper PostgreSQL knowleadge.
Because PL/pgSQL is not case sensitive language, camel case is not advised (better to use snake case).
If it is possible, don't use ISAM style:
FOR _id IN
SELECT id FROM tab1
LOOP
SELECT * INTO r FROM tab2 WHERE tab2.id = _id
It is significantly slower than join (for more iterations)
FOR r IN
SELECT tab2.*
FROM tab1 JOIN tab2 ON tab1.id = tab2.id
LOOP
..
Cycles are bad for performance. This part is not really nice:
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
VALUES (row_STab.sk_id, returnedPID, row_STab.price);
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
WHERE row_STab.sk_id = "SerialKeys".sk_id;
END LOOP;
Possible solutions:
Use bulk commands instead:
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
SELECT sk_id, returnedPID, price
FROM "SearchTable"; -- using case sensitive identifiers is way to hell
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
FROM "SearchTable"
WHERE "SearchTable".sk_id = "SerialKeys".sk_id;
The less performance of ISAM style depends on number of iterations. For low iteration it is not important, for higher number it is death.