What I like to achieve is
a: display all Items that are in all of the selected category's
b: return / update the category list with category's available based on selection
I like items to be stored and be found by use of the adjacency list model or nested sets.
I've experimented with both and may use advice what would be the best for this case.
Currently I'm using (testing with) the adjacency list model like this:
items:
ID | item_name
====================
1 | car
2 | boat
3 | bike
items_cats: (many to many)
iid | cid
====================
1 | 1
1 | 2
1 | 4
1 | 7
2 | 1
2 | 3
2 | 4
2 | 7
3 | 1
3 | 3
3 | 4
3 | 8
categorys:
ID | cat_name | parent_id
========================
1 | safety: | 0 (0 = no parent)
2 | safe | 1
3 | dangerous | 1
4 | fun: | 0
5 | a bit | 5
6 | boring | 5
7 | funny | 5
8 | cool | 5
So its no problem to get items based on cid but how would you:
1st: selection:
1- Display all items who have cat id: cid 7 (funny)?
2- return (array/object) of all category's who have items that also contain cid 7?
Would you all do this in one query or would two be more efficient?
2nd: selection:
3- Display all items who have cat id: cid 7 and also contain cat id '3' (dangerous)
4- return (array/object) of all category's who have items that contain cid 7 and cid 3?
For selecting on multiple category's I found the flowing solution. Is this a good one and would there be to gain any performance especially when the number of category's grow?
SELECT
DISTINCT t1.product_id, t1.category_id
FROM
items_cats t1
INNER JOIN
items_cats t1b
ON t1.iid =t1b.iid
WHERE
t1.cid=3 AND
t1b.cid=7
To get a list of all items that have category ID = 7, start with your many:many table
select
i.item_name
from
items_cat ic
join items i
on ic.iid = i.id
where
ic.cid = 7
to get all categories associated with any item that has the category ID of 7, you can expand from the first and get categories associate for those item IDs
select DISTINCT
ic2.cid,
c.cat_name,
coalesce( CatParent.cat_name, "" ) as ParentCategoryName
from
( select distinct ic.iid
from items_cat ic
where ic.cid = 7 ) QualifiedItems
JOIN items_cat ic2
on QualifiedItems.iid = ic2.iid
JOIN categorys c
on ic2.cid = c.id
LEFT JOIN categorys CatParent
on c.parent_id = CatParent.ID
For 3 and 4, it would be similar, but to qualify BOTH (or anytime, more than one), you need to apply an OR, a GROUP BY and make sure that the final count matches those you were trying to qualify
select
i.item_name
from
items_cat ic
join items i
on ic.iid = i.id
where
ic.cid in( 3, 7 )
group by
i.item_name
having
count(*) = 2
So you can better understand and apply these principles, I'll leave the last one for you to try and implement... If you really get stuck, let me know... :)
Related
I have 3 tables:
Question (id, questionText)
QuestionCategory (id, categoryName)
Question_QuestionCategory (questionId, categoryId)
Sample Data:
Table Question:
id | questionText
1 | 2 + 2 = ?
2 | 10 x 5 / 3 + 5 = ?
3 | USA is located in which continent?
Table QuestionCategory:
id | categoryName
1 | Easy
2 | Hard
3 | Math
4 | Geography
Table Question_QuestionCategory:
questionId | categoryId
1 | 1
1 | 3
2 | 2
2 | 3
3 | 1
3 | 4
The Question_QuestionCategory table is a relation table that stores the foreign keys from the question and questionCategory tables.
My problem is: I need a select that returns to me a question that has the Hard and Math categories at the same time (the question with id 2 in this case). How can I do that?
You can do that by using aggregation an checking if the distinct count of categories is equal to the number of categories you asked for. To only get one row as a result you can use LIMIT.
SELECT q.id,
q.text
FROM question q
INNER JOIN question_questioncategory qc
ON qc.question = q.id
INNER JOIN questioncategory c
ON c.id = qc.categoryid
WHERE c.categoryname IN ('Hard',
'Math')
GROUP BY q.id,
q.text
HAVING count(DISTINCT c.categoryname) = 2
LIMIT 1;
I think I have a somewhat trivial question but I can't figure out how this works. I have the following Companies and Products tables with a simple Many-To-Many relationship.
How would I have to extend this query, so that the results just contains let's say all companies which have products with id 1 AND 2?
I tried adding wheres and havings wherever I could imagine but all i could get was all companies which have products with id x (without the additional and)
Companies Table
id | name
-----------------
1 | Company 1
2 | Company 2
3 | Company 3
Companies_Products Table
id | product_id | company_id
----------------------------
1 | 1 | 1
2 | 2 | 1
3 | 3 | 1
4 | 1 | 2
5 | 1 | 3
6 | 2 | 3
Products Table
id | name
-----------------
1 | Product A
2 | Product B
3 | Product C
Statement
SELECT companies.name,
companies.id AS company_id,
products.id AS product_id
FROM companies
LEFT JOIN company_products
ON companies.id = company_products.company_id
INNER JOIN products
ON company_products.product_id = products.id
If you want ALL companies with associated products 1 and 2, you can write this query:
SELECT c.name,
c.id AS company_id
FROM companies c
WHERE (SELECT COUNT(*)
FROM company_products cp
WHERE cp.company_id = c.id
AND cp.product_id in ('1', '2')
) = 2
Go to Sql Fiddle
If you want to know informations about associated product in the main query so you must use a join in addition of existing query.
Maybe you could using the following subquery in your query:
SELECT company_id, count(*) as no_companies
FROM Companies_Products
WHERE product_id IN (1, 2)
HAVING count(*) = 2
(In this case company an product must be coupled only once.) It returns all the company_ids with product 1 and 2.
There always some discussion about subquery's and performance, but I don't think you will notice.
You could make this function flexible by using a array.
pseudo code:
$parameter = array(1, 2);
...
WHERE product_id IN $parameter
HAVING count(*) = count($parameter)
Please say so if you need more help.
I have a database in which I need to find some missing entries and fill them in.
I have a table called "menu", each restaurant has multiple dishes and each dish has 4 different language entries (actually 8 in the main database but for simplicity lets go with 4), I need to find out which dishes for a particular restaurant are missing any language entries.
select * from menu where restaurantid = 1
i get stuck there, something along the lines of where language 1 or 2 or 3 or 4 doesn't exist which is the complicated bit because I need to see the languages that exist in order to see the language that's missing because I can't display something that isn't there. I hope that makes sense?
In the example table below restaurant 2 dishid 2 is missing language 3, that's what i need to find.
+--------------+--------+----------+-----------+
| RestaurantID | DishID | DishName | Language |
+--------------+--------+----------+-----------+
| 1 | 1 | Soup | 1 |
| 1 | 1 | Soúp | 2 |
| 1 | 1 | Soupe | 3 |
| 1 | 1 | Soupa | 4 |
| 1 | 2 | Bread | 1 |
| 1 | 2 | Bréad | 2 |
| 1 | 2 | Breade | 3 |
| 1 | 1 | Breada | 4 |
| 2 | 1 | Dish1 | 1 |
| 2 | 1 | Dísh1 | 2 |
| 2 | 1 | Disha1 | 3 |
| 2 | 1 | Dishe1 | 4 |
| 2 | 2 | Dish2 | 1 |
| 2 | 2 | Dísh2 | 2 |
| 2 | 2 | Dishe2 | 4 |
+--------------+--------+----------+-----------+
An anti-join pattern is usually the most efficient, in terms of performance.
Your particular case is a little more tricky, in that you need to "generate" rows that are missing. If every (ResturantID,DishID) should have 4 rows, with Language values of 1,2,3 and 4, we can generate that set of all rows with a CROSS JOIN operation.
The next step is to apply an anti-join... a LEFT OUTER JOIN to the rows that exist in the menu table, so we get all the rows from the CROSS JOIN set, along with matching rows.
The "trick" is to use a predicate in the WHERE clause that filters out rows where we found a match, so we are left rows that didn't have a match.
(It seems a bit strange at first, but once you get your brain wrapped around the anti-join pattern, it becomes familiar.)
So a query of this form should return the specified result set.
SELECT d.RestaurantID
, d.DishID
, lang.id AS missing_language
FROM (SELECT 1 AS id UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) lang
CROSS
JOIN (SELECT e.RestaurantID, e.DishID
FROM menu e
GROUP BY e.RestaurantID, e.DishID
) d
LEFT
JOIN menu m
ON m.RestaurantID = d.RestaurantID
AND m.DishID = d.DishID
AND m.Language = lang.id
WHERE m.RestaurantID IS NULL
ORDER BY 1,2,3
Let's unpack that bit.
First we get a set containing the numbers 1 thru 4.
Next we get a set containing the (RestaurantID, DishID) distinct tuples. (For each distinct Restaurant, a distinct list of DishID, as long as there is at least one row for any Language for that combination.)
We do a CROSS JOIN, matching every row from set one (lang) with every row from set (d), to generate a "complete" set of every (RestaurantID, DishID, Language) we want to have.
The next part is the anti-join... the left outer join to menu to find which of the rows from the "complete" set has a matching row in menu, and filtering out all the rows that had a match.
That may be a little confusing. If we think of that CROSS JOIN operation producing a temporary table that looks like the menu table, but containing all possible rows... we can think of it in terms of pseudocode:
create temporary table all_menu_rows (RestaurantID, MenuID, Language) ;
insert into all_menu_rows ... all possible rows, combinations ;
Then the anti-join pattern is a little easier to see:
SELECT r.RestaurantID
, r.DishID
, r.Language
FROM all_menu_rows r
LEFT
JOIN menu m
ON m.RestaurantID = r.RestaurantID
AND m.DishID = r.DishID
AND m.Language = r.Language
WHERE m.RestaurantID IS NULL
ORDER BY 1,2,3
(But we don't have to incur the extra overhead of creating and populating the temporary table, we can do that right in the query.)
Of course, this isn't the only approach. We could use a NOT EXISTS predicate instead of an anti-join, though this is not usually as efficient. The first part of the query is the same, to generate the "complete" set of rows we expect to have; what differs is how we identify whether or not there is a matching row in the menu table:
SELECT d.RestaurantID
, d.DishID
, lang.id AS missing_language
FROM (SELECT 1 AS id UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) lang
CROSS
JOIN (SELECT e.RestaurantID, e.DishID
FROM menu e
GROUP BY e.RestaurantID, e.DishID
) d
WHERE NOT EXISTS ( SELECT 1
FROM menu m
WHERE m.RestaurantID = d.RestaurantID
AND m.DishID = d.DishID
AND m.Language = lang.id
)
ORDER BY 1,2,3
For each row in the "complete" set (generated by the CROSS JOIN operation), we're going to run a correlated subquery that checks whether a matching row is found. The NOT EXISTS predicate returns TRUE if no matching row is found. (This is a little easier to understand, but it usually doesn't perform as well as the anti-join pattern.)
You can use the following statement if each menu item should have a record on each language (8 in real life 4 in example). You can change the number 4 to 8 if you want to see all menu items per restaurant that doesn't have all 8 entries.
SELECT RestaurantID,DishID, COUNT( * )
FROM Menu
GROUP BY RestaurantID,DishID
HAVING COUNT( * ) <4
Say I have a table of people...
person:
-----------
id | person
---+-------
1 | Jim
2 | Bob
3 | Frank
...and I have a table of items...
item:
----------------
id | item | type
---+------+-----
1 | 21 | 2
2 | 10 | 5
3 | 11 | 1
4 | 9 | 1
...and I also have a table describing who has what...
person_item:
-------------
item | person
-----+-------
1 | 2
2 | 1
3 | 1
How can I create a single query that will tell me when an individual has more than one item of a particular type? I only want the query to concern itself with items of type (1, 2, 3).
The results from the query should be in the following format:
---------------
person | item
| item
--------+------
person | item
| item
| item
--------+------
... etc.
This is what I have tried... but it produces garbage...
SELECT person.id, item.id FROM person_item AS pi
JOIN item AS i ON i.id = pi.item
JOIN person AS p ON p.id = pi.item
WHERE item.type IN (1,2,3)
HAVING COUNT(pi.person) > 1;
The query is suspect because you have a having clause but not a group by clause. Also, you are using table names when you have very reasonable aliases. And, you want to count distinct items within a person/type combination, not just for a person.
Taking these into account, try this query:
SELECT p.id, i.type, group_concat(i.item) as items
FROM person_item pi join
item i
ON i.id = pi.item join
person p
ON p.id = pi.person
WHERE i.type IN (1,2,3)
group by p.id, i.type
HAVING COUNT(distinct i.id) > 1;
This also provides the list of items as the third things returned.
If you only want to see person and item id's, you don't need to join to person - just access person_item (with a link to item for item_type). However, if you want each combination on a separate line, you will have to access person_item twice - like so:
select pi.person, pi.item
from person_item pi
join (select p.person
from person_item p
join item i on p.item = i.item_id and i.type in (1,2,3)
group by p.person
having count(*) > 1) c
on pi.person = c.person
given the following table (describing a many-to-many relation):
ID | PageID | TagID
--------------------
1 | 1 | 1
2 | 1 | 2
3 | 2 | 2
4 | 2 | 3
how do i select 'all PageIDs having all of a list of TagIDs'?
in other words: i want all pages tagged with TagIDs 1 and 2 (so it's only PageID 1 in this example)
after some googling i found the term 'relational division' which might be relevant here, but i didn't quite get it. anyone having a simple query for my problem?
If you have the list of tagids in a table, then it is a simple join and group by:
select pageId
from t join
list l
on t.tagId = l.tagId cross join
(select count(*) cnt from list l) as const
group by pageId
having count(*) = max(cnt)
The having clause checks that all tags are present. If there might be duplicates, then you would want to replace the "count(*)" with "count(distinct tagid)" in both cases.