EDIT: SQL Fiddle here
I'm working on a product feed. I get a list of offers;
Every offer has 1 product
Every product belongs to 1 category
A category can be a subcategory to another category
I have three tables (of which i will only show you the relevant rows)
Offers:
OFFERS
___________________
| id | product_id |
-------------------
| 1 | 16 |
-------------------
| 2 | 54 |
-------------------
| 3 | 52 |
-------------------
| 4 | 20 |
-------------------
| 5 | 7 |
-------------------
| 6 | 5 |
-------------------
Products:
PRODUCTS
_______________
| id | cat_id |
---------------
| 16 | 1 |
---------------
| 54 | 3 |
---------------
| 52 | 4 |
---------------
| 20 | 1 |
---------------
| 7 | 15 |
---------------
| 5 | 3 |
---------------
Categories:
CATEGORIES
_____________________________________________________________
| id | display_name | original_name | subcat_of | is_active |
-------------------------------------------------------------
| 1 | Cars | automobiles | 0 | 1 |
-------------------------------------------------------------
| 2 | | motorcycles | 0 | 0 |
-------------------------------------------------------------
| 3 | Muscle cars | muscle-cars | 1 | 1 |
-------------------------------------------------------------
| 4 | Hybrid cars | treehugwagons | 1 | 1 |
-------------------------------------------------------------
I have to write two queries. The first one needs to
return the names and count the amount of offers for a given main category and its subcategories
but only if that main category is active
and if a category has no display_name, use the original_name
I think i have this one down:
SELECT
offers.id AS offer_id,
product_id,
products.cat_id,
CASE
WHEN categories.display_name <> ''
THEN categories.display_name
ELSE categories.original_name
END AS cat_name,
COUNT(offers.id) as num_offers
FROM
offers
INNER JOIN
products
ON
product_id = products.id
INNER JOIN
categories
ON
cat_id = categories.id
WHERE
categories.is_active = 1
AND
(categories.id = :cat_id OR categories.subcat_of = :cat_id)
GROUP BY
cat_name
ORDER BY
cat_name ASC
I'm pretty sure this query is far from ideal, but for now, it works.
It is the second query I need that gives me problems. That one needs to:
return the names and count the amount of offers for a given main category and its subcategories and return the sum of those counts per main category
but only if that main category is active
and if a category has no display_name, use the original_name
I could use some PHP to do the summing myself, but I'd be surprised if something that easy could not be done in SQL.
I believe it is possible and fairly simple, assuming you don't have sub-sub-categories:
SELECT CASE
WHEN c_main.display_name <> ''
THEN c_main.display_name
ELSE c_main.original_name
END cat_name,
COUNT(o.id) as num_offers
FROM offers o
JOIN products p
ON o.product_id = p.id
JOIN categories c
ON p.cat_id = c.id
AND (c.id = :cat_id OR c.subcat_of = :cat_id)
/* AND c.is_active = 1 /* Include if necessary */
JOIN categories c_main
ON c_main.id = :cat_id
AND c_main.is_active = 1
GROUP BY cat_name
ORDER BY cat_name ASC
Your first query I would write as:
SELECT CASE
WHEN c.display_name <> ''
THEN c.display_name
ELSE c.original_name
END cat_name,
COUNT(o.id) as num_offers
FROM offers o
JOIN products p
ON o.product_id = p.id
JOIN categories c
ON p.cat_id = c.id
AND (c.id = :cat_id OR c.subcat_of = :cat_id)
AND c.is_active = 1
GROUP BY cat_name
ORDER BY cat_name ASC
As an aside:
I would also consider NULLing out display_names that are empty, then you can replace
CASE
WHEN c_main.display_name <> ''
THEN c_main.display_name
ELSE c_main.original_name
END cat_name
With:
COALESCE(c_main.display_name, c_main.original_name) cat_name
It can't be done in MySQL since MySQL doesn't support recursive queries. You have three choices:
Emulate recursive/hierarchical queries with some complex function. (Related)
Switch to SQL Server which supports recursive queries using CTE.
Write a bit of PHP.
It's up to you :)
As a side note i leave here an interesting slide on Models for Hierarchical data with SQL and PHP written by SO user #Bill Karwin which may suit your situation.
Related
I would like to count the number of rows or in this case the number of products which are in the same category and return zero if there are no products in the category.
The tables in my query looking like this:
category category_lang media
------------ ------------- ---------
category_id | published category_id | name | alias media_id | category_id
----------------------- -------------------------- ----------------------
1 | 1 1 | One | one 1 | 1
2 | 1 2 | Two | two 2 | 2
3 | 1 3 | Three| three 3 | 3
media_lang product_category
------------------------------- ----------------
media_id | url | file_name product_id | category_id
------------------------------- ------------------------
1 | /images/ | file1.jpg 1 | 1
2 | /images/ | file2.jpg 2 | 1
3 | /images/ | file3.jpg 3 | 2
and I would like a result like this:
category_id | category_name | alias | media_id | url | file_name | count
1 | One | one | 1 | /images/ | file1.jpg | 2
2 | Two | two | 2 | /images/ | file2.jpg | 1
3 | Three | three | 3 | /images/ | file3.jpg | 0
My query currently looks like this
SELECT
c.`category_id`,
ca.`name`,
ca.`alias`,
m.`media_id`,
ma.`url`,
ma.`file_name`,
COUNT(p.`product_id`) AS `count`
FROM `category` c
LEFT JOIN `category_lang` ca ON (c.`category_id` = ca.`category_id`)
LEFT JOIN `media` m ON (c.`category_id` = m.`category_id`)
LEFT JOIN `media_lang` ma ON (m.`media_id` = ma.`media_id`)
LEFT JOIN `product_category` p ON (c.`category_id` = p.`category_id`)
WHERE c.`published` = 1
ORDER BY ca.`name`
my logic is obviously wrong because this query will return this:
category_id | category_name | alias | media_id | url | file_name | count
1 | One | one | 1 | /images/ | file1.jpg | 3
How can I achieve the desired result?
I think the most painless (and sane) way to approach this would be to just aggregate the product counts for each category from the product_category table in a separate subquery, and then just join this to what you already have:
SELECT
c.category_id,
ca.name,
ca.alias,
m.media_id,
ma.url,
ma.file_name,
COALESCE(t.cat_count, 0) AS cat_count
FROM category c
LEFT JOIN category_lang ca
ON c.category_id = ca.category_id
LEFT JOIN media m
ON c.category_id = m.category_id
LEFT JOIN media_lang ma
ON m.media_id = ma.media_id
LEFT JOIN
(
SELECT category_id, COUNT(*) AS cat_count
FROM product_category
GROUP BY category_id
) t
ON c.category_id = t.category_id
WHERE c.published = 1
ORDER BY ca.name
Note here that your product_category table does not have any category entries with no products. This is not a problem, because in the LEFT JOIN we can simply treat a NULL count as being zero. A NULL value would occur if a given category did not match to anything in the subquery.
Seems to me that it is where you are doing the count and that you doing have the criteria for doing the count. Adding an extra condition where product category id equals category id in your category table should fix the incorrect count.
SELECT
c.`category_id`,
ca.`name`,
ca.`alias`,
m.`media_id`,
ma.`url`,
ma.`file_name`,
COUNT(p.`product_id`) AS `count`
FROM `category` c
LEFT JOIN `category_lang` ca ON (c.`category_id` = ca.`category_id`)
LEFT JOIN `media` m ON (c.`category_id` = m.`category_id`)
LEFT JOIN `media_lang` ma ON (m.`media_id` = ma.`media_id`)
LEFT JOIN `product_category` p ON (c.`category_id` = p.`category_id`)
WHERE (c.`published` = 1) and (c.`category_id` = p.`category_id`)
ORDER BY ca.`name`
So I have a table of "Articles" that has a foreign key of "category_id".
Articles
| id | title | pub_date | category_id |
-----------------------------------
| 0 | abc | 23423443 | 1 |
| 1 | def | 23423423 | 2 |
| 2 | ghi | 24234234 | 1 |
| 3 | jkl | 23423424 | 3 |
| 4 | mop | 23432435 | 2 |
Categories
| id | title |
----------------
| 1 | News |
| 2 | Feature |
| 3 | Review |
I have the title of a category.
I would like to, in one query, ascertain the id of said category and use it to return articles where the category_id = id and publish date is less than the current date time.
Is this possible?
I am using Postgres but I am looking at this from an academic standpoint so answers in any SQL dialect would be fine as I am happy to do the translation myself for the education.
select *
from Articles
where pub_date <= now() and
category_id = (select id from Categories where title="TITLE")
Is this the kind of thing you're after? I don't know if this is PostgresSQL or not.
select A.id, A.title, A.pub_date, C.id, C.title
from Articles A join Categories C on A.category_id=C.id
where C.title = MY_CATEGORY_TITLE and
a.pub_date < CURRENT_DATE_TIME
I'm ignoring the need for indexes, orders etc.
it's just a simple join
try
SELECT A.id,A.title,A.pub_date,A.category_id,
C.title
FROM Articles A
INNER JOIN Categories C
ON A.category_id = C.id
WHERE pub_date < NOW()
AND C.title = "your title"
I have the following tables:
members
This stores a list of members for our system.
---------------------
| member_id | name |
---------------------
| 1 | Bob |
---------------------
| 2 | Joe |
---------------------
| 3 | Tom |
---------------------
| 4 | Bill |
---------------------
| 5 | Will |
---------------------
categories
This stores the categories for our system. Categories are not visible to members by default. A member must have a valid licence to be able to access a category (see below).
----------------------
| cat_id | name |
----------------------
| 1 | Cat1 |
----------------------
| 2 | Cat2 |
----------------------
| 3 | Cat3 |
----------------------
licences
Stores the licences that a member has. One member can have many licences. Licences can have a life time and will expire. Once a licence expires, the member can no longer view the category.
------------------------------------------------------
| id | catid | subid | valid_from | valid_to |
------------------------------------------------------
| 1 | 1 | 1 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 2 | 1 | 2 | 1999-01-01 | 2001-01-02 |
------------------------------------------------------
| 3 | 1 | 3 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 4 | 1 | 4 | 1999-01-01 | 2000-01-01 |
------------------------------------------------------
| 5 | 1 | 5 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 6 | 2 | 1 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 7 | 2 | 2 | 1999-01-01 | 2001-01-02 |
------------------------------------------------------
| 8 | 2 | 3 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 9 | 2 | 4 | 1999-01-01 | 2000-01-01 |
------------------------------------------------------
| 10 | 2 | 5 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 11 | 3 | 1 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 12 | 3 | 2 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
preferences
The preferences table stores whether a member wishes to receive e-mails that are are sent relating to a category. The member can set a preference of '1' for 'wish to receive' or '0' for 'do not wish to receive'. A quirk is that if the member has no record (or a null value) we make an assumption that they wish to receive.
-----------------------------------
| id | catid | subid | pref |
-----------------------------------
| 1 | 1 | 1 | 0 |
-----------------------------------
| 2 | 2 | 1 | 1 |
-----------------------------------
| 3 | 3 | 1 | 1 |
-----------------------------------
| 4 | 1 | 2 | 0 |
-----------------------------------
| 5 | 1 | 3 | 1 |
-----------------------------------
| 6 | 2 | 3 | 0 |
-----------------------------------
recipients
When an email is sent out based on a category, the recipient is logged so we don't email them more than once.
-----------------------------
| id | emailid | subid |
-----------------------------
| 1 | 1 | 1 |
-----------------------------
| 2 | 1 | 2 |
-----------------------------
I'm trying to write a query the fetches all members, and their related licence for a range of category IDs, their preferences and also make sure that they don't have a record in the recipients table.
In pseudo query:
SELECT [all members, their licence info, and preference setting]
FROM [members table]
WHERE [member doesnt exist in the recipients table for a given emailID]
The issue is that I need to check against multiple categoryIDs, but return just one result and only if the preference is set to 1 (or null, or doesn't exist).
So with the example data, given we are searching for categoryIDs 1,2 and 3 (A member must have a licence for at least one of these categories) and checking against emailID of 1, the only result should be for member_id 3 (Tom) with preference ID of 6 (because it's set to 1) and licence ID of 3 (because it's valid and the preference ID of 6 corresponds to it and it is set to 1). The second result should be member_id 5 (Will) as he has a licence to catids 1 and 2, he hasn't received the email with ID of 1 and he has no specific preference set.
Reason being: Members 1 and 2 are in the recipient table for emailID 1, member 2's licence has also expired, member 4's licence has expired and member 5 has their preference set to 0.
The query I've written which isn't working quite right is:
SELECT
members.member_id,
members.name,
licence.catid as licencedToCat,
categories.cat_name as categoryName,
licence.valid_from as licenceStart,
licence.valid_to as licenceEnd,
preferences.pref
FROM (`members`)
JOIN `licence` ON `licence`.`subid`=`members`.`member_id`
JOIN `preferences` ON `preferences`.`subid`=`members`.`member_id`
JOIN `categories` ON `categories`.`cat_id`=`licence`.`catid`
WHERE `licence`.`catid` IN (1,2,3)
AND `start_date` <= '2014-12-16'
AND `end_date` >= '2014-12-16'
AND (pref='1' OR pref IS NULL)
AND `members`.`member_id` NOT IN (SELECT subid FROM `recipients` WHERE `recipients`.`emailid`='1')
GROUP BY `licence`.`subid`
The issue is that the query is returning results saying users have a preference set to 1 where they actually don't even have a record set for that category.
The desired output is any member(s) along with the licence they have for the category but only if their preference for that category is 1/null/doesn't exist AND only if they don't appear in the recipients table for a given emailID.
So, if a member has 2 licences
I appreciate this was a long read, so thanks if you're still here! Any ideas on how to tweak my query to solve this?
I think part of your problem here is that you're using all inner joins. Like you said, a user may not have a preference, so a row may not be returned in your query. That being said, it seems like you want to inner join most tables, as it appears you only want members who have licenses, but you want to see all licenses regardless of whether that user has a preference. So, I made preferences an outer joined table:
SELECT m.*, l.catid AS licenseCat, c.name AS categoryName,
l.valid_from AS licenseStart, l.valid_to AS licenseEnd, p.pref AS preference
FROM members m
JOIN licenses l ON l.subid = m.member_id
JOIN categories c ON c.cat_id = l.catid
LEFT JOIN preferences p ON p.catid = c.cat_id AND p.subid = l.subid;
Once I had done that, I wrote the subquery that pulled the member_id of all members who are in the recipients table with the specified email:
SELECT subid
FROM recipients
WHERE emailid = 1;
Now you can insert that into your original query, and add your other requirements:
SELECT m.*, l.catid AS licenseCat, c.name AS categoryName,
l.valid_from AS licenseStart, l.valid_to AS licenseEnd, IFNULL(p.pref, 0) AS preference
FROM members m
JOIN licenses l ON l.subid = m.member_id
JOIN categories c ON c.cat_id = l.catid
LEFT JOIN preferences p ON p.catid = c.cat_id AND p.subid = l.subid
WHERE c.cat_id IN (1, 2, 3) AND
l.valid_from <= '2014-12-06' AND l.valid_to >= '2014-12-06' AND
m.member_id NOT IN (SELECT subid FROM recipients WHERE emailid = 1)
AND (p.pref = 1 OR p.pref IS NULL);
You said in your question that this should return member_id 3 (which is Tom) but that does not match your results because member 5 has no preferences, so we should assume they want an email right? I'm also not sure how to group this for you. If a member has multiple subscriptions, which one do you want to keep?
I built an SQL Fiddle and tested what I have and it's really close. I hope this can at least push you in the right direction and I will edit the answer as needed.
EDIT
The following will give you what you want, but it is not always recommended. If you really don't care about the subscription dates (as long as it meets the criteria in the where clause) and you really don't care about the category for the user, just add GROUP BY m.member_id to get one row for each member.
So, the final query is like these, tested and working:
SELECT
m.member_id,
m.email,
l.catid as licencedToCat,
c.cat_name as categoryName,
l.valid_from as licenceStart,
l.valid_to as licenceEnd,
COALESCE(p.pref, 1) pref
FROM members m
JOIN licence l ON l.subid = m.member_id
JOIN categories c ON c.cat_id = l.catid
LEFT JOIN preferences p ON p.subid= m.member_id AND p.cat_id = l.cat_id
LEFT JOIN recipients r ON r.subid = m.member_id
WHERE l.catid IN (1,2,3)
AND start_date <= '2014-12-16' AND end_date >= '2014-12-16'
AND COALESCE(p.pref, 1) = 1
AND COALESCE(r.emailid, 0) = 0-- assuming with emailid = 0 it remains valid as recipient
GROUP BY m.member_id
However, for the purpose of the query it should only have DISTINCT m.* in the SELECT clause which would discard the GROUP BY
I have slight problem with mysql query. I have two tables:
bioshops
+------------+-------------+
| bioshop_id | name |
+------------+-------------+
| 1 | Bioshop1 |
| 2 | Bioshop2 |
+------------+-------------+
bioshop_have_product
+----+-----------------+--------------+
| id | bioshop_id | product_id |
+----+-----------------+--------------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
| 4 | 2 | 3 |
+----+-----------------+--------------+
The tables are much more complex but this is the important structure. prodict_id in bioshop_have_product is also FK. I need to select bioshops witch contains all products that I ask. Example:
if I need bioshops with product 1 it should return Bioshop1 and Bioshop2 with all products
if I need bioshops with product 1 and 2 it should return Bioshop1 with all products
My query is:
SELECT bs.name AS name,
bs.id AS bioshop_id,
bshd.id AS id,
bshd.product_id AS product_id
FROM bioshops bs
JOIN bioshop_have_product bshp
ON bs.bioshop_id = bshp.bioshop_id
WHERE (bshp.bioshop_id = bs.bioshop_id AND bshp.product_id = '1')
AND (bshp.bioshop_id = bs.bioshop_id AND bshp.product_id = '2')
but this returns nothing and I want it to return Bioshop1 because only Bioshop1 countains both objects.
You can try something like this:
SELECT bs.name AS name,
bs.id AS bioshop_id,
bshp.id AS id,
bshp.product_id AS product_id
FROM bioshop bs
JOIN bioshop_have_product bshp
ON bs.id = bshp.bioshop_id AND
(SELECT COUNT(*) FROM bioshop_have_product WHERE product_id IN (1, 2) AND bs.id = bioshop_id) = X
where X should be equal to the count of different products you whant to check, for instance 2 in your second case.
SELECT bioshop_id
FROM bioshop_have_product
WHERE product_id IN (1,2)
GROUP
BY bioshop_id
HAVING COUNT(*) = 2;
Say I have a database that has people, grocery stores, and items you can buy in the store, like so:
Stores People Foods
----------------- ------------------ ------------------
| id | name | | id | name | | id | name |
----------------- ------------------ ------------------
| 1 | Giant | | 1 | Jon Skeet | | 1 | Tomatoes |
| 2 | Vons | | 2 | KLee1 | | 2 | Apples |
| 3 | Safeway | ------------------ | 3 | Potatoes |
----------------- ------------------
I have an additional table which keep track of which stores sell what:
Inventory
--------------------
| store_id| food_id|
--------------------
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
--------------------
And I have another table that has shopping lists on it
Lists
---------------------
| person_id| food_id|
---------------------
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 3 |
---------------------
My question is, given a person, or their id, what is the best way to figure out what stores they can go to so they will get everything on their list. Is there a pattern for these types of computations in MySQL?
My attempt (very ugly and messy) is something like:
-- Given that _pid is the person_id we want to get the list of stores for.
SELECT stores.name, store_id, num, COUNT(*) AS counter
FROM lists
INNER JOIN inventory
ON (lists.food_id=inventory.food_id)
INNER JOIN (SELECT COUNT(*) AS num
FROM lists WHERE person_id=_pid
GROUP BY person_id) AS T
INNER JOIN stores ON (stores.id=store_id)
WHERE person_id=_pid
GROUP BY store_id
HAVING counter >= num;
Thanks for your time!
Edit SQL Fiddle with Data
If I were to solved the problem, I'll join the four tables with their linking column (specifically the foreign keys) then a subquery on the HAVING clause to count the number of items on the list for each person. Give this a try,
SET #personID := 1;
SELECT c.name
FROM Inventory a
INNER JOIN Foods b
ON a.food_id = b.id
INNER JOIN Stores c
ON a.store_id = c.id
INNER JOIN Lists d
ON d.food_id = b.id
WHERE d.person_id = #personID
GROUP BY c.name
HAVING COUNT(DISTINCT d.food_id) =
(
SELECT COUNT(*)
FROM Lists
WHERE person_ID = #personID
)
SQLFiddle Demo
#JohnWoo: why DISTINCT?
Another one...
SET #pid=2;
SELECT store_id, name
FROM inventory
JOIN lists ON inventory.food_id=lists.food_id
JOIN stores ON store_id=stores.id
WHERE person_id=#pid
GROUP BY store_id
HAVING COUNT(*)=(
SELECT COUNT(*)
FROM lists
WHERE person_id=#pid
);