Echo SQL data in order of date added using Union All - php

So I am working with a client to implement a similar system as the "badges and privileges system" on StackExchange. Although in her system, she is looking to use points and rewards for her staff. It's the same basic principle. The users are rewarded points for good team work and gain rewards from these points. I thought it would be handy to add the same kind of feature which SE uses to display these in the top nav bar, where it shows your rep and badges in order of the date you have earned either of them. This is my issue, I have found help retrieving the data together from the two separate tables but am not sure how I would display these results in order of date earned? As an example:
User ID #1 has earned 50 points on 18/12/2015 would be in ap_user_points table
User ID #1 has earned 'The Gift Voucher' reward on '17/12/2015'
If I simply:
echo $row8['reward'] . $row8['points_added']
It would echo as:
The Gift Voucher 50
Where I need it in order by date as:
50
The Gift Voucher
If you look at your rep and badge icon in the nav bar you'll see what I'm getting at here, it's a similar system.
<?php
$user_id = $_SESSION['userid'];
$sql8 = "
SELECT r.reward_id,
r.user_id,
r.reward as reward,
r.date_earned as date_earned,
r.badge_desc,
NULL AS points_added,
NULL AS added_for,
NULL AS date_added
FROM ap_user_rewards as r
WHERE r.user_id = '$user_id'
UNION ALL
SELECT NULL,
NULL,
NULL,
NULL,
NULL,
p.points_added AS points_added,
p.added_for AS added_for,
p.date_added AS date_added
FROM ap_user_points as p
WHERE p.user_id = '$user_id' ORDER BY date_earned DESC, date_added DESC;";
$result8 = $conn->query($sql8);
if ($result8->num_rows > 0) {
// output data of each row
while($row8 = $result8->fetch_assoc()) {
////// NOT SURE WHAT TO ECHO HERE?
}
}
?>

Add another column to the result set. In that new column, populate it from both queries... looks like it would be the date_added expression in the first query and the date_earned expression in the second query. When those are in the same column, then ordering is easy. (This also assumes that these expressions are of the same or compatible datatypes, preferably DATE, DATETIME or TIMESTAMP.)
Then you can order by ordinal position, e.g. ORDER BY 2 to order by the second column in the resultset.
SELECT a1
, b1
, NULL
, NULL
, a1 AS sortexpr
FROM ...
UNION ALL
SELECT NULL
, NULL
, x2
, y2
, x2 AS sortexpr
FROM ...
ORDER BY 5 DESC
That's just one possibility. If you can't add an extra column, to line up the expressions from the two queries, then you need a way to discriminate which query is returning the row. I typically include a literal as a discriminator column.
Then you can use implicit-style UNION syntax, wrapping the queries in parens...
( SELECT 'q1' AS `source`
, a1
, b1 AS date_earned
, NULL
, NULL AS date_added
FROM ...
)
UNION ALL
( SELECT 'q2' AS `source`
, NULL
, NULL AS date_earned
, x2
, y2 AS date_added
FROM ...
)
ORDER BY IF(`source`='q1',date_earned,date_added) DESC
Followup
I may have misunderstood the question. I though the question was how to get the rows from a UNION/UNION ALL returned in a particular order.
Personally, I would write the query to include a discriminator column, and then line up the columns as much as I could, so they would be processed the same.
As an example:
SELECT 'reward' AS `source`
, r.date_earned AS `seq`
, r.user_id AS `user_id`
, r.date_earned AS `date_earned`
, r.reward_id
, r.reward
, r.badge_desc
, NULL AS `points_added`
, NULL AS `added_for`
FROM r ...
UNION ALL
SELECT 'points' AS `source`
, p.date_added AS `seq`
, p.user_id AS `user_id`
, p.date_added AS `date_earned`
, NULL
, NULL
, NULL
, p.points_added AS `points_added`
, p.added_for AS `added_for`
FROM p ...
ORDER BY 2 DESC, 1 DESC
(It's probably not really necessary to return user_id, since we already know what the value will be. I've returned it here to demonstrate how the columns from the two resultsets can be "lined up".)
Then, when I fetched the rows...
if ( $row8['source'] == 'points' ) {
# process columns from a row of 'points' type
echo $row8['badge_desc'];
echo $row8['user_id'];
} elsif ( $row8['source'] == 'reward' ) {
# process columns from a row of 'reward' type
echo $row8['added_for'];
echo $row8['user_id'];
}
That's how I would do it.

