How can I join these 3 tables? - mysql

MySQL "table_a":
+----+-------+
| id | title |
+----+-------+
Now I can do a search like so:
$term = mysql_real_escape_string($_GET['term']);
mysql_query("SELECT * FROM `table_a`
WHERE MATCH(`title`) AGAINST('$term' IN BOOLEAN MODE) LIMIT 0,5");
However I want to add another compontent.
MySQL "table_b":
+----+----------+
| id | category |
+----+----------+
MySQL "table_c":
+------------+------------+
| table_a_id | table_b_id |
+------------+------------+
So, I want to look for a specific category in table_b that is linked to table_a according to table_c.
BUT, it isn't always the case that a category is linked to table_a (so it could happen that there just aren't any entries in table_c that are connected to table_a).
AND, if there is a category linked to table_a I want to be able to either search for the title in table_a or the category in table_b (so both should be possible, so category shouldn't overrule title, or the other way around). But if that's not possible, then title should overrule category.
Here's what I came up with so far, but the problem is
a) it doesn't work
b) it doesn't include the title OR category as I had just explained
$term = mysql_real_escape_string($_GET['term']);
mysql_query("SELECT a.*, LEFT JOIN (SELECT c.table_a_id FROM table_b AS b
, table_c AS c WHERE b.category = '$term' AND b.id = c.table_b_id)
AS d ON d.table_a_id = a.id FROM table_a AS a
WHERE MATCH(a.title) AGAINST('$term' IN BOOLEAN MODE) LIMIT 0,5");
Any help would be greatly appreciated.

You really want to join the table_b and table_c data together and the perform your left join against that. Something like this should work for you. If there are multiple categories entries that match, you will have duplicate records, since you only want data from a.*. For debugging, I would add d.* as well so you can see why there are duplicates if there are any:
SELECT a.*
from table_a a
LEFT JOIN (SELECT c.table_a_id, b.category
from table_b b,
table_c c
where c.table_b_id = b.id) d
on a.id = d.table_a_id
WHERE d.category = '$_GET[term]'
or MATCH(a.title) AGAINST('$_GET[term]' IN BOOLEAN MODE)
LIMIT 0,5
But, if you are doing anything other than testing, you absolutely have to escape the $_GET terms or, even better used parameterized SQL. If you aren't familiar with creating prepared statements, see this StackOverflow answer.

Related

Understanding use of multiple SUMs with LEFT JOINS in mysql

