SQL: Find entries with matching criteria in different table - sql

I have two tables, Event and EventTag
CREATE TABLE event (
id INT PRIMARY KEY,
content TEXT
)
CREATE TABLE event_tag (
event_id INT,
type VARCHAR(255),
value VARCHAR(255)
)
Each event has zero or more tags. The query I'd like to express in SQL is:
Give me all Event (all columns in the table) that have associated tags with EventTag.type="foo" and EventTag.value="bar".
This is easy for one tag criterion (for example, with a join and a where, as answered here), but how do I tackle the situation of two or more criteria? So: Give me the events that have an associated tag "foo" equal to "bar" and (!) an event tag "qux" equal to "quux"? I thought about joining the tag table 'n' times, but I'm not sure if it's a good idea.

The best way to solve this problem is to not use the EAV database model (Entity-Attribute-Value). You're running into just the first of many problems with this anti-pattern. A quick Google search on "EAV model" should reveal some of the other problems in store for you if you choose not to redesign. Normally your Event table should have a column for foo and a column for qux.
One possible solution that you can use, if you insist (or are forced) to go down this path:
SELECT id, content
FROM Event
WHERE id IN
(
SELECT
E.id
FROM
Event E
INNER JOIN Event_Tag T ON
T.event_id = E.id AND
(
(T.type = 'foo' AND T.value = 'bar') OR
(T.type = 'qux' AND T.value = 'quux')
)
GROUP BY
E.id
HAVING
COUNT(*) = 2
)
If you put your various type/value pairs into a temporary table or as a CTE then you can JOIN to that instead of listing out all of the pairs that you want. That syntax will be dependent on your RDBMS though.

Use Or operand for multiple case/criteria
SELECT * FROM Event e join Event_tag on e.eventId = et.eventtagid where ((EventTag.type="foo" and EventTag.value="bar") or (EventTag.type="po" and EventTag.value="yo"))
or if the values is dyanmic, then depending on your programming language that interface SQL, you can write a query
For example in java I can do it using
SELECT * FROM Event e join Event_tag et on e.eventid = et.eventtagid where (EventTag.type=? and EventTag.value=?)
Where I assign the above SQL string to Query and set the parameters for it.

Select id from EVENT ev
INNER JOIN EVENT_TAG et
ON ev.id = et.event_ID
WHERE et.type = 'foo'
AND et.value = 'bar'
Obviously you can put any thing you want between the parentheses to find what ever types you want.

Related

SQL Query Pull data from another table IF NULL

