SQL query with two EXISTS statements behaving differently than expected - sql

The following SQL query is intended to find label_item_lists which have label_items with given names.
SELECT lils.id FROM label_item_lists AS lils
INNER JOIN label_items AS items ON lils.id = items.label_item_list_id
WHERE EXISTS(SELECT * FROM label_item_lists WHERE items.name=?)
OR EXISTS(SELECT * FROM label_item_lists WHERE items.name=?)
It properly returns the ids of label_item_lists having an item with either name. However, the same query using the AND operator rather than OR returns no results.
SELECT lils.id FROM label_item_lists AS lils
INNER JOIN label_items AS items ON lils.id = items.label_item_list_id
WHERE EXISTS(SELECT * FROM label_item_lists WHERE items.name=?)
AND EXISTS(SELECT * FROM label_item_lists WHERE items.name=?)
There are definitely label_item_list entries that have label_items matching both names provided. In fact the OR SQL query returns the id twice for these entries, but the AND query returns no results. For this reason I think I might be missing an important piece of info on how these JOINed queries with EXISTS work. Thanks for any assistance!
----------------------------------------------------------------
| label_items | id | name | label_item_list_id |
----------------------------------------------------------------
| Row1 | 1 | foo | 1 |
----------------------------------------------------------------
| Row2 | 2 | bar | 1 |
----------------------------------------------------------------
| Row3 | 3 | bar | 2 |
----------------------------------------------------------------
--------------------------------
| label_item_lists | id |
--------------------------------
| Row1 | 1 |
--------------------------------
| Row2 | 2 |
--------------------------------
I want to return the first label_item_list but not the second, as it only has one of the two names I am searching for, 'foo' and 'bar'.

try changing the where condition from
WHERE EXISTS(SELECT * FROM label_item_lists WHERE items.name=?)
AND EXISTS(SELECT * FROM label_item_lists WHERE items.name=?)
to
WHERE EXISTS(SELECT * FROM label_item_lists lst WHERE lst.name=?)
AND EXISTS(SELECT * FROM label_item_lists lst WHERE lst.name=?)

In your query AND will not return anything because on same output row it will apply both filters which will never happen hence it is giving blank output.
And Or operator will never check condition after OR operator until first condition is false.
Try something like this, # is just a place holder to distinguish between two searches:
select * from label_items lil
where label_item_list_id
in (
select li.label_item_list_id from
label_items li
inner join label_items l1
on li.label_item_list_id = l1.label_item_list_id
and li.name <> l1.name
where concat(li.name,'#',l1.name) = 'foo#bar')

