Dynamic SQL, comparing two records one column at a time - oracle

Scenario: we have flashback set up on certain tables in a Oracle database. Every now and then, we want to see what fields changed from one row to another. We can inspect visually of course but that is error-prone.
So I had the "brilliant" idea to try to step through the rows, store the current record into one record variable, and the prior record into another one. Then, field-by-field, compare each field, and if different, print out the field name and the values. Something like this:
DECLARE CURSOR myflash IS SELECT * FROM myflashtable;
OLDRECORD myflashtable%ROWTYPE;
NEWRECORD myflashtable%ROWTYPE;
dynamic_statement varchar2(4000);
cursor colnames is select * from all_tab_columns where table_name = 'myflashtable';
begin
if not myflash%ISOPEN then
open myflash;
end if;
fetch myflash into NEWRECORD;
while myflash%FOUND loop;
for columnnames in colnames loop
/* cobble together dynamic SQL along the lines of
"if oldrecord.column_name != newrecord.column_name
then print some information``....end if;"
*/
execute immediate dynamic_statement;
end loop;
OLDRECORD := NEWRECORD;
fetch myflash into NEWRECORD;
end loop;
end;
Naturally this didn't work. Initially it gave me "invalid SQL statement" and I added begin/end onto the dynamic SQL. When I tried running that version, it gave me an error because it doesn't know about the old/new records. When I run without doing the execute, but just dumping the generated SQL, it is stepping through all the columns on each of the records, so that part of the logic is working.
I'm quite sure there's a better way to do this, or perhaps to make it work. One thought was to do something like declaring old/new value variables, then using dynamic SQL to move the old/new record fields to each of those:
EXECUTE IMMEDIATE 'oldvalue := OLDRECORD.'||columnnames.column_name;
EXECUTE IMMEDIATE 'newvalue := NEWRECORD.'||columnnames.column_name;
IF oldvalue != newvalue then
/* print some stuff */
END IF:
but of course the trick is that the target variable would have to handle columns of a bunch of different types - char, date, etc. So there'd need to be variants of old/newvalue variables, and logic to handle that, and it was turning into not-so-much-fun.
Any suggestions for a more elegant way to do this? I've checked around the site and haven't had much like finding anything that quite seemed like what I'm trying to do.

You are on the right track. But it is quite some more programming work to do. Read the old and new table in a join linking it with the correct primary key and loop through it. You can use DMBS_SQL package to build a dynamic cursor and loop through the tables.

Related

Trying to use EXECUTE IMMEDIATE, cannot compile procedure

