conditional join with input value in postgreslq function - postgresql

I have three tables:
create table id_table (
id integer
);
insert into id_table values (1),(2),(3);
create table alphabet_table (
id integer,
letter text
);
insert into alphabet_table values (1,'a'),(2,'b'),(3,'c');
create table greek_table (
id integer,
letter text
);
insert into greek_table values (1,'alpha'),(2,'beta');
I like to create a function that join id_table with either alphabet_table or greek_table on id. The choice of the table depends on an input value specified in the function. I wrote:
CREATE OR REPLACE FUNCTION choose_letters(letter_type text)
RETURNS table (id integer,letter text) AS $$
BEGIN
RETURN QUERY
select t1.id,
case when letter_type = 'alphabet' then t2.letter else t3.letter end as letter
from id_table t1,
alphabet_table t2 ,
greek_table t3
where t1.id = t2.id and t1.id = t3.id;
END;
$$LANGUAGE plpgsql;
I ran select choose_letter('alphabet'). The problem with this code is that when id_table joins with alphabet_table, it does not pick up id, No 3. It seems that inner joins are done for both alphabet_table and greek_table (so it only picks up the common ids, 1 and 2). To avoid this problem, I wrote:
CREATE OR REPLACE FUNCTION choose_letters(letter_type text)
RETURNS table (id integer, letter text) AS $$
BEGIN
RETURN QUERY
select t1.id,
case when letter_type = 'alphabet' then t2.letter else t3.letter end as letter
from id_table t1
left join alphabet_table t2 on t1.id=t2.id
left join greek_table t3 on t1.id=t3.id
where t2.letter is not null or t3.letter is not null;
END;
$$LANGUAGE plpgsql;
Now it pick up all the 3 ids when id_table and alphabet_table join. However, When I ran select choose_letter('greek'). The id no. 3 appears with null value in letter column despite the fact that I specified t3.letter is not null in where clause.
What I'm looking for is that when I ran select choose_letters('alphabet'), the output needs to be (1,'a'), (2,'b'),(3,'c'). select choose_letters('greek') should produce (1,'alpha'),(2,'beta). No missing values nor null. How can I accomplish this?

Learn to use proper, explicit JOIN syntax. Simple rule: Never use commas in the FROM clause.
You can do what you want with LEFT JOIN and some other logic:
select i.id,
coalesce(a.letter, g.letter) as letter
from id_table i left join
alphabet_table a
on i.id = a.id and letter_type = 'alphabet' left join
greek_table g
on i.id = g.id and letter_type <> 'alphabet'
where a.id is not null or g.id is not null;
The condition using letter_type needs to be in the on clauses. Otherwise, alphabet_table will always have a match.

Gordon Linoff's answer above is certainly correct, but here is an alternative way to write the code.
It may or may not be better from a performance perspective, but it is logically equivalent. If performance is a concern you'd need to run EXPLAIN ANALYZE on the query an inspect the plan, and do other profiling.
Some good parts about this are the inner join makes the join clause and where clause simpler and easier to reason about. It's also more straight forward for the execution engine to parallelize the query.
On the downside it looks like code duplication, however, the DRY principle is often misapplied to SQL. Repeating code is less important than repeating data reads. What you're aiming to do is not scan the same data multiple times.
If there is no index on the fields you are joining or the letter_type then this could end up doing a full table scan twice, and be worse. If you do have the indexes then it can do it with index range scans nicely.
SELECT
i.id,
a.letter
FROM id_table i
INNER JOIN alphabet_table a
ON i.id = a.id
WHERE letter_type = 'alphabet'
UNION ALL
SELECT
i.id,
g.letter
FROM id_table i
INNER JOIN greek_table g
ON i.id = g.id
WHERE letter_type <> 'alphabet'

The first problem is your tables or not structured properly, You would have created single table like char_table (id int, letter text, type text) type will specify whether it is alphabet or Greek.
Another solution is you can write two SQL queries one in if condition other one is in else part

Related

IF Condition Returning too Many Values

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.

can I pass an argument when I use a view in postgresql?

for instance,
I want to make a view for a particular search,
create view search_in_structure as
select a,b,c,d,e
from
t1 natural inner join t2 natural inner join t3 ...
where
a ilike '%search_string%'
or b ilike '%search_string%'
or c ilike '%search_string%'
or f ilike '%search_string%';
it doesn't make sense because I can't modify search_string. Is there a mechanism to provide a value for search_string so it will execute the view statement with proper modification, something like :
select a,b from search_in_structure where search_string='postgresql 4ever';
if it's not possible, what solution would you recommend me to use and achieve the same result?
The only solution I can think of, would be to make a function (for example, search_in_structure (IN search text, OUT a text, OUT b text ...) returns record) and call it like :
select a,b from (select search_in_structure('postgresql 4ever'));
But as I am still a postgresql noob, I want to have expert suggestions.
A function is the way to go:
create function search_in_structure(p_search_value text)
returns table (a text, b text, c text, d text)
as
$$
select a,b,c,d,e
from t1
natural join t2
natural join t3 ...
where
a ilike '%'||p_search_value||'%'
or b ilike '%'||p_search_value||'%'
or c ilike '%'||p_search_value||'%'
or f ilike '%'||p_search_value||'%'
$$
language sql;
Then you can do:
select *
from search_in_structure('foobar');

