PosgreSQL - loop syntax - sql

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.

Related

How to query a club column in Oracle [duplicate]

I would like to find the distinct CLOB values that can assume the column called CLOB_COLUMN (of type CLOB) contained in the table called COPIA.
I have selected a PROCEDURAL WAY to solve this problem, but I would prefer to give a simple SELECT as the following: SELECT DISTINCT CLOB_COLUMN FROM TABLE avoiding the error "ORA-00932: inconsistent datatypes: expected - got CLOB"
How can I achieve this?
Thank you in advance for your kind cooperation. This is the procedural way I've thought:
-- Find the distinct CLOB values that can assume the column called CLOB_COLUMN (of type CLOB)
-- contained in the table called COPIA
-- Before the execution of the following PL/SQL script, the CLOB values (including duplicates)
-- are contained in the source table, called S1
-- At the end of the excecution of the PL/SQL script, the distinct values of the column called CLOB_COLUMN
-- can be find in the target table called S2
BEGIN
EXECUTE IMMEDIATE 'TRUNCATE TABLE S1 DROP STORAGE';
EXECUTE IMMEDIATE 'DROP TABLE S1 CASCADE CONSTRAINTS PURGE';
EXCEPTION
WHEN OTHERS
THEN
BEGIN
NULL;
END;
END;
BEGIN
EXECUTE IMMEDIATE 'TRUNCATE TABLE S2 DROP STORAGE';
EXECUTE IMMEDIATE 'DROP TABLE S2 CASCADE CONSTRAINTS PURGE';
EXCEPTION
WHEN OTHERS
THEN
BEGIN
NULL;
END;
END;
CREATE GLOBAL TEMPORARY TABLE S1
ON COMMIT PRESERVE ROWS
AS
SELECT CLOB_COLUMN FROM COPIA;
CREATE GLOBAL TEMPORARY TABLE S2
ON COMMIT PRESERVE ROWS
AS
SELECT *
FROM S1
WHERE 3 = 9;
BEGIN
DECLARE
CONTEGGIO NUMBER;
CURSOR C1
IS
SELECT CLOB_COLUMN FROM S1;
C1_REC C1%ROWTYPE;
BEGIN
FOR C1_REC IN C1
LOOP
-- How many records, in S2 table, are equal to c1_rec.clob_column?
SELECT COUNT (*)
INTO CONTEGGIO
FROM S2 BETA
WHERE DBMS_LOB.
COMPARE (BETA.CLOB_COLUMN,
C1_REC.CLOB_COLUMN) = 0;
-- If it does not exist, in S2, a record equal to c1_rec.clob_column,
-- insert c1_rec.clob_column in the table called S2
IF CONTEGGIO = 0
THEN
BEGIN
INSERT INTO S2
VALUES (C1_REC.CLOB_COLUMN);
COMMIT;
END;
END IF;
END LOOP;
END;
END;
If it is acceptable to truncate your field to 32767 characters this works:
select distinct dbms_lob.substr(FIELD_CLOB,32767) from Table1
You could compare the hashes of the CLOB to determine if they are different:
SELECT your_clob
FROM your_table
WHERE ROWID IN (SELECT MIN(ROWID)
FROM your_table
GROUP BY dbms_crypto.HASH(your_clob, dbms_crypto.HASH_SH1))
Edit:
The HASH function doesn't guarantee that there will be no collision. By design however, it is really unlikely that you will get any collision. Still, if the collision risk (<2^80?) is not acceptable, you could improve the query by comparing (with dbms_lob.compare) the subset of rows that have the same hashes.
add TO_CHAR after distinct keyword to convert CLOB to CHAR
SELECT DISTINCT TO_CHAR(CLOB_FIELD) from table1; //This will return distinct values in CLOB_FIELD
Use this approach. In table profile column content is NCLOB. I added the where clause to reduce the time it takes to run which is high,
with
r as (select rownum i, content from profile where package = 'intl'),
s as (select distinct (select min(i) from r where dbms_lob.compare(r.content, t.content) = 0) min_i from profile t where t.package = 'intl')
select (select content from r where r.i = s.min_i) content from s
;
It is not about to win any prizes for efficiency but should work.
select distinct DBMS_LOB.substr(column_name, 3000) from table_name;
If truncating the clob to the size of a varchar2 won't work, and you're worried about hash collisions, you can:
Add a row number to every row;
Use DBMS_lob.compare in a not exists subquery. Exclude duplicates (this means: compare = 0) with a higher rownum.
For example:
create table t (
c1 clob
);
insert into t values ( 'xxx' );
insert into t values ( 'xxx' );
insert into t values ( 'yyy' );
commit;
with rws as (
select row_number () over ( order by rowid ) rn,
t.*
from t
)
select c1 from rws r1
where not exists (
select * from rws r2
where dbms_lob.compare ( r1.c1, r2.c1 ) = 0
and r1.rn > r2.rn
);
C1
xxx
yyy
To bypass the oracle error, you have to do something like this :
SELECT CLOB_COLUMN FROM TABLE COPIA C1
WHERE C1.ID IN (SELECT DISTINCT C2.ID FROM COPIA C2 WHERE ....)
I know this is an old question but I believe I've figure out a better way to do what you are asking.
It is kind of like a cheat really...The idea behind it is that You can't do a DISTINCT of a Clob column but you can do a DISTINCT on a Listagg function of a Clob_Column...you just need to play with the partition clause of the Listagg function to make sure it will only return one value.
With that in mind...here is my solution.
SELECT DISTINCT listagg(clob_column,'| ') within GROUP (ORDER BY unique_id) over (PARTITION BY unique_id) clob_column
FROM copia;

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