Trying to write a procedure which takes no values in, adds a sale price column to my existing product table, then loops through to calculate a sale price and insert that into the new column.
I haven't been able to get anything to work, I think it's something to do with Oracle not liking ALTER TABLE to be run from inside a procedure, but I don't know, and I don't know enough to direct my attempts anywhere else.
This is my attempt
CREATE or REPLACE PROCEDURE ProductLineSale as
BEGIN
DECLARE
NewSalePrice NUMBER(6,2):=0;
EXECUTE IMMEDIATE 'alter table ' || Product || 'add or replace column' || 'SalePrice NUMBER(6,2);'
FOR p in (SELECT ProductStandardPrice FROM Product
group by ProductStandardPrice)
LOOP
CASE WHEN p.ProductStandardPrice>=400 THEN NewSalePrice:=.9*price
WHEN p.ProductStandardPrice<400 THEN NewSalePrice:=.85*price
INSERT INTO Product(SalePrice)
VALUES(NewSalePrice)
END LOOP;
END ProductLineSale
Product is the literal name of the Product table in my database. SalePrice is what I would like the new column to be named.
SQLDeveloper won't compile the procedure. The error I get is fairly cryptic as well:
Error(2,10): PLS-00103: Encountered the symbol "=" when expecting one of the following: constant exception table long double ref char time timestamp interval date binary national character nchar.
There are a host of errors... The ones that jump out at me on a first pass.
The requirement doesn't make sense. Adding a column in a procedure doesn't make sense. You create a procedure because you want code to be reusable. Adding a column can only be done once, hence it is by definition not reusable.
A procedure has to be compiled before it can be executed. If there is a reference to a column that doesn't exist, the procedure will fail to compile. Thus, if you want to add a column to the table using dynamic SQL, all subsequent references to the column (i.e. your insert statement) would need to use dynamic SQL as well.
Your DDL statement is incorrect. There is no add or replace clause, it's alter table product add SalePrice NUMBER(6,2). Note that when you're building your string, you also have to ensure that there is a space between the clause add and the column name SalesPrice-- one of the two strings you're concatenating together would need that.
It doesn't make sense to have a declare where you do. You can declare variables between the as and the begin one line above. You are allowed to create a nested PL/SQL block there with the declare but then you'd need a matching begin and end that you don't have.
If you're going to use a case statement in PL/SQL, you'd need an end case. You would also need to have a semicolon ; after each expression.
Your insert statement is also missing a semicolon.
Logically, I am hard pressed to imagine that you really want have an insert here. It doesn't make logical sense to create a bunch of new rows in the table when you add a new column. I would assume that you want to update the value of the new column in existing rows. Which, presumably, requires that your cursor selects the primary key column(s) and potentially changes whether and what you're grouping by.
Product and price are being used as local variables in the execute immediate statement and in the case statement but aren't defined. I'm guessing that you just want to hard code the name of the table you're altering and that price is supposed to reference the name of a column in the table that you need to select in your cursor but I'm not sure.
This case statement is syntactically valid (or would be if price resolves to something valid). Many of the other corrections are less obvious because of the reasons I detailed above.
case when p.ProductStandardPrice>=400
then NewSalePrice:=.9*price;
when p.ProductStandardPrice<400
THEN NewSalePrice:=.85*price;
end case;
If I was to speculate at what you actually want (given that this is a homework assignment with requirements that don't actually make sense), I'd guess something like
CREATE or REPLACE PROCEDURE ProductLineSale
as
begin
execute immediate 'alter table Product add SalePrice NUMBER(6,2)';
execute immediate 'update product ' ||
' set SalePrice = (case when ProductStandardPrice >= 400 ' ||
' then 0.9 * Price ' ||
' else 0.85 * Price ' ||
' end) ';
end ProductLineSale;
If you're going to use dynamic SQL, it almost always makes sense to declare a local variable, build the SQL statement in that variable, and then execute it so that you can debug things by printing out the statement you've build to debug it.

How to use DB link in a variable in for loop

I have below query which I am trying to write in a procedure after begin clause. I dont want to use it as a cursor because of some dependency.
I want to make my db link dynamic instead of hardcoding it and for this reason i put my entire for loop in variable. If i take the variable out then my procedure is working fine. I dont want to change logic of my code while trying to make dblink dynamic.
But this part of loop is not working and throwing an error as
encounter the symbol end of the file when expecting one of the following:
PROCEDURE TMP_CHECK
IS
open CS for NESS_QUERY;
loop
fetch CS into REC;
exit when CS%notfound;
INSERT INTO TMP_Data(ID,NAME,ID_TST,CHK_DATE,VALUE,CHECK,SOURCE) VALUES
(IN_SEQ_NO,DB_NAME,DB_ID,REC.DAY_ID,REC.nb_ord,'ORDS','LEOSOFT');
COMMIT;
END LOOP;
CLOSE CS;
END LOOP;
END;
Dynamic SQL is hard because it turns compilation errors into runtime errors. It looks like your query has several compilation errors: duplicate table aliases, out-of-scope alias references, cross joins between the remote tables (unless that is deliberate, in which case yuck!). So the first thing to do is get the query running as straight SQL, only then make it dynamic.
Also don't include commented code in your template SQL. Things are already hard enough, why make them even harder by doing stuff like this?
ORDER BY
-- TE.market asc,
-- TE.entity asc,
TE.dayiid ASC)'
So, now we've got that out of the way let's look at the logic of what you're trying to do. We cannot drop dynamic segments of PL/SQL into a program. This just won't work ...
LQUERY='
FOR REC IN(
SELECT
... because you have not written a complete PL/SQL statement. But there is a way to do what you want: use a cursor variable. We can open a ref cursor for static and dynamic queries. Find out more.
The following is for illustrative purposes only: you haven't explained your business logic, so this is not necessarily the best way of doing things. But it should solve your immediate problem:
declare
....
l_order number;
l_dayiid number;
l_ety_id number;
rc sys_refcursor;
begin
...
FOR IIS_DB IN C_DB
LOOP
IN_DB_LINK:=LEO_DB.DATABASE_LINK;
IN_DAY:=LEO_DB.DAY_ID;
open rc for
'SELECT order,dayiid,ety_id
from ...
ORDER BY TE.dayiid ASC)';
loop
fetch rc into l_order, l_dayiid, l_ety_id;
exit when rc%notfound;
...
end loop;
close rc;
" PLS-00487: Invalid reference to variable 'REC'"
I think your problem is this:
fetch CS into REC;
You have defined REC as a string but clearly it should be a record type, which needs to match the projection of the query you're fetching. So you need to define something like this:
Type rec_t is record (
nb_ord number,
day_id number,
entity number
);
REC rec_t;
Now you can fetch a record into REC and reference its attributes.
Incidentally the nvl() you've written to supply NB_ORD is wrong. The first argument is the one you are testing for null: 500 will never be null so that's what you'll get for every row. You need to swap the parameters round.

