Query returning exact number of rows - sql

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;

Related

Oracle capture count of minus operation

I have 2 tables (TABLE_A & TABLE_B) where I'm using the MINUS command to see if there are differences in the tables.
In my example below you can see that TABLE_A has an additional row.
Is there a way to capture the numeric difference between the two tables, in this case 1 row.
If there is a difference >0 then display the value. Although my example is small it could contain many rows. Therefore I would only like to do the MINUS command once if possible. I'm also also amenable to alternative solutions and not tied to the MINUS command or if this can be done with SQL only that will work too.
Thanks in advance for your expertise and all who answer.
CREATE TABLE TABLE_A(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
nm VARCHAR(30)
);
/
CREATE TABLE TABLE_B(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
nm VARCHAR(30)
);
/
BEGIN
FOR I IN 1..4 LOOP
INSERT INTO TABLE_A (nm) VALUES('Name '||I);
end loop;
FOR I IN 1..3 LOOP
INSERT INTO TABLE_B (nm) VALUES('Name '||I);
end loop;
END;
-- MINUS operation
SELECT nm FROM TABLE_A
MINUS
SELECT nm FROM TABLE_B;
Output:
NM
Name 4
Pseudo code
Do minus command
If difference >0 then display rows
There are many ways for this, you can try 1 as below -
SELECT COUNT(*)
FROM (SELECT nm FROM TABLE_A
MINUS
SELECT nm FROM TABLE_B);
Another method maybe -
SELECT COUNT(*)
FROM TABLE_A A
WHERE NOT EXISTS (SELECT NULL
FROM TABLE_B B
WHERE A.nm = B.nm)
If I understood the question correctly you can do it using analytic count:
select *
from (
select v.*,count(*)over() cnt
from (
SELECT nm FROM TABLE_A
MINUS
SELECT nm FROM TABLE_B
) v
)
where cnt>=4;
DBFiddle: https://dbfiddle.uk/?rdbms=oracle_21&fiddle=0ac62f3d1ea835f60427a1da8efb965e

How to check if values exists in a table - postgres PLPGSQL

ERD
users
id
name
groups
id
name
users_in_groups
user_id
group_id
Problem summary
I'm writing a stored procedure in postgres that recieves a group name and users array and adds users to the group, and I want to assert first that the users exists in users - because I want to raise a custom error so I can catch it my server (if I rely on the default errors like - FK violation, I cannot classify it specifically enough in my server).
The stored procedure
CREATE FUNCTION add_users_to_group(group_name text, users text[])
RETURNS VOID AS $$
DECLARE
does_all_users_exists boolean;
BEGIN
SELECT exist FROM (
WITH to_check (user_to_check) as (select unnest(users))
SELECT bool_and(EXISTS (
SELECT * FROM users where id = to_check.user_to_check
)) as exist from to_check) as existance INTO does_all_users_exists;
IF NOT does_all_users_exists THEN
RAISE EXCEPTION '%', does_all_users_exists USING ERRCODE = 'XXXXX';
-- TODO: loop through each user and insert into users_in_groups
END;
$$ LANGUAGE PLPGSQL VOLATILE STRICT SECURITY INVOKER;
The problem
When I execute the function with users that exists in the users table, I get the error I throw and the message is: f (so my variable was false), but when I run only the query that gives me the existance of the all the users:
WITH to_check (user_to_check) as (select unnest(users))
SELECT bool_and(EXISTS (
SELECT * FROM users where id = to_check.user_to_check
)) as exist from to_check
I get true. but I get it inside a table like so:
#
exist (boolean)
1
true
so I guess I need to extract the true somehow.
anyway I know there is a better solution for validating the existance before insert, you are welcome to suggest.
Your logic seems unnecessarily complex. You can just check if any user doesn't exist using NOT EXISTS:
SELECT 1
FROM UNNEST(users) user_to_check
WHERE NOT EXISTS (SELECT 1 FROM users u WHERE u.id = user_to_check)
When you want to avoid issues with unique and foreign key constraints, you can SELECT and INSERT the records that you need for the next step. And you can do this for both tables (users and groups) in a single query, including the INSERT in users_in_groups:
CREATE FUNCTION add_users_to_group(group_name text, users text[])
RETURNS VOID AS $$
WITH id_users AS (
-- get id's for existing users:
SELECT id, name
FROM users
WHERE name =any($2)
), dml_users AS (
-- create id's for the new users:
INSERT INTO users (name)
SELECT s.name
FROM unnest($2) s(name)
WHERE NOT EXISTS(SELECT 1 FROM id_users i WHERE i.name = s.name)
-- Just to be sure, not sure you want this:
ON conflict do NOTHING
-- Result:
RETURNING id
), id_groups AS (
-- get id for an existing group:
SELECT id, name
FROM users
WHERE name = $1
), dml_group AS (
-- create id's for the new users:
INSERT INTO groups (name)
SELECT s.name
FROM (VALUES($1)) s(name)
WHERE NOT EXISTS(SELECT 1 FROM id_groups i WHERE i.name = s.name)
-- Just to be sure, not sure you want this:
ON conflict do NOTHING
-- Result:
RETURNING id
)
INSERT INTO users_in_groups(user_id, group_id)
SELECT user_id, group_id
FROM (
-- get all user-id's
SELECT id FROM dml_users
UNION
SELECT id FROM id_users
) s1(user_id)
-- get all group-id's
, (
SELECT id FROM dml_group
UNION
SELECT id FROM id_groups
) s2(group_id);
$$ LANGUAGE sql VOLATILE STRICT SECURITY INVOKER;
And you don't need PLpgSQL either, SQL will do.

Execute 2 loops for inserting values in SQL

