I have 2 tables: users and their points.
Users have fields:
id
name
Points have fields:
id
start_date
end_date
count_of_points
user_id
So some users may have or not have points. Points entries limited by time interval (from start_date to end_date) and have count of points that user have at this interval.
I need display table of users sorted by total sum of points at this moment (timestamp must be between start_date and end_date) and display this sum value later in view. If user have no points this count must be equals 0.
I have something like this:
$qb = $this->getEntityManager()
->getRepository('MyBundle:User')
->createQueryBuilder('u');
$qb->select('u, SUM(p.count_of_points) AS HIDDEN sum_points')
->leftJoin('u.points', 'p')
->orderBy('sum_points', 'DESC');
$qb->groupBy('u');
return $qb->getQuery()
->getResult();
But this have no limits by date interval and have no field for sum points that I can use in view from object.
I tried to find how to solve this task and I made something like this in SQL:
SELECT u.*, up.points FROM users AS u LEFT OUTER JOIN
(SELECT u.*, SUM(p.count_of_points) AS points FROM `users` AS u
LEFT OUTER JOIN points AS p ON p.user_id = u.id
WHERE p.start_date <= 1463578691 AND p.end_date >= 1463578691
) AS up ON u.id = up.id ORDER BY up.points DESC
But this query give me only users with entries in points table, so I think I must use another JOIN to add users without points. It's complicated query. I have no idea how implements this in doctrine because DQL can't use inner queries with LEFT JOIN.
Maybe there are other ways to solve this task? Maybe my tables schema is wrong and I can do this different way?
EDIT: forgot the date conditions. Corrected answer:
In plain MySQL your query would look like this:
SELECT u.id, u.name, COALESCE(SUM(p.count_of_points),0) AS sum_points
FROM Users u
LEFT JOIN Points p ON p.user_id=u.id
WHERE (p.start_date <= 1463578691 AND p.end_date >= 1463578691) OR p.id IS NULL
GROUP BY u.id
ORDER BY sum_points DESC
The COALESCE function sends back the first not NULL argument, so if a user doesn't have points, the sum would result in NULL, but the COALESCE in 0.
I'm not sure of the translation using the Doctrine query builder, but you could try:
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('u')
->addSelect('COALESCE(SUM(p.count_of_points),0) AS sum_points')
->from('User', 'u')
->leftjoin('u.points', 'p')
->where('(p.start_date <= ?1 AND p.end_date >= ?1) OR p.id IS NULL')
->groupBy('u.id')
->orderBy('sum_points','DESC')
->setParameter(1, $date_now);
Related
I have users and likes tables. A foreign key of the latter references id from users table. The task at hand is to retrieve all distinct users who have more than 100 likes in March 2018. I'm trying to extract date-related values from a column with a type TIMESTAMP
I've come up with only seeing that pretty much all of them have some likes in that period:
SELECT DISTINCT u.name
FROM users AS u
JOIN likes AS l ON u.id = l.user_id
WHERE MONTH(l.timestamp) = 3 AND YEAR(l.timestamp) = 2018;
I guess I have to make use of COUNT() and GROUP BY somehow, but all my struggles were leading to syntax errors. Please give a hand.
You don't want select distinct. You want group by and having:
SELECT u.name
FROM users u JOIN
likes l
ON u.id = l.user_id
WHERE MONTH(l.timestamp) = 3 AND YEAR(l.timestamp) = 2018
GROUP BY u.name
HAVING COUNT(*) > 100;
To be honest, it is better to write the WHERE clause as:
WHERE l.timestamp >= '2018-03-01' AND l.timestamp < '2018-04-01'
This allows the SQL engine to use an index on timestamp, if one is available.
Im having trouble structuring my MySQL query to return an accurate comment count, sum of votes, and the active users vote.
My tables are
wall_posts ( id, message, username, etc )
comments ( id, wall_id, username, text, etc )
votes ( id, wall_id, vote (+1 or -1), username )
My query looks like this
SELECT
wall_posts.*,
COUNT( comments.wall_id ) AS comment_count,
COALESCE( SUM( v1.vote ), 0 ) AS vote_tally,
v2.vote
FROM
wall_posts
LEFT JOIN comments ON wall_posts.id = comments.wall_id
LEFT JOIN votes v1 ON wall_posts.id = v1.wall_id
LEFT JOIN votes v2 ON wall_posts.id = v2.wall_id AND v2.username=:username
WHERE
symbol =: symbol
GROUP BY
wall_posts.id
ORDER BY
date DESC
LIMIT 15
It works for always returning the correct value for the specific active users vote (+1 or -1) or null if hasnt voted. If there are no comments on an item, the total vote sum is correct. If there are any comments, the vote sum will always be equal to the comment count, possibly with a negative sign if there are down votes but always equal to the amount of comments.
I think its obviously the way ive connected my tables but i just cant figure out why its copying the comment count, 1000000 points to someone who can explain this to me :)
You need to perform the aggregate operations in subqueries. Right now instead you're JOINing all of the tables (pre-aggregation) together. If you remove the aggregates (and the GROUP BY) you'll see the large mass of data which doesn't really mean anything.
Instead, try this (note I'm using a VIEW):
CREATE VIEW walls_posts_stats AS
SELECT
wall_posts.id,
COALESCE( comments_stats.comment_count, 0 ) AS comment_count,
COALESCE( votes_stats.vote_tally, 0 ) AS vote_tally
FROM
wall_posts
LEFT OUTER JOIN
(
SELECT
wall_id,
COUNT(*) AS comment_count
FROM
comments
GROUP BY
wall_id
) AS comments_stats ON wall_posts.id = comments_stats.wall_id
LEFT OUTER JOIN
(
SELECT
wall_id,
SUM( vote ) AS vote_tally
FROM
votes
GROUP BY
wall_id
) AS votes_stats ON wall_posts.id = votes_stats.wall_id
Then you can query it JOINed with your original wall data:
SELECT
wall_posts.*, -- note: avoid the use of * in production queries
stats.comment_count,
stats.vote_tally,
user_votes.vote
FROM
wall_posts
INNER JOIN walls_posts_stats AS stats ON wall_posts.id = stats.id
LEFT OUTER JOIN
(
SELECT
wall_id,
vote
FROM
votes
WHERE
username = :username
) AS user_votes ON wall_posts.id = user_votes.wall_id
ORDER BY
date DESC
LIMIT 15
Hypothetically you could combine it into a single large query (basically copy+paste the VIEW body into the INNER JOIN walls_posts_stats clause) but I feel that would introduce maintainability issues.
While MySQL does support views, it does not support parameterized views (aka composable table-valued functions; stored procedures are not composable) so that's why the user_votes subquery isn't in the walls_posts_stats VIEW.
I want this query to show all users no matter what. And the calculate number of logins that happened in a certain time period (or just retun zero if there are no entries).
Right now, it only show users that have entries in the log in that time period.
SELECT
COUNT(user_log.type) AS logins,
users.name,
users.email,
users.user_id
FROM
users
LEFT JOIN
user_log
ON
users.user_id = user_log.user_id
WHERE
user_log.type = 8
AND
user_log.date >= :from
AND
user_log.date <= :to
GROUP BY
users.user_id
user_log.type = 8 means the log entry is a login
:from is a timestamp with the start date
:to is a timestamp with the end date
I use PDO
First of all, you're misusing a pernicious MySQL extension to GROUP BY. You'll have a hard time debugging. So you need to GROUP BY users.name, users.email. users.id.
The way your query is written, with items from your LEFT JOINed table in your WHERE clause, converts LEFT JOIN to INNER JOIN. So you're losing the unmatched rows from your first table.
Move those WHERE items to the ON clause and things will work the way you want. Like so.
SELECT
COUNT(user_log.type) AS logins,
users.name,
users.email,
users.user_id
FROM users
LEFT JOIN user_log
ON (
users.user_id = user_log.user_id
AND
user_log.type = 8
AND
user_log.date >= :from
AND
user_log.date <= :to
)
GROUP BY
users.name, users.email. users.id
If you use a where constraint on the second table with a LEFT JOIN, you'll have the same result as if you have used an INNER JOIN, because you don't allow NULL value.
Here a solution you can try, using a subrequest :
SELECT users.name,
users.email,
users.user_id,
IF (logins IS NOT NULL, logins, 0) AS logins
FROM users
LEFT JOIN (
SELECT user_id, COUNT(type) AS logins
FROM user_log
WHERE
type = 8
AND
date >= :from
AND
date <= :to
GROUP BY
user_id
) AS l ON l.user_id = users.user_id
I'm trying to perform a select within a where clause.
Basically, I have a number of users, and trying to see which were active. Active means they logged activity in last 30 days. But, if I join the user table with activity table, then I get duplicate user IDs (because each user may have logged multiple actions).
So I was looking at putting a select inside a where that would check, for each user, that there was at least one action.
SELECT u FROM `users` u
where (
select count(*) FROM `user_activity` ua
where ua.user_id = u.user_id and ua.last_login between "2012-04-01 00:00:00" and "2012-04-30 23:59:59"
) >= 1
SELECT u
FROM users u
where EXISTS ( select null
FROM user_activity ua
where ua.user_id = u.user_id
and ua.last_login between "2012-04-01 00:00:00" and "2012-04-30 23:59:59"
LIMIT 1)
Thanks to #Ami for pointing about about LIMIT 1 in subquery that potentially could improve performance a bit
Yes, you can nest a select inside a where clause like so:
SELECT * FROM mytable WHERE users in(SELECT users FROM user_activity);
But I don't think you can nest an aggregate function like count(*) inside a where clause. So I would try this first and then the aggregate function, but try to write your select without the aggregate. I don't have you data in front of me so I can't help there.
Yes, you can put a SELECT in a WHERE clause.
I would avoid the correlated subquery with a JOIN to see if it improved the performance:
SELECT DISTINCT `user`
FROM users u
JOIN user_activity ua
ON ua.user_id = u.user_id
AND ua.last_login BETWEEN '2012-04-01 00:00:00' AND '2012-04-30 23:59:59'
I am trying to create a custom sort that involves the count of some records in another table. For example, if one record has no records associated with it in the other table, it should appear higher in the sort than if it had one or more records. Here's what I have so far:
SELECT People.*, Organizations.Name AS Organization_Name,
(CASE
WHEN Sent IS NULL AND COUNT(SELECT * FROM Graphics WHERE People.Organization_ID = Graphics.Organization_ID) = 0 THEN 0
ELSE 1
END) AS Status
FROM People
LEFT JOIN Organizations ON Organizations.ID = People.Organization_ID
ORDER BY Status ASC
The subquery within the COUNT is not working. What is the correct way to do something like this?
Update: I moved the case statement into the order by clause and added a join:
SELECT People.*, Organizations.Name AS Organization_Name
FROM People
LEFT JOIN Organizations ON Organizations.ID = People.Organization_ID
LEFT JOIN Graphics ON Graphics.Organization_ID = People.Organization_ID
GROUP BY People.ID
ORDER BY
CASE
WHEN Sent IS NULL AND Graphics.ID IS NULL THEN 0
ELSE 1
END ASC
So if if the People record does not have any graphics, Graphics.ID will be null. This achieves the immediate need.
If what you tried does not work, it can be done by joining against a subquery, and placing the CASE expression into ORDER BY as well:
SELECT
People.*,
orgcount.num
FROM People JOIN (
SELECT Organization_ID, COUNT(*) AS num FROM Graphics GROUP BY Organization_ID
) orgcount ON People.Organization_ID = orgcount.num
ORDER BY
CASE WHEN Sent IS NULL AND orgcount.num = 0 THEN 0 ELSE 1 END,
orgcount.num DESC
You could use an outer join to the Graphics table to get the data needed for your sort.
Since I don't know your schema, I made an assumption that the People table has a primary key column called ID. If the PK column has a different name, you should substitute that in the GROUP BY clause.
Something like this should work for you:
SELECT People.*, (count(Distinct Graphics.Organization_ID) > 0) as Status
FROM People
LEFT OUTER JOIN Graphics ON People.Organization_ID = Graphics.Organization_ID
GROUP BY People.ID
ORDER BY Status ASC
Fairly straight forward with a LEFT JOIN provided you have some kind of primary key in the People table to GROUP on;
SELECT p.*, sent IS NOT NULL or COUNT(g.Organization_ID) Status
FROM People p LEFT JOIN Graphics g ON g.Organization_ID = p.Organization_ID
GROUP BY p.primary_key
ORDER BY Status
Demo here.