Add Column to one Table that is COUNT of another - php

I have a list of subscribers in table Subscribers. Every time they receive a copy of their subscription, a new record is created in Subscriptions_Fulfilments for each Subscribers.ID.
I can create a table showing each Subscriber ID and the number of copies they received with the following query:
SELECT Sub_ID, COUNT(Sub_ID) fcount FROM `Subscriptions_Fulfilments`
GROUP BY Sub_ID
But I need to create a compound query that returns Subscribers along with a column showing the COUNT(Sub_ID) of Subscriptions_Fulfilments.
So I have two questions:
A) How would you make a query to create a table that shows each Subscriber and the number of times they've received their subscription, based on the COUNT of that Subscriber's ID in Subscriptions_Fulfilments?
B) I'm operating under the assumption that a single MySql query accomplishing this would be more efficient than, say, running two queries, the one above and a SELECT * FROM Subscriptions, and combining the resulting arrays in PHP. I have a feeling I know the answer but I'd like to positively learn something new today.
Unfortunately, after too many tries, I'm clearly not good enough at queries for this and I have very little past the above query to show for it. I apologize if this ends up being a dup, I searched long and hard before asking, but it's quite difficult to search precisely for Query help...

Here is a simple example showing the Subscribers ID and the no of subscription they have received. Hope it helps.
Step 1: select the ids from the Subscriber table
Step 2: select the no of counts of subscriptions received by each subscriber.
Step 3: Join both the table ON the basis of ID.
SELECT SubId, noSub FROM
Subscribers sb JOIN (SELECT SubId AS sid, COUNT(*)AS noSub FROM Subscriptions_Fulfilments GROUP BY SubId)AS ss ON sb.SubId = ss.sid

One of the big advantages of a relational database is the ability to do joins and combinations of the data in your tables in a way that allows for this functionality without having to actually store it in a separate table.
You can accomplish this with a subquery like this:
SELECT Subscribers.name, fulfilments.count FROM Subscribers
INNER JOIN (
SELECT id, count(*) as count FROM Subscriptions_Fulfilments
GROUP BY Sub_Id
)fulfilments ON subscribers.id = fulfilments.id
This might not be 100% what you're looking for and I might have messed up your names, but hopefully this will start to get you in the neighborhood of being correct?

Simply try execute this query:
Select distinct Sub_ID, count from (SELECT Sub_ID, COUNT(Sub_ID) fcount FROM Subscriptions_Fulfilments
GROUP BY Sub_ID);

Related

Showing users who liked an item in an item list

