Firebird SQL access parameter by name in stored procedure - sql

Is there a way to access an output parameter by name in a Firebird (v1.5) stored procedure? The result fields should be the month of a specified year and it would be easier if I don't have to copy my code 12 times.
Here is an example (the final code is more complex than this):
SET TERM !!;
CREATE PROCEDURE MY_PROC (
I_PARAM INTEGER)
RETURNS (
O_PARAM_1 INTEGER, O_PARAM_2 INTEGER, O_PARAM_3 INTEGER, O_PARAM_4 INTEGER,
O_PARAM_5 INTEGER, O_PARAM_6 INTEGER, O_PARAM_7 INTEGER, O_PARAM_8 INTEGER,
O_PARAM_9 INTEGER, O_PARAM_10 INTEGER, O_PARAM_11 INTEGER, O_PARAM_12 INTEGER)
AS
declare MONTH_ID INTEGER;
BEGIN
MONTH_ID = 1;
while (MONTH_ID <= 12) do
begin
select
count(*)
from
MY_TABLE M
where
M.MONTH_ID = I
into
>>>> :O_PARAM_???; -- need access by name
MONTH_ID = MONTH_ID + 1;
end
suspend;
END!!
SET TERM ;!!

This isn't possible. Firebird needs to know at compile time which variable it is going to use. Dynamically referencing the output parameter is not possible.
As an intermediate solution, you could assign the result to a temporary variable, and then only do the assignment to the right variable in a long chain of if-else. It is still code duplication but less than repeating the query over and over.
You could also execute a single query that produces the results for all months at once. This would lead to a slight code bloat, but probably be more efficient:
select
(SELECT count(*) from MY_TABLE where MONTH_ID = 1),
(SELECT count(*) from MY_TABLE where MONTH_ID = 2),
(SELECT count(*) from MY_TABLE where MONTH_ID = 3),
-- ...and so on
into :O_PARAM_1, :O_PARAM_2, :O_PARAM_3;
If you used a more recent Firebird version like Firebird 2.5, you could use a CTE (although for this simple query it doesn't simplify it a lot):
WITH counts AS (SELECT MONTH_ID, count(*) AS MONTH_COUNT from MY_TABLE M GROUP BY MONTH_ID)
select
(SELECT MONTH_COUNT from counts where MONTH_ID = 1),
(SELECT MONTH_COUNT from counts where MONTH_ID = 2),
(SELECT MONTH_COUNT from counts where MONTH_ID = 3),
-- ...and so on
into :O_PARAM_1, :O_PARAM_2, :O_PARAM_3;
A totally different solution would be to abandon the idea of an executable stored procedure (although with the presence of SUSPEND in your current solution it is actually selectable with always one row), and instead use a selectable procedure that returns the result as rows. If that is a solution depends on your actual data needs.
CREATE PROCEDURE MY_PROC (
I_PARAM INTEGER)
RETURNS (
MONTH_ID INTEGER, MONTH_COUNT INTEGER)
AS
BEGIN
FOR
select MONTH_ID, count(*)
from MY_TABLE M
GROUP BY MONTH_ID
into :MONTH_ID, :MONTH_COUNT
BEGIN
SUSPEND;
END
END

Related

Redshift comma separated string to column [duplicate]

I am wondering how to convert comma-delimited values into rows in Redshift. I am afraid that my own solution isn't optimal. Please advise. I have table with one of the columns with coma-separated values. For example:
I have:
user_id|user_name|user_action
-----------------------------
1 | Shone | start,stop,cancell...
I would like to see
user_id|user_name|parsed_action
-------------------------------
1 | Shone | start
1 | Shone | stop
1 | Shone | cancell
....
A slight improvement over the existing answer is to use a second "numbers" table that enumerates all of the possible list lengths and then use a cross join to make the query more compact.
Redshift does not have a straightforward method for creating a numbers table that I am aware of, but we can use a bit of a hack from https://www.periscope.io/blog/generate-series-in-redshift-and-mysql.html to create one using row numbers.
Specifically, if we assume the number of rows in cmd_logs is larger than the maximum number of commas in the user_action column, we can create a numbers table by counting rows. To start, let's assume there are at most 99 commas in the user_action column:
select
(row_number() over (order by true))::int as n
into numbers
from cmd_logs
limit 100;
If we want to get fancy, we can compute the number of commas from the cmd_logs table to create a more precise set of rows in numbers:
select
n::int
into numbers
from
(select
row_number() over (order by true) as n
from cmd_logs)
cross join
(select
max(regexp_count(user_action, '[,]')) as max_num
from cmd_logs)
where
n <= max_num + 1;
Once there is a numbers table, we can do:
select
user_id,
user_name,
split_part(user_action,',',n) as parsed_action
from
cmd_logs
cross join
numbers
where
split_part(user_action,',',n) is not null
and split_part(user_action,',',n) != '';
Another idea is to transform your CSV string into JSON first, followed by JSON extract, along the following lines:
... '["' || replace( user_action, '.', '", "' ) || '"]' AS replaced
... JSON_EXTRACT_ARRAY_ELEMENT_TEXT(replaced, numbers.i) AS parsed_action
Where "numbers" is the table from the first answer. The advantage of this approach is the ability to use built-in JSON functionality.
If you know that there are not many actions in your user_action column, you use recursive sub-querying with union all and therefore avoiding the aux numbers table.
But it requires you to know the number of actions for each user, either adjust initial table or make a view or a temporary table for it.
Data preparation
Assuming you have something like this as a table:
create temporary table actions
(
user_id varchar,
user_name varchar,
user_action varchar
);
I'll insert some values in it:
insert into actions
values (1, 'Shone', 'start,stop,cancel'),
(2, 'Gregory', 'find,diagnose,taunt'),
(3, 'Robot', 'kill,destroy');
Here's an additional table with temporary count
create temporary table actions_with_counts
(
id varchar,
name varchar,
num_actions integer,
actions varchar
);
insert into actions_with_counts (
select user_id,
user_name,
regexp_count(user_action, ',') + 1 as num_actions,
user_action
from actions
);
This would be our "input table" and it looks just as you expected
select * from actions_with_counts;
id
name
num_actions
actions
2
Gregory
3
find,diagnose,taunt
3
Robot
2
kill,destroy
1
Shone
3
start,stop,cancel
Again, you can adjust initial table and therefore skipping adding counts as a separate table.
Sub-query to flatten the actions
Here's the unnesting query:
with recursive tmp (user_id, user_name, idx, user_action) as
(
select id,
name,
1 as idx,
split_part(actions, ',', 1) as user_action
from actions_with_counts
union all
select user_id,
user_name,
idx + 1 as idx,
split_part(actions, ',', idx + 1)
from actions_with_counts
join tmp on actions_with_counts.id = tmp.user_id
where idx < num_actions
)
select user_id, user_name, user_action as parsed_action
from tmp
order by user_id;
This will create a new row for each action, and the output would look like this:
user_id
user_name
parsed_action
1
Shone
start
1
Shone
stop
1
Shone
cancel
2
Gregory
find
2
Gregory
diagnose
2
Gregory
taunt
3
Robot
kill
3
Robot
destroy
Here are two ways to achieve this.
In my example, I'm assuming that I am accepting a comma separated list of values. My values look like schema.table.column.
The first involves using a recursive CTE.
drop table if exists #dep_tbl;
create table #dep_tbl as
select 'schema.foobar.insert_ts,schema.baz.load_ts' as dep
;
with recursive tmp (level, dep_split, to_split) as
(
select 1 as level
, split_part(dep, ',', 1) as dep_split
, regexp_count(dep, ',') as to_split
from #dep_tbl
union all
select tmp.level + 1 as level
, split_part(a.dep, ',', tmp.level + 1) as dep_split_u
, tmp.to_split
from #dep_tbl a
inner join tmp on tmp.dep_split is not null
and tmp.level <= tmp.to_split
)
select dep_split from tmp;
the above yields:
|dep_split|
|schema.foobar.insert_ts|
|schema.baz.load_ts|
The second involves a stored procedure.
CREATE OR REPLACE PROCEDURE so_test(dependencies_csv varchar(max))
LANGUAGE plpgsql
AS $$
DECLARE
dependencies_csv_vals varchar(max);
BEGIN
drop table if exists #dep_holder;
create table #dep_holder
(
avoid varchar(60000)
);
IF dependencies_csv is not null THEN
dependencies_csv_vals:='('||replace(quote_literal(regexp_replace(dependencies_csv,'\\s','')),',', '\'),(\'') ||')';
execute 'insert into #dep_holder values '||dependencies_csv_vals||';';
END IF;
END;
$$
;
call so_test('schema.foobar.insert_ts,schema.baz.load_ts')
select
*
from
#dep_holder;
the above yields:
|dep_split|
|schema.foobar.insert_ts|
|schema.baz.load_ts|
in conclusion
If you only care about one single column in your input (the X delimited values), then I think the stored procedure is easier/faster.
However, if you have other columns you care about and want to keep those columns along with your comma separated value column now transformed to rows, OR, if you want to know the argument (original list of delimited values), I think the stored procedure is the way to go. In that case, you can just add those other columns to your columns selected in the recursive query.
You can get the expected result with the following query. I'm using "UNION ALL" to convert a column to row.
select user_id, user_name, split_part(user_action,',',1) as parsed_action from cmd_logs
union all
select user_id, user_name, split_part(user_action,',',2) as parsed_action from cmd_logs
union all
select user_id, user_name, split_part(user_action,',',3) as parsed_action from cmd_logs
Here's my equally-terrible answer.
I have a users table, and then an events table with a column that is just a comma-delimited string of users at said event. eg
event_id | user_ids
1 | 5,18,25,99,105
In this case, I used the LIKE and wildcard functions to build a new table that represents each event-user edge.
SELECT e.event_id, u.id as user_id
FROM events e
LEFT JOIN users u ON e.user_ids like '%' || u.id || '%'
It's not pretty, but I throw it in a WITH clause so that I don't have to run it more than once per query. I'll likely just build an ETL to create that table every night anyway.
Also, this only works if you have a second table that does have one row per unique possibility. If not, you could do LISTAGG to get a single cell with all your values, export that to a CSV and reupload that as a table to help.
Like I said: a terrible, no-good solution.
Late to the party but I got something working (albeit very slow though)
with nums as (select n::int n
from
(select
row_number() over (order by true) as n
from table_with_enough_rows_to_cover_range)
cross join
(select
max(json_array_length(json_column)) as max_num
from table_with_json_column )
where
n <= max_num + 1)
select *, json_extract_array_element_text(json_column,nums.n-1) parsed_json
from nums, table_with_json_column
where json_extract_array_element_text(json_column,nums.n-1) != ''
and nums.n <= json_array_length(json_column)
Thanks to answer by Bob Baxley for inspiration
Just improvement for the answer above https://stackoverflow.com/a/31998832/1265306
Is generating numbers table using the following SQL
https://discourse.looker.com/t/generating-a-numbers-table-in-mysql-and-redshift/482
SELECT
p0.n
+ p1.n*2
+ p2.n * POWER(2,2)
+ p3.n * POWER(2,3)
+ p4.n * POWER(2,4)
+ p5.n * POWER(2,5)
+ p6.n * POWER(2,6)
+ p7.n * POWER(2,7)
as number
INTO numbers
FROM
(SELECT 0 as n UNION SELECT 1) p0,
(SELECT 0 as n UNION SELECT 1) p1,
(SELECT 0 as n UNION SELECT 1) p2,
(SELECT 0 as n UNION SELECT 1) p3,
(SELECT 0 as n UNION SELECT 1) p4,
(SELECT 0 as n UNION SELECT 1) p5,
(SELECT 0 as n UNION SELECT 1) p6,
(SELECT 0 as n UNION SELECT 1) p7
ORDER BY 1
LIMIT 100
"ORDER BY" is there only in case you want paste it without the INTO clause and see the results
create a stored procedure that will parse string dynamically and populatetemp table, select from temp table.
here is the magic code:-
CREATE OR REPLACE PROCEDURE public.sp_string_split( "string" character varying )
AS $$
DECLARE
cnt INTEGER := 1;
no_of_parts INTEGER := (select REGEXP_COUNT ( string , ',' ));
sql VARCHAR(MAX) := '';
item character varying := '';
BEGIN
-- Create table
sql := 'CREATE TEMPORARY TABLE IF NOT EXISTS split_table (part VARCHAR(255)) ';
RAISE NOTICE 'executing sql %', sql ;
EXECUTE sql;
<<simple_loop_exit_continue>>
LOOP
item = (select split_part("string",',',cnt));
RAISE NOTICE 'item %', item ;
sql := 'INSERT INTO split_table SELECT '''||item||''' ';
EXECUTE sql;
cnt = cnt + 1;
EXIT simple_loop_exit_continue WHEN (cnt >= no_of_parts + 2);
END LOOP;
END ;
$$ LANGUAGE plpgsql;
Usage example:-
call public.sp_string_split('john,smith,jones');
select *
from split_table
You can try copy command to copy your file into redshift tables
copy table_name from 's3://mybucket/myfolder/my.csv' CREDENTIALS 'aws_access_key_id=my_aws_acc_key;aws_secret_access_key=my_aws_sec_key' delimiter ','
You can use delimiter ',' option.
For more details of copy command options you can visit this page
http://docs.aws.amazon.com/redshift/latest/dg/r_COPY.html

connect by level does not adjust with the change of the field in each row

Let me start my question by setting up my scenario.
I have a test2 table which contains only 2 fields: productid and productlife, I would like to explicitly list all the years along with the products
for example,
With product A, I would like to list
Y15|Y16|Y17|Y18|Y19, A
and for product B, using the same rule, I should get
Y18|Y19, B
My sql does not produce the result I am looking for:
SELECT
(
SELECT listagg("Year",'|') within GROUP (
ORDER BY "Year") "Prefix"
FROM
(
SELECT 'Y'
||(TO_CHAR(SYSDATE,'yy')-LEVEL) "Year"
FROM dual
CONNECT BY level<=r.productlife
)
) "Prefix", productid
FROM TEST2 r
How should it be corrected? I would think the field productlife in each record will control the level in the statement but it does not seem to do so..
Please advise.
Below is the script to create my example for your convenience.
CREATE TABLE "TEST2"
( productid VARCHAR2(20 BYTE),
productlife NUMBER
) ;
Insert into TEST2 (productid,productlife) values ('A',5);
Insert into TEST2 (productid,productlife) values ('B',2);
Thanks!
If it doesn't absolutely have to use connect by, another approach might be to write it explicitly as a function:
with
function list_years(n number) return varchar2 as
end_year date := trunc(sysdate,'YYYY');
start_year date := add_months(end_year, (n*-12));
y date;
years_list varchar(200);
begin
for i in reverse 1..n loop
y := add_months(sysdate, -12 * i);
years_list := years_list || to_char(y,'"Y"YY"|"');
end loop;
return rtrim(years_list,'|');
end list_years;
select productid
, productlife
, list_years(productlife) as prefix
from test2
/
DBFiddle
This query will do it, but I feel like there must be a better way:
SELECT t.productid, s.yr
FROM test2 t
INNER JOIN (SELECT ROWNUM RN, TO_CHAR(SYSDATE, 'YY')-ROWNUM+1 YR
FROM dual d
CONNECT BY level <= (SELECT max(productlife)
FROM test2)) s ON t.productlife >= s.rn
ORDER BY t.productid, s.yr;
Here is a SQLFiddle for you: SQLFiddle

How to use a global temporary table in a function?

so I have a function that takes in 3 IN parameters (hour, date, code)
and returns a number.
My function code is below:
create or replace FUNCTION get_max_value (rhr NUMBER,
rdate VARCHAR2,
rcode VARCHAR2)
RETURN NUMBER
IS
rvalue_day NUMBER;
--
BEGIN
SELECT MAX (v.value)
INTO rvalue_day
FROM table v
JOIN rel_table_1 sv ON (v.value_id = sv.value_id)
JOIN look_up_table ff ON (sv.form_field_id = ff.form_field_id)
WHERE v.date = rdate
AND v.code = rcode
AND v.hr_num = rhr
AND (v.code = 'PASS' OR v.code IS NULL);
RETURN rvalue_day;
END;
Because of performance issues, I am trying to use a global temporary table that grabs the values (v.value) and the primary_identifier associated to it (value_Id). My code is below:
with table_c as
(
select value, value_id
from table where date = rdate
AND code = rcode
AND hr_num = rhr
)
select MAX (v.value)
FROM table_c v
JOIN rel_table_1 sv ON (v.value_id = sv.value_id)
JOIN look_up_table ON (sv.form_field_id = ff.form_field_id)
WHERE ff.code_desc = rcode;
Is there a way I can incorporate the above method into a function so that it can accept values for multiple parameters? I currently have a stored proc that is trying to derive a value by inserting 3 values into those 3 parameters...
Thanks in advance!
This is not an answer to your question "how can I optimize this function".
I'm going to show you that the very idea of using a function can be a reason of your performance problems - and I'am guessing that this is the case in your case.
Please look at the below very simple case:
create table ttest as
select * from all_objects
fetch first 10000 rows only;
create index ttest_ix on ttest(object_type);
create or replace function get_max(p_object_type varchar)
return number
is
ret_val number;
begin
select max(object_id) into ret_val
from ttest
where object_type = p_object_type;
return ret_val;
end;
/
create or replace view get_max_view as
select object_type, max(object_id) as max_id
from ttest
group by object_type
;
The view get_max_view is an equivalent of the function get_max, you can use both for example in this way in queries:
select object_id, object_type, get_max(object_type) as max_id
from ttest;
select object_id, object_type,
(select max_id from get_max_view x where x.object_type = t.object_type) as max_id
from ttest t;
And now please examine the case where both the above queries are run against 10000 records - to do so I nest both queries as subqueries, and caclulate a sum of all their results:
set timings on;
select sum(max_id)
from (
select object_id, object_type, get_max(object_type) as max_id
from ttest
);
SUM(MAX_ID)
-----------
214087478
Elapsed: 00:00:11.764
select sum(max_id)
from (
select object_id, object_type,
(select max_id from get_max_view x where x.object_type = t.object_type) as max_id
from ttest t
);
SUM(MAX_ID)
-----------
214087478
Elapsed: 00:00:00.011
Please examine the times - 11.76 seconds vs. 11 miliseconds.
This is over 1000 times faster - 100000% faster !!!
This is why I suggested you in the comment to replace this functin by the view, because this is the most probably cause of your performance issues, and trying to optimize this function is probably the wrong way.

SQLSTATE[42601]: Syntax error: 7 ERROR: subquery must return only one column Using Function

I get the error "subquery must return only one column" but i tried to use differents away to return the first record when i'm selecting the curProd.
I'm using this function, but i get the the errror as far as i know in:
curProd := (
SELECT "KeysForSale".*
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
The all function is:
CREATE FUNCTION "paymentRun"(buyer_id integer, payment_date DATE, payMethod paymentMethod, paid_amount double precision, payDetails text) RETURNS VOID AS
$$
DECLARE
row_STab "SearchTable"%rowtype;
curProd "KeysForSale"%rowtype;
totalPrice double precision;
returnedPID integer;
BEGIN
--For each entry in the search table
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
--We retrieve the associated product info, together with an available key
curProd := (
SELECT "KeysForSale".*
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
--Either there is no such product, or no keys for it
IF curProd IS NULL THEN
RAISE EXCEPTION 'Product is not available for purchase.';
END IF;
--Product's seller is the buyer - we can't let that pass
IF curProd.user_id = buyer_id THEN
RAISE EXCEPTION 'A Seller cannot purchase their own product.';
END IF;
--Fill in the rest of the data to prepare the purchase
UPDATE "SearchTable"
SET "SearchTable".price = (
CASE curProd.discounted_price IS NOT NULL -- if there was a discounted price, use it
WHEN TRUE THEN curProd.discounted_price
ELSE curProd.price
END
), "SearchTable".sk_id = curProd.sk_id
WHERE "SearchTable".product_id = curProd.product_id;
END LOOP;
--Get total cost
totalPrice := (
SELECT SUM("SearchTable".price)
FROM "SearchTable"
);
--The given price does not match the actual cost?
IF totalPrice <> paid_amount THEN
RAISE EXCEPTION 'Payment does not match cost!';
END IF;
--Create a purchase while keeping it's ID for register
INSERT INTO "Purchases" (purchase_id, final_price, user_id, paid_date, payment_method, details)
VALUES (DEFAULT, totalPrice, buyer_id, payment_date, payMethod, payDetails)
RETURNING purchase_id INTO returnedPID;
--For each product we wish to purchase
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
VALUES (row_STab.sk_id, returnedPID, row_STab.price);
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
WHERE row_STab.sk_id = "SerialKeys".sk_id;
END LOOP;
END
$$
LANGUAGE 'plpgsql' ;
Thank you in advance
Because the question has an incorrect answer, I'm providing an answer beyond the comment. The code that you want is:
curProd := (
SELECT "KeysForSale"
FROM "KeysForSale"
WHERE row_STab.product_id = "KeysForSale".product_id AND (("KeysForSale".begin_date < payment_date AND "KeysForSale".end_date > payment_date) OR ("KeysForSale".discounted_price IS NULL))
ORDER BY "KeysForSale".discounted_price ASC NULLS LAST
LIMIT 1
);
The difference is the lack of .*. Your version is returning a bunch of columns -- which is the error you are getting. You want to return a single record. The table name provides this.
I also think that parentheses will have the same effect:
SELECT ("KeysForSale".*)
For this case you should not to use syntax:
var := (SELECT ..).
Preferred should be SELECT INTO:
SELECT * INTO curProd FROM ...
The syntax SELECT tabname FROM tabname is PostgreSQL's proprietary, and although it is works well, better to not use, due unreadability for all without deeper PostgreSQL knowleadge.
Because PL/pgSQL is not case sensitive language, camel case is not advised (better to use snake case).
If it is possible, don't use ISAM style:
FOR _id IN
SELECT id FROM tab1
LOOP
SELECT * INTO r FROM tab2 WHERE tab2.id = _id
It is significantly slower than join (for more iterations)
FOR r IN
SELECT tab2.*
FROM tab1 JOIN tab2 ON tab1.id = tab2.id
LOOP
..
Cycles are bad for performance. This part is not really nice:
FOR row_STab IN
(
SELECT *
FROM "SearchTable"
)
LOOP
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
VALUES (row_STab.sk_id, returnedPID, row_STab.price);
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
WHERE row_STab.sk_id = "SerialKeys".sk_id;
END LOOP;
Possible solutions:
Use bulk commands instead:
INSERT INTO "PurchasedKeys"(sk_id, purchase_id, price)
SELECT sk_id, returnedPID, price
FROM "SearchTable"; -- using case sensitive identifiers is way to hell
UPDATE "SerialKeys"
SET "SerialKeys".user_id = buyer_id
FROM "SearchTable"
WHERE "SearchTable".sk_id = "SerialKeys".sk_id;
The less performance of ISAM style depends on number of iterations. For low iteration it is not important, for higher number it is death.

Query returning exact number of rows

I have a table that stores two foreign keys, implementing a n:m relationship.
One of them points to a person (subject), the other one to a specific item.
Now, the amount of items a person may have is specified in a different table and I need a query which would return the same number of rows as the number of items a person may have.
The rest of the records may be filled with NULL values or whatever else.
It has proven to be a pain to solve this problem from the application side, so I've decided to try a different approach.
Edit:
Example
CREATE TABLE subject_items
(
sub_item integer NOT NULL,
sal_subject integer NOT NULL,
CONSTRAINT pkey PRIMARY KEY (sub_item, sal_subject),
CONSTRAINT fk1 FOREIGN KEY (sal_subject)
REFERENCES subject (sub_id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk2 FOREIGN KEY (sub_item)
REFERENCES item (item_id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
)
I need a query/function which would return all subject items (subject may have 5 items)
but there are only 3 items assigned to the subject.
Return would be somewhat like:
sub_item | sal_subject
2 | 1
3 | 1
4 | 1
NULL | 1
NULL | 1
I am using postgresql-8.3
Consider this largely simplified version of your plpgsql function. Should work in PostgreSQL 8.3:
CREATE OR REPLACE FUNCTION x.fnk_abonemento_nariai(_prm_item integer)
RETURNS SETOF subject_items AS
$BODY$
DECLARE
_kiek integer := num_records -- get number at declaration time
FROM subjekto_abonementai WHERE num_id = _prm_item;
_counter integer;
BEGIN
RETURN QUERY -- get the records that actualy exist
SELECT sub_item, sal_subject
FROM sal_subject
WHERE sub_item = prm_item;
GET DIAGNOSTICS _counter = ROW_COUNT; -- save number of returned rows.
RETURN QUERY
SELECT NULL, NULL -- fill the rest with null values
FROM generate_series(_counter + 1, _kiek);
END;
$BODY$ LANGUAGE plpgsql VOLATILE STRICT;
Details about plpgsql in the manual (link to version 8.3).
Could work like this (pure SQL solution):
SELECT a.sal_subject
, b.sub_item
FROM (
SELECT generate_series(1, max_items) AS rn
, sal_subject
FROM subject
) a
LEFT JOIN (
SELECT row_number() OVER (PARTITION BY sal_subject ORDER BY sub_item) AS rn
, sal_subject
, sub_item
FROM subject_items
) b USING (sal_subject, rn)
ORDER BY sal_subject, rn
Generate the maximum rows per subject, let's call them theoretical items.
See the manual for generate_series().
Apply a row-number to existing items per subject.
Manual about window functions.
LEFT JOIN the existing items to the theoretical items per subject. Missing items are filled in with NULL.
In addition to the table you disclosed in the question, I assume a column that holds the maximum number of items in the subject table:
CREATE temp TABLE subject
( sal_subject integer, -- primary key of subject
max_items int); -- max. number of items
Query for PostgreSQL 8.3, substituting for the missing window function row_number():
SELECT a.sal_subject
, b.sub_item
FROM (
SELECT generate_series(1, max_items) AS rn
, sal_subject
FROM subject
) a
LEFT JOIN (
SELECT rn, sal_subject, arr[rn] AS sub_item
FROM (
SELECT generate_series(1, ct) rn, sal_subject, arr
FROM (
SELECT s.sal_subject
, s.ct
, ARRAY(
SELECT sub_item
FROM subject_items s0
WHERE s0.sal_subject = s.sal_subject
ORDER BY sub_item
) AS arr
FROM (
SELECT sal_subject
, count(*) AS ct
FROM subject_items
GROUP BY 1
) s
) x
) y
) b USING (sal_subject, rn)
ORDER BY sal_subject, rn
More about substituting row_number() in this article by Quassnoi.
I was able to come up to this simplistic solution:
First returning all the values i may select then looping returning null values while we have the right amount. Posting it here if someone would stumble on the same problem.
Still looking for easier/faster solutions if they exist.
CREATE OR REPLACE FUNCTION fnk_abonemento_nariai(prm_item integer)
RETURNS SETOF subject_items AS
$BODY$DECLARE _kiek integer;
DECLARE _rec subject_items;
DECLARE _counter integer;
BEGIN
/*get the number of records we need*/
SELECT INTO _kiek num_records
FROM subjekto_abonementai
WHERE num_id = prm_item;
/*get the records that actualy exist */
FOR _rec IN SELECT sub_item, sal_subject
FROM sal_subject
WHERE sub_item = prm_item LOOP
return
next _rec;
_counter := COALESCE(_counter, 0) + 1;
END LOOP;
/*fill the rest with null values*/
While _kiek > _counter loop
_rec.sub_item := NULL;
_rec.sal_subject := NULL;
Return next _rec;
_counter := COALESCE(_counter, 0) + 1;
end loop;
END;$BODY$
LANGUAGE plpgsql VOLATILE;