ORACLE: Using CTEs (Common Table Expressions) with PL/SQL

First off, my background is in SQL Server. Using CTEs (Common Table Expressions) is a breeze and converting it to a stored procedure with variables doesn't require any changes to the structure of the SQL other than replacing entered values with variable names.
In Oracle PL/SQL however, it is a completely different matter. My CTEs work fine as straight SQL, but once I try to wrap them as PL/SQL I run into a host of issues. From my understanding, a SELECT now needs an INTO which will only hold the results of a single record. However, I am wanting the entire recordset of multiple values.
My apologies if I am missing the obvious here. I'm thinking that 99% of my problem is the paradigm shift I need to make.
Given the following example:
NOTE: I am greatly over simplifying the SQL here. I do know the below example can be done in a single SQL statement. The actual SQL is much more complex. It's the fundamentals I am looking for here.
WITH A as (SELECT * FROM EMPLOYEES WHERE DEPARTMENT = 200),
B as (SELECT * FROM A WHERE EMPLOYEE_START_DATE > date '2014-02-01'),
C as (SELECT * FROM B WHERE EMPLOYEE_TYPE = 'SALARY')
SELECT 'COUNTS' as Total,
(SELECT COUNT(*) FROM A) as 'DEPT_TOTAL',
(SELECT COUNT(*) FROM B) as 'NEW_EMPLOYEES',
(SELECT COUNT(*) FROM C) as 'NEW_SALARIED'
FROM A
WHERE rowcount = 1;
Now if I want to make this into PL/SQL with variables that are passed in or predefined at the top, it's not a simple matter of declaring the variables, popping values into them, and changing my hard-coded values into variables and running it. NOTE: I do know that I can simply change the hard-coded values to variables like :Department, :StartDate, and :Type, but again, I am oversimplifying the example.
There are three issues I am facing here that I am trying to wrap my head around:
1) What would be the best way to rewrite this using PL/SQL with declared variables? The CTEs now have to go INTO something. But then I am dealing with one row at a time as opposed to the entire table. So CTE 'A' is a single row at a time, and CTE B will only see the single row as opposed to all of the data results of A, etc. I do know that I will most likely have to use CURSORS to traverse the records, which somehow seems to over complicate this.
2) The output now has to use DBMS_OUTPUT. For multiple records, I will have to use a CURSOR with FETCH (or a FOR...LOOP). Yes?
3) Is there going to a big performance issue with this vs. straight SQL in regards to speed and resources used?
Thanks in advance and again, my apologies if I am missing something really obvious here!
First, this has nothing to do with CTEs. This behavior would be the same with a simple select * from table query. The difference is that with T-SQL, the query goes into an implicit cursor which is returned to the caller. When executing the SP from Management Studio this is convenient. The result set appears in the data window as if we had executed the query directly. But this is actually non-standard behavior. Oracle has the more standard behavior which might be stated as "the result set of any query that isn't directed into a cursor must be directed to variables." When directed into variables, then the query must return only one row.
To duplicate the behavior of T-SQL, you just have to explicitly declare and return the cursor. Then the calling code fetches from the cursor the entire result set but one row at a time. You don't get the convenience of Sql Developer or PL/SQL Developer diverting the result set to the data display window, but you can't have everything.
However, as we don't generally write SPs just to be called from the IDE, it is easier to work with Oracle's explicit cursors than SQL Server's implicit ones. Just google "oracle return ref cursor to caller" to get a whole lot of good material.
Simplest way is to wrap it into an implicit for loop
begin
for i in (select object_id, object_name
from user_objects
where rownum = 1) loop
-- Do something with the resultset
dbms_output.put_line (i.object_id || ' ' || i.object_name);
end loop;
end;
Single row query without the need to predefine the variables.