This is an issue that I've deemed impractical to implement but I would like to get some feedback to confirm.
I have a product and users database, where users can like products, the like data is stored in a reference table with just pid and uid.
The client request is to show 3 users who have liked every product in the product listing.
The problem is, its not possible to get this data in one query for the product listing,
How I once implemented and subsequently un-implemented it was to perform a request for the users who have liked the products during the loop through the product list.
ie.
foreach($prods as $row):
$likers = $this->model->get_likers($row->id);
endforeach;
That works, but obviously results in not only super slow product listings, and also creates a big strain on the database/cpu.
The final solution that was implemented was to only show the latest user who has liked it (this can be gotten from a join in the products list query) and have a link showing how many people have liked, and upon clicking on it, opens a ajax list of likers.
So my question is, is there actually a technique to show likers on the product list, or is it simply not possible to execute practically? I notice actually for most social media sites, they do not show all likers on the listings, and do employ the 'click to see likers' method. However, they do show comments per items on the listing, and this is actually involves the same problem doesn't it?
Edit: mock up attached on the desired outcome. there would be 30 products per page.
By reading your comment reply to Alex.Ritna ,yes you can get the x no. of results with per group ,using GROUP_CONCAT() and the SUBSTRING_INDEX() it will show the likers seperated by comma or whatever separator you specified in the query (i have used ||).ORDER BY clause can be used in group_concat function.As there is no schema information is available so i assume you have one product table one user table and a junction table that maintains the relation of user and product.In the substring function i have used x=3
SELECT p.*,
COUNT(*) total_likes,
SUBSTRING_INDEX(
GROUP_CONCAT( CONCAT(u.firstname,' ',u.lastname) ORDER BY some_column DESC SEPARATOR '||'),
'||',3) x_no_of_likers
FROM product p
LEFT JOIN junction_table jt ON(p.id=jt.product_id)
INNER JOIN users u ON(u.id=jt.user_id)
GROUP BY p.id
Fiddle
Now at your application level you just have to loop through the products and split the x_no_of_likers by separator you the likers per product
foreach($prods as $row):
$likers=explode('||',$row['x_no_of_likers']);
$total_likes= $row['total_likes'];
foreach($likers as $user):
....
endforeach;
endforeach;
Note there is a default 1024 character limit set on GROUP_CONCAT() but you can also increase it by following the GROUP_CONCAT() manual
Edit from comments This is another way how to get n results per group, from this you can get all the fields from your user table i have used some variables to get the rank for product group ,used subquery for junction_table to get the rank and in outer select i have filtered records with this rank using HAVING jt.user_rank <=3 so it will give three users records per product ,i have also used subquery for products (SELECT * FROM product LIMIT 30 ) so the first 30 groups will have 3 results for each,for below query limit cannot be used at the end so i have used in the subquery
SELECT p.id,p.title,u.firstname,u.lastname,u.thumbnail,jt.user_rank
FROM
(SELECT * FROM `product` LIMIT 30 ) p
LEFT JOIN
( SELECT j.*,
#current_rank:= CASE WHEN #current_rank = product_id THEN #user_rank:=#user_rank +1 ELSE #user_rank:=1 END user_rank,
#current_rank:=product_id
FROM `junction_table` j ,
(SELECT #user_rank:=0,#current_rank:=0) r
ORDER BY product_id
) jt ON(jt.product_id = p.id)
LEFT JOIN `users` u ON (jt.`user_id` = u.`id`)
HAVING jt.user_rank <=3
ORDER BY p.id
Fiddle n results per group
You should be able to get a list of all users that have liked all products with this sql.
select uid,
count(pid) as liked_products
from product_user
group by uid
having liked_products = (select count(1) from products);
But as data grows this query gets slow. Better then to maintain a table with like counts that is maintained through a trigger or separately. On every like/dislike the counter is updated. This makes it easy to show the number of likes for each product. Then if the actual users that liked that product is wanted do a separate call (on user interaction) that fetches the specific likes for one product). Don't do this for all products on a page until actually requested.
I am assuming the size of both these tables is non-trivially large. You should create a new table (say LastThreeLikes), where the columns would be pid,uid_1,uid_2 and uid_3, indexed by pid. Also, add a column to your product table called numLikes.
For each "like" that you enter into your reference table, create a trigger that also populates this LastThreeLikes table if the numLikes is less than 3. You can choose to randomly update one of the values anyway if you want to show new users once in a while.
While displaying a product, simply fetch the uids from this table and display them back.
Note that you also need to maintain a trigger for the "Unlike" action (if there is any) to re-populate the LastThreeLikes table with a new user id.
Problem
The problem is the volume of data. From the point of view that you need two integer value as a answer you should forget about building a heavy query from your n<->n relations table.
Solution
Generates a storable representation using the file_put_contents() with append option each time a user likes a product. I don't have enough room to write the class in here.
public function export($file);
3D array format
array[product][line][user]
Example:
$likes[1293][1][456]=1;
$likes[82][2][656]=1;
$likes[65][3][456]=1;
.
.
.
Number of users who like this particular product:
$number_users_like_this_product = count($likes[$idProduct]);
All idUser who like this particular product:
$users_like_this_product = count($likes[$idProduct][$n]);
All likes
$all_likes = count($likes);
Deleting a like
This loop will unset the only line where $idProduct and $IdUser you want. Since all the variables are unsigned integer it is very fast.
for($n=1, $n <= count($likes[$idProduct]), $n++)
{
unset($likes[$idProduct][$n][$idUser]);
}
Conclusion
Get all likes will be easy as:
include('likes.php');
P.S If you want to give a try i will be glad to optimize my stuff and share it. I've created the class in 2012.

mySQL logic: output of table join not correct

