PHP/MySQL: Many-to-many/intersect table question - php

I'm not really sure how to phrase the question, so let me just give an example of the problem:
Suppose there's a table which maps items to categories. Each item can have any number of categories, and each category can of course hold any number of items. So you have a table that looks like this:
items_categories
id item_id category_id
The problem is, I want to select all item id's which have specific category id's. For example, select all item_id's with category_id's of 1 and 2: I want to find all items that are associated with categories both 1 and 2. Obviously I can't use an AND statement, and an OR statement would return all item_id's with either category, but not necessarily both.
Here is my solution and the best thing I can think of: select all item_ids with category_id equal to 1 OR 2; iterate through the results in PHP and keep track of how many item_ids are associated with a category_id; and then unset all item_ids in the results that don't have the specified number of categories. Here's a snippet of my code:
// assume $results is an array of rows from the db
// query: SELECT * FROM items_categories WHERE category_id = 1 OR category_id = 2;
$out = array();
foreach ($results as $result)
{
if (isset($out[$result['item_id']]))
$out[$result['item_id']] ++;
else
$out[$result['item_id']] = 1;
}
foreach ($out as $key=>$value)
{
if ($value != 2)
unset($out($key));
}
return array_keys($out); // returns array of item_ids
Obviously if you have lots of different categories, you're selecting and processing way more information than you should theoretically need to. Any ideas?
Thanks!
Edit: Here's an example of a table and the information I want from it:
id item_id category_id
1 1 1
2 1 2
3 2 1
4 3 2
So say I'm interested in getting all of the items with categories 1 and 2. How do I get item #1 from my example table, given that I want only items with categories #1 and #2? If I select everything with categories 1 or 2 (as in my example above), I have to select the whole table in this case and "manually" remove item_id's 2 and 3, since they aren't associated with both category 1 and category 2. Hope this helps clarify a little.
Final edit: I figured it out, despite my apparent inability to describe what I'm trying to do, heh. Here's the query I came up with, for the record:
SELECT *
FROM
(
SELECT item_id, COUNT(*) as count
FROM items_categories
WHERE category_id IN (1, 2)
GROUP BY item_id
) table_count
WHERE count = 2;
In this case, the "(1, 2)" could be replaced with "(category_id1, category_id2, ...)", and the "2" at the end would be replaced with the number of categories I'm searching for.
So it finds out how many categories match the criteria for each item, and since I only want items where ALL the categories match, it only selects those where the number of categories equals the number of categories I'm looking for. This is of course assuming there are no duplicate categories or anything like that.
Thanks for the responses!

It seems that what is troubling you is that you are forced to do a linear search which of course takes O(n) time, but if you select elements from your database in sorted order, then can't you just use a binary search in O(lg n) time?
I hope this helps, If not, then maybe I misunderstood your question and I'd like you to clarify it a little bit.

SELECT
foo
FROM
bar
WHERE
foo IN (1,2)
is this what you are looking for?

This is something you should be getting the database to do rather than PHP.
SELECT item_id # We want a list of item ids
FROM cat_items # Gets the item ID list from the cat_items table
WHERE cat_id IN (1, 2, 7, 11) # List of categories you want to search in
GROUP BY item_id; # As the same item can appear in more than one category this line will eliminate duplicates
This query does assume that the data in cat_items is accurate, in other words that the category and item IDs point to valid entries in the categories and items tables respectively. If you're using a database with foreign key support (The InnoDB engine for MySQL, Postgres, etc) enforcing foreign keys is not difficult.
To get a list of IDs in each category in the format you want, that's easily done on the SQL side too.
SELECT *
FROM cat_items
WHERE cat_id IN (1, 2, 7, 11)
GROUP BY cat_id, item_id;
If you just want a count of how many items are in each category you can also do that in SQL
SELECT cat_id, COUNT(item_id) AS items
FROM cat_items
WHERE cat_id IN (1, 2, 7, 11)
GROUP BY cat_id;
If you need more data than just the ID then you can join against the table you need the data from.
SELECT items.*
FROM cat_items
JOIN items ON cat_items.item_id = items.id
WHERE cat_id IN (1, 2, 7, 11)
GROUP BY item_id;

SELECT item_id FROM items_categories WHERE category_id = 1 AND item_id IN (SELECT item_id FROM items_categories WHERE category_id = 2)

Related

How can I order query by another table?

I have a long query, but I keep it simplified:
$query = $this->db->query(" SELECT id FROM oc_products ORDER BY ...? ");
Here is the problem. In this table I have all the products, but I have a second table, oc_seller_products, where I have same column ID, which match with oc_products.
I want to be ordered first id's which dont appear in oc_seller_products, and at last, appear id's which appear in oc_seller_products.
For example: in oc_products I have ID: 1,2,3,4,5
In oc_seller_products I have only ID: 4
I need to be ordered like this: 5,3,2,1 and the last: 4
I have a marketplace, so I want first my products to appear, on category page, then my sellers products.
I really have no idea how to do that.
select op.id
from oc_products op
left join oc_seller_products osp using (id)
order by osp.id is null desc, op.id desc
osp.id is null will be 1 when there is not an oc_seller_products record and 0 when there is, so sort by that, descending, first. And then after that, within those two categories, you seem to want descending id order.

How to Order MYSQL Results By Number of rows in a another table