I am using a procedure to get data from a table called Datastream which has 1000 rows and I have to read 100 records in a cursor once. And for each record match the primary key with foreign key in table Masterdata and move all the matching records in in multiple tables. Then the function should get the next 100 records from the table and do the same.
P.S: I need to use 2 loops as a condition.
I'm stuck with this error :
ORA-01722: invalid number
EDIT 1: Solved the above error, table datatype for 'product_id' was different then what it was supposed to be.
New Error: Everything seems fine, the procedure runs, but my tables aren't populated by the insert query. The output line prints SupID and PName.
I'm using the below code:
create or replace procedure newting is
s integer := 1;
e integer := s+99;
total_S integer := 1;
cursor endCount is
select datastream_id, ROW_NUMBER() OVER ( ORDER BY datastream_id )
from datastream
where datastream_id <= (select (count(*)/100) from datastream);
cursor transC is
SELECT datastream_id, product_id, customer_id, customer_name, outlet_id, outlet_name, quantity_sold, d_date
from datastream
where datastream_id between s and e
order by datastream_id;
TYPE val1 IS TABLE OF datastream.datastream_id%type;
v1 val1;
TYPE val2 IS TABLE OF datastream.product_id%type;
v2 val2;
TYPE val3 IS TABLE OF datastream.customer_id%type;
v3 val3;
TYPE val8 IS TABLE OF datastream.d_date%type;
v8 val8;
PName masterdata.product_name%TYPE;
SupID masterdata.supplier_id%TYPE;
SName masterdata.supplier_name%TYPE;
PPrice masterdata.sale_price%TYPE;
begin
open endCount;
open transC;
fetch transC bulk collect into v1,v2,v3,v4,v5,v6,v7,v8;
close endCount;
close transC;
for x in endCount
loop
for y in v1.first .. v1.last
loop
select product_name, supplier_id, supplier_name, sale_price into PName, SupID, SName, PPrice
from masterdata m
where m.product_id=v2(y);
Dbms_output.put_line(SupID);
insert into product (product_id, product_name) select v2(y), PName from dual --error in this line
where not exists (select * from product where product_id=v2(y));
insert into customer (customer_id, customer_name) select v3(y), v4(y) from dual
where not exists (select * from customer
where customer_id=v3(y));
insert into d_time (d_date, d_year, d_month, d_day)
select v8(y), to_char(v8(y),'YY'), to_char(v8(y),'MM'), to_char(v8(y),'DD')
from dual
where not exists (select * from d_time
where d_date=v8(y));
total_S := v7(y) * PPrice;
insert into sales_fact (transaction_id, product_id, supplier_id, outlet_id, customer_id, d_date, quantity, price, total_sales)
select v1(y), v2(y), SupID, v5(y), v3(y), v8(y), v7(y), PPrice, total_S
from dual
where not exists (select * from sales_fact
where transaction_id = v1(y));
end loop;
s:=s+99;
e:=e+99;
end loop;
end;
Any leads on this would be highly appreciated.
Thanks.
When you try to convert a character string into a number you could get An ORA-01722 error, basically the message says string cannot be converted into a number. Check all your DML(INSERT) specially the ones for dates. my advice is comment all (INSERT) but one, try to run it and if there is not issue, so proceed with the second one and keep going till you manage to find the problematic one and fix it(divide and conquer). Something else(just an observation), I believe the 'COMMIT' is out of this proc scope right?
Hope this could help.

PosgreSQL - loop syntax

I have the following loop (simplified for an example):
DO $$
DECLARE
l record;
BEGIN
FOR l IN SELECT id, country_id FROM logo LOOP
WITH cs AS (
INSERT INTO logo_settings (targeted) VALUES (true)
RETURNING id
)
INSERT INTO logo_settings_targeted (logo_settings_id, country_id)
VALUES
( (SELECT id FROM cs),
logo.country_id,
);
END LOOP;
END;
END $$;
The body of a loop works fine. But if I wrap it into a loop (I would like to make records for all records from logo table) - it shows an error saying:
ERROR: syntax error at or near "END"
Position: 712
END;
^
meaning the last EnND before END $$; which does not give much sense to me. I do not know what to fix to make it running. Any hints?
There is one end to many. end loop closes the loop body:
DO $$
DECLARE
l record;
BEGIN
FOR l IN SELECT id, country_id FROM logo LOOP
WITH cs AS (
INSERT INTO logo_settings (targeted) VALUES (true)
RETURNING id
)
INSERT INTO logo_settings_targeted (logo_settings_id, country_id)
VALUES
( (SELECT id FROM cs),
logo.country_id,
);
END LOOP;
END $$;
Additionally to answer of #Andronicus, this is one possible way to do this without PL/pgsql:
Click: demo:db<>fiddle
WITH countries AS (
SELECT id, country_id,
row_number() OVER () -- 1
FROM logo
), ins_settings AS (
INSERT INTO logo_settings(targeted)
SELECT true FROM countries c
RETURNING id
)
INSERT INTO logo_settings_targeted (logo_settings_id, country_id)
SELECT
ins.id, c.country_id
FROM
(SELECT id, row_number() OVER () FROM ins_settings) ins -- 2
JOIN countries c ON c.row_number = ins.row_number
Put the first SELECT query (which you used for the loop) into a second CTE at the top.
The trick is, that you can join the outputs from the logo SELECT statement and the first insert. You can use the fact, that both outputs have the same number of rows. Simply add a column with a row count to both outputs. This can be done, for example, using the row_number() window function. As you can see, for the logo SELECT I already did this directly in the CTE (1), for the INSERT output I added this in a subquery (2). Now there are the same identifiers on both tables, which can be used for the join.
The join is the basis for the second INSERT.

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.