I am trying to query two tables: finished_events and flagged_events. 1st of all I need everything related to the company_id so
SELECT *
FROM finished_events
WHERE company_id=$id
ORDER by schedule, timestamp
I then changed this to:
SELECT * FROM finished_events
INNER JOIN flagged_events
ON finished_events.company_id=flagged_events.company_id
WHERE finished_events.company_id=$id
ORDER by finished_events.schedule, finished_events.timestamp
I have tried using FULL JOIN, LEFT JOIN, and RIGHT JOINs all unsuccessful. Specifically what I want is to get is a combined effort of the following code:
$sql = "SELECT *
FROM finished_events
WHERE company_id=$id
ORDER by schedule, time_stamp";
$flagged_sql = "SELECT *
FROM flagged_events
WHERE company_id=$id
ORDER by schedule, time_stamp";
The tables are a bit different so UNION won't work here. I can post dummy database entries but this won't be of too much help as I need all from both tables. The 2 links between the tables would be the company_id and the schedule columns. Essentially what is going on behind the scenes is timestamps being put into a different table to which I then process either into finished_events or flagged_events. Flagged events will need the user to do something about it until it is a finished event. So this script is generating the data for the GUI, hence why I need to query both tables and create an associative array of customer details then an array of events (from these 2 tables). So creating the assoc_array is no problem I just need to get this query to spit out all the events and order them correctly. Let me know if you need anything specific to solve this one, thanks :)
EDIT
SQL Fiddle: http://sqlfiddle.com/#!2/d4c30/1
this almost fixes it but not quite right, it repeats entries at the bottom
If I understood correctly, this may be useful for you:
SELECT a.* FROM (
SELECT *, 'finished' as event_type FROM finished_events
UNION
SELECT *, 'flagged' as event_type FROM flagged_events) a
ORDER BY a.schedule, a.time_stamp

Get and order based on most recent entry while in multiple mysql joins

I have the following statement that finds all the players of a team in the current season. The players are ordered by their handicap. If their handicaps are the same they are ordered by the oldest added_date, meaning newer members are lower down the list.
SELECT players.playerid_p,
players.fname,
players.sname,
players.tel,
players.mob,
players.email,
season_players.captain
FROM season_players
LEFT JOIN players ON (season_players.playerid_f = players.playerid_p)
LEFT JOIN handicaps ON (handicaps.playerid_f = players.playerid_p)
WHERE season_players.seasonid_f = '$currentSeason'
AND season_players.teamid_f = '".$row["teamid_p"]."'
GROUP BY players.playerid_p
ORDER BY handicaps.handicap ASC, handicaps.added_date ASC
The handicaps table can have multiple entries per player for any reviews they have had.
I can't figure out how to make the latest handicap to be used for the ordering (something maybe to do with MAX on added_date?) yet at the same time if two or more handicaps are the same it order them by oldest registered first based on added_date.
In stead of trying to figure out how to satisfy your needs in current query, wouldn't it be an option to create a separate table, let's say "Handicaps_latest", which only stores player_id and required info of latest review. The reason for doing this is because you are only trying to get(columns in your select clause) the information about players and nothing really needed from handicaps table. In this case, a handicaps table with multiple entries per player might not be a good table to join. But considering that those data might be required in other logic, so leave them there and create a branch new table only storing latest review data could be an option for your case. But it requires some extra work apparently, that is, whenever you insert a new entry into your original handicpas table, a particular entry in handicaps_latest needs to be updated.
SELECT x.*
FROM my_table x
JOIN
( SELECT id,MAX(other_column) max_other_column FROM my_table GROUP BY id) y
ON y.id = x.id
AND y.max_other_column = x.other_column;

Users Rating update, recalculation

