Query only takes the first value of subquery - sql

I am creating an SQL function to report, the parameters I receive are several strings containing the PK separated by , example:
#ID_Model_list = '1,2'
#ID_Station_list = '1,4,7,8,10'
in my SQL query I perform a subquery, with it I convert the variables I receive into a column, example:
SELECT CAST(VALUE AS INT) AS ID FROM string_split(#ID_Model_list,',')
would be the same as: SELECT CAST(value AS int) AS ID FROM string_split('1,2',',')
Result:
If I add the code above to my query, it only takes the first value of the column that results from the subquery
CREATE FUNCTION V_Scrap_Report
(#ID_Model_list varchar, #ID_Station_list varchar, #fecha datetime, #fechafin datetime)
RETURNS TABLE
AS RETURN
(SELECT S.IDScrap
, S.fecha
, M.modelo
, E.estacion
, C.correccion
, S.elemento
, P.nombre
, P.numeroparte
, Sp.cantidad
FROM dbo.Scrap S
FULL OUTER JOIN dbo.Modelo M ON S.IDModelo = M.IDModelo
FULL OUTER JOIN dbo.Estacion E ON E.IDEstacion = S.IDEstacion
FULL OUTER JOIN dbo.Scrapcorreccion Sc ON S.IDScrap = Sc.IDScrap
FULL OUTER JOIN dbo.Correccion C ON C.IDCorrecion = Sc.IDCorrecion
FULL OUTER JOIN dbo.Scraparte Sp ON S.IDScrap = Sp.IDScrap
JOIN dbo.Parte P ON Sp.IDParte = P.IDParte
WHERE S.fecha >= #fecha
AND S.fecha <= DATEADD(HOUR,23.9999,#fechafin)
AND S.IDModelo = (SELECT CAST(VALUE AS INT) AS ID FROM string_split(#ID_Model_list,','))
AND S.IDEstacion = (SELECT VALUE FROM string_split(#ID_Station_list,',')))
The above function is only returning results when S.IDModelo = 1 AND S.IDEstacion = 1 does not take into account that there is:
S.IDModelo = 2 AND S.IDEstacion = 1
S.IDModelo = 1 AND S.IDEstacion = 4
S.IDModelo = 1 AND S.IDEstacion = 7
S.IDModelo = 1 AND S.IDEstacion = 8
S.IDModelo = 2 AND S.IDEstacion = 10
When I call the function I do it like this:
SELECT * FROM V_Scrap_Report('1,2','1,4,7,8,10','2022-07-18','2022-07-20')
oddish, if i change ... V_Scrap_Report('1,2'... by ... V_Scrap_Report('2,1'... just bring
S.IDModelo = 2 AND S.IDEstacion = 1
what could be missing in the query so as not to skip matches?

The comments and Bohemian's answer give you a few specific things that are wrong with your query that you need to look at, but I think what you really need is a different understanding of what you're doing. So...
A select returns a set. (Technically a bag because it can contain duplicates, but we'll ignore that).
A set can have zero members, or one member, or more members. There's a set of integers greater than 1 and less than 4, and that set is {2, 3}. There's a set of integers less than 1 and greater than 4, and that set is {} aka "the empty set".
So a set is a collection of zero or more things, not one thing.
When you're comparing things, you can't compare a collection of things with just one thing. Suppose I have a bag of oranges in my left hand, and one orange in my right hand. Does the orange in my right hand taste the same as the bag of oranges in my left hand? The question doesn't really make sense, does it? You have to compare the one orange in my right hand with each orange in the bag, individually.
So if I ask SQL to evaluate whether orange_in_right_hand = { first_orange_in_bag, second_orange_in_bag, third_orange_in_bag } what do you want it to do? It's a question that doesn't really make sense.
You have a situation like this in your query:
where S.IDEstacion = (SELECT VALUE FROM string_split(#ID_Station_list,',')))
You do a similar comparison with #ID_model_list.
The left hand side of that operation is one value. The right hand side of that operation is the result of a select, which can return more than one value. In this case, the output of the string_split function. You are asking SQL to determine whether the one thing is equal to potentially many things.
This doesn't really make sense as we saw with the oranges. And because it doesn't make sense, it will actually cause an error.
So why aren't you getting an error? Because in your specific case, the set returned by string_split will happen to only have one member, because you have a bug in the code.
Let's look at #ID_station_list. Your input to the string_split is the #ID_Station_list parameter, which you have said is a varchar. But you didn't say how long it is. In this case, that means it will be treated as being one character long:
declare #ID_station_list varchar;
set #ID_station_list = '1,4,7,8,10';
select #ID_station_list;
What do you think this will return? It will return the string value '1'. All of the other characters got thrown away, because you didn't say #ID_station_list was a varchar big enough to hold the value you gave it. You just said it was a varchar, and SQL will assume you meant varchar(1) in this case.
So the value you are passing to string_split function is just the value '1'. So you get one value back when you split this string.
SQL Server will then look at that and think "well, ok, you are asking me whether a single value is equal to the result of a select, which you really shouldn't be doing because it doesn't make sense, but in this particular case I will do it for you without telling you the problem because there was only one member in the set".
If you fix your parameter declaration by, say, making #ID_station_list a varchar(100) and giving it the value '1,2,3', you'll get an error.
So how should you compare the single value IDEstacion with the set returned by string_split? You tell SQL to check whether the value is in the set instead of checking whether it equals the set. Hence Stu's comment.

If you put where clause conditions on an outer joined table, you effectively convert the join to an inner join.
Move such conditions to the join condition:
...
FROM dbo.Scrap S
FULL OUTER JOIN dbo.Modelo M ON S.IDModelo = M.IDModelo
AND S.IDModelo = (SELECT CAST(VALUE AS INT) AS ID FROM string_split(#ID_Model_list,','))
FULL OUTER JOIN dbo.Estacion E ON E.IDEstacion = S.IDEstacion
AND S.IDEstacion = (SELECT VALUE FROM string_split(#ID_Station_list,',')))
FULL OUTER JOIN dbo.Scrapcorreccion Sc ON S.IDScrap = Sc.IDScrap
FULL OUTER JOIN dbo.Correccion C ON C.IDCorrecion = Sc.IDCorrecion
FULL OUTER JOIN dbo.Scraparte Sp ON S.IDScrap = Sp.IDScrap
JOIN dbo.Parte P ON Sp.IDParte = P.IDParte
WHERE S.fecha >= #fecha
AND S.fecha <= DATEADD(HOUR,23.9999,#fechafin)

Related

Should I use an SQL full outer join for this?

Consider the following tables:
Table A:
DOC_NUM
DOC_TYPE
RELATED_DOC_NUM
NEXT_STATUS
...
Table B:
DOC_NUM
DOC_TYPE
RELATED_DOC_NUM
NEXT_STATUS
...
The DOC_TYPE and NEXT_STATUS columns have different meanings between the two tables, although a NEXT_STATUS = 999 means "closed" in both. Also, under certain conditions, there will be a record in each table, with a reference to a corresponding entry in the other table (i.e. the RELATED_DOC_NUM columns).
I am trying to create a query that will get data from both tables that meet the following conditions:
A.RELATED_DOC_NUM = B.DOC_NUM
A.DOC_TYPE = "ST"
B.DOC_TYPE = "OT"
A.NEXT_STATUS < 999 OR B.NEXT_STATUS < 999
A.DOC_TYPE = "ST" represents a transfer order to transfer inventory from one plant to another. B.DOC_TYPE = "OT" represents a corresponding receipt of the transferred inventory at the receiving plant.
We want to get records from either table where there is an ST/OT pair where either or both entries are not closed (i.e. NEXT_STATUS < 999).
I am assuming that I need to use a FULL OUTER join to accomplish this. If this is the wrong assumption, please let me know what I should be doing instead.
UPDATE (11/30/2021):
I believe that #Caius Jard is correct in that this does not need to be an outer join. There should always be an ST/OT pair.
With that I have written my query as follows:
SELECT <columns>
FROM A LEFT JOIN B
ON
A.RELATED_DOC_NUM = B.DOC_NUM
WHERE
A.DOC_TYPE IN ('ST') AND
B.DOC_TYPE IN ('OT') AND
(A.NEXT_STATUS < 999 OR B.NEXT_STATUS < 999)
Does this make sense?
UPDATE 2 (11/30/2021):
The reality is that these are DB2 database tables being used by the JD Edwards ERP application. The only way I know of to see the table definitions is by using the web site http://www.jdetables.com/, entering the table ID and hitting return to run the search. It comes back with a ton of information about the table and its columns.
Table A is really F4211 and table B is really F4311.
Right now, I've simplified the query to keep it simple and keep variables to a minimum. This is what I have currently:
SELECT CAST(F4211.SDDOCO AS VARCHAR(8)) AS SO_NUM,
F4211.SDRORN AS RELATED_PO,
F4211.SDDCTO AS SO_DOC_TYPE,
F4211.SDNXTR AS SO_NEXT_STATUS,
CAST(F4311.PDDOCO AS VARCHAR(8)) AS PO_NUM,
F4311.PDRORN AS RELATED_SO,
F4311.PDDCTO AS PO_DOC_TYPE,
F4311.PDNXTR AS PO_NEXT_STATUS
FROM PROD2DTA.F4211 AS F4211
INNER JOIN PROD2DTA.F4311 AS F4311
ON F4211.SDRORN = CAST(F4311.PDDOCO AS VARCHAR(8))
WHERE F4211.SDDCTO IN ( 'ST' )
AND F4311.PDDCTO IN ( 'OT' )
The other part of the story is that I'm using a reporting package that allows you to define "virtual" views of the data. Virtual views allow the report developer to specify the SQL to use. This is the application where I am using the SQL. When I set up the SQL, there is a validation step that must be performed. It will return a limited set of results if the SQL is validated.
When I enter the query above and validate it, it says that there are no results, which makes no sense. I'm guessing the data casting is causing the issue, but not sure.
UPDATE 3 (11/30/2021):
One more twist to the story. The related doc number is not only defined as a string value, but it contains leading zeros. This is true in both tables. The main doc number (in both tables) is defined as a numeric value and therefore has no leading zeros. I have no idea why those who developed JDE would have done this, but that is what is there.
So, there are matching records between the two tables that meet the criteria, but I think I'm getting no results because when I convert the numeric to a string, it does not match, because one value is, say "12345", while the other is "00012345".
Can I pad the numeric -> string value with zeros before doing the equals check?
UPDATE 4 (12/2/2021):
Was able to finally get the query to work by converting the numeric doc num to a left zero padded string.
SELECT <columns>
FROM PROD2DTA.F4211 AS F4211
INNER JOIN PROD2DTA.F4311 AS F4311
ON F4211.SDRORN = RIGHT(CONCAT('00000000', CAST(F4311.PDDOCO AS VARCHAR(8))), 8)
WHERE F4211.SDDCTO IN ( 'ST' )
AND F4311.PDDCTO IN ( 'OT' )
AND ( F4211.SDNXTR < 999
OR F4311.PDNXTR < 999 )
You should write your query as follows:
SELECT <columns>
FROM A INNER JOIN B
ON
A.RELATED_DOC_NUM = B.DOC_NUM
WHERE
A.DOC_TYPE IN ('ST') AND
B.DOC_TYPE IN ('OT') AND
(A.NEXT_STATUS < 999 OR B.NEXT_STATUS < 999)
LEFT join is a type of OUTER join; LEFT JOIN is typically a contraction of LEFT OUTER JOIN). OUTER means "one side might have nulls in every column because there was no match". Most critically, the code as posted in the question (with a LEFT JOIN, but then has WHERE some_column_from_the_right_table = some_value) runs as an INNER join, because any NULLs inserted by the LEFT OUTER process, are then quashed by the WHERE clause
See Update 4 for details of how I resolved the "data conversion or mapping" error.

Write an additional column to query result with different values everytime

I've been searching for quite a while now and I haven't been able to find an answer for what I was looking. I have the following query:
SELECT DISTINCT o.titulo, o.fecha_estreno
FROM Obra o
WHERE (o.titulo LIKE '%Barcelona%' AND EXISTS(SELECT p.id_obra FROM Pelicula p WHERE p.id_obra = o.id_obra)) OR EXISTS(SELECT DISTINCT pa.id_obra
FROM Participa pa
WHERE pa.id_obra = o.id_obra AND EXISTS(SELECT DISTINCT l.nombre FROM Lugar l
WHERE l.nombre LIKE '%Barcelona%' AND EXISTS(SELECT DISTINCT tl.id_lugar FROM TieneLugar tl
WHERE tl.id_lugar = l.id_lugar AND tl.id_profesional = pa.id_profesional))) OR EXISTS(SELECT DISTINCT er.id_obra
FROM EstaRelacionado er
WHERE er.id_obra = o.id_obra AND EXISTS(SELECT DISTINCT k.keyword
FROM Keywords k
WHERE k.id_keyword = er.id_keyword AND k.keyword LIKE '%Barcelona%'));
What it basically does is it searches for every movie in my database which is related in some way to the city it gets. I wanted to have a third column showing for every result, with the reason the row is showing as a result (for example: TITLE CONTAINS IT, or ACTOR FROM THE MOVIE BORN THERE, etc.)
Thank you for your patience and help!
EDIT: As suggested, here are some examples of output. The column should show just the first cause related to the movie:
TITULO FECHA_ESTRENO CAUSE
---------- ---------------- ----------
Barcelona mia 1967 TITLE
https://www.postgresql.org/docs/7.4/static/functions-conditional.html
The SQL CASE expression is a generic conditional expression, similar
to if/else statements in other languages:
CASE WHEN condition THEN result
[WHEN ...]
[ELSE result]
END
CASE clauses can be used wherever an expression is valid. condition is an expression that returns a boolean result. If
the result is true then the value of the CASE expression is the result
that follows the condition. If the result is false any subsequent WHEN
clauses are searched in the same manner. If no WHEN condition is true
then the value of the case expression is the result in the ELSE
clause. If the ELSE clause is omitted and no condition matches, the
result is null.
Example for your case:
SELECT (CASE WHEN EXISTS(... l.nombre LIKE '%Barcelona%') THEN 'TITLE CONTAINS IT' WHEN <conditon for actor> THEN 'ACTOR WA BORN THERE' WHEN ... END) as reason
Here is one solution.
Create a subquery for each search condition.
include the reason in the subqueries' projections
outer join the subqueries so it doesn't matter which one hist
filter to make sure that at least one of your subqueries has a positive result
use coalesce() to get one reason.
I haven't done all your conditions, and I've probably mangled your logic but this is the general idea:
SELECT o.titulo
, o.fecha_estreno
, coalesce(t1.reason, t2.reason) as reason
FROM Obra o
left outer join ( select id_obra, 'title contains it' as reason
from Obra
where titulo LIKE '%Barcelona%' ) t1
on t1.id_obra o.id_obra
left outer join ( select distinct pa.id_obra , 'takes place there' as reason
from Participa pa
join TieneLugar tl
on tl.id_profesional = pa.id_profesional
join Lugar l
on tl.id_lugar = l.id_lugar
where l.nombre LIKE '%Barcelona%' ) t2
on t2.id_obra o.id_obra
WHERE t1.id_obra is not null
or t2.id_obra is not null
/
coalesce() just returns the first non-null value which means you won't see multiple reasons if you get more than one hit. So order the arguments to put the most powerful reasons first.
Also, you should consider consider using Oracle Text. It's the smartest way to wrangle this sort of keyword searching. Find out more.

How to return a potential NULL in SQL while performing a comparison on the potentially null value

Developing a query that will return information about an item stored across 4 tables. When all the fields have values my query works fine, however some of the data has null fields (which I can't change) that I need to perform my comparison on. When these show up, the row doesn't show up in the query results, even if all the other fields have values.
Here is what I have so far:
select [Item_NO], [Color_Name], [size_code], [style_code], [PERM_UNIT_PRICE]
FROM [USICOAL].[dbo].[ITEM], [USICOAL].[dbo].[COLOR], [USICOAL].[dbo].[SIZE], [USICOAL].[dbo].[STYLE]
where [ITEM_NO] in ('191202002944', '191202003026')
AND [USICOAL].[dbo].[ITEM].[COLOR_ID] = [USICOAL].[dbo].[COLOR].[COLOR_ID]
AND [USICOAL].[dbo].[ITEM].[SIZE_ID] = [USICOAL].[dbo].[SIZE].[SIZE_ID]
AND [USICOAL].[dbo].[ITEM].[STYLE_ID] = [USICOAL].[dbo].[STYLE].[STYLE_ID]
For these 2 items numbers, the Size_ID field is null. How can I get the results to reflect this null?
SELECT
[Item_NO]
,[Color_Name]
,[size_code]
,[style_code]
,[PERM_UNIT_PRICE]
FROM
[USICOAL].[dbo].[ITEM] i
LEFT OUTER JOIN
[USICOAL].[dbo].[COLOR] c
ON c.[COLOR_ID] = i.[COLOR_ID]
LEFT OUTER JOIN
[USICOAL].[dbo].[SIZE] s
ON s.[SIZE_ID] = i.[SIZE_ID]
LEFT OUTER JOIN
[USICOAL].[dbo].[STYLE] t
ON t.[STYLE_ID] = i.[STYLE_ID]
WHERE
[ITEM_NO] in ('191202002944', '191202003026')

trouble with IN clause in sql

SELECT bm.WinningNumber,bd.BetOnNumber,"WinLossAmount" =
CASE
WHEN 2 in (2,1) THEN ('WIN')
END
FROM BettingMaster bm inner join BettingDetail bd on bm.GameID = bd.GameID
where bd.GameID = 1
This works as a Charm and I get 'WIN' in WinLossAmount. Now I actually have value 2 in column WinningNumber(varchar) and value 2,1 in column BetOnNumber(varchar). I retried the statement as
SELECT bm.WinningNumber,bd.BetOnNumber,"WinLossAmount" =
CASE
WHEN bm.WinningNumber in (bd.BetOnNumber) THEN ('WIN')
END
FROM BettingMaster bm inner join BettingDetail bd on bm.GameID = bd.GameID
where bd.GameID = 1
This doesn't work. Is it not possible this way?! Any help??
IN operator in SQL searches values in the row set. But in the second example you try to find INT value in the STRING so you can't use IN this way.
In the most SQL systems you can try to use following condition. Where + is a concatenate operator in MSSQL (in Oracle ||, in MySQL CONCAT())
WHEN ','+bd.BetOnNumber+',' LIKE '%,'+CAST(bm.WinningNumber AS VARCHAR(10))+',%'
THEN ...
Also in MySQL you can use FIND_IN_SET() function:
WHEN FIND_IN_SET(bm.WinningNumber,bd.BetOnNumber) THEN ...
PS: Both ways can't use indexes so you shouldn't use them on big tables.
I believe the problem is that in the first statement, SQL is comparing 2 to 1 and then 2 to 2, all integers. Obviously, finding the match and working.
In the second case, you're comparing 2 to a string of "1,2" and 2 does not equal "1,2". You'll need to split the varchar into a series of integers before you compare it. Take a look at:
Parse comma-separated string to make IN List of strings in the Where clause
This should help you out.
You can't just use IN and then specify a column name, you need to use a subquery:
SELECT bm.WinningNumber ,
bd.BetOnNumber ,
"WinLossAmount" = CASE WHEN bm.WinningNumber IN (
SELECT bd.BetOnNumber
FROM BettingMaster bm
INNER JOIN BettingDetail bd ON bm.GameID = bd.GameID
WHERE bd.GameID = 1 ) THEN ( 'WIN' )
END
FROM BettingMaster bm
INNER JOIN BettingDetail bd ON bm.GameID = bd.GameID
WHERE bd.GameID = 1

Why does my sql logic fail first time it runs but not second time?

I have this sql:
SELECT
sa.answertext
FROM dbo.tblsurveyanswer sa
INNER JOIN dbo.tblsurvey s
ON s.surveyid = sa.surveyid
INNER JOIN dbo.tblquestion q
ON q.questionid = sa.questionid
INNER JOIN dbo.tblshqscriteria c
ON c.shqscriteriaid = q.shqscriteriaid
INNER JOIN dbo.tblshqsdescription d
ON d.shqsdescriptionid = q.shqsdescriptionid
INNER JOIN dbo.tblrepairpercentage rp
ON rp.repairpercentageid = sa.repairpercentageid
WHERE
(c.shqscriteria = 'qwerty')
OR
(c.shqscriteria = 'zxcvb' AND ISNUMERIC(sa.answertext) = 1 AND CAST(sa.answertext AS float) < 5)
First time I execute it fails with "Error converting data type varchar to float."
Second time* I execute it succeeds - returning no rows because there are no 'qwerty' or 'zxcvb' shqscriteria
*actually sometimes I have to hit execute up to 8 times before I get the failure
The order in which predicates are evaluated in SQL Server is not guaranteed (and certainly not guaranteed to be from left to right).
So:
c.shqscriteria = 'zxcvb' AND ISNUMERIC(sa.answertext) = 1 AND CAST(sa.answertext AS float) < 5
It may attempt the CAST operation on a value for which ISNUMERIC() returns 0. The actual order in which the predicates are evaluated can vary for many different reasons - as you've discovered, the slightest thing can change it.
Also, ISNUMERIC is a mostly useless function. CAST('£' as float) will fail, but ISNUMERIC('£') is 1. It answers the question no-one has ever asked - "Can I convert this string into even just one of the numeric datatypes (I don't care which one)?"
The way to resolve this kind of issue is to split your query into two parts - the first of which selects appropriate data for conversion and stores the values in a (temp table/table variable), the second part actually performs the conversion. Or if you're on SQL Server 2012 or later, look into the TRY_ conversion functions:
c.shqscriteria = 'zxcvb' AND TRY_CAST(sa.answertext AS float) < 5
As long as you're not using aggregates, you should be able to rely on CASE's order of execution:
WHERE
...
OR
(
c.shqscriteria = 'zxcvb'
AND 5 > CASE WHEN ISNUMERIC(sa.answertext) = 1
THEN CAST(sa.answertext AS float)
ELSE 6
END
)
Note however that ISNUMERIC(col) = 1 does not always mean that col is eligible to be converted to a specific numeric type.