How to retrieve parsed dynamic pl Sql

I have many PL/SQL functions and procedures that execute dynamic sql.
Is it possible to extract the parsed statements and dbms_output as an debugging aid ?
What I really want is to see the parsed sql (sql statement with substituted parameters).
Example:
I have a dynamic SQL statement like this
SQ:='SELECT :pComno as COMNO,null t$CPLS,t$CUNO,t$cpgs,t$stdt,t$tdat,t$qanp,t$disc,:cS Source FROM BAAN.TTDSLS031'||PCOMNO --1
|| ' WHERE' ||' TRIM(T$CUNO)=trim(:CUNO)' --2
|| ' AND TRIM(T$CPGS)=trim(:CPGS)' --3
|| ' AND T$QANP = priceWorx.fnDefaultQanp ' --4
|| ' AND priceWorx.fdG2J(sysdate) between priceWorx.fdG2J(t$stdt) and priceWorx.fdG2J(t$tdat)' --5
|| ' AND rownum=1 order by t$stdt';--6
execute immediate SQ into R using
PCOMNO,'C' --1
,PCUNO-- 2
,PCPGS;-- 3
What will be the statement sent to the server ?
You can display the bind variables associated with a SQL statement like this:
select v$sql.sql_text
,v$sql_bind_capture.*
from v$sql_bind_capture
inner join v$sql on
v$sql_bind_capture.hash_value = v$sql.hash_value
and v$sql_bind_capture.child_address = v$sql.child_address
--Some unique string from your query
where lower(sql_text) like lower('%priceWorx.fdG2J(sysdate)%');
You probably would like to see the entire query, with all the bind variables replaced by their actual values. Unfortunately, there's no easy way to get exactly what you're looking for, because of the following
issues.
V$SQL_BIND_CAPTURE doesn't store all of the bind variable information. The biggest limitation is that it only displays data "when the bind variable is used in the WHERE or HAVING clauses of the SQL statement."
Matching the bind variable names from the bind capture data to the query is incredibly difficult. It's easy to get it working 99% of the time, but that last 1% requires a SQL and PL/SQL parser, which is basically impossible.
SQL will age out of the pool. For example, if you gather stats on one of the relevant tables, it may invalidate all queries that use that table. You can't always trust V$SQL to have your query.
Which means you're probably stuck doing it the ugly way. You need to manually store the SQL and the bind variable data, similar to what user1138658 is doing.
You can do this with the dbms_output package. You can enable and disable the debug, and get the lines with get_line procedure.
I tested with execute immediate, inserting in a table and it works.
I recently answered another question with a example of using this.
One possible solution of this is to create a table temp(id varchar2,data clob); in your schema and then put the insert statement wherever you want to find the parsed key
insert into temp values(seq.nextval,v_text);
For example
declare
v_text varchar2(2000);
begin
v_text:='select * from emp'; -- your dynamic statement
insert into temp values(seq.nextval,v_text); --insert this script whenever you want to find the actual query
OPEN C_CUR FOR v_text;
-----
end;
Now if you see the table temp, you'll get the data for that dynamic statement.