Related

MySQL best/first score difference query optimisation

Can anyone help me optimise this query? I have the following table:
cdu_user_progress:
--------------------------------------------------------------
|id |uid |lesson_id |game_id |date |score |
--------------------------------------------------------------
For each user, I'm trying to obtain the difference between the best and first scores for a particular game_id for a particular lesson_id, and order the results by that difference ('progress' in my query):
SELECT ms.uid AS id, ms.max_score - fs.first_score AS progress
FROM (
SELECT up.uid, MAX(CASE WHEN game_id = 3 THEN score ELSE NULL END) AS max_score
FROM cdu_user_progress up
WHERE (up.uid IN ('1671', '1672', '1673', '1674', '1675', '1676', '1679', '1716', '1725', '1726', '1937', '1964', '1996', '2062', '2065', '2066', '2085', '2086')) AND (up.lesson_id = '65') AND (up.score > '-1')
GROUP BY up.uid
) ms
LEFT JOIN (
SELECT up.uid, up.score AS first_score
FROM cdu_user_progress up
INNER JOIN (
SELECT up.uid, MIN(CASE WHEN game_id = 3 THEN date ELSE NULL END) AS first_date
FROM cdu_user_progress up
WHERE (up.uid IN ('1671', '1672', '1673', '1674', '1675', '1676', '1679', '1716', '1725', '1726', '1937', '1964', '1996', '2062', '2065', '2066', '2085', '2086')) AND (up.lesson_id = '65') AND (up.score > '-1')
GROUP BY up.uid
) fd ON fd.uid = up.uid AND fd.first_date = up.date
) fs ON fs.uid = ms.uid
ORDER BY progress DESC
Any help would be most appreciated!
Absent any EXPLAIN output or index definitions, we can't make any recommendations. (I noted in a comment that it looks like some join predicates are missing, if we don't have guaranteed uniqueness on the (uid,date) tuple in cdu_user_progress... there's potential that we are going to get rows that are for a different lesson_id or a score that isn't greater than '-1'.
In the query text, immediately before ) fs , I'd be adding
AND up.lesson_id = '65'
AND up.score > '-1'
GROUP BY up.uid
I'd also wrap the up.score column (in the SELECT list of the fd view) in an aggregate function, either MIN() or MAX(), for compliance with the ANSI standard (even though it isn't required by MySQL when SQL_MODE doesn't include ONLY_FULL_GROUP_BY)
If I didn't have a suitable index defined, I'd consider adding an index:
... ON cdu_user_progress (lesson_id, uid, score, game_id, date)
There's some overhead for the derived tables (materializing the inline views) and those derived tables aren't going to have indexes on them (in MySQL 5.5 and earlier.) But the GROUP BY in each inline view ensures that we'll have less than 20 rows, so that's not really going to be a problem.
So, if there's a performance issue, it's in the view queries. Again, we'd really need to see the output from EXPLAIN and the index definitions, and some cardinality estimates, in order to make recommendations.
FOLLOWUP
Given that there's not a unique constraint on (uid,date), I'd add those predicates in the fs view query. I'd also use unique table aliases in the query (for each references to cdu_user_progress) to make both the statement and the EXPLAIN output easier to read. Also, adding the GROUP BY clause and the aggregate function in the fd view... I'd write the query like this:
SELECT ms.uid AS id
, ms.max_score - fs.first_score AS progress
FROM ( SELECT up.uid
, MAX(CASE WHEN up.game_id = 3 THEN up.score ELSE NULL END) AS max_score
FROM cdu_user_progress up
WHERE up.uid IN ('1671','1672','1673','1674','1675','1676','1679','1716','1725','1726','1937','1964','1996','2062','2065','2066','2085','2086')
AND up.lesson_id = '65'
AND up.score > '-1'
GROUP BY up.uid
) ms
LEFT
JOIN ( SELECT uo.uid
, MIN(uo.score) AS first_score
FROM ( SELECT un.uid
, MIN(CASE WHEN un.game_id = 3 THEN un.date ELSE NULL END) AS first_date
FROM cdu_user_progress un
WHERE un.uid IN ('1671','1672','1673','1674','1675','1676','1679','1716','1725','1726','1937','1964','1996','2062','2065','2066','2085','2086')
AND un.lesson_id = '65'
AND un.score > '-1'
GROUP BY un.uid
) fd
JOIN cdu_user_progress uo
ON uo.uid = fd.uid
AND uo.date = fd.first_date
AND uo.lesson_id = '65'
AND uo.score > '-1'
GROUP BY uo.uid
) fs
ON fs.uid = ms.uid
ORDER BY progress DESC
And I believe that would make the index I recommended above suitable for all of the references to cdu_user_progress.

