Ok, here's a fun one. I have 2 tables: tbl_notes, tbl_notes_categories
Simply, the tbl_notes has a categoryid, and I correlate the 2 tables with that ID. So, nothing too complicated.
I force users to choose a category, from a dropdown input, and stop them from submitting if they don't select something.
However, I want to change this, primarily for learning JOINs and how far I can go with them.
Sooooooo, I am not going to force a user to select a category, and instead, I will default the categoryid to zero, in the tbl_notes. (most users will select a category, but this is for other instances)
In the query, I am locked to showing only the notes that have a categoryid that exists in the tbl_notes_categories table. But, I would like to have a condition if the categoryid is not recognized OR is equal to zero, then specify another String. Like "--Unassigned--", or "--Category does not exist--"
Here's my original query:
SELECT n.notesubject, c.categoryname
FROM `tbl_notes` n, `tbl_notes_categories` c
WHERE n.categoryid = c.categoryid
This will not let me see the notes without a categoryid, so I pulled this one:
SELECT n.notesubject, c.categoryname
FROM `tbl_notes` n
LEFT JOIN `tbl_notes_categories` c ON n.categoryid = c.categoryid
And that helps, but I'm stuck at the 'condition' of displaying alternate text, in the case of a missing category record from the categories table.
In MySQL you can use IFNULL:
SELECT
n.notesubject,
IFNULL(c.categoryname, 'Unknown') AS categoryname
FROM tbl_notes AS n
LEFT JOIN tbl_notes_categories AS c
ON n.categoryid = c.categoryid
This will work if the category is not found, but it will also work if the category id is zero assuming that you don't have a matching row in your category table because then it will also not be found. If for some reason you do want a row in the categroy table with id zero then you can just set its name to 'Unknown'.
Note that IFNULL is MySQL specific. The function COALESCE will also work and is supported by more databases.
For IF/ELSE statements in general in MySQL can use IF or for a more general solution use a CASE expression: CASE WHEN condition THEN expr1 ELSE expr2 END.
Related
So I have this three tables:
product (p)
product_has_component (phc)
product_component (pc)
Product have multiple components, at least one, and table phc store info how the joining should go. So, I can check which components given product has - that is easy.
But now I want to do opposite check: after selecting multiple components, I want to know every product, that posses all of them.
I made this query:
SELECT DISTINCT phc.product_id
FROM product_has_component phc
JOIN product_component pc
ON pc.id = phc.component_id
AND (pc.component_character = 'A' OR pc.component_character = 'y')
And it returns products id numbers, that have either 'A' OR 'y' component.
But how can I get those having 'A' AND 'y' components?
Obviously, replacing OR with AND will return nothing, so I guess this whole query's structure is just incorrect.
I also thought about doing something like select all with 'A', then select all with 'y' and then try to find common produts, but this seem to be very ineffective solution.
I'll try to explain my problem. I have two tables. In the first one each record is identified by a unique INT code (counter). In the second the code from the first table is one of the fields (and may be repeated in various records).
I want to make a SELECT CODE in the second table, based on WHERE parameters, knowing I will get as result a recordset with possibly repeated CODES, and use this recordset for another SELECT in the first table, WHERE CODE IN the above recordset (from the second table).
Is this possible ?? And if yes, how to do this ?
Usually, if I use the WHERE IN clause, the array can contain repeated values like WHERE Code IN "3,4,5,6,3,4,2" ... right ? The difference here is that I want to use a previously Selected recordset in place of the array.
Is this possible ?? Sure is.
And if yes, how to do this ? Like most questions answers depends. There's more than one way to skin this cat; and depending on data (volume of records), and indexes answers can vary.
You can use a distinct or group by to limit the table A records because the join from A--> b is a 1--> many thus we need to distinct or group by the values from A as they would be repeated. But if you need values from B as well, this is the way to do it.
Select A.Code, max(count B.A_CODE) countOfBRecords
from B
LEFT JOIN A
on A.Code = B.A_Code
WHERE B.Paramater = 'value'
and B.Paramater2 = 'Value2'
group by A.Code)
Or using your approach (works if you ONLY need values/columns from table A.)
Select A.Code
from A
Where code in (Select B.A_CODE
From B WHERE B.Paramater = 'value'
and B.Paramater2 = 'Value2')
But these can be slow depending on data/indexes.
You don't need the distinct on the inner query as A.Code only exists once and thus wouldn't be repeated. It's the JOIN which would cause the records to repeat not the where clause.
-Correlated Subquery will return a single A.Code works if you ONLY need values from table A.
Select A.Code
From A
Where exists (Select 1
from B
where b.paramter = value ...
AND A.Code = B.A_CODE)
Since there's no join no A.records would be repeated. On larger data sets this generally performs better .
This last approach works because it Correlates the outer table with sub select Note this can only go 1 level in a relationship. If you had multiple levels deep trying to join this way, it woudln't work.
Introduction
Sometimes instead of a join you can deliberately use a scalar subquery to check that not more than one row was found. For example you might have this query to look up nationality for some person rows.
select p.name,
c.iso
from person p
join person_country_map pcm
on p.id = pcm.person
join country c
on pcm.country = c.id
where p.id in (1, 2, 3)
Now, suppose that the person_country_map is not a functional mapping. A given person may map to more than one country - so the join may find more than one row. Or indeed, a person might not be in the mapping table at all, at least as far as any database constraints are concerned.
But for this particular query I happen to know that the persons I am querying will have exactly one country. That is the assumption I am basing my code on. But I would like to check that assumption where possible - so that if something went wrong and I end up trying to do this query for a person with more than one country, or with no country mapping row, it will die.
Adding a safety check for at most one row
To check for more than one row, you can rewrite the join as a scalar subquery:
select p.name,
(
select c.iso
from person_country_map pcm
join country c
on pc.country = c.id
where pcm.person = p.id
) as iso
from person p
where p.id in (1, 2, 3)
Now the DBMS will give an error if a person queried maps to two or more countries. It won't return multiple entries for the same person, as the straightforward join would. So I can sleep a bit easier knowing that this error case is being checked for even before any rows are returned to the application. As a careful programmer I might check in the application as well, of course.
Is it possible to have a safety check for no row found?
But what about if there is no row in person_country_map for a person? The scalar subquery will return null in that case, making it roughly equivalent to a left join.
(For the sake of argument assume a foreign key from person_country_map.country to country.id and a unique index on country.id so that particular join will always succeed and find exactly one country row.)
My question
Is there some way I can express in SQL that I want one and exactly one result? A plain scalar subquery is 'zero or one'. I would like to be able to say
select 42, (select exactly one x from t where id = 55)
and have the query fail at runtime if the subquery wouldn't return a row. Of course, the above syntax is fictional and I am sure it wouldn't be that easy.
I am using MSSQL 2008 R2, and in fact this code is in a stored procedure, so I can use TSQL if necessary. (Obviously ordinary declarative SQL is preferable since that can be used in view definitions too.) Of course, I can do an exists check, or I can select a value into a TSQL variable and then explicitly check it for nullness, and so on. I could even select results into a temporary table and then build unique indexes on that table as a check. But is there no more readable and elegant way to mark my assumption that a subquery returns exactly one row, and have that assumption checked by the DBMS?
You are making this harder than it needs to be
For sure you need a FK relationship on person.id to person_country_map.person
You either have unique constraint on person_country_map.person or you don't?
If you don't have a unique constraint then yes you can have multiple records for the same person_country_map.person.
If you want to know if you have any duplicate then
select pcm.person
from person_country_map pcm
group by pcm.person
having count(*) > 1
If there is more than one then you just need to determine which one
select p.name,
min(c.iso)
from person p
join person_country_map pcm
on p.id = pcm.person
join country c
on pcm.country = c.id
where p.id in (1, 2, 3)
group by p.name
In MSSQL it appears that isnull only evaluates its second argument if the first is null. So in general you can say
select isnull(x, 0/0)
to give a query which returns x if non-null and dies if that would give null. Applying this to a scalar subquery,
select 42, isnull((select x from t where id = 55), 0/0)
will guarantee that exactly one row is found by the select x subquery. If more than one, the DBMS itself will produce an error; if no row, the division by zero is triggered.
Applying this to the original example leads to the code
select p.name,
-- Get the unique country code of this person.
-- Although the database constraints do not guarantee it in general,
-- for this particular query we expect exactly one row. Check that.
--
isnull((
select c.iso
from person_country_map pcm
join country c
on pc.country = c.id
where pcm.person = p.id
), 0/0) as iso
from person p
where p.id in (1, 2, 3)
For a better error message you can use a conversion failure instead of division by zero:
select 42, isnull((select x from t where id = 55), convert(int, 'No row found'))
although that will need further convert shenanigans if the value you are fetching from the subquery is not itself an int.
I am trying to list all of the stores that sell products that contain specific guitar parts for a guitar rig.
I have a guitarRig database. Guitar rigs have parts (I.e. amplifier, cabinet, microphone, guitarType, guitarStringType, patchCord, effectsPedal) which come from products, which are purchased from stores.
Products are purchased for a price as dictated by my PurchaseInformation table.
Here are my tables:
Table Part:
name
guitarRig references GuitarRig(name)
product references Product(name)
Table Product:
name
part references Part(name)
barcodeNumber
Table PurchaseInformation:
price
product references Product(name)
purchasedFrom references Store(name)
Table Store:
name
storeLocation
So far what I have is this:
SELECT p.name AS Part, prod.name AS Product, sto.name AS Store
FROM Part p, ProductInformation prod, Store sto, PurchaseInfo purch
WHERE sto.storeNumber = purch.storeNumber
AND purch.product = prod.name
AND prod.Part = p.name
AND p.name =
(
SELECT name
FROM Part
WHERE name LIKE '%shielded%'
)
GROUP BY p.name;
The error I get is that it returns more than 1 row, however, this is what I want! I want to list the stores that sell products that contain the part I am searching for.
The quick fix is to replace the equality comparison operator ( = ) with the IN operator.
AND p.name IN
(
SELECT name ...
I say that's the quick fix, because that will fix the error, but this isn't the most efficient way to write the query. And it's not clear your query is going return the result set you specified or actually expect.
I strongly recommend you avoid the old-school comma join operator, and use the JOIN keyword instead.
Re-structuring your query into an equivalent query yields this:
SELECT p.name AS Part
, prod.name AS Product
, sto.name AS Store
FROM Part p
JOIN ProductInformation prod
ON prod.Part = p.name
JOIN PurchaseInfo purch
ON purch.product = prod.name
JOIN Store sto
ON sto.storeNumber = purch.storeNumber
WHERE p.name IN
(
SELECT name
FROM Part
WHERE name LIKE '%shielded%'
)
GROUP BY p.name;
Some notes. The GROUP BY clause is going to collapse all of the joined rows into a single row for each distinct part name. That is, you are only going to get one row back for each part name.
It doesn't sound like that's what you want. I recommend you remove that GROUP BY, and add an ORDER BY, at least until you figure out what resultset you are getting, and if that's the rows you want to return.
Secondly, using the IN (subquery) isn't the most efficient approach. If p.name matches a value returned by that subquery, since p is a reference to the same Part table, this:
WHERE p.name IN
(
SELECT name
FROM Part
WHERE name LIKE '%shielded%'
)
is really just a more complicated way of saying this:
WHERE p.name LIKE '%shielded%'
I think you really want something more like this:
SELECT p.name AS Part
, prod.name AS Product
, sto.name AS Store
FROM Part p
JOIN ProductInformation prod
ON prod.Part = p.name
JOIN PurchaseInfo purch
ON purch.product = prod.name
JOIN Store sto
ON sto.storeNumber = purch.storeNumber
WHERE p.name LIKE '%shielded%'
ORDER BY p.name, prod.name, sto.name
That's going to return all rows from Part that include the string 'shielded' somewhere in the name.
We're going to match those rows to all rows in ProductInformation that match that part. (Note that with the inner join, if a Part doesn't have at least one matching row in ProductInformation, that row from Part will not be returned. It will only return rows that find at least one "matching" row in ProductionInformation.
Similarly, we join to matching rows in PurchaseInfo, and then to Store. Again, if there's not matching row from at least one Store, we won't get those rows back. This query is only going to return rows for Parts that are related to at least on Store. We won't get back any Part that's not in a Store.
The rows can be returned in any order, so to make the result set deterministic, we can add an ORDER BY clause. It's not required, it doesn't influence the rows returned, it only affects the sequence the rows that get returned.
This query displays the correct result but when doing an EXPLAIN, it lists it as a "Dependant SubQuery" which I'm led to believe is bad?
SELECT Competition.CompetitionID, Competition.CompetitionName, Competition.CompetitionStartDate
FROM Competition
WHERE CompetitionID NOT
IN (
SELECT CompetitionID
FROM PicksPoints
WHERE UserID =1
)
I tried changing the query to this:
SELECT Competition.CompetitionID, Competition.CompetitionName, Competition.CompetitionStartDate
FROM Competition
LEFT JOIN PicksPoints ON Competition.CompetitionID = PicksPoints.CompetitionID
WHERE UserID =1
and PicksPoints.PicksPointsID is null
but it displays 0 rows. What is wrong with the above compared to the first query that actually does work?
The seconds query cannot produce rows: it claims:
WHERE UserID =1
and PicksPoints.PicksPointsID is null
But to clarify, I rewrite as follows:
WHERE PicksPoints.UserID =1
and PicksPoints.PicksPointsID is null
So, on one hand, you are asking for rows on PicksPoints where UserId = 1, but then again you expect the row to not exist in the first place. Can you see the fail?
External joins are so tricky at that! Usually you filter using columns from the "outer" table, for example Competition. But you do not wish to do so; you wish to filter on the left-joined table. Try and rewrite as follows:
SELECT Competition.CompetitionID, Competition.CompetitionName, Competition.CompetitionStartDate
FROM Competition
LEFT JOIN PicksPoints ON (Competition.CompetitionID = PicksPoints.CompetitionID AND UserID = 1)
WHERE
PicksPoints.PicksPointsID is null
For more on this, read this nice post.
But, as an additional note, performance-wise you're in some trouble, using either subquery or the left join.
With subquery you're in trouble because up to 5.6 (where some good work has been done), MySQL is very bad with optimizing inner queries, and your subquery is expected to execute multiple times.
With the LEFT JOIN you are in trouble since a LEFT JOIN dictates the order of join from left to right. Yet your filtering is on the right table, which means you will not be able to use an index for filtering the USerID = 1 condition (or you would, and lose the index for the join).
These are two different queries. The first query looks for competitions associated with user id 1 (via the PicksPoints table), which the second joins with those rows that are associated with user id 1 that in addition have a null PicksPointsID.
The second query is coming out empty because you are joining against a table called PicksPoints and you are looking for rows in the join result that have PicksPointsID as null. This can only happen if
The second table had a row with a null PickPointsID and a competition id that matched a competition id in the first table, or
All the columns in the second table's contribution to the join are null because there is a competition id in the first table that did not appear in the second.
Since PicksPointsID really sounds like a primary key, it's case 2 that is showing up. So all the columns from PickPointsID are null, your where clause (UserID=1 and PicksPoints.PicksPointsID is null) will always be false and your result will be empty.
A plain left join should work for you
select c.CompetitionID, c.CompetitionName, c.CompetitionStartDate
from Competition c
left join PicksPoints p
on (c.CompetitionID = p.CompetitionID)
where p.UserID <> 1
Replacing the final where with an and (making a complex join clause) might also work. I'll leave it to you to analyze the plans for each query. :)
I'm not personally convinced of the need for the is null test. The article linked to by Shlomi Noach is excellent and you may find some tips in there to help you with this.