Using the GROUP BY command, it is possible to LEFT JOIN multiple tables and still get the desired number of rows from the first table.
For example,
SELECT b.title
FROM books `b`
LEFT JOIN orders `o`
ON o.bookid = b.id
LEFT JOIN authors `a`
ON b.authorid = a.id
GROUP BY b.id
However, since behind the scenes MYSQL is doing a cartesian product on the tables, if you include more than one SUM command you get incorrect values based on all the hidden rows. (The problem is explained fairly well here.)
SELECT b.title,SUM(o.id) as sales,SUM(a.id) as authors
FROM books `b`
LEFT JOIN orders `o`
ON o.bookid = b.id
LEFT JOIN authors `a`
ON b.authorid = a.id
GROUP BY b.id
There are a number of answers on SO about this, most using sub-queries in the JOINS but I am having trouble applying them to this fairly simple case.
How can you adjust the above so that you get the correct SUMs?
Edit
Example
books
id|title|authorid
1|Huck Finn|1
2|Tom Sawyer|1
3|Python Cookbook|2
orders
id|bookid
1|1
2|1
3|2
4|2
5|3
6|3
authors
id|author
1|Twain
2|Beazley
2|Jones
The "correct answer" for total # of authors of the Python Cookbook is 2. However, because there are two joins and the overall dataset is expanded by the join on number of orders, SUM(a.id) will be 4.
You are correct that by joining multiple tables you would not get the expected results.
But in this case you should use COUNT() instead of SUM() and count the distinct orders or authors.
Also by your design you should count the names of the authors and not the ids of the table authors:
SELECT b.title,
COUNT(DISTINCT o.id) as sales,
COUNT(DISTINCT a.author) as authors
FROM books `b`
LEFT JOIN orders `o` ON o.bookid = b.id
LEFT JOIN authors `a` ON b.authorid = a.id
GROUP BY b.id, b.title
See the demo.
Results:
| title | sales | authors |
| --------------- | ----- | ------- |
| Huck Finn | 2 | 1 |
| Tom Sawyer | 2 | 1 |
| Python Cookbook | 2 | 2 |
When dealing with separate aggregates, it is good style to aggregate before joining.
Your data model is horribly confusing, making it look like a book is written by one author only (referenced by books.authorid), while this "ID" is not an author's ID at all.
Your main problem is: You don't count! We count with COUNT. But you are mistakenly adding up ID values with SUM.
Here is a proper query, where I am aggregating before joining and using alias names to fight confusion and thus enhance the query's readability and maintainability.
SELECT
b.title,
COALESCE(o.order_count, 0) AS sales,
COALESCE(a.author_count, 0) AS authors
FROM (SELECT title, id AS book_id, authorid AS author_group_id FROM books) b
LEFT JOIN
(
SELECT id as author_group_id, COUNT(*) as author_count
FROM authors
GROUP BY id
) a ON a.author_group_id = b.author_group_id
LEFT JOIN
(
SELECT bookid AS book_id, COUNT(*) as order_count
FROM orders
GROUP BY bookid
) o ON o.book_id = b.book_id
ORDER BY b.title;
i don't think that your query would work like you eexspected.
Assume one book could have 3 authors.
For Authors:
So you would have three rows for that book in your books table,each one for every Author.
So a
SUM(b.authorid)
gives you the correct answer in your case.
For Orders:
you must use a subselect like
LEFT JOIN (SELECT SUM(id) o_sum,bookid FROM orders GROUP BY bookid) `o`
ON o.bookid = b.id
You should really reconsider your approach with books and authors.

SQL: many to many relationships select where multiple criteria

given these tables :
id_article | title
1 | super article
2 | another article
id_tag | title
1 | great
2 | awesome
id_relation | id_article | id_tag
1 | 1 | 1
2 | 1 | 2
3 | 2 | 1
I'd like to be able to select all articles that are "great" AND "awesome" (eventually, I'll probably have to implement OR too)
And basically, if I do a select on articles the relation table joining on id_article: of course, I cant join two different values of id_tag. Only lead I had with concatenating IDs to test as a string, but that seems so lame, there has to be a prettier solution.
Oh and if it matters, I use a MySQL server.
EDIT: for ByWaleed, the typical sql select that would surely fail that I cited in my original question:
SELECT
a.id_article,
a.title
FROM articles a, relations r
WHERE
r.id_article = a.id_article and r.id_tag = 1 and r.id_tag = 2
wouldnt work because r.id_tag cant obviously be 1 and 2 on the same line. I doubt w3schools has an article on that. My search on google didnt yield any result, probably because I searched with the wrong keyword.
If you do all the joins as normal, then aggregate the rows to one group by article, then you can assert that they must have at least two different tags.
(Having already filtered to great and/or awesome, that means they have both.)
SELECT
a.id_article,
a.title
FROM
articles a
INNER JOIN
relations r
ON r.id_article = a.id_article
INNER JOIN
tags t
ON t.id_tag = r.id_tag
WHERE
t.title IN ('great', 'awesome')
GROUP BY
a.id_article,
a.title
HAVING
COUNT(DISTINCT t.id_tag) = 2
(The DISTINCT is to avoid the possibility of one article having 'great' twice, for example.)
To do OR, you just remove the HAVING clause.
One approach is to aggregate by article, and then assert that the article both the "great" and "awesome" tags:
SELECT
a.id_article,
a.title
FROM articles a
INNER JOIN relations r
ON a.id_article = r.id_article
INNER JOIN tags t
ON r.id_tag = t.id_tag
WHERE
t.title IN ('great', 'awesome')
GROUP BY
a.id_article,
a.title
HAVING
MIN(t.title) <> MAX(t.title);
Demo
The logic here is that we first limit records, for each article, to only those of the two targets tags. Then we assert, in the HAVING clause, that both tags appear. I use a MIN/MAX trick here, because if the min and max differ, then it implies that there are two distinct tags.
Step 1: Use a temp table to get all articles with titles.
Step 2: If an article occurs multiple times in your temp table, that means it has great and awesome as titles.
Try:
CREATE TEMPORARY TABLE MyTempTable (
select t1.id_article, t2.title
from table1 t1
inner join table3 t3 on t3.id_article = t1.id_article
inner join table2 t2 on t2.id_tag = t3.id_tag
)
select m.id_article
from MyTempTable m
group by m.id_article
having count(*)>1
Edit: This solution assumes there are two possible tags, great and awesome. If more, please add a "where" clause to the select query for creating the temp table like where t2.title in ('great','awesome')