I have 2 Tables:
1 - cat
Id|category
2 - groups
Id|groupname|category
I want to sort the data from the cat table by the number of rows in the category (group) table.
For example in groups table, category row have the following data:
Education - 20
Fun - 50
Nature - 30
I wanna show cat as:
Fun
Nature
Education
How can I do it?
Can't you just count the amount of rows in the one table and order your results by the resulting count?
SELECT
*,
(SELECT COUNT(*) FROM category WHERE category.cat_id=cat.id) as count
FROM cat
ORDER BY count DESC;
Note To get a better answer, you'll need to be more specific about your table structure and what you would like to achieve.

Select Query for these tables

I have two tables items and item_details.
To show the items in the feeds page I use the below JOIN QUERY.
SELECT i.item_id,i.time,id.text,id.photo FROM items i LEFT JOIN item_details id ON i.item_id=id.item_id
but, it returns multiple rows of same item id. The only work around I find is first select the items table only, then select the item_detail table while looping in PHP. I feel that's not a best practice and may effect the performance.
Kindly suggest a better method to select the items.
Problem:
You are joining with item_details which has 3 records with same item_id. So the result will have more records for that item_id.
Solution:
I think this is what you after:
SELECT i.item_id,i.time,MAX(id.text) as text,MAX(id.photo) as photo
FROM items i LEFT JOIN
item_details id ON i.item_id=id.item_id
GROUP BY i.item_id,i.time
Sample result:
item_id time text photo
---------------------------------------------------------------------
2 87213 (null) (null)
4 2029 another text row for item id 4 a photo for item id 4
Sample result in SQL Fiddle
NB: This method will not work as expected if you have multiple records with same item_id having different values for text and photo columns.

MySQL get results with MySQL only

Say I had a MySQL table as pictured below, and I wanted to find any product_id's which have the filters 2 AND 5 (so in the table this would be the product_id of 30), is this possible in MySQL? Or would I have to get product id's where the filter_id is 2 OR 5, and sort it in PHP?
SELECT product_id, COUNT(*) AS i FROM mytable WHERE filter_id IN (2,5) GROUP BY product_id HAVING i = 2;
This will match any rows containing any of the required filter_ids and group together those with the same product_id. The name i has been given to a column giving the number of rows in the group. Assuming that every combination of product_id to filter_id will be unique then you know that any group with two rows will have matched both of the filter_ids, so the HAVING clause will finally filter out the rows that only matched one of the filter_ids.

Find occurrences of tagged items in a mysql table

Let's say I have this table:
item_id tag_id
------- ------
1 1
1 2
2 2
2 3
As you can probably imagine, it's a table where i have references to some items and tags that belong to them. An item can have more than one tag and one tag can be selected for more than one item.
Let's say I have also a specific tag collecion (f.ex. tag_id = 50, 73 and 119) and an "items" table with an id (referred by item_id).
Is there an efficient query that gives me:
the count of items with those tags
the items themselves?
What I've tried
SELECT COUNT(*) FROM
(
SELECT COUNT(*) AS c FROM items_tags it JOIN items i ON i.id = it.item_id
WHERE (tag_id=7 OR tag_id=95 OR tag_id=150) AND `status`='active'
GROUP BY item_id
) t1 WHERE c=3 <-- c= number of tags
I can have both results but with a very (it seems) inefficient query. After an examination with EXPLAIN, I would like to get rid of the "range" given by the ORs.
Refining my problem: The problem is that I was given a very poorly written PHP framework that iterates 900+ times through various tag IDs. Let's say you have one or more fixed IDs (the selected tags) and it iterates through all the 900+ tags to find the number of occurrencies of the items who have in common the given tags PLUS the iterated one (it's a function to refine the search, showing only the elements that have all the given tags plus one).
The given code works this way: I select one or more tags and their ID go into the querystring. Let's say I've selected tags 54 and 77.
The code must find every item ID for the items that have BOTH tags 54 and 77 and list them one by one: we obtain the "items with selected tags" list.
Then, it offers the choice to refine the search, and here comes the odd part: the PHP code cycles throug ALL the 900+ tags, and for every iteration it takes a tag, and counts how many items have ALL the tags 54, 77 and the one in the iteration.
If the count is > 0, it displays the name of the tag with the count number, filtering out every tag whose items don't have any link to the selected tags.
It would be nice to achieve the same result in a less "intensive" way.
To get the list of item ids that match all the tags, you can use this query:
SELECT items.id
FROM items
JOIN items_tags ON items.id = items_tags.item_id
WHERE (items_tags.tag_id IN (7,95,150))
AND (items.status = 'active')
GROUP BY items.id
HAVING COUNT(DISTINCT items_tags.tag_id) = 3
Note that if you are sure that you never have duplicate tags for the same item, you can substitute COUNT(DISTINCT items_tags.tag_id) with COUNT(*) for efficiency.
To get a count of these items, wrap this in a COUNT query:
SELECT COUNT(*)
FROM (
SELECT items.id
...
) t
To get a list of the items, wrap it in this SELECT query:
SELECT *
FROM items
WHERE id IN (
SELECT items.id ...
)
UPDATE
To get the item counts for each of the remaining tags when combined with the original list, you can do this:
SELECT tag_id, COUNT(DISTINCT item_id)
FROM items_tags
WHERE item_id IN (
SELECT items.id
...
)
AND tag_id NOT IN (7,95,150)
GROUP BY tag_id

Categories