postgresql WITH statement doesn't find auxiliary statements - postgresql

I have this query:
WITH words_not AS (
SELECT keywords.id
FROM keywords
WHERE keyword = any(array['writing'])
),actes_not AS (
SELECT actes_keywords.acte_id
FROM actes_keywords
WHERE actes_keywords.keyword_id IN (words_not)
)
SELECT actes.id,
actes.acte_date
FROM actes
WHERE actes.id <> all(actes_not);
This returns the following error:
ERROR: column "words_no" does not exist
LINE 1: ...ctes_keywords WHERE actes_keywords.keyword_id IN (mots_non))...
Each auxiliary statement in the WITH query is good (tested) and I thought I was staying pretty close to the manual: https://www.postgresql.org/docs/current/static/queries-with.html
I don't see why the auxiliary statement in the WITH query is not recognised.

You can't use a table reference in an IN (..) clause. You need a sub-query:
WITH words_not AS (
SELECT keywords.id
FROM keywords
WHERE keyword = any(array['writing'])
), actes_not AS (
SELECT actes_keywords.acte_id
FROM actes_keywords
WHERE actes_keywords.keyword_id IN (select id from words_not) --<< HERE
)
SELECT actes.id,
actes.acte_date
FROM actes
WHERE actes.id <> all(select id from actes_not); --<< HERE

Related

Postgres Crosstab query with CTE (with clause)

Recently started working on Postgres and need to pivot data.
I wrote the following query:
select *
from crosstab (
$$
with tmp_kv as (
select distinct pat_id
,col.name as key, replace(replace(replace(value, '[',''), ']', ''),'"','') as value
from (
select p.Id as pat_id, nullif(kv.key,'undefined')::int as key, trim(kv.value::text,'"') as value
from pat_table p
left join e_table e on e.pat_id = p.id and e.id is null
,jsonb_each_text(p.data) as kv
) t
left join lateral (
select name::text as name from public.config_fields fld
where id = t.key
) col on true
)
select pat_id, key, value
from tmp_kv
where nullif(trim(key),'') is not null
order by pat_id, key
$$,$$
select distinct key from tmp_kv -- (Get error "relation "tmp_kv" does not exist" )
where nullif(trim(key),'') is not null
order by 1
$$
) as (
pat_id bigint
...
...
);
Query works if I take the WITH clause out into temporary table. But will be deploying it to production with read replicas, so need it to be working with a CTE. Is there a way?
The two queries passed as strings to the crosstab() function are separate queries.
A CTE can only be attached to a single query.
What you ask for is strictly impossible.
Since you have to spell out the (static) return type for crosstab() anyway, and the result of the query in the 2nd parameter has to match that, it's pointless to use a query with a dynamic result as 2nd parameter to begin with.

Coalesce sentence containing an insert into clause fails in PostgreSQL

This is my trivial test table,
create table test (
id int not null generated always as identity,
first_name. varchar,
primary key (id),
unique(first_name)
);
As an alternative to insert-into-on-conflict sentences, I was trying to use the coalesce laziness to execute a select whenever possible or an insert, only when select fails to find a row.
coalesce laziness is described in documentation. See https://www.postgresql.org/docs/current/functions-conditional.html
Like a CASE expression, COALESCE only evaluates the arguments that are needed to determine the result; that is, arguments to the right of the first non-null argument are not evaluated. This SQL-standard function provides capabilities similar to NVL and IFNULL, which are used in some other database systems.
I also want to get back the id value of the row, having being inserted or not.
I started with:
select coalesce (
(select id from test where first_name='carlos'),
(insert into test(first_name) values('carlos') returning id)
);
but an error syntax error at or near "into" was found.
See it on this other DBFiddle
https://www.db-fiddle.com/f/t7TVkoLTtWU17iaTAbEhDe/0
Then I tried:
select coalesce (
(select id from test where first_name='carlos'),
(with r as (
insert into test(first_name) values('carlos') returning id
) select id from r
)
);
Here I am getting a WITH clause containing a data-modifying statement must be at the top level error that I don't understand, as insert is the first and only sentence within the with.
I am testing this with DBFiddle and PostgreSQL 13. The source code can be found at
https://www.db-fiddle.com/f/hp8T1iQ8eS4wozDCBhBXDw/5
Different method: chained CTEs:
CREATE TABLE test
( id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY
, first_name VARCHAR UNIQUE
);
WITH sel AS (
SELECT id FROM test WHERE first_name = 'carlos'
)
, ins AS (
INSERT INTO test(first_name)
SELECT 'carlos'
WHERE NOT EXISTS (SELECT 1 FROM test WHERE first_name = 'carlos')
RETURNING id
)
, omg AS (
SELECT id FROM sel
UNION ALL
SELECT id FROM ins
)
SELECT id
FROM omg
;
It seems that the returning value from the insert into clause is not equivalent in nature to the scalar query of a select clause. So I try encapsulating the insert into into an SQL function and it worked.
create or replace function insert_first_name(
_first_name varchar
) returns int
language sql as $$
insert into test (first_name)
values (_first_name)
returning id;
$$;
select coalesce (
(select id from test where first_name='carlos'),
(select insert_first_name('carlos'))
);
See it on https://www.db-fiddle.com/f/73rVXgqGfrG4VmjrAk6Z3i/2
This is a refinement on #wildplasser accepted answer. it avoids comparing first_name twice and uses coalesce instead of union all. Kind of an selsert in just one sentence.
with sel as (
select id from test where first_name = 'carlos'
)
, ins as (
insert into test(first_name)
select 'carlos'
where (select id from sel) is null
returning id
)
select coalesce (
(select id from sel),
(select id from ins)
);
See it at https://www.db-fiddle.com/f/goRh4TyAebTkEZFHk6WbtK/6