This is what I eventually came up with! I'm not 100% confident yet, but it has worked so far. I added a bit of functionality in Ruby and ActiveRecord to allow for as many necessary matches as desired and to return only those which match exactly (without any extra names not in the list).
items = ["foo", "bar"]
db = ActiveRecord::Base.connection
query = <<-EOS
SELECT lils.id FROM label_item_lists AS lils
JOIN label_items AS items ON items.label_item_list_id = lils.id
WHERE lils.id IN (
SELECT label_item_list_id FROM label_items AS items
WHERE items.name IN (#{(['?'] * items.length).join(',')})
) AND lils.id NOT IN (
SELECT label_item_list_id FROM label_items AS items
WHERE items.name NOT IN (#{(['?'] * items.length).join(',')})
)
GROUP BY lils.id
HAVING COUNT(DISTINCT items.name) = #{items.length}
EOS
query = ActiveRecord::Base.send(:sanitize_sql_array, [query, *(items*2)])
Basically it checks that a name is both IN the list of given names (items array) AND it also checks that the name IS NOT outside (or NOT IN) the list of given names. Any list of label_items that has a non-matching name is excluded by the latter IN query. This is helpful so that a label_item_list with both "foo" and "bar" but also "lorem" is not included.

Related

Remove duplicates after joining three different tables

I'm working now on SQL query for Polarion widget, where I want get items with unresolved comments.
Unfortunately, I have a problem with joining three tables where one has optional empty/null.
In Polarion is a few different tables to contain comments, most of them has a resolved status. I'm using WORKITEM, COMMENT, MODULECOMMENT tables, because sometimes workitem has comment here, sometimes in another table.
Difference is one: when workitem has "normal" comment, then there is always FK key in COMMENT table, but not each workitem has a FK MODULE key (table structure).
In default widget, I can use SQL query with a one big limit:
I can't replace whole select and from, because there is a static line: SELECT WORKITEM.C_URI and FROM WORKITEM, so I can only add something (I tried DISTINCT ON, but how can you see, I can't replace that line)
Polarion has another problem too, each document has headings, texts etc. and each of this element is separate workitem with the same FK_URI_MODULE (and documents use MODULECOMMENT table).
I want remove duplicates from MODULECOMMENT (ignore situation where workitem.FK_URI_MODULE is empty/null, because it's normal state).
The best what I created, It's here (in this situation I replace is null to is not null, because It's return less items):
select workitem.c_uri, workitem.FK_URI_MODULE
from workitem
left join COMMENT on COMMENT.FK_URI_WORKITEM = WORKITEM.C_URI
left join MODULECOMMENT on MODULECOMMENT.FK_URI_MODULE = WORKITEM.FK_URI_MODULE
where true
and workitem.fk_uri_project = 231
and (comment.c_resolved is not null
or modulecomment.c_resolved is not null);
and my results:
c_uri | fk_uri_module
--------+---------------
7952 | 7940
7949 | 7940
7953 | 7940
7964 | 7940
124141 | 124138
609 |
1347 |
609 |
but I want get something like that:
c_uri | fk_uri_module
--------+---------------
7952 | 7940
124141 | 124138
609 |
1347 |
609 |
Don't generate the duplicates to begin with by not joining, but using an EXISTS condition:
select workitem.c_uri, workitem.fk_uri_module
from workitem
where exists (select *
from comment
left join modulecomment on modulecomment.fk_uri_module = workitem.fk_uri_module
where comment.fk_uri_workitem = workitem.c_uri
and (comment.c_resolved is not null
or modulecomment.c_resolved is not null))
where workitem.fk_uri_project = 231
You can use DISTINCT ON for this, which returns only the first record of a group:
demos:db<>fiddle
select DISTINCT ON (workitem.FK_URI_MODULE)
workitem.c_uri, workitem.FK_URI_MODULE
from workitem
left join COMMENT on COMMENT.FK_URI_WORKITEM = WORKITEM.C_URI
left join MODULECOMMENT on MODULECOMMENT.FK_URI_MODULE = WORKITEM.FK_URI_MODULE
where true
and workitem.fk_uri_project = 231
and (comment.c_resolved is not null
or modulecomment.c_resolved is not null)
The interesting part is the NULL records. The query above returns only one of the NULL records, because DISTINCT ON also removes duplicates for NULL.
So, if you want to keep all NULL values, we need to treat the NULL records differently:
SELECT DISTINCT ON (fk_uri_module) -- 1
*
FROM <your_query>
WHERE fk_uri_module IS NOT NULL
UNION ALL
SELECT -- 2
*
FROM <your_query>
WHERE fk_uri_module IS NULL
Apply DISTINCT ON on all records with non-NULL values
UNION ALL records with NULL afterwards
To avoid executing your entire query twice, you can move them into a CTE (WITH clause) and reference it later:
WITH my_result AS (
-- <your_query>
)
SELECT DISTINCT ON (fk_uri_module)
*
FROM my_result
WHERE fk_uri_module IS NOT NULL
UNION ALL
SELECT
*
FROM my_result
WHERE fk_uri_module IS NULL
Edit:
demo:db<>fiddle
If you cannot use DISTINCT ON, a valid alternative for this is the row_number() window function:
SELECT
*
FROM (
select
workitem.c_uri, workitem.FK_URI_MODULE,
row_number() OVER (PARTITION BY workitem.FK_URI_MODULE)
from workitem
left join COMMENT on COMMENT.FK_URI_WORKITEM = WORKITEM.C_URI
left join MODULECOMMENT on MODULECOMMENT.FK_URI_MODULE = WORKITEM.FK_URI_MODULE
where true
and workitem.fk_uri_project = 231
and (comment.c_resolved is not null
or modulecomment.c_resolved is not null)
) s
WHERE row_number = 1

Join Tables to return 1 or 0 based on multiple conditions

I am working on a project management website and have been asked for a new feature in a review meeting section.
A meeting is held to determine whether to proceed to the next phase, and I need to maintain a list of who attended each phase review meeting. I need to write an SQL query to return all people, with an additional column that states they have already been added before.
There are two tables involved to get my desired result, with the relevant columns listed below:
Name: PersonList
ID | Name | Division
Name: reviewParticipants
ProjectID | PersonID | GateID
The query I am looking for is something that returns all people in PersonList, with an additional "hasAttended" bit that is TRUE if reviewParticipants.ProjectID = 5 AND reviewParticpants.CurrentPhase = 'G0' ELSE FALSE.
PersonName | PersonID | hasAttended
Mr Smith | 1 | 1
Mr Jones | 2 | 0
I am not sure how to structure such a query with multiple conditions in a (left?) join, that would return as a different column name and data type, so I would appreciate if anybody can point me in the right direction?
With the result of this query I am going to add a series of checkboxes, and use this additional bit to mark it checked, or not, for page refreshes.
You can use LEFT JOIN as well:
SELECT DISTINCT p.*
,CASE WHEN rp.id IS NOT NULL THEN 1 ELSE 0 END AS hasAttended
FROM personlist p
LEFT JOIN reviewParticipants rp ON rp.personid = p.id
AND rp.projectid = 5
AND rp.currentphase = 'GO'
I agree with Gordon Linoff: I would prefer an int or tinyint over a bit value,
You can use exists to see if there is a matching row.
select p.*,
(case when exists (select 1
from reviewParticipants rp
where rp.personid = p.id and
rp.projectid = 5 and
rp.currentphase = 'GO'
)
then 1 else 0 end)
from personlist p;
I see no reason to prefer a bit over an integer, but you can return a bit if you really prefer.
This will do :
select a.* from PersonList a where a.hasAttended=1 and
a.Id in (select b.PersonId from reviewParticipants b
where b.ProjectID =5 and exists (
select 1 from reviewParticipants c where c.CurrentPhase = 'G0'and
c.Project =b.projectId
)
)

Update inner join result Oracle

In my java code I have foreach loop which iterates though list
foreach(MyObject obj:list){
String status = obj.getStatus();
String is = obj.getId();
// DB call
1. To update Status in Table A
jdbcobj.updtastatus(status,id);
2. Get status from table B
String tableBStatu= jdbcobj.getstatufromtableB(status,id):
obj.setStatus(tableBStatus):
}
To avoid 2 dB calls in for loop I am using inner join and trying to achieve same output as above
I am using inner-join and get the new result set based on common field.I want to update the result set but I am unable to figure out how?
I have two tables "A" and "B".
Table "A" has columns id,name,statusA
Table "B" has columns id,city,statusB
As stated at start, I am using inner-join and my query looks like this.
Select A.id A.statusA,B.statusB FROM A INNER JOIN ON B where A.id=B.id
Which gives me result as "id", status from table "A" and status from table "B".
Now i want use the inner-join result, to update statusA column from table "A" and set value ="DONE"
And want to use the statusB column value in java object.
String statusfromColumnB = get statusB col value
and set in my java object like this
myObj.setStatus(statusfromColumnB)
Sample Data
Suggest a solution.
If I understand you correctly, an Oracle MERGE query could properly respond to your need :
Consider :
MERGE INTO A
USING B ON (A.id = B.id)
WHEN MATCHED THEN UPDATE SET A.statusA = B.statusB
This query will update the status in table A from that of the corresponding record in table B.
Oracle merge is a vendor-specific statement that is optimized for multi-rows upserts (inserts/updates).
Demo on DB Fiddle :
Select A.id, A.statusA, B.statusB FROM A INNER JOIN B ON A.id=B.id
ID | STATUSA | STATUSB
-: | :------ | :--------
1 | Pending | Initiated
2 | Pending | Completed
MERGE INTO A
USING B ON (A.id = B.id)
WHEN MATCHED THEN UPDATE SET A.statusA = B.statusB
2 rows affected
Select A.id, A.statusA, B.statusB FROM A INNER JOIN B ON A.id=B.id
ID | STATUSA | STATUSB
-: | :-------- | :--------
1 | Initiated | Initiated
2 | Completed | Completed
If you want to set statusA to a fixed value instead, then you could go :
MERGE INTO A
USING B ON (A.id = B.id)
WHEN MATCHED THEN UPDATE SET A.statusA = 'Finished'
Do you want something like this?
update a
set (status, somewhereelsecolumn) =
(select 'DONE', <whatever>
from b
where A.id = B.id
)
where exists (select 1 from b where a.id = b.id);

LIKE clause INNER JOIN

I have a problem with 2 tables:
table A
IDA | Description
1 | BIG_MAN
2 | BIG_MANCA
3 | WEB_BrowserCOM
4 | WEB_BrowserCO
5 | Other
Table B
IDB | FileName
1 | BIG_MAN_98267828_29292
2 | BIG_MANCA_8282836662_92992
3 | WEB_BrowserCO_7263562_82828
4 | WEB_Browser_28828288_826662
5 | WEB_BrowserCOM_9374664_9933
What I would like to do is get the IDA doing a inner join with table B:
;WITH FileDetails AS
(
SELECT
FL.IDA,
FL.Description
FROM TableA FL
WHERE IsActive = 1
)
SELECT
FD.IDA,
FLL.FileName
FD.Description
FROM TableB FLL
INNER JOIN TableA FD
ON FLL.FileName LIKE (FD.Filename)+'%'
However I get:
IDA | FileName |Description
1 | BIG_MAN_98267828_29292 |BIG_MAN
1 | BIG_MANCA_8282836662_92992 |BIG_MAN
4 | WEB_BrowserCO_7263562_82828 |WEB_BrowserCO
4 | WEB_Browser_28828288_826662 |WEB_BrowserCO
4 | WEB_BrowserCOM_9374664_9933 |WEB_BrowserCO
Any idea to solve this and get the correct IDA?
The problem you have is that you're not including a trailing underscore in your like clause. Try this:
WITH FileDetails AS
(
SELECT
FL.IDA,
FL.Description
FROM TableA FL
WHERE IsActive = 1
)
SELECT
FD.IDA,
FLL.FileName
FD.Description
FROM TableB FLL
INNER JOIN TableA FD
ON FLL.FileName LIKE (FD.Filename)+'$_%' escape '$'
I suspect that you just need a better like pattern:
SELECT FD.IDA, FLL.FileName, FD.Description
FROM TableB FLL INNER JOIN
TableA FD
ON FLL.FileName LIKE FD.Filename + '%$_%' ESCAPE '$';
This looks for the name followed by an underscore. The ESCAPE is needed because '_' is a wildcard.
Depending on your data you may find the answers already provided are sufficient, but I wasn't keen on them ...
They make two assumptions which may not hold:
The underscores within your "descriptions" will not create a match with other data by being interpreted as a wildcard.
Add to TableB a filename of 'BIGAMAN_123456_123456' to illustrate the issue.
When one description matches another except for a "tail" that tail doesn't begin with an underscore.
Add to TableA a description of 'BIG_MAN_7' to illustrate the issue.
If SQL-Server had good native handling of RegEx objects, I'd make a suggestion along those lines, but since it doesn't:...
SELECT
FD.IDA,
FLL.FileName,
FD.Description
FROM TableB FLL
INNER JOIN #TableA FD ON
left(FLL.FileName,len(fd.description)+1) = fd.description + '_' AND
len(replace(fll.FileName,fd.description + '_','')) -
len(replace(replace(fll.FileName,fd.description + '_',''),'_','')) = 1
The first part of the join does the same job as the like versions but avoids treating any subtrings as wildcards. The second part takes the remainder of the filename, and checks that it contains only one underscore.

Is there a simpler way to write this query? [MS SQL Server]

I'm wondering if there is a simpler way to accomplish my goal than what I've come up with.
I am returning a specific attribute that applies to an object. The objects go through multiple iterations and the attributes might change slightly from iteration to iteration. The iteration will only be added to the table if the attribute changes. So the most recent iteration might not be in the table.
Each attribute is uniquely identified by a combination of the Attribute ID (AttribId) and Generation ID (GenId).
Object_Table
ObjectId | AttribId | GenId
32 | 2 | 3
33 | 3 | 1
Attribute_Table
AttribId | GenId | AttribDesc
1 | 1 | Text
2 | 1 | Some Text
2 | 2 | Some Different Text
3 | 1 | Other Text
When I query on a specific object I would like it to return an exact match if possible. For example, Object ID 33 would return "Other Text".
But if there is no exact match, I would like for the most recent generation (largest Gen ID) to be returned. For example, Object ID 32 would return "Some Different Text". Since there is no Attribute ID 2 from Gen 3, it uses the description from the most recent iteration of the Attribute which is Gen ID 2.
This is what I've come up with to accomplish that goal:
SELECT attr.AttribDesc
FROM Attribute_Table AS attr
JOIN Object_Table AS obj
ON obj.AttribId = obj.AttribId
WHERE attr.GenId = (SELECT MIN(GenId)
FROM(SELECT CASE obj2.GenId
WHEN attr2.GenId THEN attr2.GenId
ELSE(SELECT MAX(attr3.GenId)
FROM Attribute_Table AS attr3
JOIN Object_Table AS obj3
ON obj3.AttribId = attr3.AttribId
WHERE obj3.AttribId = 2
)
END AS GenId
FROM Attribute_Table AS attr2
JOIN Object_Table AS obj2
ON attr2.AttribId = obj2.AttribId
WHERE obj2.AttribId = 2
) AS ListOfGens
)
Is there a simpler way to accomplish this? I feel that there should be, but I'm relatively new to SQL and can't think of anything else.
Thanks!
The following query will return the matching value, if found, otherwise use a correlated subquery to return the value with the highest GenId and matching AttribId:
SELECT obj.Object_Id,
CASE WHEN attr1.AttribDesc IS NOT NULL THEN attr1.AttribDesc ELSE attr2.AttribDesc END AS AttribDesc
FROM Object_Table AS obj
LEFT JOIN Attribute_Table AS attr1
ON attr1.AttribId = obj.AttribId AND attr1.GenId = obj.GenId
LEFT JOIN Attribute_Table AS attr2
ON attr2.AttribId = obj.AttribId AND attr2.GenId = (
SELECT max(GenId)
FROM Attribute_Table AS attr3
WHERE attr3.AttribId = obj.AttribId)
In the case where there is no matching record at all with the given AttribId, it will return NULL. If you want to get no record at all in this case, make the second JOIN an INNER JOIN rather than a LEFT JOIN.
Try this...
Incase the logic doesn't find a match for the Object_table GENID it maps it to the next highest GENID in the ON clause of the JOIN.
SELECT AttribDesc
FROM object_TABLE A
INNER JOIN Attribute_Table B
ON A.AttrbId = B.AttrbId
AND (
CASE
WHEN A.Genid <> B.Genid
THEN (
SELECT MAX(C.Genid)
FROM Attribute_Table C
WHERE A.AttrbId = C.AttrbId
)
ELSE A.Genid
END
) -- Selecting the right GENID in the join clause should do the job
= B.Genid
This should work:
with x as (
select *, row_number() over (partition by AttribId order by GenId desc) as rn
from Attribute_Table
)
select isnull(a.attribdesc, x.attribdesc)
from Object_Table o
left join Attribute_Table a
on o.AttribId = a.AttribId and o.GenId = a.GenId
left join x on o.AttribId = x.AttribId and rn = 1