Tricky MySQL JOIN query

I have a text input which upon keyup I want to update the options in an adjacent select field.
I have these 2 tables in my database:
Models:
ID modelName brandID
1 NA140 3
1 SRL 1
1 SRS 1
1 SRF 1
1 SMS 2
1 SMU 2
Brands:
ID brandName
1 Samsung
2 Bosch
3 Panasonic
In the select field I want to list all the brandNames from the brands table but list them in relevance to the text input.
So if 'SR' is typed the order of modelNames would be SRF, SRL, SRS, SMU, SMS, NA140 and then the corresponding brandName grabbed as the result but only list each brand once.
How would I write this query?
I have this basic idea which I think is what I need...
JOIN models & brands ON m.brandID = b.ID
MATCH modelName to string%
SELECT UNIQUE brandName
The only way I can think is to first do a select on where it matches the user input, get those results, then do it again where it does, and combine the results with a union, like so:
(SELECT `t1`.`modelName`, `t1`.`brandID`,`t2`.`brandName` FROM `Models` as `t1` INNER JOIN `Brands` AS `t2` ON `t1`.`brandID`=`t2`.`ID` WHERE `t1`.`modelName` LIKE 'SR%' ORDER BY `t1`.`modelName`,`t2`.`brandName`)
UNION
(SELECT `t1`.`modelName`, `t1`.`brandID`,`t2`.`brandName` FROM `Models` as `t1` INNER JOIN `Brands` AS `t2` ON `t1`.`brandID`=`t2`.`ID` WHERE `t1`.`modelName` NOT LIKE 'SR%' ORDER BY `t1`.`modelName`,`t2`.`brandName`)
There may be a more efficient way to do this, this is what I could think of before I head out to the store :)
select distinct brandName
from Models m inner join Brands b
on m.brandID = b.ID
where m.modelName like 's%'
Fiddle
I came up with a similar query (simplified):
select brandName from (
select b.brandName as brandName, m.modelName
from brands b join models m
on m.brandid = b.id
where m.modelName like '%sr%'
union
select b.brandName as brandName, m.modelName
from brands b join models m
on m.brandid = b.id
where m.modelName not like '%sr%') as curb
group by brandName
SQLfiddle: http://sqlfiddle.com/#!2/593b36/30
(Not exact naming of brands and models)
SELECT DISTINCT b.brandName
FROM brands b
JOIN models m
ON m.brandID = b.id
WHERE m.modelName LIKE '%$variable%'
UNION
SELECT b.brandName
FROM brands b
JOIN models m
ON m.brandID = b.id
WHERE m.modelName NOT LIKE '%$variable%'
What is needed is a function to calculate the distance between the value in the field, anchor, and the values in the table, this function can be the difference between the HEX of the anchor and the HEX of the table values.
SELECT m.id, modelName, brandName
FROM Model m
INNER JOIN Brand b ON m.brandID = b.ID
ORDER BY ABS(CONV(HEX(RPAD('SR', 5, ' ')), 16, 10)
- CONV(HEX(RPAD(modelName, 5, ' ')), 16, 10))
SQLFiddle Demo
Before passing the values to HEX, to have the correct result, the string need to be padded so that they all have the same length, that why there is a RPAD.
To calculate the difference the values are converted from base-16 to base-10, as the distance is only positive ABS is used.
If there are model with name longer than five char the parameter of the RPAD need to be modified to the length of the longest model.
Are you looking for something like this?
SELECT DISTINCT brandname
FROM models m JOIN brands b
ON m.brandid = b.id
ORDER BY modelname LIKE 'S%' DESC,
modelname LIKE 'SR%' DESC,
modelname
Output:
| BRANDNAME |
|-----------|
| Samsung |
| Bosch |
| Panasonic |
Here is SQLFiddle demo
UPDATE: For SMU% input string the query should look
SELECT DISTINCT brandname
FROM models m JOIN brands b
ON m.brandid = b.id
ORDER BY modelname LIKE 'S%' DESC,
modelname LIKE 'SM%' DESC,
modelname LIKE 'SMU%' DESC,
modelname
Output:
| BRANDNAME |
|-----------|
| Bosch |
| Samsung |
| Panasonic |
Here is SQLFiddle demo
I believe that the solution shouldn't be in the query part... at least for each letter typed. Maybe you could do one query for the first letter, retrieve the data for that letter into the client part, put the names in an array, and order in the client side.
Doing it in the server side is obviously slower, but making a query per each letter typed, taking into account how many times users type wrong letters, delete (would make a new query), and type again (new query), is terrible in terms of performance.
Putting the calculations in the client's side is much better. you could redo the query only if the first letter has been deleted and typed a new one.
May be you need some why to make new queries each time, but.. maybe a different approach cab help finding a better solution...