How to optimize SELECT DISTINCT when using multiple Joins?

I have read that using cte's you can speed up a select distinct up to 100 times. Link to the website . They have this following example:
USE tempdb;
GO
DROP TABLE dbo.Test;
GO
CREATE TABLE
dbo.Test
(
data INTEGER NOT NULL,
);
GO
CREATE CLUSTERED INDEX c ON dbo.Test (data);
GO
-- Lots of duplicated values
INSERT dbo.Test WITH (TABLOCK)
(data)
SELECT TOP (5000000)
ROW_NUMBER() OVER (ORDER BY (SELECT 0)) / 117329
FROM master.sys.columns C1,
master.sys.columns C2,
master.sys.columns C3;
GO
WITH RecursiveCTE
AS (
SELECT data = MIN(T.data)
FROM dbo.Test T
UNION ALL
SELECT R.data
FROM (
-- A cunning way to use TOP in the recursive part of a CTE :)
SELECT T.data,
rn = ROW_NUMBER() OVER (ORDER BY T.data)
FROM dbo.Test T
JOIN RecursiveCTE R
ON R.data < T.data
) R
WHERE R.rn = 1
)
SELECT *
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
How would one apply this to a query that has multiple joins? For example i am trying to run this query found below, however it takes roughly two and a half minutes. How would I optimize this accordingly?
SELECT DISTINCT x.code
From jpa
INNER JOIN jp ON jpa.ID=jp.ID
INNER JOIN jd ON (jd.ID=jp.ID And jd.JID=3)
INNER JOIN l ON jpa.ID=l.ID AND l.CID=3
INNER JOIN fa ON fa.ID=jpa.ID
INNER JOIN x ON fa.ID=x.ID
1) GROUP BY on every column worked faster for me.
2) If you have duplicates in some of the tables then you can also pre select that and join from that as an inner query.
3) Generally you can nest join if you expect that this join will limit data.
SQL join format - nested inner joins

Comparing data between tables in same database

My requirement is i have to compare data between two different tables with same schema in same database ,
For the moment I am making comparison in different fields in same table and if some validation fails error will be stored in one table like :
IF (NEW.vision IS NULL and new.vispres IS NOT NULL)
THEN INSERT INTO exception_detail( noces,exception) VALUES
(new.no,'please check the values if vision is null then vispres should also be null');
END IF;
The same kind of comparison i want to do with two tables for same element (no) eg
IF (TABLE1.NEW.vispres IS NULL and TABLE2.new.vispres IS NOT NULL)
THEN INSERT INTO exception_detail( noces,exception) VALUES
(new.no,'please check the values if vispres is null for number 5 in table 1 then vispres should also be null for number 5 in Table 2 ');
END IF;
Please help
Thank you in advance
Can I do something like :
SELECT q1.* FROM TABLE1 q1
INNER JOIN TABLE2 q2 ON (q1.noces = q2.noces);
I think it will give all the records from both tables where noces is same
In continuation now i want to compare each row of the output, and if data is not same it must throw exception, IS there a possibility like :
foreach row of above output{
if (q1.name != q2.name)
Do something ;
if (q2.address < q1.address)
Do something ;
}
but it all must be in one query or trigger
I would look at essentially three cases.
ID's the same, but data differs
ID in table_a not found in table_b
ID in table_b not found in table_a
SQL might look like this:
SELECT (r).* FROM ( -- Wrapper query
SELECT a as r
FROM table_a a
JOIN table_b b ON a.id = b.id AND a <> b
UNION ALL
SELECT a as r
FROM table_a a
LEFT JOIN table_b b ON a.id = b.id
WHERE b.id IS NULL
UNION ALL
SELECT b AS r
FROM table_b b
LEFT JOIN table_a a ON a.id = b.id
WHERE a.id IS NULL
) t;
It might be possible to fold this into a case and a full outer join but this should give you the basic idea.

T-SQL A question about inner join table variable

in my stored procedure I have a table variable contains rows ID. The are 2 scenarios - that table variable is empty and not.
declare #IDTable as table
(
number NUMERIC(18,0)
)
In the main query, I join that table:
inner join #IDTable tab on (tab.number = csr.id)
BUT:
as we know how inner join works, I need that my query returns some rows:
when #IDTable is empty
OR
return ONLY rows that exist in
#IDTable
I tried also with LEFT join but it doesn't work. Any ideas how to solve it ?
If `#IDTable' is empty then what rows do you return? Do you just ignore the Join on to the table?
I'm not sure I get what you're trying to do but this might be easier.
if (Select Count(*) From #IDTable) == 0
begin
-- do a SELECT that doesn't join on to the #IDTable
end
else
begin
-- do a SELECT that joins on to #IDTable
end
It is not optimal, but it works:
declare #z table
(
id int
)
--insert #z values(2)
select * from somTable n
left join #z z on (z.id = n.id)
where NOT exists(select 1 from #z) or (z.id is not null)