I have a system running a SQL Server Express database and I need to pull some data from it. I have the basic SQL query created but I have found that some data is located elsewhere.
The basic premise is I have a database of Repair Orders, Vehicles And Customers. The Vehicles are usually added via a VIN decoder so they have ID's associated from a MAKE and MODEL table. However in the case of a VIN not decoding the application allows the user to manually enter this information and then it is stored in another table named "UserVehicleAttributes". In this table there is the VehicleID, AttributeName, & AttributeValue.
UserAttributeId VehicleId AttributeName AttributeValue
-----------------------------------------------------------
364 6829 Model Sedona
365 6830 Make Kia
366 6830 Model Sedona
So what I need is if the Make or Model comes up as NULL from the Vehicle table, I can display what as manually entered in.
I found that there is an existing function in the DB that looks to be able to do what I need but I don't know how to use it as part of my query.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER FUNCTION [SM].[fnVehicleModelName]()
RETURNS TABLE
AS
RETURN
(
SELECT DISTINCT v.VehicleId,
CASE
WHEN v.SubModelId IS NULL THEN ISNULL(ua.[AttributeValue],'')
ELSE smm.[Name]
END as Model
FROM SM.Vehicle v (NOLOCK)
LEFT OUTER JOIN
(SELECT sm.SubModelId, m.[Name] + ' ' + sm.[Name] as Name
FROM DMV.SubModel (NOLOCK) sm
INNER JOIN DMV.Model m (NOLOCK)
ON sm.ModelId = m.ModelId ) as smm
ON v.SubModelId = smm.SubModelId
LEFT OUTER JOIN SM.UserVehicleAttributes ua (NOLOCK)--
ON v.VehicleId = ua.VehicleId and ua.AttributeName = 'Model'
Any help is greatly appreciated. I am not very good with SQL (obviously) but I am trying to figure this one out.
I'm not sure why you're making this a function with no parameters - that's kinda the same thing as a view. Consider if using a view here might simplify the situation.
You're correct that ISNULL is what you want to use here, but I think the join should be more simple. Your situation is basically "pull the column value from whichever table has a non-null value, giving preference to one table first"
In the outer join, all the columns from the outer joined tables will be null if there's not a match, and if there is a match, all the values should be filled in. Knowing that... you should be able to do something like this... (as an example to clarify how this concept works, not solving your query for you)
select v.VehicleId,
VehicleName = isnull(Model.Name, UserVehicle.Name)
from Vehicle v
left outer join Model on Model.VehicleID = Vehicle.VehicleID
left outer join UserVehicle on UserVehicle.VehicleID = Vehicle.VehicleId
So, what that does is join the possible rows from either table, and the ISNULL macro selects whichever value is non-null. Do that for the rest of the columns, and fix the join condition to whatever your conditions are, and you should be golden.
That function has no parameters, if you want to use it rewrite it as a view, but it only shows model, so you can use subqueries like this:
SELECT
VehicleId,
CASE
WHEN Make IS NULL
THEN ( SELECT AttributeValue FROM UserVehicleAttributes
WHERE VehicleId = Vehicles.VehicleId
AND AttributeName = 'Make' )
ELSE Make
END AS Make,
CASE
WHEN Model IS NULL
THEN ( SELECT AttributeValue FROM UserVehicleAttributes
WHERE VehicleId = Vehicles.VehicleId
AND AttributeName = 'Model' )
ELSE Model
END AS Model
FROM
Vehicles

Setting up database structure

I am trying to figure out the best way to restructure my database as I didn't plan ahead and now I am a little stuck on this part :)
I have a Table called Campaigns and a Table called Data Types.
Each campaign is a unique record that holds about 10 fields of data.
The data types contains 3 fields - ID, Type, Description
When You create a campaign, you can select as many data types as you would like.
1, 2 or all 3 of them.
My concern / question is - How can I store what the user selected with the campaign record?
I need to be able to pull in the campaign details but also know which data types were selected.
How I originally had it set up was the data types were in 1 field, comma separated but learned is not ideal to do that.
What would be the best way to accomplish this? Storing the data as XML ?
UPDATE -
Here is an example of the query I was trying to get to work (its probably way off).
BEGIN
SET NOCOUNT ON;
BEGIN
SELECT *
FROM (SELECT A.[campaignID] as campaignID,
A.[campaignTitle],
A.[campaignDesc],
A.[campaignType],
A.[campaignStatus],
A.[duration],
A.[whoCreated],
B.[campaignID],
B.[dataType],
(SELECT *
FROM Tags_Campaign_Settings
WHERE campaignID = #campaignID) AS dataTypes
FROM Tags_Campaigns AS A
INNER JOIN
Tags_Campaign_Settings AS B
ON A.[campaignID] = B.[campaignID]
WHERE A.[campaignID] = #campaignID
) AS a
FOR XML PATH ('campaigns'), TYPE, ELEMENTS, ROOT ('root');
END
END
Create a join table called Campain_DataType with campaignId and dataTypeId. Make sure they're foreign key constrained to the respective tables. When you query for campaign data, you can either create a separate query to get the data type information based on the campaignId, or you can do a left outer join to fetch campaigns and their data types together.
If you want to collapse the 3 data types into the same row, then give the following a shot. It's definitely on the hacky side, and it'll only work with a fixed number of data types. If you add another data type, you'll have to update this query to support it.
SELECT
Campaign.ID,
Campaign.foo,
Campaign.bar,
dataType1.hasDataType1,
dataType2.hasDataType2,
dataType3.hasDataType3
FROM
Campaign
LEFT OUTER JOIN
( SELECT
1 as hasDataType1,
Campaign_DataType.campaignID
FROM
DataType
INNER JOIN Campaign_DataType ON Campaign_DataType.dataTypeId = DataType.id
WHERE
DataType.Type = 'Type1'
) dataType1 ON dataType1.campaignID = Campaign.ID
LEFT OUTER JOIN
( SELECT
1 as hasDataType2,
Campaign_DataType.campaignID
FROM
DataType
INNER JOIN Campaign_DataType ON Campaign_DataType.dataTypeId = DataType.id
WHERE
DataType.Type = 'Type2'
) dataType2 ON dataType2.campaignID = Campaign.ID
LEFT OUTER JOIN
( SELECT
1 as hasDataType3,
Campaign_DataType.campaignID
FROM
DataType
INNER JOIN Campaign_DataType ON Campaign_DataType.dataTypeId = DataType.id
WHERE
DataType.Type = 'Type3'
) dataType3 ON dataType3.campaignID = Campaign.ID
The record you receive for each Campaign will have three fields: hasDataType1, hasDataType2, hasDataType3. These columns will be 1 for yes, NULL for no.
Looks to me like what you want here is a crosstab query. Take a look at:
Sql Server 2008 Cross Tab Query

SQL - Returning Each Record Only Once

Here's my tables:
tblBusiness
BusinessID, BusinessName
tblTags
TagID, Tag
tblBusinessTagLink
BusinessID, TagID
Any business can have multiple tags applied to it. Now lets say a user is filtering down so that they find only businesses that are tagged 'Office Supplies' and 'Technology'
What SQL statement should I use? Is there a better design for my tables than what I've presented here?
SELECT
b.BusinessId,
b.BusinessName
FROM
tblBusiness AS b
INNER JOIN tblBusinessTagLink AS l ON l.BusinessId = b.BusinessId
INNER JOIN tblTags AS t ON t.TagId = l.TagId
WHERE
t.TagName IN ('Technology', 'Office Supplies')
GROUP BY
b.BusinessId,
b.BusinessName
This selects all businesses that are in either one of the categories. To select only those in both categories, you could append a
HAVING COUNT(*) = 2
The method you are using (three tables to represent a m:n relationship) is the standard way to solve this task, you can keep that.
Personally, I would not use "hungarian notation" for table names (i.e. no "tbl") and I would not use plural table names (i.e. not "Tags"), especially when the other tables are not plural either.
Answering the first comment below:
For larger data sets, the performance of this query relies on indexes. All the primary keys need an index, naturally. In tblBusinessTagLink you should have a composite index covering both fields and one additional index for the field that does not come first in the composite index.
The WHERE keywords LIKE '%technology%' idea is a bad one, mostly because for any LIKE conditions other than start-of-field searches an index cannot be used (i.e. performance will degrade rapidly as your data set grows) and partly because it should be WHERE ','+keywords+',' LIKE '%,technology,%' to begin with or you will get partial matches/false positives.
Also, it might be a bit more efficient to query by TagId. This way you can remove one table from the JOIN entirely:
FROM
tblBusiness AS b
INNER JOIN tblBusinessTagLink AS l ON l.BusinessId = b.BusinessId
WHERE
l.TagId IN (1, 2)
If you intend to query by TagName however, an index on this field will be absolutely necessary as well.
You can use simple JOIN to get record
SELECT t.Tag, b.BusinessName
FROM tblBusiness b, tblTags t, tblBusinessTagLink l
WHERE t.TagID = l.TagID
AND l.BusinessID = b.BusinessID
AND t.Tag = 'Office Supplies'
You can use the INTERSECT set operation to merge the 2 queries (one for 'Office Supplies' and one for 'Technology').
However if you are using MySQL (which does not support INTERSECT), you can use a UNION ALL with a 'HAVING COUNT(*) = 2' like this.
EDIT:
You can also use the second option without the UNION ALL like so:
select Name from tblBusiness
left join tblBusinessTagLink on tblBusinessTagLink.BusinessID = tblBusiness.ID
left join tblTags on tblTags.TagID = tblBusinessTagLink.TagID
where Tag = 'Office Supplies' or Tag = 'Technology'
group by name
having count(Name) = 2;

Get values from all sub-divided child tables

I have three tables as below.
TransactionTable
----------------
TransactionID
Status
Value
FileNo (int)
FileType - 'E' indicates Email, 'D' Indicates Document
EmailTable
----------
EmailFileNo (Identity)
ReceivedDate
....
....
....
DocumentsTable
---------------
DocFileNo (Identity)
ReceivedDate
.....
.....
There is one to many relationship between EmailTable and TransactionTable and also between DocumentsTable and TransactionTable
What is the name for such type of relationship... I just used the term sub-divided child tables
I need to select TransactionID, ReceivedDate, Value where status is 'P'...
I could get the result using
Select A.TransactionID, IsNull(B.ReceivedDate, C.ReceivedDate) as ReceivedDate, A.Value
From TransactionTable as A
Left outer join EmailTable as B on A.FileNo = B.EmailFileNo and A.FileType='E'
Left outer join DocumentsTable as C on A.FileNo = C.DocFileNo and A.FileType = 'D'
where A.Status = 'P'
The above query gives me the result as expected... Is this the way it should be done or is there a better way to handle such scenarios ?
Edit : Included the where clause, which got missed during copy paste operation. Thanks for pointing this out.
Your query looks good. The only comment I'd make is that I don't see you satisfying the Status='P' condition that you specified in your requirements.
Select A.TransactionID, IsNull(B.ReceivedDate, C.ReceivedDate) as ReceivedDate, A.Value
From TransactionTable as A
Left outer join EmailTable as B
on A.FileNo = B.EmailFileNo
and A.FileType='E'
Left outer join DocumentsTable as C
on A.FileNo = C.DocFileNo
and A.FileType = 'D'
where A.Status = 'P'
Someone might have a better response, but that's pretty much it. You could opt for COALESCE instead of ISNULL which permit a variable number of arguments, so you can add a third option if both are Email and Documents are NULL for some reason.
Everything that follows is just commentary on the schema. The table structure has a problem, but I'm sure you're now coding after these tables are already established, so this isn't necessarily a call for action. You probably have to live with them as they are.
My instinctive response would have been to assign TransactionId to the child tables, because they are not formally children right now. They are autonomous objects that TransactionTable happens to refer to.
I had similar problem before where I had a key column that didn't have a clear definition and I eventually opted against it. It's not possible to build a formal constraint/foreign key for FileNo on TransactionTable, because FileNo could be defined on either of the two tables.
(Incidentally your status = 'P' check is missing from your query.)
Also if you keep adding new filetype beyond 'E' and 'D' you are going to have to keep extending the query to new tables. A File table of some form, with the key fields on might have been one way of resolving this. [for all I know you may already have some sort of File table]
Not sure if any of this helps you, though. There's no way to improve upon your query without changing the table structures.

SQL Query to retrieve data while excluding a set of rows

I have basically four tables (SQL Server):
Objects:
id
ObjectName
Components
id
ComponentName
ObjectsDetails:
ObjectID
ComponentID
ExclusionTable
id
ComponentID
Basically, these tables describe Objects and what Objects are made of (what components)
For example, Object "A" may be made out of component "A" and component "B".
In this case, the tables would be populated this way:
Objects:
id ObjectName
1 A
Components:
id ComponentName
1 A
2 B
ObjectDetails:
ObjectID ComponentID
1 1
1 2
Now, "ExclusionTable" may have a list of components that are to be excluded from a search (therefore, excluding entire objects if the object is made out of at least one of those components).
For example, I would like to ask:
"Give me all the Objects that are not made out of components A and B".
Therefore, my question is:
Is there a way to write a query for that ? No views, no stored procedures please.. my SQL engine does not support that.
I tried something like:
SELECT DISTINCT ObjectName FROM Objects INNER JOIN ObjectsDetails ON Objects.id =
ObjectDetails.ObjectID WHERE ObjectsDetails.ComponentID NOT IN (1,2)
in case ExclusionTable tells us that Components A and B needs to be excluded.
Of course, that doesn't work...
I tried a few variations using WHERE NOT EXISTS (SELECT * FROM ExclusionTable) but I am not proficient enough in SQL to understand how to get it to work using one query only (if it is even possible).
Thanks!
You should avoid doing queries with [not] in (select ...)
SELECT DISTINCT ObjectName
FROM Objects
INNER JOIN ObjectsDetails ON Objects.id = ObjectDetails.ObjectID
LEFT JOIN ExclusionTable on ExclusionTable.ComponentId = ObjectsDetails.ComponentID
where ExclusionTable.ComponentId is null;
This will retrieve only rows for which the ComponentID is not in ExclusionTable.
Update:
SELECT ObjectName
FROM Objects
INNER JOIN ObjectsDetails ON Objects.id = ObjectDetails.ObjectID
LEFT JOIN ExclusionTable on ExclusionTable.ComponentId = ObjectsDetails.ComponentID
group by ObjectName
having count(distinct ObjectsDetails.ComponentID) = sum(case when ExclusionTable.id is null then 1 else 0 end)
New approach, I think the only other way I could do it is basically to compare the number of components per object with the number of components in the object not included on the list. When these number are equal, no component is on the excluded list and we can show the object.
I'm sorry I can't make a test right now, please use EXPLAIN select ... to compare the queries, if they work.
Basically, if you need to get all objects not made from A or B, you need to get all objects EXCEPT those made from A or B.
SELECT DISTINCT Id, ObjectName
FROM Objects
WHERE Id NOT IN (
SELECT DISTINCT ObjectDetails.ObjectID
FROM ObjectDetails
INNER JOIN Components ON ObjectDetails.ComponentID = Components.Id
WHERE Components.ComponentName = 'A' OR Components.ComponentName = 'B'
)
Would that be what you're looking for?
EDIT: Of course, you can omit the join if you already have the component ids - then just put those in the where clause to filter them out.
select id, objectname
from Objects
left outer join
( select objectid from ObjectsDetails od inner join Exclusiontable et
on od.ComponentID= et.ComponentID) excludedid
on Objects.ID = excludedid.ObjectID and excludedid.ObjectID is null