counting number of cars that each owner has - oracle

I'm trying to learn plsql and got stuck in understanding some basic stuff. Here is a challenge that I'm trying to solve.
I have two tables. One holds information about owners and the other is information about cars. I want to to write an anonymous block that joins these two tables and with a for loop based on amount of cars that is registered to each owner prints how many cars each person own. furthermore I want an if statement which distinguishes between 1 Car (singular) and 2, 3 Cars (plural).
the tables are these:
CREATE TABLE owners(
id_nr VARCHAR2(13) PRIMARY KEY,
f_name VARCHAR2(20),
s_name VARCHAR2(20));
CREATE TABLE cars(
reg_nr VARCHAR2(6) PRIMARY KEY,
id_nr REFERENCES owners(pnr),
model VARCHAR2(20),
year NUMBER(4),
date DATE);
The result may look like something like this:
19380321-7799, Hans, Anderson, Owns: 1 car
19490321-7899, Mike, Erikson, Owns: 2 cars
.
.
.
etc.
I tried many different ways but each time i get some errors.
I would appreciate any help and hints that helps me understand it.
Thanks!

Well, here's one way to do it. If you want to practice loops, you could add a second loop inside to loop over the cars table and print all the cars each owner has.
declare
v_suffix varchar2(1);
begin
for o in (select owners.id_nr, f_name, s_name,
(select count(1) from cars where cars.id_nr = owners.id_nr) as num_cars
from owners)
loop
if o.num_cars = 1
then v_suffix = null -- singular
else v_suffix = 's' -- plural
end if;
dbms_output.put_line(o.id_nr || ', ' || o.f_name || ', ' || o.s_name
|| ' Owns: ' || o.num_cars || ' car' || v_suffix);
end loop;
end;
/

Related

Find value by any column in table