MySql In an inner join does it matter which table comes first?

I'm selecting data to output posts a user has book marked.
The main table which holds the ids of the posts a user has bookMarked, is called bookMarks. This is the table based on which posts will be selected from the posts table, to display to the user.
bookMarks
id | postId | userId
--------------------------
1 | US01 | 1
2 | US02 | 1
3 | US01 | 2
4 | US02 | 2
posts
id | postId | postTitle
--------------------------
1 | US01 | Title 1
2 | US02 | Title 2
3 | US03 | Title 3
4 | US04 | Title 4
My sql is currently like this:
select a.postsTitle
from posts a
inner join bookmarks b
on b.userId = a.userId
and b.userId = :userId
Notice, I have the table posts put first before the table bookmarks. But, since I'm selecting based on whats there in bookmarks, is it necessary I declare the table bookmarks first instead of post in the sql statement? Will doing it the way I'm doing it cause and problems in data selection or efficiency?
Or should I do it like:
select b.postsTitle
from bookmarks a
inner join posts b
on a.userId = b.userId
and a.userId = :userId
Notice, I have table bookmarks put first here.
Instead of the following:
select a.postsTitle
from posts a
inner join bookmarks b
on b.userId = a.userId
and b.userId = :userId
You should consider formatting your JOIN in this format, using the WHERE clause, and proper capitalization:
SELECT p.postsTitle
FROM bookmarks b
INNER JOIN posts p
ON p.userId = b.userId
WHERE b.userId = :userId
While it makes no difference (performance wise) to MySQL which order you put the tables in with INNER JOIN (MySQL treats them as equal and will optimize them the same way), it's convention to put the table that you are applying the WHERE clause to first. In fact, assuming proper indexes, MySQL will most likely start with the table that has the WHERE clause because it narrows down the result set, and MySQL likes to start with the set that has the fewest rows.
It's also convention to put the joined table's column first in the ON clause. It just reads more logically. While you're at it, use logical table aliases.
The only caveat is if you don't name your columns and instead use SELECT * like the following:
SELECT *
FROM bookmarks b
INNER JOIN posts p
ON p.userId = b.userId
WHERE b.userId = :userId
You'll get the columns in the order they're listed in the query. In this case, you'll get the columns for bookmarks, followed by the columns for posts.
Most would say never use SELECT * in a production query, but if you really must return all columns, and you needed the columns from posts first, you could simply do the following:
SELECT p.*, b.*
FROM bookmarks b
INNER JOIN posts p
ON p.userId = b.userId
WHERE b.userId = :userId
It's always good to be explicit about the returned result set.
There is no effect on query performance or final resultset with respect to placement of table on either side of JOIN clause if INNER JOIN is used .
The only difference observed is in order of columns returned and that too only if SELECT * is used . Suppose you have tableA(aid,col1,col2) and tableB(bid,col3)
SELECT *
FROM tableA
INNER JOIN tableB
ON tableA.aid=tableB.bid
returns column in order
aid|col1|col2|bid|col3
On otherhand
SELECT *
FROM tableB
INNER JOIN tableA
ON tableA.aid=tableB.bid
returns column in order
bid|col3|aid|col1|col2|
But it matters in case of LEFT JOIN or RIGHT JOIN.