PL/SQL Trigger - Dynamically reference :NEW or :OLD

Is it possible to dynamically reference the :NEW/OLD pseudo records, or copy them?
I'm doing a audit trigger for a very wide table, so would like to avoid having separate triggers for insert/delete/update.
When updating/inserting I want to record the :NEW values in the audit table, when deleting I want to record the :OLD values.
create or replace trigger audit_tgr
before insert or update or delete on 'table_name'
for each row
begin
if (inserting or updating) then
insert into audit table (a,b,c) values(:new.a,:new.b,:new.c);
else
insert into audit table (a,b,c) values(:old.a,:old.b,:old.c);
end;
You could try:
declare
l_deleting_ind varchar2(1) := case when DELETING then 'Y' end;
begin
insert into audit_table (col1, col2)
values
( CASE WHEN l_deleting_ind = 'Y' THEN :OLD.col1 ELSE :NEW.col1 END
, CASE WHEN l_deleting_ind = 'Y' THEN :OLD.col2 ELSE :NEW.col2 END
);
end;
I found that the variable was required - you can't access DELETING directly in the insert statement.
WOW, You want to have only ONE insert in your trigger to avoid what?
"I have a single insert statement INSERT INTO HIST ( EMP_ID, NAME ) VALUES (:NEW.EMP_ID , :NEW.NAME ) ; when deleting though, I want to use :OLD , not not have a seperate insert statement for that. "
It's a wide table. SO? It's not like there no REPLACE in text editors, you're not going to write the Insert again, just copy, paste, select, replace :NEW with :OLD.
Tony does have a solution but I seriously doubt that performs better than 2 inserts would perform.
What's the big deal?
EDIT
the main thing I'm trying to avoid is having to managed 2 inserts when the table changes. – Matthew Watson
I battle this attitude all the time. Those who write Java or C++ or .Net have a built-in RBO... Do this, this is good. Don't do that, that's bad. They write code according to these rules and that's fine. The problem is when these rules are applied to databases. Databases don't behave the same way code does.
In the code world, having essentially the same code in two "places" is bad. We avoid it. One would abstract that code to a function and call it from the two places and thus avoid maintaining it twice, and possibly missing one, etc. We all know the drill.
In this case, while it's true that in the end I recommend two inserts, they are separated by an ELSE. You won't change one and forget the other one. IT'S Right There. It's not in a different package, or in some compiled code, or even somewhere else in the same trigger. They're right beside each other, there's an ELSE and the Insert is repeated with :NEW, instead of :OLD. Why am I so crazed about this? Does it really make a difference here? I know two inserts won't be worse than other ideas, and it could be better.
The real reason is being prepared for the times when it does matter. If you're avoiding two inserts just for the sake of maintenance, you're going to miss the times when this makes a HUGE difference.
INSERT INTO log
SELECT * FROM myTable
WHERE flag = 'TRUE'
ELSE -- column omitted for clarity
INSERT INTO log
SELECT * FROM myTable
WHERE flag = 'FALSE'
Some, including Matthew, would say this is bad code, there are two inserts. I could easily replace 'TRUE' and 'FALSE' with a bind variable and flip it at will. And that's what most people would do. But if True is .1% of the values and 99.9% is False, you want two inserts, because you want two execution plans. One is better off with an index and the other an FTS. So, yes, you do have two Inserts to maintain. That's not always bad and in this case it's good and desirable.
You can use a compound trigger and programmatically check if it us I/U/D.
Compound Triggers
Why don't you use Oracle's built in standard or fine-grained auditing?
Use a compound trigger, as others have suggested. Save the old or new values, as appropriate, to variables, and use the variables in your insert statement:
declare
v_col1 table_name.col1%type;
v_col2 table_name.col2%type;
begin
if deleting then
v_col1 := :old.col1;
v_col2 := :old.col2;
else
v_col1 := :new.col1;
v_col2 := :new.col2;
end if;
insert into audit_table(col1, col2)
values(v_col1, v_col2);
end;

Resources