MySQL Subquery Optimization - where not in(subquery) - php

I'm trying to optimize the following query. I'm thinking an outer join would do the trick, but I can't wrap my mind around how to put it together.
// ---------------------------------
// Simplified representation of data
// ---------------------------------
create table views (
user_id,
article_id
)
create table article_attributes (
article_id,
article_attribute_id
)
create table articles (
id,
title,
date
)
Views table has tens of millions of records.
Articles table has a couple hundred thousand.
I'm trying to match all articles with a certain attribute associated with it, and that have not been viewed by a user.
What I have tried, but doesn't scale well:
select a.title, a.sid as article_id, a.total_views as times_read, a.date
from articles a
join article_attributes att on att.article_id = a.sid
where a.sid not in(
select v.article_id
from views v
join article_attributes att on att.article_id = v.article_id
where user_id = 132385
and att.article_attribute_id = 10
group by v.article_id
)
and att.article_attribute_id = 10
and a.date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 day)
order by total_views desc
limit 5
This works fine, but gets significantly slower the more articles the user has viewed. Any ideas or suggestions would be appreciated.

SELECT a.title, a.sid AS article_id, a.total_views AS times_read, a.date
FROM articles a
JOIN article_attributes att
ON a.id = att.article_id AND att.article_attribute_id = 10
LEFT JOIN views v
ON a.id = v.article_id AND v.user_id = 132385
WHERE v.user_id IS NULL
The first join gets you only the articles with the given attribute.
The second join takes the first join's result and returns rows with the user_id and all the remaining rows from first result that don't have the user_id.(Basically ALL articles with attribute 132385 with the user_id being either 10 or NULL)
Then all we want is that result where user_id is NULL
Try to avoid nested queries and let the engine do it's job. Note you can tag on your other filters (DATE, ORDER BY) on the end.

Try this query
select a.title, a.sid as article_id, a.total_views as times_read, a.date
from
articles a
left join
views v
on
a.sid = v.article_id AND v.article_id is null
join
article_attributes att
on
att.article_id = v.article_id AND v.user_id = 132385 AND att.article_attribute_id = 10
where
a.date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 day)
order by
total_views desc limit 5
Create necessary index for articles table (total_views, sid, date)
view table (article_id, user_id)
article_attributes table (article_id, article_attribute_id)
Hope this helps.

Instead of using the subquery as a where condition, I suggest using it in a join. Also, I suggest you not to use group by in your subquery, but select distinct:
select
a.title, a.sid as article_id, a.total_views as times_read, a.date
from
(articles a
inner join article_attributes att on a.sid = att.article_id)
left join (
select distinct
v.article_id
from views v
inner join article_attributes att on v.article_id = att.article_id
where
user_id = 132385
and att.article_atribute_id = 10
) as b on a.sid = b.article_id
where
b.article_id is null
and att.article_attribute_id = 10
and a.date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 day)
Hope this helps

EXISTS should work better than IN:
SELECT a.title,
a.sid AS article_id,
a.total_views AS times_read,
a.date
FROM articles a
JOIN article_attributes att ON att.article_id = a.sid
WHERE NOT EXISTS (SELECT 0
FROM views v
JOIN article_attributes att ON att.article_id = v.article_id
WHERE user_id = 132385
AND att.article_attribute_id = 10
AND v.article_id = a.sid )
AND att.article_attribute_id = 10
AND a.date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
ORDER BY total_views DESC LIMIT 5

Related

Multiple Counts in MYSQL PHP Query