SELECT ... INTO a variable within a postgres CTE

I want to do this inside a plpgsql function
WITH set1 AS (
select *
from table1
where ... -- reduce table1 to the small working set once for multiple reuse
), query_only_for_select_into AS (
select id
into my_variable_declared_earlier
from set1
where foo = 'bar'
)
select my_variable_declared_earlier as my_bar
, *
from set1
where foo <> 'bar'
but Postgres throws the error
ERROR: SELECT ... INTO is not allowed here
I'm guessing it's because the select ... into is in the CTE. But I can't find anything in the documentation or on the web about it. Maybe I just messed up the select ... into syntax?
SQL has no variables - they are part of a procedural language (e.g. PL/pgSQL), not the query language.
But I don't see the reason why you need one:
WITH set1 AS (
select *
from table1
where ... -- reduce table1 to the small working set once for multiple reuse
), query_only_for_select_into AS (
select id as my_variable_declared_earlier
from set1
where foo = 'bar'
)
select qs.my_variable_declared_earlier as my_bar,
*
from set1
join query_only_for_select_into qs on ...
where foo <> 'bar'
If you are certain that query_only_for_select_into only returns a single row, you can use:
select qs.my_variable_declared_earlier as my_bar,
*
from set1
cross join query_only_for_select_into qs
where foo <> 'bar'
SELECT ... INTO variable is PL/pgSQL syntax, and you can only use it with the top level SELECT (the one that returns results. Think of it like that: PL/pgSQL runs an SQL statement and stores the result somewhere.
But you don't need that: simply include query_only_for_select_into in the FROM clause of the main query, then you can access its columns there.

How does one Create a Parameterized Recursive CTE to flatten a heirarchy within a Scalar Function?

I'm trying to create a scalar function to determine whether a user of a provided ID or any of their subordinates have orders under a collection of provided order IDs.
Note I am using my own User-Defined Table Type of IntegerIdTableType to take in the collection of OrderIds.
CREATE FUNCTION DoOrdersExistUnderUserOrUsersSubordinates
(
#orderIds dbo.IntegerIdTableType READONLY,
#userId INT
)
RETURNS BIT
AS
BEGIN
RETURN
(
WITH GetUserIds(ordinateUserId)
AS
(
SELECT ordinateUserId UserId
UNION ALL
SELECT GetUserIds(Subordinate.Id)
FROM UsersAccounts.Users Subordinates
WHERE Subordinates.SupervisorId = #ordinateUserId
)
SELECT CASE WHEN EXISTS
(
SELECT 1
FROM Orders
WHERE Orders.Id IN
(
SELECT Id
FROM #orderIds
)
AND Orders.UserId IN
(
SELECT UserId
FROM GetUserIds(#userId)
)
)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
)
END
Here is some sample data for both my Orders and Users tables.
Users
Orders
Expected Results
When calling DoOrdersExistUnderUserOrUsersSubordinates with the following values, I expect the following results.
I'm having 2 issues with this function:
Syntax errors:
Incorrect syntax near the keyword 'WITH'.
Incorrect syntax near ')'.
'GetUserIds' is not a recognized built-in function name
The above seems to happen even without being wrapped in a function.
I don't know what the correct way to pass a parameter to a recursive CTE is but I have seen examples where the declaration of the CTE has a name in brackets which I assumed to be a parameter
I've tried putting a semi-colon immediately before the WITH even though it's the only statement in the function and I just get Incorrect syntax near ';'. instead of Incorrect syntax near the keyword 'WITH'.
I've also tried getting rid of the BEGIN and END and that gives me Incorrect syntax near 'RETURN'., plus Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. If I don't include the redundant semi-colon.
How do I get around all of this?
Surely Recursive CTE's must be able to take in a parameter or what would they recurse on?
UPDATE:
After battling with Example F of the documentation linked by Zohar_Peled, I eventually figured out that parameters aren't passed into the CTE as such, but rather joined to it then persisted within it through the brackets of its declaration. Whatever is then defined in the corresponding SELECTs is output through the parameters to whatever called the CTE (in this case, either the outer SELECT Id FROM UserNodes statement or the CTE itself (for the recursion)).
I changed the SQL statement within the function to the following and it worked as expected outside of the function.
WITH UserNodes([Root User ID], Id, SupervisorId)
AS
(
SELECT Users.Id, Users.Id, Users.SupervisorId
FROM UsersAccounts.Users
WHERE Users.SupervisorId IS NULL
UNION ALL
SELECT [Root User ID],
Users.Id,
Users.SupervisorId
FROM UsersAccounts.Users
JOIN UserNodes [Subordinate Descendant Users] ON [Subordinate Descendant Users].Id = Users.SupervisorId
)
SELECT CASE WHEN EXISTS
(
SELECT 1
FROM Orders
WHERE Orders.Id IN
(
SELECT Id
FROM #orderIds
)
AND Orders.UserId IN
(
SELECT Id
FROM UserNodes
WHERE [Root User ID] = #userId
)
)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
This works fine alone (with the required variables provided to substitute the missing function parameters) but as soon as I put it back into the CREATE FUNCTION block, I'm faced with the same syntax errors as before (excluding 2.).
As stated, I'm not able to test this, but this is what I'm suggesting you change:
CREATE FUNCTION DoOrdersExistUnderUserOrUsersSubordinates
(
#orderIds dbo.IntegerIdTableType READONLY,
#userId INT
)
RETURNS BIT
AS
BEGIN
declare #bln bit
;WITH UserNodes([Root User ID], Id, SupervisorId)
AS
(
SELECT Users.Id, Users.Id, Users.SupervisorId
FROM UsersAccounts.Users
WHERE Users.SupervisorId IS NULL
UNION ALL
SELECT [Root User ID],
Users.Id,
Users.SupervisorId
FROM UsersAccounts.Users
JOIN UserNodes [Subordinate Descendant Users] ON [Subordinate Descendant Users].Id = Users.SupervisorId
)
SELECT #bln = CASE WHEN EXISTS
(
SELECT 1
FROM Orders
WHERE Orders.Id IN
(
SELECT Id
FROM #orderIds
)
AND Orders.UserId IN
(
SELECT Id
FROM UserNodes
WHERE [Root User ID] = #userId
)
)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
RETURN #bln
END
Let me know if it works...

T-SQL Why can I reffer only once to temporary object?

with tmp_rows as
(
select * from [dbo].[customer]
)
select * from tmp_rows;
select count(*) from tmp_rows;
I can't get the count of the tmp_rows because I get the error: Invalid object name 'tmp_rows'
If I comment the "select *" query everything is OK
I need to select all rows and then get their count, how to do that ?
with tmp_rows as
(
select * from [dbo].[customer]
)
select * from tmp_rows;
select ##rowcount;
By declaring you statement in using with you are declaring a CTE - further information on CTE's can be found here
A temporary object created using the with keyword can only be used once. You can create a temporary table if you want to use it more than once:
select *
into #tmp_tows
from dbo.customer
select * from #tmp_rows
select count(*) from #tmp_rows
drop table #tmp_rows
This works even if you want to do something different with the result twice, for example getting the count before the result.
The CTE starting with WITH ends with the semicolon ; .
But you can have more than 1 CTE in a WITH statement:
with tmp_rows as
(
select * from [dbo].customer
),
count_rows as
(
select COUNT(*) count_rows from tmp_rows
)
select * from count_rows, tmp_rows;
tmp_rows is a Common Table Expression (CTE), and CTEs are scoped at the statement level:
-- 1st statement, works fine.
with tmp_rows as (select * from [dbo].[customer])
select * from tmp_rows;
-- 2nd statement, returns an error, since tmp_rows is out of scope.
select count(*) from tmp_rows;
By the time your second statement executes, tmp_rows is already out of scope.
Be aware that CTEs are like locally-scoped views, not tables. The result set is never materialized. If you need to materialize a result set, use a local temporary table or table variable instead.