I have a table with fields id, votes(for each users), rating.
Task: Counting user rating based on votes for him and for others. that is, each time i update the field votes needed recalculation field rating.
Which means some can be on the 3rd place. voted for him and that he would be stood up to 2rd place, and the other vice versa - from 2 to 3. (in rating fiels)
How to solve this problem? Each time update the field to count users ratings on php and do a lot of update query in mysql is very expensive.
If you want to get the ratings with a select without having a rating column, then this is the way. However from a performance perspective I cannot guarantee this will be your best option. The way it works is that if two users have the same amount of votes they will have the same rating and then it will skip ahead the necessary number for the next different rating:
set #rating:=0;
set #count:=1;
select id,
case when #votes<>votes then #rating:=#rating+#count
else #rating end as rating,
case when #votes=votes then #count:=#count+1
else #count:=1 end as count,
#votes:=votes as votes
from t1
order by votes desc
sqlfiddle
This gives you an extra column which you can ignore, or you could wrap this select in to a subquery and have:
select t2.id,t2.votes,t2.rating from (
select id,
case when #votes<>votes then #rating:=#rating+#count
else #rating end as rating,
case when #votes=votes then #count:=#count+1
else #count:=1 end as count,
#votes:=votes as votes
from t1
order by votes desc) as t2
but the sqlfiddle is strangely giving inconsistent results so you'd have to do some testing. If anyone knows why this is I'd be interested in knowing the reason.
If you want to get the rating for just one user then doing the subquery option and using a where after the from should give you the desired result. sqlfiddle - but again, inconsistent results, run it a few times and sometimes it gives rating as 10 other times as 30. I think testing in your db to see what happens will be best.
Well it depends on a lot of factors
Do you have a large system that is growing exponentially?
Do you require the voting data for historical reporting?
Do users need to register when they vote?
Will this system be use only for one voting type throughout the system life cycle or will more voting on different subjects take place?
If all of the answers are NO then your current update method will work just fine. Just ensure that you apply best coding and MySQL table practices anyway.
Let assume most or all your answers were YES then I would suggest the following:
Every time a vote takes place INSERT the record into your table
Using INSERT, add a timestamp, user id if not possible then maybe an ip address/location
Assign a subject id as foreign key from the vote_subject table. In this table store the subject and date of voting
Now you can create a SELECT statement that can count the votes and calculate the ratings. The person top of the vote count list will get rating 1 in the SELECT. Furthermore you can filter per subject, per day, per user and you should also be able to determine volume depending on the result required.
All this of course dependent on how your system will scale in future. This might be way overkill but something to think about.
Yes aggregations are expensive. You could update a rank table every five minutes or so and query from there. The query as you probably already now is this:
select id, count(*) as votes
from users
group by id
order by votes desc
Instead of having the fields id, votes and rating, alter the table to have the fields id, rating_sum and rating_count. Each time you have a new rating you quering the database like this:
"UPDATE `ratings` SET `rating_count` = `rating_count` + 1, `rating_sum` = `rating_sum`+ $user_rating WHERE `id` = $id"
Now the rating is just the average -> rating_sum / rating_count. No need to have a field with the rating.
Also, to prevent a user rate more than one times, you could create a table named rating_users that will have 2 foreign keys the users.id and ratings.id. The primary key will be (users.id, ratings.id). So each time a user tries to rate first you check this table.
I would recommend doing this when querying the data. It would be much simpler. Order by votes descending.
Perhaps create a view and use the view when querying the data.
You could try something like this:
SET #rank := 0
select id, count(*) as votes, #rank := #rank + 1
from users
group by id
order by votes desc
Or
SET #rank := 0
select id, votes, #rank := #rank + 1
from users
order by votes desc

multiple criteria search

I have multiple tables with Customer data (ex Customer Name, Customer Contact Name, Customer Service Item etc).
I need to enable search on these multiple columns across tables. Then I have to use the search result to pull Customer information (I need Customer ID, or Customer Name).
What is the best way to do this?
Possible Solutions:
Offer multiple filters (different search boxes), and then handle each result separately. (The client does not prefer this, and wants it in a single box.)
Create a temp table (CustomerID, Search Field Values).
Create index !?
User inner join, and put logic into handling the search result!!!!
Thanks.
try something like:
SELECT
c.*
FROM CustomerTable c
INNER JOIN (SELECT
CustomerID
FROM Table1
WHERE columnA =filter1
UNION
SELECT
CustomerID
FROM Table2
WHERE columnB =filter2
UNION
SELECT
CustomerID
FROM Table3
WHERE columnC =filter3
) dt ON c.CustomerID=dt.CustomerID
http://dev.mysql.com/doc/refman/5.1/en/fulltext-search.html
I do believe this similar to Oracle Text Search etc which is used in Oracle applications to allow more intelligent searches, "google-likish".
So it is a fulltext index which is to be created.
Doing it with inner joins (or worse, copying stuff around in temporary tables) might work but the code will be complex and you might kill performance.
the only thing you can do if he customer insists on making a search that works like this is to create a TEXT column, FULLTEXT index it, and concatenate all of the columns you want to search in to this column. if you do this, i suggest that you write your queries in this form to guarantee correct matches while maintining a sort orderthat makes sense:
select *
from sometable
where match(search_column) against('$search_terms' in boolean mode)
order
by match(search_column) against('$search_terms')

Categories