There is a complex query which generates a report. The query has several sub queries that generate 3-columns table for different products. Each sub query returns one row. All returned rows then need to be united.
But there is one requirement. If there are no result rows for a sub query we need to include the corresponding product to the final report anyway, but specify that Trades_Count is equal to zero.
I can achieve this using set of variables. The following code will work perfectly in MS SQL Server:
DECLARE #PRODUCT_NAME_1 nvarchar(100);
DECLARE #OFFER_VALID_DATE_1 datetime;
DECLARE #TRADES_COUNT_1 int;
DECLARE #PRODUCT_NAME_2 nvarchar(100);
DECLARE #OFFER_VALID_DATE_2 datetime;
DECLARE #TRADES_COUNT_2 int;
--Product 1
select #PRODUCT_NAME_1 = PRODUCT_NAME, #OFFER_VALID_DATE_1 = MAX(EXPIRY_DATE), #TRADES_COUNT_1 = COUNT(DEAL_NUMBER)
from (
--Data extractions with several joins goes here....
) as TempTable1
GROUP BY PRODUCT_NAME
--Product 2
select #PRODUCT_NAME_2 = PRODUCT_NAME, #OFFER_VALID_DATE_2 = MAX(EXPIRY_DATE), #TRADES_COUNT_2 = COUNT(DEAL_NUMBER)
from (
--Data extractions with several joins goes here....
) as TempTable2
GROUP BY PRODUCT_NAME
SELECT ISNULL(#PRODUCT_NAME_1,'Product 1') AS PRODUCT_NAME, #OFFER_VALID_DATE_1 AS MAX_MATURITY, ISNULL(#TRADES_COUNT_1,0)
UNION
(
SELECT ISNULL(#PRODUCT_NAME_2,'Product 2') AS PRODUCT_NAME, #OFFER_VALID_DATE_2 AS MAX_MATURITY, ISNULL(#TRADES_COUNT_2,0)
)
I think that I haven’t used anything T-SQL specific, but pure ANSI-SQL (I’m not 100% sure though).
So this is not working in Oracle.
First of all it requires having only one DECLARE keyword. Then it forces me using Begin … End execution scope. Then it doesn’t allow me to assign variables like I do (see example above) – I need to use “Select INTO” statement instead. After all calculations are done it doesn’t allow me selecting values from local variables. Heck.
Does anyone know how to make it work in Oracle?
Thanks!
PL/SQL is different than t-sql, I did a change with some comments for you, but definitely look at the links from Andy. This was ran in oracle's free SQL Developer (which also has a "Translation Scratch Handler (tools>Migration>Translation Scratch Handler) that may be of use.
--this creates a refcursor to allow us to simply print the results
var refc refcursor
/
declare --here we declare our variables
product_name_1 varchar2(15) ;
offer_valid_date_1 date ;
trade_count_1 number ;
product_name_2 varchar2(15) ;
offer_valid_date_2 date ;
trade_count_2 number ;
begin
begin --this creates a block so we may handle any exceptions just to this
select PRODUCT_NAME, MAX(EXPIRY_DATE), COUNT(DEAL_NUMBER)
into product_name_1 , offer_valid_date_1 , trade_count_1
--in oracle you select INTO, not var=COL
from (
--Data extractions with several joins goes here....
select
123 PRODUCT_NAME,
sysdate EXPIRY_DATE,
5 DEAL_NUMBER
from dual --this is a 'fake' table to generate some data for testing
) TempTable1 --drop the "as"
GROUP BY PRODUCT_NAME ;
exception --if not data is found, then this error is thrown
--if multiple values are thrown an error will also be thrown (not caught here)
when no_data_found then
product_name_1 := null ; --note, to do a var = , we use "var := value;"
offer_valid_date_1 := null;
trade_count_1 := null;
end ;
begin
select PRODUCT_NAME, MAX(EXPIRY_DATE), COUNT(DEAL_NUMBER)
into product_name_2 , offer_valid_date_2 , trade_count_2
--in oracle you select INTO, not var=COL
from (
--Data extractions with several joins goes here....
select 555 PRODUCT_NAME, sysdate EXPIRY_DATE, 6 DEAL_NUMBER
from dual
) TempTable2 -- drop the "as"
GROUP BY PRODUCT_NAME ;
exception --if not data is found, then this error is thrown
--if multiple values are thrown an error will also be thrown (not caught here)
when no_data_found then
product_name_2 := null ;
offer_valid_date_2 := null;
trade_count_2 := null;
end ;
open :refc for --you cannot just have a select statement, you must "open" a cursor for it
--oracle IsNull is NVL (or NVL2 or you can do a case or decode...)
SELECT nvl(PRODUCT_NAME_1,'Product 1') AS PRODUCT_NAME
, OFFER_VALID_DATE_1 AS MAX_MATURITY
, nvl(TRADE_COUNT_1,0)
FROM DUAL --you also must have a table, DUAL is an oracle table for this tasks
UNION
SELECT nvl(PRODUCT_NAME_2,'Product 2') AS PRODUCT_NAME
, OFFER_VALID_DATE_2 AS MAX_MATURITY
, nvl(TRADE_COUNT_2,0)
FROM DUAL;
end ;
/
--now print the results, if you did this in a proc you would simple have this as an output
print refc;
-------------
PRODUCT_NAME MAX_MATURITY NVL(:B1,0)
-------------------------------------- ----------------------
123 18.FEB.2011 08:43 1
555 18.FEB.2011 08:43 1
Oracle concepts used here:
Dual Table , NVL, Variables, pl/sql Exception
and look at this http://www.dba-oracle.com/t_convent_sql_server_tsql_oracle_plsql.htm
PL/SQL formats procedural blocks differently than T-SQL.
You'll want to use the following structure:
DECLARE
astring varchar2(1000);
anumber number;
BEGIN
my SQL code here...
END;
You don't use the # either in PL/SQL. Just use variables names directly.
Related
I use Oracle 12c. I know that my next question is not new but I am little bit confused and need help.
I have this SQL statement:
SELECT *
FROM TABLE_NAME
WHERE CREATE_DATE BETWEEN TO_DATE(FIRST_DATE, 'YYYY-MM-DD')
AND TO_DATE(SECOND_DATE , 'YYYY-MM-DD')
Questions:
How correctly to use SELECT request in stored procedure?
That SQL statement returns more than 1 row, is it mean that I need to use cursor?
If table has 15 columns, as output I need to set all of them?
EDIT:
CREATE OR REPLACE PROCEDURE PROCEDURE_NAME
(
FIRST_DATE IN VARCHAR2(10),
SECOND_DATE IN VARCHAR2(10)
)
AS
oracle_cursor SYS_REFCURSOR;
BEGIN
OPEN oracle_cursor FOR
SELECT *
FROM scheme_name.table_name
WHERE CREATE_DATE BETWEEN TO_DATE(FIRST_DATE, 'YYYY-MM-DD') AND TO_DATE(SECOND_DATE, 'YYYY-MM-DD');
DBMS_SQL.RETURN_RESULT(oracle_cursor);
END PROCEDURE_NAME;
How correctly to use SELECT request in stored procedure?
In a stored procedure you need to assign the queried result set to a variable (or variables) which match the projection:
select << column >>, << column >>
into << variable >>, << variable >>
from table_name
....
That SQL statement returns more than 1 row, is it mean that I need to use cursor?
A cursor is one way of handling it. Although a cursor loop is usually the better approach:
for r in ( SELECT *
FROM TABLE_NAME
WHERE CREATE_DATE BETWEEN TO_DATE(FIRST_DATE, 'YYYY-MM-DD')
AND TO_DATE(SECOND_DATE , 'YYYY-MM-DD')
) loop
Populating a collection variable is another approach, using the BULK COLLECT:
select << column >>
bulk collect into << collection >>
from table_name
....
If table has 15 columns, as output I need to set all of them?
You can choose to create fifteen distinct variables. However, if your query's projection matches the table's projection (which it does with select *) you can use the %rowtype construct to define a record variable or a collection:
declare
l_rec TABLE_NAME%rowtype; -- single row
type t_rec is table of TABLE_NAME%rowtype; -- multiple rows
l_recs t_rec
begin
SELECT *
bulk collect into l_recs
FROM TABLE_NAME
WHERE CREATE_DATE BETWEEN TO_DATE(FIRST_DATE, 'YYYY-MM-DD')
AND TO_DATE(SECOND_DATE , 'YYYY-MM-DD');
I need to take the result and show that data in web page.
For returning results you probably just need to return a Ref Cursor. This is just a pointer which maps to constructs like JDBC ResultSet and ODBC ResultSet. For PHP this would be an oci_new_cursor. Find out more.
Create or replace procedure get_recs
(FIRST_DATE in varchar2,
SECOND_DATE in varchar2,
OUT_RECS out sys_refcursor
) is
Begin
Open out_recs for
SELECT *
FROM TABLE_NAME
WHERE CREATE_DATE BETWEEN TO_DATE(FIRST_DATE, 'YYYY-MM-DD')
AND TO_DATE(SECOND_DATE , 'YYYY-MM-DD');
End;
Incidentally, you seem to expect to pass the parameters as strings: it would be better to pass them as actual dates.
I have a created a procedure as
create or replace procedure availability(num in number) as
begin
delete from vehicle_count;
insert into vehicle_count from select engine_no,count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
end;
/
The procedure was created successfully but now i have to write a separate query to view the contents of vehicle_count as
select * from vehicle_count;
I tried inserting the select statement into the procedure after insertion but it showed a error stating "an INTO clause is expected in the select statement".
How can I create procedure to select the required contents and display it in a single execute statement?
Table schema
vehicle(vehicle_no,engine_no,offence_count,license_status,owner_id);
vehicle_count(engine_no,engine_count);
Check this (MS SQL SERVER)-
create or alter procedure availability(#num as int) as
begin
delete from vehicle_count;
insert into vehicle_count
output inserted.engine_no,inserted.count_engine_no
select engine_no,count(engine_no) as count_engine_no
from vehicle
where engine_no=#num
group by engine_no;
end;
If you want to use a SELECT into a PL/SQL block you should use either a SELECT INTO or a loop (if you want to print more rows).
You could use something like this:
BEGIN
SELECT engine_no, engine_count
INTO v_engine, v_count
FROM vehicle_count
WHERE engine_no = num;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_engine := NULL;
v_count := NULL;
END;
v_engine and v_count are two variables. You can declare them in your procedure, and they will contain the values you want to print.
You said that the procedure you wrote (actually, you posted here) compiled successfully. Well, sorry to inform you - that's not true. This is not a valid syntax:
insert into vehicle_count from select engine_no,count(engine_no)
----
from? Here?
Consider posting true information.
As of your question (if we suppose that that INSERT actually inserted something into a table):
at the beginning, you delete everything from the table
as SELECT counts number of rows that share the same ENGINE_NO (which is equal to the parameter NUM value), INSERT inserts none (if there's no such NUM value in the table) or maximum 1 row (because of aggregation)
therefore, if you want to display what's in the table, all you need is a single SELECT ... INTO statement whose result is displayed with a simple DBMS_OUTPUT.PUT_LINE which will be OK if you're doing it interactively (in SQL*Plus, SQL Developer, TOAD and smilar tools). Regarding table description, I'd say that ENGINE_NO should be a primary key (i.e. that not more than a single row with that ENGINE_NO value can exist in a table).
create or replace procedure availability (num in number) as
l_engine_no vehicle_count.engine_no%type;
l_engine_count vehicle_count.engine_count%type;
begin
delete from vehicle_count;
insert into vehicle_count (engine_no, engine_count)
select engine_no, count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
-- This query shouldn't return TOO-MANY-ROWS if ENGINE_NO is a primary key.
-- However, it might return NO-DATA-FOUND if there's no such NUM there, so you should handle it
select engine_no, engine_count
into l_engine_no, l_engine_count
from vehicle_count
where engine_no = num;
dbms_output.put_line(l_engine_no ||': '|| l_engine_count);
exception
when no_data_found then
dbms_output.put_line('Nothing found for ENGINE_NO = ' || num);
end;
/
There are numerous alternatives to that (people who posted their answers/comments before this one mentioned some of those), and the final result you'd be satisfied with depends on where you want to display that information.
I'm writing a pl/sql function. I need to select multiple rows from select statement:
SELECT pel.ceid
FROM pa_exception_list pel
WHERE trunc(pel.creation_date) >= trunc(SYSDATE-7)
if i use:
SELECT pel.ceid
INTO v_ceid
it only stores one value, but i need to store all values that this select returns. Given that this is a function i can't just use simple select because i get error, "INTO - is expected."
You can use a record type to do that. The below example should work for you
DECLARE
TYPE v_array_type IS VARRAY (10) OF NUMBER;
var v_array_type;
BEGIN
SELECT x
BULK COLLECT INTO
var
FROM (
SELECT 1 x
FROM dual
UNION
SELECT 2 x
FROM dual
UNION
SELECT 3 x
FROM dual
);
FOR I IN 1..3 LOOP
dbms_output.put_line(var(I));
END LOOP;
END;
So in your case, it would be something like
select pel.ceid
BULK COLLECT INTO <variable which you create>
from pa_exception_list
where trunc(pel.creation_Date) >= trunc(sysdate-7);
If you really need to store multiple rows, check BULK COLLECT INTO statement and examples. But maybe FOR cursor LOOP and row-by-row processing would be better decision.
You may store all in a rowtype parameter and show whichever column you want to show( assuming ceid is your primary key column, col1 & 2 are some other columns of your table ) :
SQL> set serveroutput on;
SQL> declare
l_exp pa_exception_list%rowtype;
begin
for c in ( select *
from pa_exception_list pel
where trunc(pel.creation_date) >= trunc(SYSDATE-7)
) -- to select multiple rows
loop
select *
into l_exp
from pa_exception_list
where ceid = c.ceid; -- to render only one row( ceid is primary key )
dbms_output.put_line(l_exp.ceid||' - '||l_exp.col1||' - '||l_exp.col2); -- to show the results
end loop;
end;
/
SET SERVEROUTPUT ON
BEGIN
FOR rec IN (
--an implicit cursor is created here
SELECT pel.ceid AS ceid
FROM pa_exception_list pel
WHERE trunc(pel.creation_date) >= trunc(SYSDATE-7)
)
LOOP
dbms_output.put_line(rec.ceid);
END LOOP;
END;
/
Notes from here:
In this case, the cursor FOR LOOP declares, opens, fetches from, and
closes an implicit cursor. However, the implicit cursor is internal;
therefore, you cannot reference it.
Note that Oracle Database automatically optimizes a cursor FOR LOOP to
work similarly to a BULK COLLECT query. Although your code looks as if
it fetched one row at a time, Oracle Database fetches multiple rows at
a time and allows you to process each row individually.
I'm using PLSQL to create a procedure to update a table based on querying another table, and I'm using a loop for the purpose. FOr testing, I'm holding off on the procedure code, and just trying to make the following script work. But it keeps throwing errors including "Encountered the symbol "end-of-file"" or "invalid SQL statement", based on minor tweaks. Where am I going wrong?
DECLARE CURSOR cur IS
SELECT * FROM Summary;
BEGIN
FOR rec in cur
LOOP
UPDATE Award
SET monthly_sales = (
SELECT COUNT(*)
FROM Sales
WHERE employee_id = rec.employee_id
AND to_char(Sales.SALES_DATE,'YY/MM')=to_char(SYSDATE,'YY/MM')
)
WHERE Summary.employee_id = rec.employee_id
END LOOP
END;
/
As well as the missing semicolons that Sentinel pointed out, your where clause is incorrect:
WHERE Summary.employee_id = rec.employee_id;
Perhaps you meant:
WHERE award.employee_id = rec.employee_id;
since you're updating the AWARD table?
However, I would question why you're using a row-by-row (aka slow-by-slow) approach for this, when you could easily do it in a single statement? Perhaps something like the following would give you the correct results (untested, since you gave no table create scripts or sample input data etc):
merge into award tgt
using (select sls.employee_id,
count(*) monthly_sales
from sales sls
where trunc(sls.sales_date,'mm') = trunc(sysdate, 'mm')
and sls.employee_id in (select employee_id from summary)
group by sls.employee_id) src
on (tgt.employee_id = src.employee_id)
when matched then
update set tgt.monthly_sales = src.monthly_sales;
You are missing semicolons at the end of lines 12 and 13:
DECLARE CURSOR cur IS
SELECT * FROM Summary;
BEGIN
FOR rec in cur
LOOP
UPDATE Award
SET monthly_sales = (
SELECT COUNT(*)
FROM Sales
WHERE employee_id = rec.employee_id
AND trunc(Sales.SALES_DATE,'month') = trunc(SYSDATE,'month')
)
WHERE Summary.employee_id = rec.employee_id;
END LOOP;
END;
/
I want to write a SQL IF statement that checks whether or not a local temporary table exists, but those kinds of tables are not recorded in the SQL Anywhere system catalog.
Note that you can do this in 11.0.1 and higher:
DROP TABLE IF EXISTS t;
If you're asking the question, "How do I drop a local temporary table without raising an error if it doesn't exist?" then the answer's simple: just DROP it and ignore any error:
BEGIN
DROP TABLE t;
EXCEPTION WHEN OTHERS THEN
END;
If you really need to know the answer to the question "Does table t exist?" you can query the table and analyze the resulting SQLSTATE. The following function makes use of several features:
ON EXCEPTION RESUME ignores any exception raised by the SELECT and passes control to the IF statement.
EXECUTE IMMEDIATE lets you write a query where the table name is in a string variable.
TOP 1 makes sure that only one row is selected even if the table contains a million rows.
ORDER BY 1 lets you meet the requirement that TOP can only be used when the result set is ordered.
SELECT 1 relieves you of the burden of specifying a column name.
INTO #dummy means the SELECT (and consequently the EXECUTE IMMEDIATE) doesn't return a result set.
If the SELECT works, it's either going to set SQLSTATE to '00000' for success or '02000' for row not found. Any other SQLSTATE means there's some serious problem with the table... like it doesn't exist.
CREATE FUNCTION f_table_is_ok
( IN #table_name VARCHAR ( 128 ) )
RETURNS INTEGER
ON EXCEPTION RESUME
BEGIN
DECLARE #dummy INTEGER;
EXECUTE IMMEDIATE STRING (
'SELECT TOP 1 1 INTO #dummy FROM ',
#table_name,
' ORDER BY 1' );
IF SQLSTATE IN ( '00000', '02000' ) THEN
RETURN 1
ELSE
RETURN 0
END IF;
END;
Here's some test code:
BEGIN
DECLARE LOCAL TEMPORARY TABLE tt ( c INTEGER );
DECLARE LOCAL TEMPORARY TABLE "t t" ( c INTEGER );
SELECT f_table_is_ok ( 'asdf' );
SELECT f_table_is_ok ( 'tt' );
SELECT f_table_is_ok ( '"t t"' );
SELECT f_table_is_ok ( '"SYS"."SYSTABLE"' );
END;
just try to drop it anyways and ignore the error...
BEGIN
DROP TABLE table;
EXCEPTION WHEN OTHERS THEN
END;