I have a table called categories and a table called business_categories_coupling. In Categories, you have the usual id, name, parent. In the Coupling table, you have business_id and category_id. Each business can have multiple categories, so I store them in that table. It kinda looks like this:
business_id category_id
73 80
73 81
73 90
74 4
74 10
Right now, my query is just selecting all the categories, doing a foreach and doing a db query in each loop to find how many businesses are in that category. Obviously not the right way to go about it.
Is there a way to do a SQL query that basically selects all the categories, gets the number of times it comes up in the coupling table, and add a count to each category?
SELECT
C.*
FROM
CATEGORIES AS C
LEFT JOIN
BUSINESS_CATEGORIES_COUPLING AS B
ON
C.id = B.category_id;
Kinda like that, but with a count somewhere. I've tried various setups but nothing works like I want. Any suggestions?
EDIT 1
Solution as provided by #phani-rahul, but I added a WHERE clause:
SELECT cat.id AS id, cat.name AS name, cat.slug AS slug, COUNT(cat.id) AS business_count
FROM categories AS cat
LEFT JOIN business_categories_coupling AS coupling ON cat.id=coupling.category_id
WHERE coupling.category_id IS NOT NULL
GROUP BY cat.id
Yes, there is.
you can use Group by clause:
select a.id as category, count(a.id) as count_of_category
from categories a
left join business_categories_coupling b on a.id=b.category_id
group by a.id
your result would be something like:
category count_of_category
80 2
81 5
90 1
. .
. .
. .
You also need to GROUP by the fields of C table.
SELECT C.id, C.field1, C.field2, COUNT(*)
FROM CATEGORIES AS C
LEFT JOIN BUSINESS_CATEGORIES_COUPLING AS B
ON (C.id = B.category_id)
GROUP BY C.id, C.field1, ...
(In MySQL you can GROUP BY the single value C.id; in other SQL dialects you can express the concept of "grouping by rows of C table" by grouping by "C.*"; in some others you need to specify all non-aggregate columns of your query, in this case all columns you select from C, one by one).
What you're looking for is a GROUP BY clause.
SELECT
C.*, count(C.id)
FROM
CATEGORIES AS C
LEFT JOIN
BUSINESS_CATEGORIES_COUPLING AS B
ON
C.id = B.category_id
GROUP BY B.category_id;
Related
I have 3 tables. image, categories, image_category.
image: id | title | imageURL
categories: cat_id | cat_name
image_category: image_id | cat_id
My current query to select all images in order newest to oldest is:
SELECT image.id as ID, image.title as title, categories.cat_name as CAT
FROM image_category
LEFT JOIN image
ON image_category.image_id = image.id
INNER JOIN categories
ON image_category.cat_id = categories.cat_id
ORDER BY ID DESC
I would like to show the newest 4 images per category. The largest image.id are the newest images.
For example. If I had 3 categories and 40 images in each category. I want to show the newest 4 images from each category. I will later be trying to show the next 4 per category after that and then the next 4 per category until there are no images left.
This solution seems like what im looking for.
SELECT i1.*
FROM item i1
LEFT OUTER JOIN item i2
ON (i1.category_id = i2.category_id AND i1.item_id < i2.item_id)
GROUP BY i1.item_id
HAVING COUNT(*) < 4
ORDER BY category_id, date_listed;
but I have a relational table connecting my image_id and category_id. Cant figure out how to implement this with that extra table join.
Would appreciate help from an SQL guru.
You're almost there, you just need to do the grouping using your item_category table since that's where the cat_id's are.
SELECT ...
FROM item_category AS c1
LEFT OUTER JOIN item_category AS c2
ON c1.cat_id = c2.cat_id AND c1.image_id < c2.image_id
GROUP BY c1.cat_id
HAVING COUNT(*) < 4
Then once you've got that, you know that c1 contains the top four images per category. You can then join c1 to the image table to get other attributes:
SELECT i.id, i.title, c.cat_name AS CAT
FROM item_category AS c1
LEFT OUTER JOIN item_category AS c2
ON c1.cat_id = c2.cat_id AND c1.image_id < c2.image_id
INNER JOIN image AS on c1.image_id = i.id
INNER JOIN categories AS c on c1.cat_id = c.id
GROUP BY c1.image_id
HAVING COUNT(*) < 4;
Although this isn't strictly legal SQL due to the single-value rule, MySQL will permit it.
Copied from comments thread:
I would fetch the full result, store it in a cache, and then iterate over it however I want, using application code. That would be far simpler and have better performance. SQL is powerful, but another solution may be easier to develop, debug, and maintain.
You can certainly use LIMIT to iterate through the result set:
SELECT i.id, i.title, c.cat_name AS CAT
FROM item_category AS c1
LEFT OUTER JOIN item_category AS c2
ON c1.cat_id = c2.cat_id AND c1.image_id < c2.image_id
INNER JOIN image AS on c1.image_id = i.id
INNER JOIN categories AS c on c1.cat_id = c.id
GROUP BY c1.image_id
HAVING COUNT(*) < 4
ORDER BY c.cat_id
LIMIT 4 OFFSET 16;
But keep in mind that doing an OFFSET means that it has to run the query over again each time you view another set of them. There are optimizations in MySQL so that it quits a query once it has found enough rows, but it's still expensive if you iterate frequently, and advance far into the series of pages.
Two possible optimizations you can use: One is to cache part of the result, on the theory that few users will want to advance through every page of a large paginated result. So for example, fetch enough to populate ten pages worth of results, and cache that. It reduces the number of queries a lot, and perhaps only 1% of the times will user advance into the next set of ten pages.
SELECT i.id, i.title, c.cat_name AS CAT
FROM item_category AS c1
LEFT OUTER JOIN item_category AS c2
ON c1.cat_id = c2.cat_id AND c1.image_id < c2.image_id
INNER JOIN image AS on c1.image_id = i.id
INNER JOIN categories AS c on c1.cat_id = c.id
GROUP BY c1.image_id
HAVING COUNT(*) < 4
ORDER BY c.cat_id
LIMIT 40 OFFSET 40; /* second set of ten pages */
Another optimization, if you can assume that any view of page N will be coming from a view of page N-1, is for the request to filter the categories based on the greatest category id seen in the N-1st page. You need to do it this way because OFFSET works by row number in the result set, but indexed offsets work by values found on those rows. These aren't the same offset if there may be gaps or unused cat_id values.
SELECT i.id, i.title, c.cat_name AS CAT
FROM item_category AS c1
LEFT OUTER JOIN item_category AS c2
ON c1.cat_id = c2.cat_id AND c1.image_id < c2.image_id
INNER JOIN image AS on c1.image_id = i.id
INNER JOIN categories AS c on c1.cat_id = c.id
WHERE c1.cat_id > 47 /* this value is the largest seen in previous page */
GROUP BY c1.image_id
HAVING COUNT(*) < 4
ORDER BY c.cat_id
LIMIT 40; /* no offset needed */
Re your comments:
... using LIMIT and OFFSET will only trim those results and not move me down the list of rows.
LIMIT is working as intended; it applies to the resulting rows after GROUP BY and HAVING have done their work.
The way I was doing it before the greatest N per category query is by
1. pulling in x amount of images,
2. Remembering which was the last image, and then
3. using a sub query on my subsequent queries to get the next x amount of images with ids smaller than than the last image. Is something like that possible with greatest N per group?
That's what my WHERE clause does in the last example above, without using a subquery. And I'm assuming you're advancing to the next higher set of cat_id's. This solution works only if you're advancing one page at a time, and in the positive direction.
All right, there's another solution for greatest-n-per-group that works with MySQL, but it relies on the user variables feature. SQLite doesn't have this feature.
SELECT * FROM (
SELECT
p.id as image_ID, p.imageURL as URL, c.cat_name as CAT, ic.cat_id,
IF(#cat=ic.cat_id, #row:=#row+1, #row:=1) AS _row, #cat:=ic.cat_id AS _cat
FROM (SELECT #cat:=null, #row:=0) AS _init
CROSS JOIN image_category AS ic
INNER JOIN portfolio AS p ON ic.image_id = p.id
INNER JOIN categories AS c on ic.cat_id = c.cat_id
ORDER BY ic.cat_id, ic.image_id
) AS x
WHERE _row BETWEEN 4 AND 6; /* or choose any range you want */
This is similar to using ROW_NUMBER() OVER (PARTITION BY cat_id) that is supported by standard SQL and most RDBMS, but SQLite doesn't support that either yet.
SELECT *
FROM (
SELECT a.id as ID,a.title as title,b.cat_name as CAT, row_number() OVER (PARTITION BY b.cat_id ORDER BY b.cat_id,a.id desc) AS n
from images a, categories b, image_category c
where a.id = c.image_id
and b.cat_id = c.cat_id
) x
WHERE n < 4
ORDER BY b.cat_id,a.id desc;
I have 3 tables : videos, categories, video_categories.
In videos, I have id, title, and other fields.
In categories, I have id and name.
In video_categories, I have id, video_id, and category_id.
One video can have multiple categories. So the video_categories table will be something like this.
id video_id category_id
1 1 1
2 1 2
3 1 3
If I want to have a list of videos and display their categories, which would be preferred?
Via PHP, call 1 query to get all videos, then loop on that to query to get each video's categories, and another query to get the category name. This will be really slow if the table is huge, right?
Via MySQL joins (need help on this). If I left join videos to video_categories, there will be 3 results of the same video_id. I can use GROUP BY or SELECT DISTINCT to get unique result, but how can I now get the categories' names?
My expected result will be something like this:
id title categories
1 Video1 pop, rock, jazz
For option 2, use GROUP_CONCAT. It will be ok
SELECT v.id, v.title, GROUP_CONCAT(c.name)
FROM videos v
INNER JOIN video_categories vc ON vc.video_id = v.id
INNER JOIN categories c ON vc.category_id = c.id
GROUP BY v.id, v.title
For Group_Concat() function , is the default separator. That's why I don't use it here.
Guessing the names of your other columns..
SELECT v.video_id, v.title, GROUP_CONCAT(c.category_name SEPARATOR ', ')
FROM videos v
LEFT JOIN video_categories vc ON vc.video_id = v.video_id
LEFT JOIN categories c ON c.category_id = vc.category_id
GROUP BY v.video_id, v.title
I am building a blog with Codeigniter and MySQL. The question I have is this, I have a table with posts and one with categories. I also have a cross reference table with post_categories. What I am trying to do is get all the categories with their names and the number of posts they have under their name.
Example output would be: Hello World(1) Test(0) etc.
What I am having a hard time finding is a SQL query that will join the three tables and get me the counts, and I am also having a hard time wrapping my head around how to make that query.
Here is my table schema:
blgpost
====
id
*Other schema unimportant
blgpostcategories
=================
postid
categoryid
blgcategories
==========
id
name
*Other schema unimportant
This should give you the output you want....
SELECT c.name, COUNT(p.id) FROM
blgcategories c
INNER JOIN blgpostcategories pc ON c.id = pc.categoryid
INNER JOIN blgpost p ON pc.postid = p.id
GROUP BY c.id
You don't need to join the three tables - the blgpost table doesn't have any information in it that you need.
SELECT COUNT(*), blgcategories.name
FROM blgcategories INNER JOIN blgpostcategories
ON blgcategories.id=blgpostcategories.categoryid
GROUP BY blgcategories.id;
SELECT name, COUNT(pc.id)
FROM blgcategories c
LEFT JOIN
blgpostcategories pc
ON pc.categoryid = c.id
GROUP BY
c.id
Using LEFT JOIN will show 0 for empty categories (those without posts linked to them) rather than omitting them.
i got three tables
CATS
id name
------------------------------
1 category1
2 category2
3 category3
4 category4
PRODUCT
id name
------------------------------
1 product1
2 product2
ZW-CAT-PRODUCT
id_cats id_product
------------------------------
1 1
3 1
4 2
now i want to get my products and their categories
product1 => category1,category3
product2 => category4
is there a way to get this array (or object or something) with one mysql query?
i tried a bit with JOINS, but it seems thats this is not exactly what i need, or?
currently i'm using 3 querys (i think thats too much).
any suggestions?
edit
and on the other way, what if i want to get ALL products of a specific category?
can this also be done in one query?
You can use GROUP_CONCAT to get a separated list in your results.
SELECT p.*,
GROUP_CONCAT(c.name SEPARATOR ',') as cats
FROM PRODUCT p
LEFT JOIN ZW-CAT-PRODUCT l
ON l.id_product=p.id
LEFT JOIN CATS c
ON c.id=l.id_cats
GROUP BY p.id
So basically, this first does some joins to get all the data. If you were to replace the GROUP_CONCAT line with just c.name, you would see a row for each product_id/category pair. The GROUP BY tells it to group results based on product ID, and then GROUP_CONCAT(c.name..) is telling to it take all the different c.name values that occur in a group (so for each product ID, since you're grouping by product ID) and concatenate those values into one string, using , as the separator.
So to get all products for a each category in the same style, it would be like this,
SELECT c.*,
GROUP_CONCAT(p.name SEPARATOR ',') as products
FROM CATS c
LEFT JOIN ZW-CAT-PRODUCT l
ON l.id_cats=c.id
LEFT JOIN PRODUCT p
ON p.id=l.id_product
GROUP BY c.id
EDIT: To get just the product rows for a particular category (as requested in comment), it's this.
SELECT p.*
FROM PRODUCT p
LEFT JOIN ZW-CAT-PRODUCT l
ON l.id_product=p.id
LEFT JOIN CATS c
ON c.id=l.id_cats
WHERE c.name='xyz';
If you need just comma-separated list of categories for every product, look at MySQL's GROUP_CONCAT() aggregate function:
SELECT p.*, GROUP_CONCAT(c.name) AS categories
FROM PRODUCT p
LEFT JOIN ZW-CAT-PRODUCT cp ON p.id = cp.id_product
LEFT JOIN CATS c ON cp.id_cats = c.id
GROUP BY p.id
To get all products of a specific category (by category ID):
SELECT p.*
FROM PRODUCT p
INNER JOIN ZW-CAT-PRODUCT cp ON p.id = cp.id_product
WHERE cp.id_cats = 42
The same, but by category name:
SELECT p.*
FROM PRODUCT p
INNER JOIN ZW-CAT-PRODUCT cp ON p.id = cp.id_product
INNER JOIN CATS c ON cp.id_cats = c.id
WHERE c.name = 'category1'
You could fit all 3 into 1 query yes, but consider having 3 huges tables, i'd rather process them one by one instead of getting the whole bulk back in 1 time.
This takes longer, but is (in my opinion) more data friendly.
So, I understand how the relationships work in mysql but I'm having a hard time figuring out how its implemented in my code.
For example, say I have the 3 tables.
Table 1: users - user id, username, user city
Table 2: categories - category id, category name
Table 3: user_categories - user id, category id
If I were to query the database for every user that was in a particular city and list them out with the all of the categories they belong to... How would I do this? Would I need to loop through the results and do a separate query for each user, then list the results? Or, is there some magic query that will return a multidimensional array?
I believe the above would be many-to-many, correct me if I'm wrong....
EDIT In the user_categories table, a user can contain more than 1 category, I'm trying to figure out how to return all of them
Thanks!
You're absolutely right, it is a many-to-many query.
And from what I understand, what you're looking for is the ability to have some kind of hierarchical result to display, meaning for one user, have an array of all the categories he's assigned to...
Couple of things you could do:
Option 1: Query the users table:
SELECT u.user_id, u.username, u.user_city WHERE city = 'somecity';
From the results, get all the user_id's that match, put them in an array.
array(1,3,4,5)
Then execute a query by joining the 2 tables categories and user_categories, and passing the array as a comma separated list in a where in:
SELECT user_categories.user_id, categories.category_name
FROM user_categories INNER JOIN categories ON user_categories.category_id = categories.category_id
WHERE user_categories.user_id IN (1,3,4,5)
This will give you a list of user-id, category name that you can use in your script with the previous results to build your result set
option 2: my preferred, use MySQL's GROUP_CONCAT(http://dev.mysql.com/doc/refman/5.0/en/group-by-functions.html#function_group-concat).
SELECT users.user_id, users.user_name, GROUP_CONCAT(categories.category_name) AS categories
FROM users
INNER JOIN user_categories ON users.id = users_categories.user_id
INNER JOIN categories ON user_categories.category_id = category.id
WHERE user.user_city = 'somecity'
GROUP BY user.user_id
This will return something like:
user_id username categories
1 u1 cat1, cat2, cat3
2 u2 cat1, cat3
You can specify the separator by using SEPARATOR in group_concat.
You need to JOIN the tables.
If I were to query the database for every user that was in a particular city and list them out with the all of the categories they belong to
SELECT *
FROM users
INNER JOIN user_categories
ON (user_id)
INNER JOIN categories
ON (category_id)
WHERE ...
You could try:
SELECT u.user_id, u.username, u.user_city, c.category_id, c.category_name
FROM users u
INNER JOIN user_categories uc ON u.user_id = uc.user_id
INNER JOIN categories c ON uc.category_id = c.category_id
WHERE u.user_city = 'Cityname';
I haven't tested this, and there might be a more efficient way to do it, but it should work.
If you are unfamiliar with joins in mysql, check this out.