Use the result of a CTE in a different block in my procedure

Can I save the returned result of a cte to a value I've declared?
I would like to use the id field of an CTE in a foor loop which occurs in a different block after the CTE inserted some values.
CREATE some_procedure()
LANGUAGE plpgsql
as $$
DECLARE
some_var uuid;
WITH cte AS(
-- do stuff
RETURNING id INTO some_var --I know it's an error, but I need something like id = some_var
)
...
EDIT:
I've tried something like this but no luck. And I can't reference the CTE once I've used a semicolon after the final insert:
as $$
DECLARE
prod_sku uuid;
attr VARCHAR[];
BEGIN
WITH ins_category AS(
INSERT INTO product_category(name)
VALUES(category_name)
ON CONFLICT (name) DO NOTHING
RETURNING id
),
ins_product AS(
INSERT INTO product(name, product_category)
SELECT product_name, ins_category.id FROM ins_category
RETURNING sku
),
sel_sku AS(
prod_sku = SELECT ins_product.sku FROM ins_product --SYNTAX ERROR
RETURNING product_sku
)
INSERT INTO product_price(base_price, product_sku)
SELECT product_base_price, prod_sku;
RAISE NOTICE 'prod_sku %', prod_sku;
...
I've also tried adding the select after the CTE/semicolone, like in the following example. But, it throws an error that ins_product does not exists, since I've already executed with semicolon.
HOW CAN I GET THE VALUE FROM THE CTE INTO A DIFFERENT BLOCK IN MY PROCEDURE?
...
ins_product AS(
INSERT INTO product(name, product_category)
SELECT product_name, ins_category.id FROM ins_category
RETURNING sku
)
INSERT INTO product_price(base_price, product_sku)
SELECT product_base_price, ins_product.sku FROM ins_product;
SELECT * FROM ins_product AS test; -- ERROR HERE
...
UPDATE 2
I need the sku field from the CTE to use it in a loop:
...
sel_sku AS(
SELECT ins_product.sku FROM ins_product
)
INSERT INTO product_price(base_price, product_sku)
SELECT product_base_price, ins_product.sku FROM ins_product;
FOREACH attr SLICE 1 IN ARRAY product_attributes
LOOP
WITH ins_attribute_type AS(
INSERT INTO product_attribute_type(name, product_sku)
SELECT attr[1], prod_sku
ON CONFLICT (name) DO NOTHING
RETURNING id
)
INSERT INTO product_attribute_value(value, attr_type)
SELECT attr[2], ins_attribute_type.id FROM ins_attribute_type;
END LOOP;
COMMIT;
Sort of.
A CTE essentially a procedure or query window scoped table variable. You can name the CTE so that you now have a named table.
If you want value variables, just query the CTE:
var whatever = Select x from CTE where y
** Update per changes to question **
The "returning" line of your CTE indicates the fields that will make up the CTE. You should be able to query these tables the same as any other temp table, using the name.
Check the code blocks in your procedure. In order to query the CTE it will need to be in the same "BLOCK" - that is between your BEGIN and your END commands. After the "BLOCK" is closed the CTE is out of scope and no longer accessible.
** Hopefully final update **
In this statement:
sel_sku AS(
prod_sku = SELECT ins_product.sku FROM ins_product --SYNTAX ERROR
RETURNING product_sku
remove the "prod_sku =" portion. sel_sku will then be single column table made up of the product_sku that fit your SELECT query

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.

Return id if a row exists, INSERT otherwise

I'm writing a function in node.js to query a PostgreSQL table.
If the row exists, I want to return the id column from the row.
If it doesn't exist, I want to insert it and return the id (insert into ... returning id).
I've been trying variations of case and if else statements and can't seem to get it to work.
A solution in a single SQL statement. Requires PostgreSQL 8.4 or later though.
Consider the following demo:
Test setup:
CREATE TEMP TABLE tbl (
id serial PRIMARY KEY
,txt text UNIQUE -- obviously there is unique column (or set of columns)
);
INSERT INTO tbl(txt) VALUES ('one'), ('two');
INSERT / SELECT command:
WITH v AS (SELECT 'three'::text AS txt)
,s AS (SELECT id FROM tbl JOIN v USING (txt))
,i AS (
INSERT INTO tbl (txt)
SELECT txt
FROM v
WHERE NOT EXISTS (SELECT * FROM s)
RETURNING id
)
SELECT id, 'i'::text AS src FROM i
UNION ALL
SELECT id, 's' FROM s;
The first CTE v is not strictly necessary, but achieves that you have to enter your values only once.
The second CTE s selects the id from tbl if the "row" exists.
The third CTE i inserts the "row" into tbl if (and only if) it does not exist, returning id.
The final SELECT returns the id. I added a column src indicating the "source" - whether the "row" pre-existed and id comes from a SELECT, or the "row" was new and so is the id.
This version should be as fast as possible as it does not need an additional SELECT from tbl and uses the CTEs instead.
To make this safe against possible race conditions in a multi-user environment:
Also for updated techniques using the new UPSERT in Postgres 9.5 or later:
Is SELECT or INSERT in a function prone to race conditions?
I would suggest doing the checking on the database side and just returning the id to nodejs.
Example:
CREATE OR REPLACE FUNCTION foo(p_param1 tableFoo.attr1%TYPE, p_param2 tableFoo.attr1%TYPE) RETURNS tableFoo.id%TYPE AS $$
DECLARE
v_id tableFoo.pk%TYPE;
BEGIN
SELECT id
INTO v_id
FROM tableFoo
WHERE attr1 = p_param1
AND attr2 = p_param2;
IF v_id IS NULL THEN
INSERT INTO tableFoo(id, attr1, attr2) VALUES (DEFAULT, p_param1, p_param2)
RETURNING id INTO v_id;
END IF;
RETURN v_id:
END;
$$ LANGUAGE plpgsql;
And than on the Node.js-side (i'm using node-postgres in this example):
var pg = require('pg');
pg.connect('someConnectionString', function(connErr, client){
//do some errorchecking here
client.query('SELECT id FROM foo($1, $2);', ['foo', 'bar'], function(queryErr, result){
//errorchecking
var id = result.rows[0].id;
};
});
Something like this, if you are on PostgreSQL 9.1
with test_insert as (
insert into foo (id, col1, col2)
select 42, 'Foo', 'Bar'
where not exists (select * from foo where id = 42)
returning foo.id, foo.col1, foo.col2
)
select id, col1, col2
from test_insert
union
select id, col1, col2
from foo
where id = 42;
It's a bit longish and you need to repeat the id to test for several times, but I can't think of a different solution that involves a single SQL statement.
If a row with id=42 exists, the writeable CTE will not insert anything and thus the existing row will be returned by the second union part.
When testing this I actually thought the new row would be returned twice (therefor a union not a union all) but it turns out that the result of the second select statement is actually evaluated before the whole statement is run and it does not see the newly inserted row. So in case a new row is inserted, it will be taken from the "returning" part.
create table t (
id serial primary key,
a integer
)
;
insert into t (a)
select 2
from (
select count(*) as s
from t
where a = 2
) s
where s.s = 0
;
select id
from t
where a = 2
;