Better way to find the latest change in sql value

I have 2 tables, one of which lists items, the other of which keeps track of when items changed in price.
price_table:
price_id int
item_id int
price_amount float
change_date date // this date is stored as Y-m-d, without a time
and
item_table:
item_id int
item_name char(40)
item_description char(40)
I know that the following finds the item_ids changed at the current date:
SELECT item_id
FROM price_table
WHERE change_date = "$today"
when
$today = date('Y-m-d');
I then run another query for EACH item_id returned (this is why I think my method is inefficient)
SELECT card_price
FROM price_table
WHERE item_id
ORDER BY change_date ASC
And get the last 2 values from the PHP array, comparing them to get the difference in price. My test had an inner join to return the item_name as well, but I've removed that to simplify the question. Is there a better way to do this? And if so, can it be expanded to use the last 2 or 3 days as the change criteria instead?
This is what triggers are for, I'm also going to talk about DATE_SUB and date intervals
Using triggers will allow you to keep the current price in the price table and keep historical prices in a separate table.
For instance, consider creating a new table called price_table_old
price_table_old:
price_id int
item_id int
price_amount float
change_date date
On the price_table, create an 'update' trigger...
insert into price_table_old ( `item_id`, `price_amount` , `change_date ` )
VALUES ( OLD.`item_id` , OLD.`price_amount` , NOW() )
To help you with trigger syntax, I copied the trigger I used for a staff table below
When ever someone changes a price, the price_table_old table is automatically updated.
From here, things are a little more logical to get the current price and comparisons of previous prices.
You could run a query like....
SELECT
price_table.item_id ,
price_table.price_amount ,
(price_table.price_amount - price_table_old.price_amount ) AS pricediff ,
price_table_old.change_date
FROM
price_table
LEFT JOIN
price_table_old ON price_table_old.item_id = price_table.item_id
ORDER BY price_table.item_id , price_table_old.change_date DESC
or stick a where in there to zone in on a specific item and/or date range and/or a limit to get the last (say) 3 price changes.
For instance, to get a list of price changes for the last 3 days on all items, you could use a statement like this
SELECT
price_table.item_id ,
price_table.price_amount ,
(price_table.price_amount - price_table_old.price_amount ) AS pricediff ,
price_table_old.change_date
FROM
price_table
LEFT JOIN
price_table_old ON price_table_old.item_id = price_table.item_id
WHERE
price_table_old.change_date > ( DATE_SUB( NOW() , INTERVAL 3 DAY ))
ORDER BY price_table.item_id , price_table_old.change_date DESC
Here is my staff table trigger...
CREATE TRIGGER `Staff-CopyOnUpdate` BEFORE UPDATE ON `staff`
FOR EACH ROW insert into staff_old
( `sid` , `title`, `firstname` , `surname` , `type`,
`email` , `notify`, `actiondate`, `action` )
VALUES
( OLD.`sid` , OLD.`title`, OLD.`firstname` , OLD.`surname` , OLD.`type`,
OLD.`email` , OLD.`notify`, NOW() , 'updated' )

How to order comments by likes/dislikes in MySQL

On my website people can thumbs up or thumbs down a comment.
To do this I use two tables:
$sql = "CREATE TABLE content
(
id INT NOT NULL AUTO_INCREMENT,
PRIMARY KEY(id),
content TEXT NOT NULL,
date date,
time time
)";
and
$sql2 = "CREATE TABLE ratings
(
rating_id INT AUTO_INCREMENT PRIMARY KEY NOT NULL ,
rating VARCHAR (10) NOT NULL ,
id INT NOT NULL ,
ip VARCHAR (50) NOT NULL
)";
The data stored in the ratings would be as follows:
Comment ID like/dislike user IP
1 l 86.42.173.83
1 d 86.42.173.43
2 l 86.42.173.79
2 l 86.42.173.34
2 d 86.42.173.22
The problem I'm having is that I'm finding it extremely difficult to create a SQL statement to order the comments by the amount of likes they have.
If anyone has any ideas on how to do this it would be greatly appreciated.
It would be easier if you stored likes as integers and not letters.
I added up the likes using a case statement and grouped by comment.
SELECT C.content,
SUM(CASE WHEN R.rating = 'l' THEN 1 ELSE -1 END) AS overallRating
FROM content C
LEFT JOIN ratings R ON R.id = C.id
GROUP BY C.content
ORDER BY overallRating
something like this will work
select content.text, count(*) likes
from content join ratings on content.id = ratings.id
group by context.text
order by likes

