I'd like to be able to print some debug information from sql script / function. Of course, I can do this by
RAISE NOTICE 'hello!'
But I also need to print a whole table's data. This doesn't work:
RAISE NOTICE '%' (SELECT * FROM table1)
Is it possible and how?
The most straightforward way would be to iterate over the rows in a for loop and use RAISE NOTICE containing each column you're interested in interpolated in it.
i.e. something like:
FOR items IN SELECT * FROM table1 LOOP
RAISE NOTICE 'col1: %, col2: %', quote_ident(items.col1), quote_ident(items.col2);
END LOOP;
where items is declared as RECORD.
Since postgres 9.3 you can use to_json() to convert record into text suitable for notice,
RAISE NOTICE '%', to_json(record1);
RAISE NOTICE will print table data without alignment, so it will be hard to read. More flexible way is to use refcursor:
DECLARE
_temp_cur1 refcursor = 'unique_name_of_temp_cursor_1';
...
BEGIN
...
OPEN _temp_cur1 FOR
SELECT *
FROM table1;
...
END
Then run function in transaction:
BEGIN;
SELECT my_func();
FETCH ALL FROM "unique_name_of_temp_cursor_1"; --here is double-quotes ""!
ROLLBACK; --do not save any changes to DB during tests (or use COMMIT;)
Such refcursor will be available for reading during the same transaction. If you do not wrap your test with BEGIN and ROLLBACK (or COMMIT), PostgreSQL will not be able to find this one.
Related
I'm writing a procedure that loops over several remote Databases using dblink, I want to include statement timeout to prevent queries hanging too long. However if such timeout occurs, whole procedure fails with:
ERROR: canceling statement due to statement timeout
SQL state: 57014
Context: while executing query on dblink connection named xxx
I want to ignore it and continue the loop.
Normally such code allows to skip exception throwing notice only, but not with dblink query canceled.
do $$
declare
exceptionMsg text;
BEGIN
select * from foo;
exception when others then get stacked diagnostics exceptionMsg = message_text;
raise notice ' ******EXCEPTION*******
%
**************', exceptionMsg;
END;
$$
It's too long to include whole procedure here, but it loops over database and commits results after each database. Everything works fine, except handling these timeouts, part of the code looks like that:
for rec in (select dbc.db_name, dbc.con_string || ' options = ''-c statement_timeout='||_queryTimeout*1000||''' ' as con_string
from db_connections dbc
)
LOOP
PERFORM dblink_connect(rec.db_name, rec.con_string);
raise notice '% start', rec.db_name ;
BEGIN
insert into results_tbl (db_name, value, query_text)
select rec.db_name, value, _queryText
from dblink(rec.db_name, format($query$
select json_agg(x.*)::text from (%1$s)x -- it's like this to avoid declaring every column used in the query
$query$, _queryText
) ) r (value text);
exception when others then get stacked diagnostics exceptionMsg = message_text;
raise notice ' ******EXCEPTION*******
%
**************', exceptionMsg;
END;
PERFORM dblink_disconnect( rec.db_name );
COMMIT;
raise notice '% done', rec.db_name ;
END LOOP;
As documented,
The special condition name OTHERS matches every error type except QUERY_CANCELED and ASSERT_FAILURE.
So you need to capture QUERY_CANCELED explicitly.
Capturing OTHERS is bad style. Only capture the exceptions you expect.
I'm trying to write a stored procedure for cloned databases. I want to check the database_name variable for a specific string. Currently, this is what I've got:
IF ((CONTAINS(:database_name, 'STRING1'))=TRUE) THEN
RAISE clone_exception;
END IF;
IF ((CONTAINS(:database_name, 'STRING2'))=TRUE) THEN
RAISE clone_exception;
END IF;
IF ((CONTAINS(:database_name, 'STRING3'))=TRUE) THEN
RAISE clone_exception;
END IF;
I don't want to have to write 3 CONTAIN blocks I'd rather keep it compact and clean, is there a way I could get all 3 STRING checks in the same block of code? I've looked into using the IN operator but I can't find documentation that doesn't have examples using only a table query which doesn't really help me since this conditional is comparing only a variable and not a table column.
It is easy to achieve with LIKE ANY:
DECLARE
database_name TEXT := '...';
BEGIN
IF (:database_name ILIKE ANY ('%STRING1%','%STRING2%','%STRING3%') THEN
RAISE clone_exception:
END IF;
END;
I tried to create loop from table variable.
do
$$
DECLARE
modified IDType;
BEGIN
INSERT into modified (id)
SELECT i.id FROM item i WHERE i.id in ('55D6F516-7D8F-4DF3-A4E5-1E3F505837A1', 'FFE2A4D3-267C-465F-B4B4-C7BB2582F1BC');
for p in select id from modified
loop
raise notice (p.id);
end loop;
end;
$$ LANGUAGE plpgsql;
the problem is there has an error that says:
SQL Error [42P01]: ERROR: relation "modified" does not exist
Where: PL/pgSQL function inline_code_block line 5 at SQL statement
what I expected is the variable can be used to loop and runs normally.
this is the full query that you can try: http://sqlfiddle.com/#!17/9caba/3
I made the query in DBEAVER app, it will have some different error message.
I suggest you can experiment with it outside sqlfiddle.
There is no such thing as a "table variable" in Postgres. You can define a record that has a the type of a table but that is something completely different.
Even if idtype is the name of a table or a record type, it still holds a single value (in case of a record, it would be a single record with multiple fields)
Why would you expect a scalar variable to be usable in a loop? Are you looking for an
array?
To loop over an array use foreach
The SELECT also seems quite strange. You can assign a value to an array directly.
The parameter for raise notice needs to be a string. If you want to "print" a variable, you need to use a placeholder in the string.
So maybe you are looking for:
do
$$
DECLARE
modified idtype[];
id idtype;
BEGIN
modified := array['55D6F516-7D8F-4DF3-A4E5-1E3F505837A1', 'FFE2A4D3-267C-465F-B4B4-C7BB2582F1BC'];
foreach id in array modified
loop
raise notice '%', id;
end loop;
end;
$$ LANGUAGE plpgsql;
The Problem: I have many delete lines in a PostgreSQL script where I am deleting data related to the same item in the database. Example:
delete from <table> where <column>=180;
delete from <anothertable> where <column>=180;
...
delete from <table> where <column>=180;
commit work;
There are about 15 delete statements deleting data that references <column>=180.
I have tried to replace the 180 with a variable so that I only have to change the variable, instead of all the lines in the code (like any good programmer would do). I can't seem to figure out how to do it, and it's not working.
NOTE: I am very much a SQL novice (I rarely use it), so I know there's probably a better way to do this, but please enlighten me on how I can fix this problem.
I have used these answers to try and fix it with no luck: first second third. I've even gone to the official PostgreSQL documentation, with no luck.
This is what I'm trying (these lines are just for testing and not in the actual script):
DO $$
DECLARE
variable INTEGER:
BEGIN
variable := 101;
SELECT * FROM <table> WHERE <column> = variable;
END $$;
I've also tried just delcaring it like this:
DECLARE variable INTEGER := 101;
Whenever I run the script after replacing one of the numbers with a variable this is the error I get:
SQL Error [42601]: ERROR: query has no destination for result data
Hint: If you want to discard the results of a SELECT, use PERFORM instead.
Where: PL/pgSQL function inline_code_block line 6 at SQL statement
Can someone tell me where I'm going wrong? It would be nice to only have to change the number in the variable, instead of in all the lines in the script, and I just can't seem to figure it out.
As #Vao Tsun said, you must define a destination to your SELECT statement. Use PERFORM otherwise:
--Test data
CREATE TEMP TABLE my_table (id, description) AS
VALUES (1, 'test 1'), (2, 'test 2'), (101, 'test 101');
--Example procedure
CREATE OR REPLACE FUNCTION my_procedure(my_arg my_table) RETURNS VOID AS $$
BEGIN
RAISE INFO 'Procedure: %,%', my_arg.id, my_arg.description;
END
$$ LANGUAGE plpgsql;
DO $$
DECLARE
variable INTEGER;
my_record my_table%rowtype;
BEGIN
variable := 101;
--Use your SELECT inside a LOOP to work with result
FOR my_record IN SELECT * FROM my_table WHERE id = variable LOOP
RAISE INFO 'Loop: %,%', my_record.id, my_record.description;
END LOOP;
--Use SELECT to populate a variable.
--In this case you MUST define a destination to your result data
SELECT * INTO STRICT my_record FROM my_table WHERE id = variable;
RAISE INFO 'Select: %,%', my_record.id, my_record.description;
--Use PERFORM instead of SELECT if you want to discard result data
--It's often used to call a procedure
PERFORM my_procedure(t) FROM my_table AS t WHERE id = variable;
END $$;
--DROP FUNCTION my_procedure(my_table);
I have 2 procedures inside a package. I am calling one procedure to get a comma separated list of user ids.
I am storing the result in a VARCHAR variable. Now when I am using this comma separated list to put inside an IN clause in it is throwing "ORA-01722:INVALID NUMBER" exception.
This is how my variable looks like
l_userIds VARCHAR2(4000) := null;
This is where i am assigning the value
l_userIds := getUserIds(deptId); -- this returns a comma separated list
And my second query is like -
select * from users_Table where user_id in (l_userIds);
If I run this query I get INVALID NUMBER error.
Can someone help here.
Do you really need to return a comma-separated list? It would generally be much better to declare a collection type
CREATE TYPE num_table
AS TABLE OF NUMBER;
Declare a function that returns an instance of this collection
CREATE OR REPLACE FUNCTION get_nums
RETURN num_table
IS
l_nums num_table := num_table();
BEGIN
for i in 1 .. 10
loop
l_nums.extend;
l_nums(i) := i*2;
end loop;
END;
and then use that collection in your query
SELECT *
FROM users_table
WHERE user_id IN (SELECT * FROM TABLE( l_nums ));
It is possible to use dynamic SQL as well (which #Sebas demonstrates). The downside to that, however, is that every call to the procedure will generate a new SQL statement that needs to be parsed again before it is executed. It also puts pressure on the library cache which can cause Oracle to purge lots of other reusable SQL statements which can create lots of other performance problems.
You can search the list using like instead of in:
select *
from users_Table
where ','||l_userIds||',' like '%,'||cast(user_id as varchar2(255))||',%';
This has the virtue of simplicity (no additional functions or dynamic SQL). However, it does preclude the use of indexes on user_id. For a smallish table this shouldn't be a problem.
The problem is that oracle does not interprete the VARCHAR2 string you're passing as a sequence of numbers, it is just a string.
A solution is to make the whole query a string (VARCHAR2) and then execute it so the engine knows he has to translate the content:
DECLARE
TYPE T_UT IS TABLE OF users_Table%ROWTYPE;
aVar T_UT;
BEGIN
EXECUTE IMMEDIATE 'select * from users_Table where user_id in (' || l_userIds || ')' INTO aVar;
...
END;
A more complex but also elegant solution would be to split the string into a table TYPE and use it casted directly into the query. See what Tom thinks about it.
DO NOT USE THIS SOLUTION!
Firstly, I wanted to delete it, but I think, it might be informative for someone to see such a bad solution. Using dynamic SQL like this causes multiple execution plans creation - 1 execution plan per 1 set of data in IN clause, because there is no binding used and for the DB, every query is a different one (SGA gets filled with lots of very similar execution plans, every time the query is run with a different parameter, more memory is needlessly used in SGA).
Wanted to write another answer using Dynamic SQL more properly (with binding variables), but Justin Cave's answer is the best, anyway.
You might also wanna try REF CURSOR (haven't tried that exact code myself, might need some little tweaks):
DECLARE
deptId NUMBER := 2;
l_userIds VARCHAR2(2000) := getUserIds(deptId);
TYPE t_my_ref_cursor IS REF CURSOR;
c_cursor t_my_ref_cursor;
l_row users_Table%ROWTYPE;
l_query VARCHAR2(5000);
BEGIN
l_query := 'SELECT * FROM users_Table WHERE user_id IN ('|| l_userIds ||')';
OPEN c_cursor FOR l_query;
FETCH c_cursor INTO l_row;
WHILE c_cursor%FOUND
LOOP
-- do something with your row
FETCH c_cursor INTO l_row;
END LOOP;
END;
/