I have two tables with identical columns, in an identical order. I have a desire to join across one of the two tables, depending on a subquery condition. For example, assume I have the following schema:
CREATE TABLE b (
bid SERIAL PRIMARY KEY,
cid INT NOT NULL
);
CREATE TABLE a1 (
aid SERIAL PRIMARY KEY,
bid INT NOT NULL REFERENCES b
);
CREATE TABLE a2 (
aid SERIAL PRIMARY KEY,
bid INT NOT NULL REFERENCES b
);
I would like a query, that performs a join across either a1 or a2 based on some condition. Something like:
WITH z AS (
SELECT cid, someCondition FROM someTable
)
SELECT *
FROM CASE z.someCondition THEN a1 ELSE a2 END
JOIN b USING (bid)
WHERE cid = (SELECT cid FROM z);
However, the above doesn't work. Is there some way to conditionally join across a1 or a2, depending on some boolean condition stored in table z?
If the conditions are exclusive (I expect they are): just do both queries and UNION ALL them, with the smart union construct:
WITH z AS (
SELECT cid
, (cid %3) AS some_condition -- Fake ...
FROM b
)
SELECT *
FROM a1
JOIN b USING (bid)
WHERE EXISTS( SELECT * FROM z
WHERE some_condition = 1 AND cid = b.cid )
UNION ALL
SELECT *
FROM a2
JOIN b USING (bid)
WHERE EXISTS( SELECT * FROM z
WHERE some_condition = 2 AND cid = b.cid )
;
A somewhat different syntax to do the same:
WITH z AS (
SELECT cid
, (cid %3) AS some_condition
FROM b
)
SELECT *
FROM a1
JOIN b ON a1.bid = b.bid
AND EXISTS( SELECT * FROM z
WHERE some_condition = 1 AND cid = b.cid )
UNION ALL
SELECT *
FROM a2
JOIN b ON a2.bid = b.bid
AND EXISTS( SELECT * FROM z
WHERE some_condition = 2 AND cid = b.cid )
;
SQL syntax does not allow conditional joins.
Probably the simplest way to achieve a similar effect is to use a dynamic query in a plpgsql function, which may look like this:
create function conditional_select(acid int, some_condition boolean)
returns table (aid int, bid int, cid int)
language plpgsql as $$
declare
tname text;
begin
if some_condition then tname = 'a1';
else tname = 'a2';
end if;
return query execute format ($fmt$
select a.aid, b.bid, b.cid
from %s a
join b using(bid)
where cid = %s;
$fmt$, tname, acid);
end $$;
select * from conditional_select(1, true)
If, like in your example, you have only a few columns that you want to output, you can use the CASE statement for every column:
SELECT CASE z.someCondition THEN a1.aid ELSE a2.aid END AS aid,
CASE z.someCondition THEN a1.bid ELSE a2.bid END AS bid
FROM b
JOIN a1 ON a1.bid = b.bid
JOIN a2 ON a2.bid = b.bid
JOIN someTable z USING (cid);
Depending on the size of tables a1 and a2 and how many columns you have to output, this may or my not be faster than Klin's solution with a function, which is inherently slower than plain SQL and even more so because of the dynamic query. Given that z.someCondition is a boolean value already, the CASE evaluation will be very fast. Small tables + few columns = this solution; large tables + many columns = Klin's solution.
Related
I am pretty new to the t-sql world and am trying to create a query that will change a value based on multiple criteria.
TSH1 is the main table that values will be changed in.
Freightview is the table that has the shipping amount I need to add into TSH1.
I want the query to look for matches between the tables and when there is one make a change to the FREIGHT line if it exists. If the FREIGHT line doesn't exist then it needs to add a line with the invoice amount from Freightview table.
My issue is the IF statement. It is returning two many values for the query to work. What do I need to change?
The last two queries are to return values that are not in each table.
SELECT *
FROM TSH1 T
JOIN Freightview FR on FR.[Shippers number] = T.sonum
IF
((SELECT [Shippers number] FROM Freightview) = (SELECT sonum FROM TSH1 T WHERE EXISTS(SELECT * FROM TSH1 T WHERE T.productnum = 'FRT-OUT' OR T.productnum = 'FRT-IN' OR T.productnum = 'FRT')))
BEGIN
UPDATE TSH1 SET tcost = FR.[Invoice Amount] FROM TSH1 T INNER JOIN Freightview FR on FR.[Shippers number] = T.sonum
WHERE T.productnum = 'FRT-OUT' OR T.productnum = 'FRT-IN' OR T.productnum = 'FRT';
END
ELSE IF
((SELECT [Shippers number] FROM Freightview) = (SELECT sonum FROM TSH1 T WHERE NOT EXISTS(SELECT * FROM TSH1 T WHERE T.productnum = 'FRT-OUT' OR T.productnum = 'FRT-IN' OR T.productnum = 'FRT')))
BEGIN
SELECT * INTO temp_table FROM TSH1 T INNER JOIN Freightview FR on FR.[Shippers number] = T.sonum
WHERE FR.[Shippers number] = T.sonum AND NOT EXISTS (SELECT productnum from TSH1 T where T.productnum = 'FRT-OUT' OR T.productnum = 'FRT-IN' OR T.productnum = 'FRT');
UPDATE temp_table SET temp_table.productnum = 'FRT', [Invoice Amount] = TT.tcost, temp_table.productid = '7240', temp_table.pd = 'FREIGHT', temp_table.qtyfulfilled = 1,
temp_table.tprice = 0, temp_table.stdcost = 0, temp_table.flag = 'D', temp_table.avgcost = NULL
FROM temp_table TT
INNER JOIN Freightview FR on TT.sonum = FR.[Shippers number];
UPDATE temp_table SET ID=NULL;
DELETE x FROM (
SELECT *, rn=row_number() over (partition by TT.sonum order by TT.soid)
FROM temp_table TT
) x
WHERE rn > 1;
INSERT INTO TSH1 SELECT * FROM temp_table;
DROP TABLE temp_table;
END
ELSE
BEGIN
SELECT *
FROM TSH1 T
LEFT JOIN Freightview FR on T.sonum = FR.[Shippers number]
WHERE FR.[Shippers number] IS NULL;
END
BEGIN
SELECT *
FROM Freightview FR
LEFT JOIN TSH1_Backup T on T.sonum = FR.[Shippers number]
WHERE T.sonum IS NULL;
END
END```
With SQL, you typically have to "think in sets". For example, a select statement returns a set of values, not just a single value1.
If I select * from T, the result might have multiple rows.
If I insert T1 select * from T2, multiple rows might be inserted into T1.
So, a statement like
if ((select c from T1) = (select c from T2))
Is sort of an odd construct. What exactly are we comparing here? On the left hand side we have zero or more rows from T1, and on the right hand side we zero or more rows from T2.
Now, you might be thinking to yourself...
Well the answer is obvious. If the two result sets are identical, then the equality comparison should return true, right?
Well... yes. It would be nice if we could do that. But that would require that SQL think of the result of a select statement as "an anonymous collection type with member-wise value equality semantics". And SQL is not that sophisticated as a language. In SQL, if you're comparing one thing to another with =, the left hand side and the right hand side should both be scalar types. "Single values", like an int, or a float, or a boolean. Not sets.
Fundamentally, it's the same reason why you can't do this:
create table T1(i int);
create table T2(j int);
if (T1 = T2) print 'tables had exactly the same content`;
So, how do you get the semantics "tell me if the contents of T1 and T2 exactly match?". There's no compact syntax to do this, you have to be verbose about it, there are lots of different ways you can "phrase" the question, and it's easy to make a mistake. Here's one correct way:
create table T1(i int);
create table T2(j int);
if not exists
(
select *
from T1
full join T2 on T1.i = T2.j
where T1.i is null or T2.j is null
) print 'tables had exactly the same content';
The logic is "match every row that you can, and tell me if there are any rows that couldn't be matched".
Now, interestingly enough SQL doesn't "validate" the comparison until it actually gets its results, so if your select statements each happen to return just a single row and single column, then the result of the select statement is treated as a scalar value, not a set, and then the equality comparison works. I sort of wish it didn't, because it's inconsistent and confuses people:
create table T1(i int);
create table T2(j int);
insert T1 values (1);
insert T2 values (1);
-- This will unfortunately succeed, and do what you intuitively "expect".
if ((select i from T1) = (select j from T2))
print 'tables both exactly one row with the same value';
But what if I put more rows into one of the tables?
create table T1(i int);
create table T2(j int);
insert T1 values (1), (2);
insert T2 values (1);
-- This will fail
if ((select i from T1) = (select j from T2))
print 'tables both exactly one row with the same value';
The error is:
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
You have some SQL that makes this same mistake:
if ((select [shippers number] from Freightview) = -- ...
I hope this answers your specific question about why you're getting the error. But hang on, let's go back and look at your requirements:
I want the query to look for matches between the tables and when there is one make a change to the FREIGHT line if it exists. If the FREIGHT line doesn't exist then it needs to add a line with the invoice amount from Freightview table.
So, you want a combination of insert and update, depending on the data. An "upsert".
TSQL has a statement which can do exactly this: Merge. Here's a simplified example to demonstrate how to use it.
create table T1(i int, c char);
create table T2(j int, c char);
insert T1 values (1, 'a');
insert T2 values (1, 'b'), (2, 'c');
merge T1 -- T1 will be "target" in the rest of the merge statement
using T2 on t2.j = T1.i -- T2 will be "source" in the rest of the merge statment
when matched then
update
set T1.c = T2.c
-- "target" isn't an alias defined by me. It's defined by the structure of "merge"
-- So this condition translates to "if there is a row in T2 with no matching row in T1"
when not matched by target then
insert (i, c)
values (T2.j, T2.c);
select * from T1;
/* result:
i c
----
1 b
2 c
*/
Formatting merge statements is hard, I've never found a way to do it that I am totally happy with.
1 That's not really accurate. SQL allows duplicate rows to exist in tables, result sets, and so on. In mathematics sets cannot have duplicate members. So technically you have to "think in bags". But people tend to say "think in sets" despite this.
Given a table with arrays of integers, the arrays should be merged so that all arrays that have overlapping entries end up as a single one.
Given the table arrays
a
------------
{1,2,3}
{1,4,7}
{4,7,9}
{15,17,18}
{18,16,15}
{20}
The result should look like this
{1,2,3,4,7,9}
{15,17,18,16}
{20}
As you can see duplicate values from a merged array may be removed and the order of the resulting entries in the array is unimportant. The arrays are integer arrays so functions from the intarray module can be used.
This will be done on a quite large table so performance is critical.
My first naive approach was to self-join the table on the && operator. Like this:
SELECT DISTINCT uniq(sort(t1.a || t2.a))
FROM arrays t1
JOIN arrays t2 ON t1.a && t2.a
This leaves two problems:
It is not recursive (it merges at most 2 arrays).
This could probably be solved with a recursive CTE.
Merged arrays re-occur in the output.
Any input is very welcome.
do $$
declare
arr int[];
arr_id int := 0;
tmp_id int;
begin
create temporary table tmp (v int primary key, id int not null);
for arr in select a from t loop
select id into tmp_id from tmp where v = any(arr) limit 1;
if tmp_id is NULL then
tmp_id = arr_id;
arr_id = arr_id+1;
end if;
insert into tmp
select unnest(arr), tmp_id
on conflict do nothing;
end loop;
end
$$;
select array_agg(v) from tmp group by id;
Pure SQL version:
WITH RECURSIVE x (a) AS (VALUES ('{1,2,3}'::int2[]),
('{1,4,7}'),
('{4,7,9}'),
('{15,17,18}'),
('{18,16,15}'),
('{20}')
), y AS (
SELECT 1::int AS lvl,
ARRAY [ a::text ] AS a,
a AS res
FROM x
UNION ALL
SELECT lvl + 1,
t1.a || ARRAY [ t2.a::text ],
(SELECT array_agg(DISTINCT unnest ORDER BY unnest)
FROM (SELECT unnest(t1.res) UNION SELECT unnest(t2.a)) AS a)
FROM y AS t1
JOIN x AS t2 ON (t2.a && t1.res) AND NOT t2.a::text = ANY(t1.a)
WHERE lvl < 10
)
SELECT DISTINCT res
FROM x
JOIN LATERAL (SELECT res FROM y WHERE x.a && y.res ORDER BY lvl DESC LIMIT 1) AS z ON true
This is my T-SQL
select Id,Profile,Type ,
case Profile
when 'Soft' then 'SID'
when 'Hard' then 'HID'
end as [Profile]
from ProductDetail p1
inner join [tableA or tableB] on xxxxxxxx
I want join tableA when Profile = Soft and join tableB when Profile = Hard, how can I do just only using T-SQL in one batch?
Thanks
You can't directly do it, but could achieve the same effect with outer joins
select Id,Profile,Type ,
case Profile
when 'Soft' then 'SID'
when 'Hard' then 'HID'
end as [Profile]
from ProductDetail p1
left outer join tableA ON tableA.x = p1.x AND p1.Profile = 'Soft'
left outer join tableB ON tableB.x = p1.x AND p1.Profile = 'Hard'
where
where
(tableA.x IS NOT NULL and p1.Profile = 'Soft')
or (tableB.x IS NOT NULL and p1.Profile = 'Hard')
Of course, you can choose different tables for inner join operation, but it must be based on some condition or variable.
For Example:
select Id,Profile,Type ,
case Profile
when 'Soft' then 'SID'
when 'Hard' then 'HID'
end as [Profile]
from ProductDetail p1
inner join tableA A
on Profile='Soft'
AND <any other Condition>
UNION
select Id,Profile,Type ,
case Profile
when 'Soft' then 'SID'
when 'Hard' then 'HID'
end as [Profile]
from ProductDetail p1
inner join tableB B
on Profile='Hard'
AND <any other Condition>
You can do this in a single statement with the same or similar case statement in your join. Below is sample code using temp tables that joins to 2 different reference tables merged into a single result set using a UNION
DECLARE #ProductDetail TABLE (Id INT, sProfile VARCHAR(100), StID INT, HdID INT)
DECLARE #TableA TABLE (StId INT, Field1 VARCHAR(100))
DECLARE #TableB TABLE (HdId INT, Field1 VARCHAR(100))
INSERT INTO #ProductDetail (Id, sProfile, StID , HdID ) VALUES (1,'Soft',1,1)
INSERT INTO #ProductDetail (Id, sProfile, StID , HdID ) VALUES (2,'Hard',2,2)
INSERT INTO #TableA (StId,Field1) VALUES (1,'Soft 1')
INSERT INTO #TableA (StId,Field1) VALUES (2,'Soft 2')
INSERT INTO #TableB (HdId,Field1) VALUES (1,'Hard 1')
INSERT INTO #TableB (HdId,Field1) VALUES (2,'Hard 2')
SELECT
p1.Id,p1.sProfile,
CASE
WHEN p1.sProfile = 'Soft' THEN StID
WHEN p1.sProfile = 'Hard' THEN HdId
END AS [Profile]
,ReferenceTable.FieldName
FROM
#ProductDetail p1
INNER JOIN
(
SELECT StID AS id, 'Soft' AS sProfile, Field1 AS FieldName
FROM #TableA AS tableA
UNION ALL
SELECT HdID AS id, 'Hard' AS sProfile, Field1 AS FieldName
FROM #TableB AS tableB
)
AS ReferenceTable
ON
CASE
WHEN p1.sProfile = 'Soft' THEN StID
WHEN p1.sProfile = 'Hard' THEN HdID
END = ReferenceTable.Id
AND p1.sProfile = ReferenceTable.sProfile
This will return the following result set:
Id sProfile Profile FieldName
1 Soft 1 Soft 1
2 Hard 2 Hard 2
I have written a very simple CTE expression that retrieves a list of all groups of which a user is a member.
The rules goes like this, a user can be in multiple groups, and groups can be nested so that a group can be a member of another group, and furthermore, groups can be mutual member of another, so Group A is a member of Group B and Group B is also a member of Group A.
My CTE goes like this and obviously it yields infinite recursion:
;WITH GetMembershipInfo(entityId) AS( -- entity can be a user or group
SELECT k.ID as entityId FROM entities k WHERE k.id = #userId
UNION ALL
SELECT k.id FROM entities k
JOIN Xrelationships kc on kc.entityId = k.entityId
JOIN GetMembershipInfo m on m.entityId = kc.ChildID
)
I can't find an easy solution to back-track those groups that I have already recorded.
I was thinking of using an additional varchar parameter in the CTE to record a list of all groups that I have visited, but using varchar is just too crude, isn't it?
Is there a better way?
You need to accumulate a sentinel string within your recursion. In the following example I have a circular relationship from A,B,C,D, and then back to A, and I avoid a loop with the sentinel string:
DECLARE #MyTable TABLE(Parent CHAR(1), Child CHAR(1));
INSERT #MyTable VALUES('A', 'B');
INSERT #MyTable VALUES('B', 'C');
INSERT #MyTable VALUES('C', 'D');
INSERT #MyTable VALUES('D', 'A');
; WITH CTE (Parent, Child, Sentinel) AS (
SELECT Parent, Child, Sentinel = CAST(Parent AS VARCHAR(MAX))
FROM #MyTable
WHERE Parent = 'A'
UNION ALL
SELECT CTE.Child, t.Child, Sentinel + '|' + CTE.Child
FROM CTE
JOIN #MyTable t ON t.Parent = CTE.Child
WHERE CHARINDEX(CTE.Child,Sentinel)=0
)
SELECT * FROM CTE;
Result:
Parent Child Sentinel
------ ----- --------
A B A
B C A|B
C D A|B|C
D A A|B|C|D
Instead of a sentinel string, use a sentinel table variable. Function will catch circular reference no matter how many hops the circle is, no issues with maximum length of nvarchar(max), easily modified for different data types or even multipart keys, and you can assign the function to a check constraint.
CREATE FUNCTION [dbo].[AccountsCircular] (#AccountID UNIQUEIDENTIFIER)
RETURNS BIT
AS
BEGIN
DECLARE #NextAccountID UNIQUEIDENTIFIER = NULL;
DECLARE #Sentinel TABLE
(
ID UNIQUEIDENTIFIER
)
INSERT INTO #Sentinel
( [ID] )
VALUES ( #AccountID )
SET #NextAccountID = #AccountID;
WHILE #NextAccountID IS NOT NULL
BEGIN
SELECT #NextAccountID = [ParentAccountID]
FROM [dbo].[Accounts]
WHERE [AccountID] = #NextAccountID;
IF EXISTS(SELECT 1 FROM #Sentinel WHERE ID = #NextAccountID)
RETURN 1;
INSERT INTO #Sentinel
( [ID] )
VALUES ( #NextAccountID )
END
RETURN 0;
END
How can I extract the values from a record as individual comuns in postgresql
SELECT
p.*,
(SELECT ROW(id,server_id,format,product_id) FROM products_images pi WHERE pi.product_id = p.id LIMIT 1) AS image
FROM products p
WHERE p.company = 1 ORDER BY id ASC LIMIT 10
Instead of
image
(3, 4, "jpeg", 7)
I would like to have
id | server_id | format | product_id
3 | 4 | jpeg | 7
Is there any way of selecting only one image for each product and return the columns directly instead of a record?
Try this:
create type xxx as (t varchar, y varchar, z int);
with a as
(
select row(table_name, column_name, (random() * 100)::int) x
from information_schema.columns
)
-- cannot cast directly to xxx, should cast to text first
select (x::text::xxx).t, (x::text::xxx).y, (x::text::xxx).z
from a
Alternatively, you can do this:
with a as
(
select row(table_name, column_name, (random() * 100)::int) x
from information_schema.columns
),
-- cannot cast directly to xxx, should cast to text first
b as (select x::text::xxx as w from a)
select
(w).t, (w).y, (w).z
from b
To select all fields:
with a as
(
select row(table_name, column_name, (random() * 100)::int) x
from information_schema.columns
),
-- cannot cast directly to xxx, should cast to text first
b as (select x::text::xxx as w from a)
select
(w).*
from b
You can do this too, but this makes the whole exercise of using ROW a pointless one when you can just remove the ROW function and re-pick it up from outside of cte/derived table. I surmised the OP's ROW came from a function; for which he should use the codes above, not the following:
with a as
(
select row(table_name, column_name, (random() * 100)::int)::xxx x
from information_schema.columns
)
select
(x).t, (x).y, (x).z
from a
Just specify the components of your struct:
SELECT a,b,c,(image).id, (image).server_id, ...
FROM (
SELECT
p.*,
(SELECT ROW(id,server_id,format,product_id) FROM products_images pi WHERE pi.product_id = p.id LIMIT 1) AS image
FROM products p
WHERE p.company = 1 ORDER BY id ASC LIMIT 10
) as subquery
But anyway, I would rework the query and use a join instead of a subclause.
SELECT DISTINCT ON (p.*) p.*,
p.id,pi.server_id,pi.format,pi.product_id
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE p.company = 1
ORDER BY id ASC
LIMIT 10
But I believe you have to specify all the p-fields in the distinct separately to ensure just one image is loaded per product.
Try this, will work on your existing code with minimal modification(if creating a type is a minimal modification for you ;-)
create type image_type as (id int, server_id int, format varchar, product_id int);
SELECT
p.*,
( (SELECT ROW(id,server_id,format,product_id)
FROM products_images pi
WHERE pi.product_id = p.id LIMIT 1)::text::image_type ).*
FROM products p
WHERE p.company = 1 ORDER BY id ASC LIMIT 10
Proof-of-concept code:
Create type first:
create type your_type_here as (table_name varchar, column_name varchar)
Actual code:
select
a.b,
( (select row(table_name, column_name)
from information_schema.columns limit 1)::text::your_type_here ).*
from generate_series(1,10) as a(b)
But I guess you should tackle it with GROUP BY' andMAXcombo or useDISTINCT ON` like what Daniel have posted
every table has an associated composite type of the same name
https://www.postgresql.org/docs/current/plpgsql-declarations.html#PLPGSQL-DECLARATION-ROWTYPES
So, this code
drop table if exists "#typedef_image"
;
create temp table "#typedef_image"(
id int,
server_id int,
format text,
product_id int
)
;
select (row(3, 4, 'jpeg', 7)::"#typedef_image").*
will work