How do you ORDER BY two tables?

I am what you would call a 'noob' at MySQL. I can insert/edit/select stuff, but anything more advanced than that stumps me. I have two tables in my database:
Table 'reviews'
id int(11)
review varchar(2500)
game int(11)
user int(11)
title varchar(200)`
and Table 'review_rating'
user int(11)
review int(11) // Corresponds to `reviews.id`
like tinyint(1)
Here is my question: Is it possible to use ORDER BY on the reviews table to order the result by the total number of review_ratings with 'like' = 1 (where 'review' = the id of the 'reviews' table) divided by the total number of review_ratings (where 'review' = the id of the 'reviews' table).
Example:
SELECT *
FROM `reviews`
WHERE `game` = ?
ORDER BY (total number of review_ratings where review = reviews.id and like = 1 /
total number of review_ratings where review = reviews.id)
LIMIT 0, 10
SELECT t.review,
Score = CASE WHEN TotalReviews<> 0 THEN LikedReviews/TotalReviews ELSE NULL END
FROM (
SELECT *,
(SELECT COUNT(*) FROM review_rating WHERE review = r.review) AS TotalReviews ,
(SELECT COUNT(*) FROM review_rating WHERE review = r.review AND like = 1) AS LikedReviews,
FROM review r
WHERE game = ?
)t
ORDER BY t.review, Score
I think it's clearer to put it in the SELECT clause:
SELECT reviews.*,
( SELECT SUM(like) / COUNT(1)
FROM review_ratings
WHERE review = reviews.id
) like_ratio
FROM reviews
WHERE game = ?
ORDER
BY like_ratio DESC
LIMIT 10
;
Notes:
Not tested; I'm away from a MySQL box at the moment.
I think you could move the subquery to the ORDER BY clause if you wanted, but it seems like a useful thing to retrieve, anyway.
I'm not sure how the above will behave if a given review has no ratings. You may need to use a CASE expression to handle that situation.
something like this would order by the total review_rating per review:
select( count(review.id) as 'total' from reviews join review_rating on review.id = review_rating.review group by review.id) order by total
the math is not exactly what you had but hopefully you will get it

MySQL query for selecting a maximum element

I have a table with 4 columns: place_id, username, counter, last_checkin
I'm writing a check-in based system and I'm trying to get a query that will give me the "mayor" of each place. The mayor is the one with most check-ins, and if there is more than 1 than the minimum last_checkin wins.
For example, if I have:
place_id, username, counter, last_checkin
123, tom, 3 , 13/4/10
123, jill, 3, 14/4/10
365, bob, 2, 15/4/10
365, alice, 1, 13/4/10
I want the result to be:
123, tom
365, bob
I'm using it in PHP code
Here is the test data:
CREATE TABLE `my_table` ( `place_id` int(11), `username` varchar(50), `counter` int(11), `last_checkin` date);
INSERT INTO `my_table` VALUES (123,'tom',3,'2010-04-13'),(123,'jill',3,'2010-04-14'),(365,'bob',2,'2010-04-15'),(365,'alice',1,'2010-04-13');
How about..
SELECT
place_id,
(SELECT username
FROM my_table MT2
WHERE MT2.place_id = MT1.place_id
ORDER BY counter DESC, last_checkin ASC
LIMIT 1) AS mayor
FROM my_table MT1
GROUP BY place_id;
Edited as Unreason suggests to have ascending order for last_checkin.
Brian's correlated query is something I would write. However I found this different take and it might perform differently depending on the data
SELECT
mt1.place_id,
mt1.username
FROM
my_table mt1 LEFT JOIN my_table mt2
ON mt1.place_id = mt2.place_id AND
(mt1.counter < mt2.counter OR
(mt1.counter = mt2.counter AND mt1.last_checkin > mt2.last_checkin)
)
WHERE
mt2.place_id IS NULL
Which uses left join to get to the top records according to certain conditions.
$data = query("SELECT max(counter) counter,username FROM table GROUP By place_id ORDER By last_checkin DESC");

Categories