I'm trying to create a leaderboard but i'm not sure how to do the mysql query.
I would like to count all the levels from a player in the skills table and get the total Level and count all the experience from a player in the experience table and get the Total Exp along with displaying the persons name from the users column.
There is 3 tables factions_mcmmo_users, factions_mcmmo_experience, factions_mcmmo_skills.
This is what i have so far but it doesn't work:
$sql = ("SELECT a.id,
(SELECT COUNT(*) FROM factions_mcmmo_experience WHERE user_id = a.id) as TotalXP,
(SELECT COUNT(*) FROM factions_mcmmo_skills WHERE user_id = a.id) as TotalLevel
FROM (SELECT DISTINCT id FROM factions_mcmmo_users) a LIMIT 10;");
Any help would be very appreciated
EDIT: I have it working now but i'm unsure if its the most efficient way to do things so if anyone could help me out if theres a better way, it would mean a lot.
I would also like to know if it's possible to display the total exp and level with commas if the number is in the thousands for example: total level 5,882 and total xp 582,882
EDIT 2:
I have figured out how to format the numbers but still don't know if my code is efficient
$sql = ("SELECT id, user,
(SELECT FORMAT(Sum(taming)+Sum(mining)+Sum(woodcutting)+Sum(repair)+Sum(unarmed)+Sum(herbalism)+Sum(excavation)+Sum(archery)+Sum(swords)+Sum(axes)+Sum(acrobatics)+Sum(fishing)+Sum(alchemy),0) FROM factions_mcmmo_skills b WHERE b.user_id = a.id) as TotalLevel,
(SELECT FORMAT(Sum(taming)+Sum(mining)+Sum(woodcutting)+Sum(repair)+Sum(unarmed)+Sum(herbalism)+Sum(excavation)+Sum(archery)+Sum(swords)+Sum(axes)+Sum(acrobatics)+Sum(fishing)+Sum(alchemy),0) FROM factions_mcmmo_experience c WHERE c.user_id = a.id) as TotalXP
FROM (SELECT id, user FROM factions_mcmmo_users) a group by id ORDER BY TotalLevel DESC, TotalXP DESC LIMIT 10;");
EDIT 3
Updated code from scaisEdge but was displaying everyones level as 1 and XP as 1, so i changed count(*) changed to sum, added an order By TotalLevel in Descending order and that seems to have worked but i can't get it to display the persons name (user column) in the user table? not sure if i was supposed to change to sum because it didn't work the other way.
$sql = ("SELECT a.id, b.TotalXP, c.TotalLevel
FROM (SELECT DISTINCT id FROM factions_mcmmo_users) a
INNER JOIN (
SELECT user_id, Sum(taming)+Sum(mining)+Sum(woodcutting)+Sum(repair)+Sum(unarmed)+Sum(herbalism)+Sum(excavation)+Sum(archery)+Sum(swords)+Sum(axes)+Sum(acrobatics)+Sum(fishing)+Sum(alchemy) as TotalXP
FROM factions_mcmmo_experience
GROUP By user_id
) b on b.user_id = a.id
INNER JOIN (
SELECT user_id, Sum(taming)+Sum(mining)+Sum(woodcutting)+Sum(repair)+Sum(unarmed)+Sum(herbalism)+Sum(excavation)+Sum(archery)+Sum(swords)+Sum(axes)+Sum(acrobatics)+Sum(fishing)+Sum(alchemy) as TotalLevel
FROM factions_mcmmo_skills
GROUP by user_id
) c on c.user_id = a.id
ORDER BY TotalLevel DESC
LIMIT 10;");
EDIT 4
Everything working but when i try to format the totals using "FORMAT(Sum(Columns), 0) on the inner joins, the EXP Total appears to work but the main Total Level is not displaying results that are over 1,000 and it breaks the leaderboard positioning, it should be sorting them on total level but it appears to be random, when u remove the format,0 it goes back to working
I would like it to display commas if the number number is the thousands for example: Total Level: 5,532 and Total EXP 5882,882
See live demo: http://mcbuffalo.com/playground/leaderboards/server/factions-mcmmo.php
Updated Code trying to use Format:
$sql = ("SELECT a.id, a.user, b.TotalXP, c.TotalLevel
FROM (SELECT id, user FROM factions_mcmmo_users) a
INNER JOIN (
SELECT user_id, FORMAT(Sum(taming)+Sum(mining)+Sum(woodcutting)+Sum(repair)+Sum(unarmed)+Sum(herbalism)+Sum(excavation)+Sum(archery)+Sum(swords)+Sum(axes)+Sum(acrobatics)+Sum(fishing)+Sum(alchemy), 0) as TotalXP
FROM factions_mcmmo_experience
GROUP By user_id
) b on b.user_id = a.id
INNER JOIN (
SELECT user_id, FORMAT(Sum(taming)+Sum(mining)+Sum(woodcutting)+Sum(repair)+Sum(unarmed)+Sum(herbalism)+Sum(excavation)+Sum(archery)+Sum(swords)+Sum(axes)+Sum(acrobatics)+Sum(fishing)+Sum(alchemy), 0) as TotalLevel
FROM factions_mcmmo_skills
GROUP by user_id
) c on c.user_id = a.id
ORDER BY TotalLevel DESC;");
EDIT 5
Changed number with PHP, everything works
Original Images
you could use an couple of inner join
$sql = ("SELECT a.id, a.name, b.TotalXP, c.TotalLevel
FROM (SELECT DISTINCT id, name FROM factions_mcmmo_users) a
INNER JOIN (
SELECT user_id, COUNT(*) as TotalXP
FROM factions_mcmmo_experience
GROUP By user_id
) b on b.user_id = a.id
INNER JOIN (
SELECT user_id, COUNT(*) as TotalLevel
FROM factions_mcmmo_skills
GROUP by user_id
) c on c.user_id = a.id
LIMIT 10

Select results from table1 based on entries on table2

I have 2 tables;
banner_views (id, b_id, b_date)- this record a banner view every time it gets displayed
banners_dynamic (id, status, static_iname, static_keywords, static_url, static_alt, static_type, static_image, b_views, b_clicks) - stores the banner data
I would like to select 3 banners_dynamic results which have had the least views in the last 7 days.
I did put somethign together (see below) but I realised it was grabbing the total views for all banner rather than uniquely by id.
SELECT *,
(SELECT COUNT(*) FROM banner_views v WHERE v.b_date >= DATE(NOW()) - INTERVAL 7 DAY) as post_count
FROM banners_dynamic b
WHERE static_keywords LIKE '%test%' AND b.status='1' AND b.static_type='1'
ORDER BY post_count ASC LIMIT 3
Can anyone point me in the correct direction?
You must join both banners_dynamic table and your subquery with corresponding banner IDs:
SELECT
b.*, p.b_count
FROM
banners_dynamic b
INNER JOIN (
SELECT
b_id,
COUNT(*) AS b_count
FROM
banner_views v
WHERE
v.b_date >= DATE(NOW() - INTERVAL 7 DAY)
GROUP BY
b_id
) p on p.b_id = b.id
WHERE
b.static_keywords LIKE '%test%'
AND b.`status` = '1'
AND b.static_type = '1'
ORDER BY
p.b_count ASC
LIMIT 3
UPDATE: You can do it even without subquery:
SELECT
b.*, COUNT(v.b_id) AS b_count
FROM
banners_dynamic b
INNER JOIN banner_views v ON v.b_id = b.id
WHERE
v.b_date >= DATE_ADD(NOW(), INTERVAL - 7 DAY)
AND b.static_keywords LIKE '%test%'
AND b.`status` = '1'
AND b.static_type = '1'
GROUP BY
v.b_id
ORDER BY
b_count ASC
LIMIT 3;
If you want to include banners without any views (count=0) then you must do a LEFT JOIN:
SELECT
b.*, COUNT(v.b_id) AS b_count
FROM
banners_dynamic b
LEFT JOIN banner_views v ON v.b_id = b.id
AND v.b_date >= DATE_ADD(NOW(), INTERVAL - 7 DAY)
WHERE
b.static_keywords LIKE '%test%'
AND b.`status` = '1'
AND b.static_type = '1'
GROUP BY
v.b_id
ORDER BY
b_count ASC
LIMIT 3;

select count of rows from 2 tables and merge into one row (mysqli)

i create a web app like facebook by php and mysqli
in my app i have a table for posts , one table for likes , and one table for comments
i want to get the number of comments and likes of each post in one row with his post_id!!!
i try some querys likes this :
select `tblpost`.`post_id`, COALESCE(TCOMM.`comment_num`,0) as `c_num`, COALESCE(TLIKE.`like_num`,0) as `l_num`
from
(select `tblpost`.`post_id`, count(*) as `like_num` from `tblpost` join `tbllikes` on `tbllikes`.`post_id` = `tblpost`.`post_id` group by `tblpost`.`post_id`
) TLIKE
inner join
(select `tblpost`.`post_id`, count(*) as `comment_num` from `tblpost` join `tblcomments` on `tblcomments`.`post_id` = `tblpost`.`post_id` group by `tblpost`.`post_id`) TCOMM
on
TCOMM.`post_id` = TLIKE.`post_id`
but i don't know what's my problem
You can do count distincts with two left joins.
Something like this would work if there are fields like_id and comment_id in the tables tbllikes and tblcomments
SELECT
tblpost.post_id AS post_id,
COUNT(DISTINCT tbllikes.like_id) AS likes,
COUNT(DiSTINCT tblcomments.comment_id) AS comments
FROM tblpost
LEFT JOIN tbllikes ON tbllikes.post_id = tblpost.post_id
LEFT JOIN tblcomments on tblcomments.post_id = tblpost.post_id
GROUP BY tblpost.post_id
First, I think you can greatly simplify your query:
select l.post_id,
COALESCE(c.comment_num, 0) as c_num, COALESCE(l.like_num, 0) as l_num
from (select l.post_id, count(*) as like_num
from tbllikes l
group by l.post_id
) l inner join
(select c.post_id, count(*) as comment_num
from tblcomments c
group by c.post_id
) c
on l.post_id = c.post_id;
This will only get you posts that have both likes and comments. To get what you want, use a left join:
select p.post_id,
COALESCE(c.comment_num, 0) as c_num, COALESCE(l.like_num, 0) as l_num
from tblpost p left join
(select l.post_id, count(*) as like_num
from tbllikes l
group by l.post_id
) l
on l.post_id = p.post_id left join
(select c.post_id, count(*) as comment_num
from tblcomments c
group by c.post_id
) c
on c.post_id = p.post_id;

How Can i Convert this multiple sub query in zend query?

i have the following query which gives desired output in mysql , now i want to implement it in zend query language,
which has different approach to implement the query..
SELECT A.NAME , B.PAYMENT , C.TOTALPROJ , D.TOTALTASK , T.ACTIVETASK , H.HOUR
FROM USERMASTER AS A
LEFT OUTER JOIN
(
SELECT A.U_ID , SUM(A.TOTALTIME * B.RATE) AS PAYMENT
FROM
(
SELECT U_ID , PROJECT_ID ,
SUM(TIME_TO_SEC(CASE WHEN endtime is null then timediff (starttime,starttime)
ELSE timediff (endtime,starttime) END )) / 3600 AS TOTALTIME
FROM LOGMASTER AS A
WHERE PROJECT_ID IS NOT NULL
GROUP BY U_ID , PROJECT_ID
) AS A
INNER JOIN PROJECTTOUSER AS B ON A.PROJECT_ID = B.PROJECT_ID AND A.U_ID = B.U_ID
GROUP BY B.U_ID
) AS B ON A.ID = B.U_ID
LEFT OUTER JOIN
(
SELECT U_ID , COUNT(*) AS TOTALPROJ FROM PROJECTTOUSER GROUP BY U_ID
) AS C ON A.ID = C.U_ID
LEFT OUTER JOIN
(
SELECT ASSIGNED_TO , COUNT(*) AS TOTALTASK FROM TASKTOTARGET GROUP BY ASSIGNED_TO
) AS D ON A.ID = D.ASSIGNED_TO
LEFT OUTER JOIN
(
SELECT ASSIGNED_TO,COUNT(*) AS ACTIVETASK FROM TASKTOTARGET WHERE
IS_ACTIVE = 0 GROUP BY ASSIGNED_TO
) AS T ON A.ID = T.ASSIGNED_TO
LEFT OUTER JOIN
(
SELECT U_ID, SEC_TO_TIME(SUM(TIME_TO_SEC(CASE WHEN endtime is null then
timediff (starttime,starttime) ELSE timediff (endtime,starttime) END ))) AS HOUR
FROM LOGMASTER WHERE INSERT_DATE >= '2013-08-20' AND INSERT_DATE <='2013-08-31'
GROUP BY U_ID
) AS H ON A.ID = H.U_ID
so if any one can guide me in how to create this query in zend then it will be very helpful, and appreciated
Each of your subqueries becomes an new Zend_Query that you can then use just like a table and pass in to the main query.
For example:
$h = new Zend_Db_Select()
->from('LOGMASTER', array('U_ID', 'HOUR' => new Zend_Db_Expr('SEC_TO_TIME(SUM(TIME_TO_SEC(CASE WHEN endtime is null then
timediff (starttime,starttime) ELSE timediff (endtime,starttime) END ))))')
->where("INSERT_DATE >= '2013-08-20'")
->where("INSERT_DATE <= '2013-08-31'")
->group('U_ID');
$mainQuery = new Zend_Db_Select()
->from('a' => 'USERMASTER', array('NAME'))
->joinLeft($h, 'A.ID = H.U_IS', array('HOUR'));
You would create each of your subqueries as its own object and then you can join them into your main query. The last argument of the join function is which columns from the subquery should be added to the main query.
With ZF's fluid interface you can keep joining tables/queries until you have built your entire query.
http://framework.zend.com/manual/1.12/en/zend.db.select.html

how would I combine these two queries?

This is my existing query:
$query = mysql_query("SELECT s.*,
UNIX_TIMESTAMP(`date`) AS `date`,
f.userid as favoritehash FROM songs s
LEFT JOIN favorites f ON f.favorite = s.id AND f.userid = '$userhash'
ORDER BY s.date DESC");
It gathers all my songs and the associated favorite information from my db.
This is a basic pagination query that returns values from my db by groups of days:
$query = mysql_query("
select * from songs t1 join (
select to_days(date) day from songs
group by day
order by day desc
limit $start_row, $items_per_page
) t2
on to_days(t1.date) = t2.day
order by day desc") or die(mysql_error());
Here is the column info for the two tables songs and favorites
Songs:
Favorites:
The correlation between favorites and songs is pretty simple where the favorite value in favorites holds the unique id of the song it is a favorite of.
My question is how would I combine these two queries so as to return all the information in the original query but grouped and paginated as it is in the second query?
SELECT B.*
FROM favorites as A
JOIN (
select * from songs t1 join (
select to_days(date) day from songs
group by day
order by day desc
limit $start_row, $items_per_page
) t2
on to_days(t1.date) = t2.day
order by day desc
) as B
ON A.Favorite = B.ID
ORDER BY B.DAY, B.ID
I don't use MySQL and not familiar with using the "limit", but this script might work. The LEFT JOIN with favorites should not increase the row count and append the columns. Would the "limit" work on the outer script also?
select s.*,UNIX_TIMESTAMP(`date`) AS `date`, f.userid as favoritehash
from songs s
join (
select to_days(date) day
from songs
group by to_days(date)
order by day desc
limit $start_row, $items_per_page
) s2 on to_days(s.date) = s2.day
LEFT JOIN favorites f ON f.favorite = s.id AND f.userid = '$userhash'

Categories