Join two tables, matching a column with multiple values - mysql

I am trying to get a product matching some custom parameters.
So I have to three tables - products, parameters and parametersitems.
Products table:
CREATE TABLE `products` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT
`Title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`Content` longtext COLLATE utf8_unicode_ci NOT NULL,
`Price` float(10,2) unsigned NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Parameter table:
CREATE TABLE `parameters` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`Label` varchar(80) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Parameter items table:
CREATE TABLE `parametersitems` (
`ProductID` int(10) unsigned NOT NULL DEFAULT '0',
`ParameterID` int(10) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`ProductID`,`ParameterID`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
So my question is how can I get only the products matching all the parameters.
The only way I could think of is joining the parameteritems table couple of times.
For example, here is a query to get the products matching two parameters:
SELECT
products.*
FROM
products
INNER JOIN
parametersitems AS paritems1
ON
paritems1.ItemID = products.ID
AND paritems1.ParameterID = 7
INNER JOIN
parametersitems AS paritems2
ON
paritems2.ItemID = products.ID
AND paritems2.ParameterID = 11
My only concern is that the SELECT query will get slower and slower if there more parameters selected.
So is there a better way to handle this problem?
Thank you

Adjust the value tested in the HAVING clause to match the number of values listed in the IN clause.
SELECT p.*
FROM products p
WHERE p.ID IN (SELECT pi.ItemID
FROM parameteritems pi
WHERE pi.ItemID = p.ID
AND pi.ParameterID IN (7,11)
GROUP BY pi.ItemID
HAVING COUNT(DISTINCT pi.ParameterID) = 2)

select p.*
from products p
inner join (
select ItemID
from parametersitems
where ParameterID in (7, 11)
group by ItemID
having count(distinct ParameterID) = 2
) pm on p.ID = pm.ItemID

SELECT
p.ID, p.Title, p.Content, p.Price
FROM
products AS p
INNER JOIN
parametersitems AS pi ON pi.ProductID = p.ID
GROUP BY
p.ID, p.Title, p.Content, p.Price
HAVING COUNT(DISTINCT pi.ParameterID) = (SELECT COUNT(ID) FROM parameters);
This will always get you products matching every parameter no matter how many parameters you add. (This could become bogus if you delete a parameter without deleting the corresponding rows in paramatersitems. This is what constraints are for.)

Related

mySQL Left Join 5 Tables?

Thanks in advance for any help. I am working with 5 tables in a mySQL database. The system is such that I have a top level table called "owners" (clients) that have local business (shops). These owners go out and create accounts at websites like yelp (citation_sources) and as such have login credential (citation_login). Once they have an account at a citation source, they add shops to the directory.
I am hoping to create one query that would select ALL of the citation sources, regardless of if an owner has an account or not, and loop through the recordset, showing login for each citation source they have an account with, as well as any shop listings.
My question pertains to doing a left join on 5 tables. I left out most fo the fields but have set up primary and foreign keys Is the sequence of the join important, ie. start with one particular table, ending with another?
I tried this command but it only brings back 33 rows when in fact there are 96 citation_sources.
I think I figured it out. I created a new table called "citation_shop" with a composite primary key - citation - shop. I then ran a query and it got me the results I was after. I ended up putting a condition in the first left join.
SELECT citation_sources.name, citation_shop.shop from citation_sources left join citation_shop on citation_sources.id = citation_shop.citation and citation_shop.shop in (6,7) left join shops on citation_shop.shop = shops.id group by citation_sources.name, citation_shop.shop limit 100
CREATE TABLE `citation_shop` (
`shop` smallint(5) UNSIGNED NOT NULL,
`citation` smallint(6) UNSIGNED NOT NULL,
`url` text NOT NULL,
`count` smallint(3) UNSIGNED NOT NULL,
`status` tinyint(1) UNSIGNED NOT NULL,
`sort` tinyint(3) UNSIGNED NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `citation_shop`
--
ALTER TABLE `citation_shop`
ADD PRIMARY KEY (`citation`,`shop`);
select owners.id as owner_id, shops.id as shop_id, citation_sources.name, citation_shop_urls.url, citation_logins.password
from owners
inner join shops on owners.id = shops.owner_id
left join citation_logins on owners.id = citation_logins.owner
left join citation_sources on citation_logins.c_source = citation_sources.id
left join citation_shop_urls on citation_sources.id = citation_shop_urls.citation_id
where owners.id = 3
group by citation_sources.name
Here are my tables in order of what I think is relevlance:
CREATE TABLE `owners` (
`id` smallint(6) UNSIGNED NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
CREATE TABLE `shops` (
`id` smallint(5) UNSIGNED NOT NULL,
`title` varchar(50) DEFAULT '',
`owner_id` smallint(5) UNSIGNED NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `citation_sources` (
`id` smallint(6) UNSIGNED NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `citation_shop_urls` (
`shop` smallint(5) UNSIGNED NOT NULL DEFAULT '0',
`citation_id` tinyint(5) UNSIGNED NOT NULL DEFAULT '0',
`owner` smallint(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `citation_logins` (
`c_source` smallint(5) UNSIGNED NOT NULL DEFAULT '0',
`owner` smallint(6) NOT NULL,
`user_name` text NOT NULL,
`password` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
In a LEFT JOIN, the first table is the one where you get all the rows, even if they don't have a match in other tables. So if you want all citation_sources, even those not associated with any owner, then citation_sources should be the table on the left of the LEFT JOIN.
To filter the owner information only to id = 3, put o.id = 3 in the ON clause that joins with owners. Then use a WHERE clause to remove all the other rows.
SELECT o.id as owner_id, s.id as shop_id, cs.name, u.url, cl.password
FROM citation_sources AS cs
LEFT JOIN citation_shop_urls AS u ON u.citation_id = cs.id
LEFT JOIN citation_logins AS cl ON cs.id = cl.c_source
LEFT JOIN owners AS o ON o.id = cl.owner AND o.id = 3
LEFT JOIN shops AS s ON s.owner_id = o.id
WHERE o.id IS NULL OR o.id = 3

inner join not giving results as expected

Product_table
Product_table_link
This be the product_table data that is stored inside the database.
This is the product_table_link data that is present inside the DB.I was trying to join these two tables where the product code=something.For instance let us take xyz as the product.
I was hoping to get the combined results of the two without any nulls present.
I tried :
SELECT s1.* FROM (SELECT p1.* FROM product_table p1 INNER JOIN product_table_link p2
ON p1.product_code=p2.product_code ) s1 WHERE product_code="xyz"
But the result is not the combination of the both tables rather it shows me the product_table.
CREATE TABLE `product_table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_name` varchar(60) NOT NULL,
`product_code` varchar(60) NOT NULL,
`product_description` text,
`product_type` varchar(20) NOT NULL,
`product_image_path` varchar(60) NOT NULL,
`product_company_name` varchar(20) NOT NULL,
`product_company_id` varchar(60) NOT NULL,
`product_landing_page` varchar(15) NOT NULL,
`product_shape` varchar(20) DEFAULT NULL,
`product_flavour` varchar(20) NOT NULL,
`product_veg_mark` varchar(8) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique` (`product_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1
CREATE TABLE `product_table_link` (
`product_code` varchar(60) NOT NULL,
`product_weight` varchar(5) NOT NULL,
`product_price` int(5) NOT NULL,
`product_quantity` int(5) NOT NULL,
PRIMARY KEY (`product_code`,`product_weight`),
CONSTRAINT `product_table_link_ibfk_1` FOREIGN KEY (`product_code`) REFERENCES `product_table` (`product_code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
How do i get the combined results of the two tables?
Change your subquery to this
SELECT * FROM product_table p1 INNER JOIN product_table_link p2
ON p1.product_code=p2.product_code
You were selecting just from p1.
You were doing select p1.* which will only select rows from p1. Inner join means you want to select rows from different tables on the basis of some conditions. You need to do select * which will select all the rows filtered from the join condition.
Your simplified final query.
SELECT s1.* FROM (SELECT p1.*,p2.product_weight,p2.product_price,p2.product_quantity
FROM product_table p1 INNER JOIN product_table_link p2
ON p1.product_code=p2.product_code)
s1 WHERE s1.product_code="xyz"
SELECT p1.* FROM product_table p1 INNER JOIN product_table_link p2
ON p1.product_code=p2.product_code
In the above subquery you are selecting values from p1 only. So, you have to change your query to what FallAndLearn has written.

3 Mysql Inner Joins with last join being the ORDER BY Clause

I have 3 tables I'm trying to inner join (ambitious I know). The first query of the join, just basically queries my members table to pipe into the second query, which is the post table that actually holds the posts for those members (users search by member info to see their posts). The third and final query is simply ordering by the frequency of the most viewed posts. I have these two queries working separately:
$sql_string = "
SELECT m.id
, m.username
, m.gender
, p.*
FROM members m
JOIN posts p
ON p.member_id = m.id
WHERE m.active='y'
AND m.gender='M'
AND m.city='Los Angeles'
AND m.state='California'
AND p.active='y';
";
which accomplishes the first 2 queries and this final query:
$sql_string2 = "SELECT post_id FROM post_views GROUP BY post_id ORDER BY COUNT(*) DESC";
Which accomplishes the final query. I just need to combine the 2, but when I do that:
$final_sql_string = "SELECT members.id, members.username, members.gender, posts.* FROM members INNER JOIN posts ON members.id = posts.member_id WHERE members.active='y' AND members.gender='M' AND members.city='Los Angeles' AND members.state='California' AND posts.active='y' INNER JOIN post_views ON posts.id = post_views.post_id GROUP BY post_views.post_id ORDER BY COUNT(*) DESC";
I get an error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'INNER JOIN post_views ON posts.id = post_views.post_id GROUP BY post_views.post_' at line 1
Any ideas? Here are my tables for anyone interested:
CREATE TABLE IF NOT EXISTS `members` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`age` varchar(3) NOT NULL,
`gender` varchar(1) NOT NULL,
`city` varchar(20) NOT NULL,
`state` varchar(50) NOT NULL,
`active` enum('y','n') NOT NULL DEFAULT 'y',
`created_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
CREATE TABLE IF NOT EXISTS `posts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`member_id` int(11) NOT NULL,
`title` text NOT NULL,
`comments` enum('y','n') NOT NULL DEFAULT 'y',
`post_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`list_weight` double NOT NULL,
`active` enum('y','n') NOT NULL DEFAULT 'y',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=47 ;
CREATE TABLE IF NOT EXISTS `post_views` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`post_id` int(11) NOT NULL,
`member_id` int(11) NOT NULL,
`post_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=287 ;
The following should work:
SELECT m.id
, m.username
, m.gender
, p.*
, pc.post_count
FROM members m
JOIN posts p
ON p.member_id = m.id
LEFT JOIN (
SELECT post_id, COUNT(*) post_count FROM post_views GROUP BY post_id
) pc ON p.id = pc.post_id
WHERE m.active='y'
AND m.gender='M'
AND m.city='Los Angeles'
AND m.state='California'
AND p.active='y'
ORDER BY post_count DESC;
"join in" the counts you gathered and order by those.
If you want to keep your "style" you may use:
SELECT m.id
, m.username
, m.gender
, p.*
FROM members m
JOIN posts p
ON p.member_id = m.id
WHERE m.active='y'
AND m.gender='M'
AND m.city='Los Angeles'
AND m.state='California'
AND p.active='y'
ORDER BY (SELECT COUNT(*) FROM post_views WHERE post_id = p.id) DESC;

Joining different tables based on column value

I have a table called notifications:
CREATE TABLE `notifications` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`type` varchar(20) NOT NULL DEFAULT '',
`parent_id` int(11) DEFAULT NULL,
`parent_type` varchar(15) DEFAULT NULL,
`type_id` int(11) DEFAULT NULL,
`etc` NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8;
Each notification is related to a different table, the value of parent_type field specifies the name of the table that I want to * join the table with. All target tables have several similar columns:
CREATE TABLE `tablename` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`is_visible` tinyint(1) NOT NULL,
`etc` NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
Currently I'm using this query for selecting notifcations that their related row in the target table exists and their is_visible field is 1:
SELECT n.id,
FROM notifications n
LEFT JOIN books b ON n.parent_id = b.id AND n.parent_type = 'book' AND b.is_visible = 1
LEFT JOIN interviews i ON n.parent_id = i.id AND n.parent_type = 'interview' AND i.is_visible = 1
LEFT JOIN other tables...
WHERE n.user_id = 1
GROUP BY n.id
But since it is a LEFT JOIN it returns the notification if it matches any table or not, how can I rewrite it so it doesn't return notifications that don't match with any row in the target table? I have also tried the CASE statement unsuccessfully.
I'm not 100% sure the syntax is right and I have no chance to test it right now, but the idea should be clear.
SELECT DISTINCT n.id
FROM notifications n
JOIN (
(SELECT b.id, 'book' AS type FROM books b WHERE b.is_visible = 1)
UNION
(SELECT i.id, 'interview' AS type FROM interviews i WHERE i.is_visible = 1)
) ids ON n.parent_id = ids.id AND n.parent_type = ids.type
WHERE n.user_id = 1

What's wrong with my group_concat SQL query?

I have three tables:
CREATE TABLE `b10g_entries` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`permalink` text NOT NULL,
`title` varchar(300) NOT NULL,
`fullcontent` text NOT NULL,
`introcontent` text NOT NULL,
`dateadded` datetime NOT NULL,
`lastedited` datetime NOT NULL,
`author` varchar(40) NOT NULL,
`comments` int(11) NOT NULL DEFAULT '0',
`published` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=299 DEFAULT CHARSET=utf8
CREATE TABLE `b10g_tag_map` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`tag_id` bigint(20) unsigned DEFAULT NULL,
`entry_id` bigint(20) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8
CREATE TABLE `b10g_tags` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
And i'm trying to get first 25 blog entries with their tags (that's why I use many-to-many relationship) using this query:
SELECT b10g_entries.*, GROUP_CONCAT( b10g_tags.name SEPARATOR ', ')
AS tags FROM b10g_entries
LEFT JOIN b10g_tag_map ON b10g_entries.id = b10g_tag_map.entry_id
LEFT JOIN b10g_tags ON b10g_tag_map.tag_id = b10g_tags.id LIMIT 0, 25;
But I only get one record back. What's wrong with this query?
Add a GROUP BY clause.
Now, you're getting a list of ALL tags found anywhere in the set. Instead, you only want the ones within the group (by entry).
SELECT b10g_entries.*, GROUP_CONCAT( b10g_tags.name SEPARATOR ', ')
AS tags FROM b10g_entries
LEFT JOIN b10g_tag_map ON b10g_entries.id = b10g_tag_map.entry_id
LEFT JOIN b10g_tags ON b10g_tag_map.tag_id = b10g_tags.id
GROUP BY b10g_entries.id
You have a GROUP_CONCAT() aggregate function, but have not used a GROUP BY clause, so your result will be one row.
Note that in MySQL it is permissible to use a GROUP BY with only one column specified while many more appear in the SELECT list, but that is not portable to other RDBMS. So instead, I have joined b10g_entries in a second time to connect all the other columns from that table, while only using the id in the GROUP BY.
SELECT
b10g_entries_all.*,
GROUP_CONCAT( b10g_tags.name SEPARATOR ', ') AS tags
FROM
/* Main table, used gor GROUP BY aggregate */
b10g_entries
/* self join to pull in other columns without needing to put them in GROUP BY */
JOIN b10g_entries b10g_entries_all ON b10g_entries.id = b10g_entries_all.id
LEFT JOIN b10g_tag_map ON b10g_entries.id = b10g_tag_map.entry_id
LEFT JOIN b10g_tags ON b10g_tag_map.tag_id = b10g_tags.id
/* group on the entry id */
GROUP BY b10g_entries.id
LIMIT 0, 25;