Reputation: 2435
I have four tables Like below
Listings:
------------------------------------------------------------------------------------------------
| list_id |user_id | name | category | fees | details |created_on |
------------------------------------------------------------------------------------------------
| 90cc57a4-f782-4c57-ac98-1965c57ece57 |user 100 |satwik| music | 500 | dummy |2015-08-02 |
------------------------------------------------------------------------------------------------
changed my list_id from a random string to UUID.
from this comment on php.net
see this stackoverflow question
In listings table list_id is primary key, i know using autoincreament is best but my requirement is like this. and s.no is primary key for rest of tables.
I need to generate a random key from PHP side because to set list_id in session and to avoid another query to set list_id in session. Likes:
----------------------------------------------------------------
|.sno | list_id | user_id | likes |
----------------------------------------------------------------
| 1 | 90cc57a4-f782-4c57-ac98-1965c57ece57 | user110 | 1 |
----------------------------------------------------------------
| 2 | 90cc57a4-f782-4c57-ac98-1965c57ece57 | user215 | 1 |
----------------------------------------------------------------
| 3 | 90cc57a4-f782-4c57-ac98-1965c57ece57 | user200 | 1 |
----------------------------------------------------------------
comments:
-------------------------------------------------------------------------
|.sno | user_id | list_id | comment |
-------------------------------------------------------------------------
| 1 | user 205| 90cc57a4-f782-4c57-ac98-1965c57ece57 | dummy comment |
-------------------------------------------------------------------------
Views:
----------------------------------------------------------------
|.sno | list_id | user_id | views |
----------------------------------------------------------------
| 1 | 90cc57a4-f782-4c57-ac98-1965c57ece57 | user110 | 2 |
----------------------------------------------------------------
| 2 | 90cc57a4-f782-4c57-ac98-1965c57ece57 | user215 | 1 |
----------------------------------------------------------------
| 3 | 90cc57a4-f782-4c57-ac98-1965c57ece57 | user200 | 1 |
----------------------------------------------------------------
I am trying to get a list of user id user100 from listings table
and need to get count of views,like,comments for the list id of the user from various tables i have.
i tried using this query from codeigniter
$this->db->select ( 'list.*,count(v.views) as views,count(l.likes) as likes,count(c.comment) as comments' )
->from ( 'listings as list' )
->join ( 'views v', 'v.list_id = list.list_id')
->join ( 'likes l', 'l.list_id = list.list_id')
->join ( 'comments c', 'c.list_id = list.list_id');
$this->db->where ( 'list.user_id', $user_id);
$query = $this->db->get ();
I am getting wrong views and likes and comments count.
is this database design good or i need to change anything. i have less awarness in using joins please help me.
EDIT:
I tried this query from answers below
$this->db->select ( 'l.*,count(distinct v.s_no) as views,count(distinct li.s_no) as likes,count(distinct c.s_no) as comments' ,false)
->from ( 'listings as l' )
->join ( 'likes li', 'l.list_id = li.list_id')
->join ( 'comments c', 'l.list_id = c.list_id')
->join ( 'views v', 'l.list_id = v.list_id')
->where ( 'l.user_id', $id);
I am getting what i want but this way of querying fails(return null) if i don't have any comments or views or likes.
Upvotes: 12
Views: 2734
Reputation: 108380
If you take a closer look at the "wrong" values, and took a look at the individual rows that are contributing to the inflated values, you'll find that you are getting the same rows counted multiple times. (With an inner join, if one of the tables has zero matching rows, that row won't be returned.)
You've got a classic cross product. What's happening with the JOIN operation, every matching row from Likes
is getting "matched" to every matching row from Views
. If there's three matching rows in Views
, and three matching rows in Likes
, you're going to get 3x3 rows returned.
Your database design looks okay.
It's the generated SQL query that is the problem.
To fix the query, either
either avoid generating a cross product or count distinct rows*
To avoid a cross product, don't JOIN
to both Likes
and Views
tables. As an alternative, you could avoid the join operations and instead use correlated subqueries in the SELECT list...
SELECT list.*
, (SELECT COUNT(1) FROM views v WHERE v.list_id = list.list_id) AS count_views
, (SELECT COUNT(1) FROM likes l WHERE l.list_id = list.list_id) AS count_likes
FROM list
WHERE ...
or, if you do use JOIN operation and generate a cross product, then get a count of unique rows by using an expression like COUNT(DISTINCT pk)
, so you don't count the same row more than once.
SELECT list.*
, COUNT(DISTINCT v.`s.no`) AS count_views
, COUNT(DISTINCT l.`s.no`) AS count_likes
To allow rows with zero counts to be returned, you'd want to use outer joins:
FROM list
LEFT JOIN views v ON v.list_id = list.list_id
LEFT JOIN likes l ON l.list_id = list.list_id
Once you understand what MySQL is doing with the SELECT statement your code is currently generating, it's just a matter of figuring out a SQL statement that will return the result you want.
Once you get a SQL statement that returns the expected results, then it's just a matter of getting CodeIgniter to run SQL statement.
Upvotes: 1
Reputation: 64466
Considering your structure your each table should have a one field defined as a primary key
and the associations of listing table (likes,views and comments) should relate to that key as a foreign key
i expect sno
field to be defined as primary key for each table ,so in your case list_id
column in likes views and comments will have reference of listing table with with auto generated list id that is 1,2,3 so on then the list_id
column in your listing table is not needed any more , same case will apply for your users table and its auto generated user ids now the query part you are using multiple joins so that there will be cross product as mentioned by @FuzzyTree and doing aggregation on the result set will give you wrong results more than expected so that you need a distinct count thats why i have defined primary keys for tables so instead of counting v.views
count distinct v.sno
same for like and comments
select l.*,
count(distinct v.sno) as views,
count(distinct li.sno) as likes,
count(distinct c.sno) as comments
from listings l
join likes li on(l.sno = li.list_id)
join comments c on(l.sno = c.list_id)
join `views` v on(l.sno = v.list_id)
where l.user_id = 'user100'
group by l.sno
Using active record you can write your query something like
$this->db->select ( 'l.*,
count(distinct v.sno) as views,
count(distinct li.sno) as likes,
count(distinct c.sno) as comments' ,false)
->from ( 'listings as l' )
->join ( 'likes li', 'l.sno = li.list_id')
->join ( 'comments c', 'l.sno = c.list_id')
->join ( 'views v', 'l.sno = v.list_id')
->where ( 'l.user_id', $user_id)
->group_by( 'l.sno');
According to your provided data set list 1 has 3 likes,3 views and 1 comment you can find attached demo of above query and also you can find update table definitions with foreign keys and cascading (will help to maintain relations)
Fiddle Demo
Edit for posts with 0 likes/views and comments
using inner join posts with 0 likes/views and comments will not be returned for this you need left join you can see updated demo and using active record you can build query something like below, define join type (left/inner/right) in join()
function's 3rd parameter
$this->db->select ( 'l.*,
count(distinct v.sno) as views,
count(distinct li.sno) as likes,
count(distinct c.sno) as comments' ,false)
->from ( 'listings as l' )
->join ( 'likes li', 'l.sno = li.list_id','left')
->join ( 'comments c', 'l.sno = c.list_id','left')
->join ( 'views v', 'l.sno = v.list_id','left')
->where ( 'l.user_id', $user_id)
->group_by( 'l.sno');
Updated Demo
Upvotes: 5
Reputation: 43
If you are afraid of too much joins, time you tried out the SQL IN statement. Do something like;
// You can include query binding if you like
$sql = SELECT column_name(s)
FROM table_name
WHERE column_name IN (SELECT column_name(s)
FROM table_name
WHERE column_name IN (value1,value2,...) WHERE SELECT column_name(s)
FROM table_name
WHERE column_name IN (value1,value2,...));
Upvotes: -1
Reputation: 97
select a.list_id,a.user_id,a.name,a.category,a.fees,a.details,a.created_on,
(select sum(likes) from Likes where user_id=a.user_id) "total likes",
(select count(*) from comments where user_id=a.user_id) "total comments",
(select sum(views) from views where user_id=a.user_id) "total views",
from Listings as a
where a.user_id ='user 100'
Upvotes: 0
Reputation: 32392
If user_id
is not unique to likes
, comments
or views
you will end up with a cross product when doing multiple joins, which will inflate your counts. Because you’re only querying one user_id, subqueries might be the best way to go.
Remember to set the second parameter to select
to false
so codeigniter doesn't try to escape the subqueries.
$this->db->select ( 'list.*,
(select count(*) from views v where v.user_id = list.user_id) as views,
(select count(*) from likes l where l.user_id = list.user_id) as likes,
(select count(*) from comments c where c.user_id = list.user_id) as comments',false)->from ( 'listings as list' );
$this->db->where ( 'list.user_id', $user_id);
$query = $this->db->get ();
Upvotes: 4
Reputation: 648
kindly change your query to get correct count as bellow.
$this->db->select ( 'list.*,count(v.views) as views,count(l.likes) as likes,count(c.comment) as comments' )
->from ( 'listings as list' )
->join ( 'comments c', 'c.list_id = list.list_id')
->join ( 'views v', 'v.list_id = list.list_id')
->join ( 'likes l', 'l.list_id = list.list_id');
$this->db->where ( 'list.user_id', $user_id);
$this->db->group_by( 'v.list_id');
$this->db->group_by( 'l.list_id');
$this->db->group_by( 'c.list_id');
$query = $this->db->get ();
Upvotes: -1