I have columns in table something like
answeremail,
answertime,
ata,
atacomment,
ataid,
atainternalnumber,
atanumber,
atatype,
author,
becomeatafromdeviation,
becomeexternalatafrominternal,
becomefastfromothertype,
briefdescription,
city,
client_answer_attachment,
clientcomment,
confirmstatus,
created_at,
deviation,
deviationnumber,
deviationtype,
duedate,
emailsent,
financeid,
forfortnox,
fromfortnox,
is_deleted,
locked,
name,
parentata,
paymenttype,
pdfurl,
projectid,
quantity,
reason,
revisiondate,
startdate,
status,
street,
suggestion,
token,
type,
unit,
userid,
zip
I want to create SELECT statment to retrive some of this column but without specify column name something like this.
SELECT * FROM ata WHERE 'field' = 'argument'
Is there any solution for this problem or either I need to specify all those column in SELECT statment ?
Not (just) in SELECT, but in WHERE. Otherwise, how will query know which columns to check?
But - beware of datatypes. Oracle will try to implicitly convert one datatype to another. Sometimes, it'll succeed (e.g. to convert number 1 to string '1'), sometimes it'll fail (e.g. convert string 'A' to number or string 'AB23F' to date value).
Therefore, although you can try to write query which will write query for you, it might take some time to actually make it work properly. PL/SQL is probably what you'll end up with.
An example which checks all tables in my schema that contain a column named PHONE_NUMBER and searches for a row whose phone number contains 654. This script returns tables that contain such a value; you'd return something else - list of columns? All columns? Can't tell.
That's just a starting point. Good luck!
DECLARE
l_str VARCHAR2(500);
l_cnt NUMBER := 0;
BEGIN
FOR cur_r IN (SELECT u.table_name, u.column_name
FROM user_tab_columns u, user_tables t
WHERE u.table_name = t.table_name
AND u.column_name = 'PHONE_NUMBER'
)
LOOP
l_str := 'SELECT COUNT(*) FROM ' || cur_r.table_name ||
' WHERE ' || cur_r.column_name || ' like (''%654%'')';
EXECUTE IMMEDIATE (l_str) INTO l_cnt;
IF l_cnt > 0 THEN
dbms_output.put_line(l_cnt ||' : ' || cur_r.table_name);
END IF;
END LOOP;
END;
There is one short solution using xml:
https://dbfiddle.uk/?rdbms=oracle_18&fiddle=e5efc164b77a55b2ecf5d4bf85d589a5
select/*+ no_xml_query_rewrite */ *
from
xmltable(
'for $row in ora:view("SAMPLE_DATA")
let $col := $row/ROW/*[text() eq $search]
where $col ne ""
return element R {
attribute X {$col},
$row
}'
passing
'BB' as "search"
columns
found_col varchar2(30) path '#X'
,row_data xmltype path '.'
);
Results:
FOUND_COL ROW_DATA
------------------------------ ----------------------------------------------------------------------------------------------------
BB <R X="BB"><ROW><COLA>10A</COLA><COLB>BB</COLB><COLC>00</COLC></ROW></R>
BB <R X="BB"><ROW><COLA>10A</COLA><COLB>BB</COLB><COLC>10</COLC></ROW></R>
BB <R X="BB"><ROW><COLA>10A</COLA><COLB>BB</COLB><COLC>20</COLC></ROW></R>
BB <R X="BB"><ROW><COLA>10A</COLA><COLB>BB</COLB><COLC>30</COLC></ROW></R>
BB <R X="BB"><ROW><COLA>10A</COLA><COLB>BB</COLB><COLC>40</COLC></ROW></R>
From your description, I think you want to retrive the columns which spicified by the where clause only, and to omit the other columns.
I don't think this is allowed by simple sql statement. In the oracle document, at the select_list section, I can't see any solution for this question.
see https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6
If you really want, you can use pl/sql.

Executing the PL/SQL block gives an error which is not understandable

The question given was to write a PL/SQL block to print the details of the customers whose total order quantity is greater than 200 where the Customer table had ID number(5) primary key, Name varchar2(20), Contact_No varchar2(10) and the Order table had Order_Id number(5) primary Key, Quantity number(4) not null, C_id number(5) references Customer(ID).
If no record is found "No Records Found" is to be printed out.
This is the Code I wrote:
SET SERVEROUTPUT ON;
begin
dbms_output.put_line('Customer_Id ' || 'Customer_Name '|| 'Customer_Phone');
for cur_d in (select o.C_ID,total AS sum(o.QUANTITY) from Orders o group by o.C_ID) loop
from Customers c
where c.ID = cur_d.C_ID and cur_d.total > 200;
dbms_output.put_line(c.ID || c.Name || c.Contact_No);
end loop;
end;
/
The error I faced was -
for cur_d in (select o.C_ID,total AS sum(o.QUANTITY) from Orders o group by o.C_ID) loop
ERROR at line 2:
ORA-06550: line 2, column 41:
PL/SQL: ORA-00923: FROM keyword not found where expected
ORA-06550: line 2, column 15:
PL/SQL: SQL Statement ignored
ORA-06550: line 3, column 1:
PLS-00103: Encountered the symbol "FROM" when expecting one of the following:
( begin case declare exit for goto if loop mod null pragma
raise return select update while with '<'an identifier'>'
'<'a double-quoted delimited-identifier'> ')
Even after you correct the substantial that have been pointed out your procedure will still fail. You indicate that if no record is found printing a message to that effect. That in itself is ambiguous. Does that mean for the no records in the data table or no records for a given customer? Either way you have no code to produce such a message. How do you expect it to be written. Finally, SQL is set based processing so you need to start thinking in terms of sets instead of loops. The following reduces the db access to a single query; the loop is only to print results.
begin
dbms_output.put_line('Customer_Id ' || 'Customer_Name '|| 'Customer_Phone' || 'Total Orders');
for cur_d in (
with order_totals as
( select c_id, sum (quantity) order_total
from orders
group by c_id
having sum(quantity) > 200
)
select c.id, c.name, c.contact_no
, case when o.c_id is null
then 'No Records Found'
else to_char(o.order_total)
end order_total
from customers c
left join order_totals o
on c.id = o.c_id
order by c.id
)
loop
dbms_output.put_line(cur_d.ID || cur_d.Name || cur_d.Contact_No || cur_d.order_total);
end loop;
end;
The results are jammed together just as you initially had them. You need to workout their presentation.
There is a "select into" part missing between "for .. loop" and "from" parts. It has to be like this in order to work
for ..... loop
select some_column -- <-- this line is missing
into some_variable -- <-- this line is missing too
from ..........
This is the kind of issue where formatting your code will make the problem obvious. If you always start a new line and indent after for xxx in ( and also place the closing bracket on its own line, and include some gaps between commands, you'll get this, which is clearly wrong:
begin
dbms_output.put_line('Customer_Id ' || 'Customer_Name '|| 'Customer_Phone');
for cur_d in (
select o.c_id, total as sum(o.quantity)
from orders o
group by o.c_id
)
loop
from customers c
where c.id = cur_d.c_id
and cur_d.total > 200;
dbms_output.put_line(c.id || c.name || c.contact_no);
end loop;
end;
The first statement inside the loop seems to be missing something, as ekochergin mentioned.
total as sum(o.quantity) is backwards as Turo mentioned.
If you want id, name and contact_no to be printed in columns, you should look at lpad and rpad for formatting them. Just concatenating them together will produce something unreadable.
The dbms_output inside the loop refers to c.id, c.name and c.contact_no, but the record is called cur_d, not c.
Also cur_d is a slightly confusing name for a record (it's not a cursor). I always use r for cursor records unless there is some other r involved that it could be confused with.

How to fix 'Error(9,7): PL/SQL: ORA-00947: not enough values' in a function?

I'm trying to get a total commission for each Sale staff and store it to a function to put it in a procedure, when I put a working select statement (three tables involved), I got Error(9,20): PL/SQL: ORA-00947: not enough values. I think because datatype only returns number in this function, but the table contains other varchar datatypes that cause this problem.
I tried to remove some columns that are varchar2 datatype but the result is not correct.
Below is my fictional code:
create or replace FUNCTION get_total_commission return number
IS
v_total_commission;
--
begin
select
sale_id, sale_acct,sale_name, sum(commission) as total_commission
into v_total from invoice_tbl invoice join commission_tbl commission
on invoice.id = commission.id join sale_tbl sale on sale.id = commssion.id
where invoice.refnr is null;
return to_char(v_total, 'FM99999.00');
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line('err: ' ||SQLERRM);
end get_total_commission;
It will be a function that will show the total amount of commission that earns by each Sale staff.
You need to use local variable to which all your four columns in the SELECT list return to. And because of conversion to character type, need to return a string type value instead of numeric for the function.
SQL> SET SERVEROUTPUT ON;
SQL> CREATE OR REPLACE FUNCTION get_total_commission RETURN varchar2 IS
v_total_commission commission_tbl.commission%type;
v_sale_id sale_tbl.sale_id%type;
v_sale_acct sale_tbl.sale_acct%type;
v_sale_name sale_tbl.sale_name%type;
BEGIN
select sale_id, sale_acct, sale_name, sum(commission) as total_commission
into v_sale_id, v_sale_acct, v_sale_name, v_total
from invoice_tbl invoice
join commission_tbl commission
on invoice.id = commission.id
join sale_tbl sale
on sale.id = commssion.id
where invoice.refnr is null;
return to_char(v_total, 'FM99999.00');
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line('err: ' || SQLERRM);
END;
the total amount of commission that earns by each Sale staff
Sounds like returning one number shorn of identifying characteristics is not the solution you need. You need a result set. Personally, this seems better fitted to a view than a function but if you want to wrap the query in a function this is how to do it:
-- obviously correct these data types to fit your actual needs
create or replace type commission_t as object(
sale_id varchar2(30)
, acct_id varchar2(30)
, sale_name varchar2(48)
, total_commission_number
);
/
create or replace type commission_nt as table of commission_t;
/
create or replace FUNCTION get_total_commission return commission_nt
IS
return_value commission_nt;
begin
select commission_t(
sale_id, sale_acct,sale_name, sum(commission) )
bulk collect into return_type
from invoice_tbl invoice
join commission_tbl commission on invoice.id = commission.id
join sale_tbl sale on sale.id = commssion.id
where invoice.refnr is null
group by sale_id, sale_acct,sale_name
;
return return_value;
end get_total_commission;
And query it like this:
select * from table (get_total_commission);
There are various rough edges with this. For instance it won't work well if your result set is huge (which obviously depends, but say more than 5000-10000 rows).
If you really just want the total commission for a single sale then you need to restrict the query by SALE_ID - and pass it as a parameter:
create or replace FUNCTION get_total_commission
(p_sale_id in sale.id%type)
return number
IS
v_total_commission number;
--
begin
select
sum(commission) as total_commission
into v_total_commission
from invoice_tbl invoice
join commission_tbl commission on invoice.id = commission.id
join sale_tbl sale on sale.id = commssion.id
where sale.id = p_sale_id
and invoice.refnr is null;
return v_total_commission ;
end get_total_commission;

PL/SQL Trigger Variable Problems

I am relatively new to PL/SQL and i am trying to create a trigger that will alert me after an UPDATE on a table Review. When it is updated I want to ge the username(User table), score(Review Table), and product name (Product Table) and print them out:
This is what I have so far:
three tables:
Review: score, userid,pid, rid
Users: userid,uname
Product: pid,pname
So Review can reference the other tables with forigen keys.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar(256);
y varchar(256);
z varchar(256);
begin
select uname into x , pname into y , score into z
from review r , product p , users u
where r.pid = p.pid and r.userid = u.userid and r.rid =new.rid;
dbms_output.put_line('user: '|| X||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end;
The problem I am having is I cannot seem to figure out how to store the selected fields into the variables and output it correctly.
DDL:
Create Table Review
(
score varchar2(100)
, userid varchar2(100)
, pid varchar2(100)
, rid varchar2(100)
);
Create Table Users
(
userid varchar2(100)
, uname varchar2(100)
);
Create Table Product
(
pid varchar2(100)
, pname varchar2(100)
);
The first problem I can see is that you're missing a colon when you refer to new.rid. The second is that you're accessing the review table inside a row-level trigger on that same table, which will give you a mutating table error at some point; but you don't need to as all the data from the inserted row is in the new pseudorow.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
l_uname users.uname%type;
l_pname product.pname%type;
begin
select u.uname into l_uname
from users u
where u.userid = :new.userid;
select p.pname
into l_pname
from product
where p.pid = :new.pid;
dbms_output.put_line('user '|| l_uname
|| ' entered a new review for product ' || l_pname
|| ' with a review score of '|| :new.score);
end;
The bigger problem is that the only person who could see the message is the user inserting tow row, which seems a bit pointless; and they would have to have output enabled in their session to see it.
If you're trying to log that so someone else can see it then store it in a table or write it to a file. As the review table can be queried anyway it seems a bit redundant though.
Having all your table columns as strings is also not good - don't store numeric values (e.g. scores, and probably the ID fields) or dates as strings, use the correct data types. It will save you a lot of pain later. You also don't seem to have any referential integrity (primary/foreign key) constraints - so you can review a product that doesn't exist, for instance, which will cause a no-data-found exception in the trigger.
It makes really no sense to use a trigger to notify themselves about changed rows. If you insert new rows into the table, then you have all info about them. Why not something like the block below instead a trigger:
create table reviews as select 0 as rid, 0 as userid, 0 as score, 0 as pid from dual where 1=0;
create table users as select 101 as userid, cast('nobody' as varchar2(100)) as uname from dual;
create table products as select 1001 as pid, cast('prod 1001' as varchar2(100)) as pname from dual;
<<my>>declare newreview reviews%rowtype; uname users.uname%type; pname products.pname%type; begin
insert into reviews values(1,101,10,1001) returning rid,userid,score,pid into newreview;
select uname, pname into my.uname, my.pname
from users u natural join products p
where u.userid = newreview.userid and p.pid = newreview.pid
;
dbms_output.put_line('user: '||my.uname||' entered a new review for Product: '||my.pname||' with a review score of: '||newreview.score);
end;
/
output: user: nobody entered a new review for Product: prod 1001 with a review score of: 10
In order to inform another session about an event you should use dbms_alert (transactional) or dbms_pipe (non transactional) packages. An example of dbms_alert:
create or replace trigger new_review_trig after insert on reviews for each row
begin
dbms_alert.signal('new_review_alert', 'signal on last rid='||:new.rid);
end;
/
Run the following block in another session (new window, worksheet, sqlplus or whatever else). It will be blocked until the registered signal is arrived:
<<observer>>declare message varchar2(400); status integer; uname users.uname%type; pname products.pname%type; score reviews.score%type;
begin
dbms_alert.register('new_review_alert');
dbms_alert.waitone('new_review_alert', observer.message, observer.status);
if status != 0 then raise_application_error(-20001, 'observer: wait on new_review_alert error'); end if;
select uname, pname, score into observer.uname, observer.pname, observer.score
from reviews join users using(userid) join products using (pid)
where rid = regexp_substr(observer.message, '\w+\s?rid=(\d+)', 1,1,null,1)
;
dbms_output.put_line('observer: new_review_alert for user='||observer.uname||',product='||observer.pname||': score='||observer.score);
end;
/
Now in your session:
insert into reviews values(2, 101,7,1001);
commit; --no alerting before commit
The another (observer) session will be finished with the output:
observer: new_review_alert for user=nobody,product=prod 1001: score=7
P.S. There was no RID in the Table REVIEW, so i'll just assume it was supposed to be PID.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar2(256);
y varchar2(256);
z varchar2(256);
BEGIN
select uname
, pname
, score
INTO x
, y
, z
from review r
, product p
, users u
where r.pid = p.pid
and r.userid = u.userid
and r.PID = :new.pid;
dbms_output.put_line('user: '|| X ||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end userNameTrigger;
You just made a mistake on the INTO statement, you can just clump them together in one INTO.

Oracle pipelined function cannot access remote table (ORA-12840) when used in a union

I have created a pipelined function which returns a table. I use this function like a dynamic view in another function, in a with clause, to mark certain records. I then use the results from this query in an aggregate query, based on various criteria. What I want to do is union all these aggregations together (as they all use the same source data, but show aggregations at different heirarchical levels).
When I produce the data for individual levels, it works fine. However, when I try to combine them, I get an ORA-12840 error: cannot access a remote table after parallel/insert direct load txn.
(I should note that my function and queries are looking at tables on a remote server, via a DB link).
Any ideas what's going on here?
Here's an idea of the code:
function getMatches(criteria in varchar2) return myTableType pipelined;
...where this function basically executes some dynamic SQL, which references remote tables, as a reference cursor and spits out the results.
Then the factored queries go something like:
with marked as (
select id from table(getMatches('OK'))
),
fullStats as (
select mainTable.id,
avg(nvl2(marked.id, 1, 0)) isMarked,
sum(mainTable.val) total
from mainTable
left join marked
on marked.id = mainTable.id
group by mainTable.id
)
The reason for the first factor is speed -- if I inline it, in the join, the query goes really slowly -- but either way, it doesn't alter the status of whatever's causing the exception.
Then, say for a complete overview, I would do:
select sum(total) grandTotal
from fullStats
...or for an overview by isMarked:
select sum(total) grandTotal
from fullStats
where isMarked = 1
These work fine individually (my pseudocode maybe wrong or overly simplistic, but you get the idea), but as soon as I union all them together, I get the ORA-12840 error :(
EDIT By request, here is an obfuscated version of my function:
function getMatches(
search in varchar2)
return idTable pipelined
as
idRegex varchar2(20) := '(05|10|20|32)\d{3}';
searchSQL varchar2(32767);
type rc is ref cursor;
cCluster rc;
rCluster idTrinity;
BAD_CLUSTER exception;
begin
if regexp_like(search, '^L\d{3}$') then
searchSQL := 'select distinct null id1, id2_link id2, id3_link id3 from anotherSchema.linkTable#my.remote.link where id2 = ''' || search || '''';
elsif regexp_like(search, '^' || idRegex || '(,' || idRegex || || ')*$') then
searchSQL := 'select distinct null id1, id2, id3 from anotherSchema.idTable#my.remote.link where id2 in (' || regexp_replace(search, '(\d{5})', '''\1''') || ')';
else
raise BAD_CLUSTER;
end if;
open cCluster for searchSQL;
loop
fetch cCluster into rCluster;
exit when cCluster%NOTFOUND;
pipe row(rCluster);
end loop;
close cCluster;
return;
exception
when BAD_CLUSTER then
raise_application_error(-20000, 'Invalid Cluster Search');
return;
when others then
raise_application_error(-20999, 'API' || sqlcode || chr(10) || sqlerrm);
return;
end getMatches;
It's very simple, designed for an API with limited access to the database, in terms of sophistication (hence passing a comma delimited string as a possible valid argument): If you supply a grouping code, it returns linked IDs (it's a composite, 3-field key); however, if you supply a custom list of codes, it just returns those instead.
I'm on Oracle 10gR2; not sure which version exactly, but I can look it up when I'm back in the office :P
To be honest no idea where the issue came from but the simplest way to solve it - create a temporary table and populate it by values from your pipelined function and use the table inside WITH clause. Surely the temp table should be created but I'm pretty sure you get serious performance shift because dynamic sampling isn't applied to pipelined functions without tricks.
p.s. the issue could be fixed by with marked as ( select /*+ INLINE / id from table(getMatches('OK'))) but surely it isn't the stuff you're looking for so my suggestion is confirmed WITH does something like 'insert /+ APPEND*/' inside it'.

Resources