I have a subflow in esql (IBM Websphere Message Broker) where I need to achieve something similar to select distinct functionality.
Some background: I have a table in an Oracle database group_errcode_ref. This table is pretty much a fixed link/mapping of ERROR_CODE and ID. ERROR_CODE is unique, but ID can be duplicated. For example error code 4000 and 4001 can both be linked to ID 1.
In my esql subflow, I have an array of error codes that varies based on the current data coming into the flow.
So what I need to do is I need to take the input error code array, and select the ID for all the error codes in the array from my table group_errcode_ref
What I have now:
declare db rows;
set db.rows[] = (select d.ID from Database.group_errcode_ref as d where d.ERROR_CODE in (select D from errCodes.Code[] as D);
errCodes is the array of error codes from the input. row is an array of all IDs that correspond to the error codes.
This is fine, but I want to remove duplicates from the db.rows[] array.
I'm not certain the best way to do this in esql, but it does not support distinct. group by, or order by
If you are using the PASSTHRU statement, then all the functionality of your database manager is supported, so distinct as well.
The only thing you have to overcome is that you cannot directly mix database and messagetree queries in PASSTHRU, everything you pass to it goes directly to the database.
So your original solution would look something like this:
set db.rows[] = PASSTHRU 'select distinct d.ID from SCHEMA.group_errcode_ref as d where d.ERROR_CODE in ' || getErrorCodesFromInput(errCodes) TO Database.DSN1;
Here getErrorCodesFromInput is a function that returns character, which contains the error codes in your input, formatted correctly for the query, e.g. (ec1, ec2, ...)
My work around ended up not doing select distinct or sorting at all but another work around.
Basically I iterate through the entire array of ERROR_CODEs, then I query for the ID that corresponds to the error_code, then I select count(*) in a table I insert them to.
This works for my particular application only because I insert the ID/Issue pairs.
Basically it looks like:
for x as errs.Error[] do
declare db row;
set db.rows[] = passthru('select ID from my_static_map_table where error_code = ?;' values(x.Code));
declare db2 row;
set db2.rows[] = passthru('select count(*) from my_table_2 where guid = ? and id = ?;' values(guid, db.ID));
if db2.COUNT == 0 then
-- Here I do an insert into my_table_2 with ID and a few other values
end if;
end for;
Not really a proper answer, but it works for my specific application. Basically loop through every error code and select one at a time, rather than sending in the entire array. Then doing an insert into another database while avoiding duplicates by another select to see if it's already been inserted.
I'll still wait a week to see if there's a better answer and accept that one.
UPDATE
I've changed my code to match Attila's solution - which is much better and what I was looking for originally
Only thing I will add is my function that formats the error codes - which is really simple:
create function FlattenErrorCodesArray(in err row) returns char begin
declare idx int 1;
declare ret char;
for x as errs.Error[] do
if idx = 1 then
set ret = '(' || cast(x.Code as char);
else
set ret = ret || ',' || cast(x.Code as char);
end if;
set idx = idx + 1;
end for;
set ret = ret || ')';
end;
Related
I am quite new to sql and have been trying to work on the following script to parameterize it.
This is my code:
select
dc.deviceid,
dc.kernel_time,
dc.crash_time,
dc.crash_process,
dps.start_time,
dps.end_time,
dps.start_kernel_time,
dps.end_kernel_time,
case
when dc.kernel_time between dps.start_kernel_time and dps.end_kernel_time then 1
when dc.crash_time between dps.start_time and dps.end_time then 2
else 3
end as flag,
ROW_NUMBER () over (partition by dc.deviceid, dc.kernel_time,
dc.crash_time, dc.crash_process order by flag) row_num
from dummy.dummy_crashes dc
left outer join (select *
from dummy.dummy_power) as dps
on dc.deviceid = dps.deviceid
and ((dc.kernel_time between (dps.start_kernel_time + 10000) and (dps.end_kernel_time + 10000)) or (dc.crash_time between dps.start_time and dps.end_time))
order by dc.crash_time;
I need to test this script by changing the start_kernel_time and end_kernel_time with a certain int parameter value (in this example shown: 10000) every time. So, instead of modifying it in the code, I would like to create a function with the int parameter of choice and run this script. Would that be possible?
I am really clueless as to how to achieve that.
My ideal idea would be something like this:
get_crashes(10000); <-- get records with adding int parameter (in start_kernel_time and end_kernel_time) as 10000
get_crashes(30000); <-- get records with adding int parameter as 30000
get_crashes(80000); <-- get records with adding int parameter as 80000
I am really trying to understand how I could achieve this?
I can't write a comment because i don't have 50rep, but here is my answer:
You can create a temp table with values that you want to pass and call cursor with simple query like:
SELECT [value] FROM *temptable*
After that, inside cursor just write script with single value from above temp table
UPDATE
DECLARE
cur CURSOR FOR select col1 from tempTable;
test_cur RECORD;
BEGIN
open cur;
LOOP
fetch cur into test_cur;
exit when test_cur = null;
if test_cur.col1 IS NOT NULL then
return next test_cur.col1;
end if;
END LOOP;
close cur;
END;
One note - I never write PostgreSQL, just have knowledge about SQL and find code on internet, so maybe you need to check documentation.
I'm struggling with a variable argument stored procedure that has to perform a SELECT on a table using every argument passed to it in its WHERE clause.
Basically I have N account numbers as parameter and I want to return a table with the result of selecting three fields for each account number.
This is what I've done so far:
function sp_get_minutes_expiration_default(retval IN OUT char, gc IN OUT GenericCursor,
p_account_num IN CLIENT_ACCOUNTS.ACCOUNT_NUM%TYPE) return number
is
r_cod integer := 0;
begin
open gc for select account_num, concept_def, minutes_expiration_def from CLIENT_ACCOUNTS
where p_account_num = account_num; -- MAYBE A FOR LOOP HERE?
return r_cod;
exception
-- EXCEPTION HANDLING
end sp_get_minutes_expiration_default;
My brute force solution would be to maybe loop over a list of account numbers, select and maybe do a UNION or append to the result table?
If you cast your input parameter as a table, then you can join it to CLIENT_ACCOUNTS
select account_num, concept_def, minutes_expiration_def
from CLIENT_ACCOUNTS ca, table(p_account_num) a
where a.account_num = ca.account_num
But I would recommend you select the output into another collection that is the output of the function (or procedure). I would urge you to not use reference cursors.
ADDENDUM 1
A more complete example follows:
create or replace type id_type_array as table of number;
/
declare
ti id_type_array := id_type_array();
n number;
begin
ti.extend();
ti(1) := 42;
select column_value into n from table(ti) where rownum = 1;
end;
/
In your code, you would need to use the framework's API to:
create an instance of the collection (of type id_type_array)
populate the collection with the list of numbers
Execute the anonymous PL/SQL block, binding in the collection
But you should immediately see that you don't have to put the query into an anonymous PL/SQL block to execute it (even though many experienced Oracle developers advocate it). You can execute the query just like any other query so long as you bind the correct parameter:
select account_num, concept_def, minutes_expiration_def
from CLIENT_ACCOUNTS ca, table(:p_account_num) a
where a.column_value = ca.account_num
I'm trying to generate new IDs for a large table. The ID's have to be consecutive and need to start at 0 (So I can't use sequence). What I come up with so far is the following function:
CREATE OR REPLACE FUNCTION genIds() RETURNS integer AS $$
DECLARE
edge RECORD;
i INTEGER := 0;
BEGIN
FOR edge IN SELECT * FROM network LOOP
UPDATE network SET id = i WHERE id = edge.id;
i := i + 1;
END LOOP;
RETURN i;
END;
$$ LANGUAGE plpgsql;
I would much rather like to not care about id = edge.id since I don't really care about the id's anyway. Is there a way to avoid having count(network) updates?
Cheers, Daniel
Is there a way to avoid having count(network) updates?
If your question is: can this done with a single statement instead of a loop, then yes this is possible:
This can be done without a loop in a single statement:
with numbered as (
select id as old_id,
row_number() over (order by id) as new_id
from network
)
update network nt
set id = nb.new_id - 1 // -1 to start at 0
from numbered nb
where nb.old_id = nt.id;
I am currently using PL/pgSQL and I have a line of codes something like this:
FOR r_var in
select distinct value as val, count(*) as count from table where value IN (input) GROUP BY value;
LOOP
--do something here
END LOOP;
Input is from the user i.e. $$'A123','B456','C789'$$
I don't know why it wasn't working but if I manually put values instead of using the input, it is working.
UPDATE:
I discover where's the problem.
code must be:
FOR r_var in
EXECUTE 'select distinct value as val, count(*) as count from table where value IN ('||input||') GROUP BY value;
LOOP
--do something here
END LOOP;
Change the function to accept a text array:
select * from my_function(array['A123','B456','C789']::text)
and use = any in the where clause:
where value = any (input)
In general loops are a bad solution.
As suggested by #a_horse in the comments it is possible to keep the function and just convert the string to array:
where value = any (string_to_array(input, ','))
Passing comma seperated input value in stored procedure can be done by -
1) Adding the values in temporary table and selecting the values in procedure
2) Calling a function to split the comma seperated values by creating table valued function to parse
you can refer to this site-
http://www.codeproject.com/Articles/6083/Passing-comma-delimited-parameter-to-stored-proced
Here's my setup:
Table 1 (table_with_info): Contains a list of varchars with substrings that I'd like to replace.
Table 2 (sub_info): Contains two columns: the substring in table_with_info that I'd like to replace and the string I'd like to replace it with.
What I'd like to do is replace all the substrings in table_with_info with their substitutions in sub_info.
This works to a point but the issue is that select replace(...) returns a new row for each one of the substituted words replaced and doesn't replace all of the ones in an individual row.
I'm explaining the best I can but I don't know if it's too clear. Here's the code an example of what's happening/what I'd like to happen.
Here's my code:
create table table_with_info
(
val varchar
);
insert into table_with_info values
('this this is test data');
create table sub_info
(
word_from varchar,
word_to varchar
);
insert into sub_info values
('this','replace1')
, ('test', 'replace2');
update table_with_info set val = (select replace("val", "word_from", "word_to")
from "table_with_info", "sub_info"
the update() function doesn't work as select() returns two rows:
Row 1: replace1 replace1 is test data
Row 2: this this is replace2 data
so what I'd like for it for the select statement to return is:
Row 1: replace1 replace1 is test data
Any thoughts? I can't create UDFs on the system I'm running.
Your UPDATE statement is incorrect in multiple ways. Consult the manual before you try to run anything like this again. You introduce two cross joins that would make this statement extremely expensive, besides yielding nonsense.
To do this properly, you need to administer each UPDATE sequentially. In a single statement, one row version eliminates the other, while each replace would use the same original row version. You can use a DO statement for this or wrap it in a plpgsql function for instance:
DO
$do$
DECLARE
r sub_info;
BEGIN
FOR r IN
TABLE sub_info
-- SELECT * FROM sub_info ORDER BY ??? -- order is relevant
LOOP
UPDATE table_with_info
SET val = replace(val, r.word_from, r.word_to)
WHERE val LIKE ('%' || r.word_from || '%'); -- avoid empty updates
END LOOP;
END
$do$;
Be aware, that the order in which updates are applied can make a difference! If the first update creates a string where the second matches (but not otherwise) ..
So, order your columns in sub_info if that can be relevant.
Avoid empty updates. Without the additional WHERE clause, you would write many new row versions without changing anything. Expensive and useless.
double-quotes are optional for legal, lower-case names.
->SQLfiddle
Expanding on Erwin's answer, a do block with dynamic SQL can do the trick as well:
do $$
declare
rec record;
repl text;
begin
repl := 'val'; -- quote_ident() this if needed
for rec in select word_from, word_to from sub_info
loop
repl := 'replace(' || repl || ', '
|| quote_literal(rec.word_from) || ', '
|| quote_literal(rec.word_to) || ')';
end loop;
-- now do them all in a single query
execute 'update ' || 'table_with_info'::regclass || ' set val = ' || repl;
end;
$$ language plpgsql;
Optionally, build a like parameter in a similar way to avoid updating rows needlessly.