how to make a MS SQL create view with unique ids?

Background:
i was first asked to do a website using mysql as the database, after that was done and sent, the clients asked me to convert it to mssql, and 3 tiers.
i did the classes and functions for transferring data between the servers, so the 3 tiers bit is about halfway done. the thing i'm struggling with now is the MsSQL.
Question:
the MySQL version of the question looks like this:
select a.id as a_id, a.first_name, a.last_name, a.agent_code,
b.id as b_id, b.user_id, b.status_admin
from tbl_user a
inner join tbl_testimonia b
on a.id = b.user_id
where b.status_admin=0
group by a.id
order by b.id desc
what it does is return testimonials that aren't approved yet, grouped to each user;
Where I am stuck:
i cant convert this to MSSQL
My best try:
Create view;
as in:
create view test as
(
select a.id as a_id, a.first_name, a.last_name, a.agent_code,
b.id as b_id, b.user_id, b.status_admin
from tbl_user a inner join tbl_testimonia b on a.id = b.user_id
where b.status_admin=0 and a.id in
(
select a.id from tbl_user a
inner join tbl_testimonia b
on a.id = b.user_id
where b.status_admin=0
group by a.id
)
)
this selects what i want, but the id field of the create view cant be grouped, meaning the id field is not unique and can have the same value.
I guess my question up to this point is, how do i uniquely select the id field in the view?
sure i can do it in PHP, but i could have done that a month ago when i first encountered the problem.
I've been trying to find the answer for a while now but cant seem to find the answer unique to my question
edit: the MSSQL doesn't work because even though the subquery is selecting unique ids the in statement in the main query makes this irrelevent
edit: example output::~ (in MySql the 3rd field is ommited)
| a_id | firstN | LastN | agent_code | b_id | user_id |status_admin|
+--------------------------------------------------------------------------+
| 32 | fn1 | ln1 | AC123213 | 14 | 32 | 0 |
| 41 | fn2 | ln2 | 12345678 | 15 | 41 | 0 |
| 32 | fn1 | ln1 | AC123213 | 16 | 32 | 0 |
edit: QUESTION SOLVED a big thx to Lieven for the simple answer
As you have already explained yourself, unlike SQL Server, MySQL allows grouping by with unaggregated expressions like this
SELECT *
FROM mytable
GROUP BY
column
As an unaggregated expression in GROUP BY returns an arbitrary record from each group and is not supposed to be used if the values vary you should be able to use the following statement as an equivalent in SQL Server
select a.id as a_id
, MIN(a.first_name)
, MIN(a.last_name)
, MIN(a.agent_code)
, MIN(b.id as b_id)
, MIN(b.user_id)
, MIN(b.status_admin)
from tbl_user a
inner join tbl_testimonia b on a.id = b.user_id
where b.status_admin=0
group by
a.id
order by
b.id desc
If you want to have non repetitive ids you can
select DISTINCT a.id from tbl_user a
inner join tbl_testimonia b
on a.id = b.user_id where b.status_admin=0
in your sub query