SQL query - joining two tables with a conditional - mysql

For each area in my game I have many levels that can be achieved. Once a user earns a certain number of points in an area, his 'progress level' increases for that particular area. I have two tables in my database. One stores the progress of the user for a particular area of my game:
Table A
userID | areaID | progressLevel | total points earnt
1 1 1 1000
1 2 1 500
Another table, B, stores how many points are required to unlock increase the progress level
areaID | progressLevel | points required
1 2 5000
1 3 9000
1 4 11000
2 2 9999
When enough points are achieved by the user then I check table B and increase the progress level of the user in table A. For example, if user 1 earns over 5000 points in area 1, I would update table A and set progress level = 2.
My problem is I want to write a query to obtain, for a particular user, all their progress levels for each area as well as the number of points required for the next level. For example, for user with id 1, I would like:
areaID | progressLevelCurrent | total points earnt | points required for next progress level
1 1 1000 4000
2 1 500 9499
Is it possible to do this in a single query?

How about this:
select A.areaID, A.progressLevel as progressLevelCurrent, A.`total points earnt`, B.`points required` - A.`total points earnt` as `points required for next progress level`
from A
inner join B on A.areaID = B.areaID and (A.progressLevel + 1) = B.progressLevel
where B.`points required` > A.`total points earnt`;

SELECT
areaID,
progressLevel AS CurrentLevel,
`total points earnt` as TotalNow,
(
SELECT (pointsTilNext - TableA.TotalNow)
FROM TableB
WHERE TableB.progressLevel = (TableA.progressLevel+1)
) AS ToNextLevel
FROM TableA
WHERE userID = ##
Edited to add:
You could also use a join, which would be a more efficient use of server capacity. The left join will return a result for a person even if the person is at the highest level, ie there is no row matching TableA.progressLevel+1
SELECT
areaID,
progressLevel AS CurrentLevel,
`total points earnt` as TotalNow,
(pointsTilNext - TableA.TotalNow) AS ToNextLevel
FROM TableA
LEFT JOIN TableB ON TableB.progressLevel = (TableA.progressLevel+1)
WHERE userID = ##

Related

Get records from multiple tables, but only show 1 per ID?

First of all im sorry for the title, it's difficult to explain what I'm trying to achieve.
I have 2 tables, a table for property records, and a table for the images uploaded for each property.
In my listing_details table I enter 1 record per property that has a unique ID and property slug. I have a prop_gallery table where I can have hundreds of records that share the same property slug so I can relate it back to my my property.
I'm trying to write a query to pull the records from both tables, but I only want to show each property once, at the moment it's looping through all the records in the gallery and showing that property for as many records their are in the gallery. Hope this makes sense?
My query is...
$listings = $db->query('
SELECT *
FROM listing_details
JOIN prop_gallery
ON prop_gallery.prop_gallery_id = listing_details.prop_slug
WHERE (prop_slug LIKE prop_gallery_id OR prop_gallery_id LIKE prop_slug)
AND listing_details.prop_mandate = 1'
)->fetchAll();
If there's a property called Liams house then there will be a record for that in listing_details and if I've uploaded 10 pictures, there will be 10 records for that in prop_gallery.
When I loop through my results this means I'm now showing Liams house 10 times, when I want to show it just the once.
EDIT
Result of the above query
prop_id prop_agent prop_title prop_slug prop_mandate id prop_gallery_id prop_gallery
37 2 House in switzerland house-in-switzerland 1 4 6 main1.png
37 2 House in switzerland house-in-switzerland 1 4 6 main2.png
37 2 House in switzerland house-in-switzerland 1 4 6 main3.png
You can use the ROW_NUMBER() function. Assuming you have a [any] property in the table listting_details you can sort rows by you can do it cleanly; I assumed the property recorded_at.
For example:
SELECT *
FROM (
SELECT *,
row_number() over(partition by prop_slug order by recorded_at) as rn
FROM listing_details d
JOIN prop_gallery g
ON g.prop_gallery_id = l.prop_slug
WHERE prop_slug LIKE prop_gallery_id OR prop_gallery_id LIKE prop_slug
AND d.prop_mandate = 1
) x
where rn = 1

Tree like data collation in SQL (Mysql)

I have two tables in my database
Table A with columns user_id, free_data, used_data
Table B with columns donor_id, receptor_id, share_data
Basically, a user (lets call x) has some data in his account which is represented by his entry in table A. The data is stored in free_data column. He can donate data to any other user (lets call y), which will show up as an entry in Table B. The same amount of data gets deducted from the user x free_data column.
While entry in Table B gets created, an entry in Table A for user y is also created with free_data value equal to share_data. Now user y can give away data to user z & the process continues.
Each user keep using their data & the entry used_data in Table A keeps on adding up to indicate how much data each user has used.
This is like a tree structure where there is a an entry with all the data (root node) who eventually gives data to others who in-turn give data to other nodes.
Now I would like to write an sql query such that, given a node x (id of entry in Table A), I should be able to sum up total data x has given & who all are beneficiaries at multiple level, all of their used_data need to be collated & showed against x.
Basically, I want to collate
Overall data x has donated.
How much of the donated data from x has been used up.
While the implementation is more graph-like, I am more interested to know if we assume it to be a tree below node x & can come up with a single sql query to be able to get the data I need.
Example
Table A
user_id, free_data, used_data
1 50 10
2 30 20
3 20 20
Table B
donor_id, receptor_id, share_data
1 2 30
1 3 20
Total data donated by 1 - 30 + 20 = 50
Total donated data used - 20 + 20 = 40
This is just one level where 1 donated to 2 & 3. 2 in turn could donated to 4 & all that data needed to be collated in a bubbled up fashion for calculating the overall donated data usage.
Yes its possible using a nested set model. There's a book by Joe Celko that describes but if you want to get straight into it there's an article that talks about it. Both the collated data that you need can be retrieved by a single select statement like this:
SELECT * FROM TableB where left > some_value1 and right < some_value2
In the above example to get all the child nodes of "Portable Electronics" the query will be:
SELECT * FROM Electronics WHERE `left` > 10 and `right` < 19
The article describes how the left and right columns should be initialised.
If I understand the problem correctly, the following should give you the desired results:
SELECT B.donor_id AS donor_id, SUM(A.used_data) AS total_used_data FROM A
INNER JOIN B ON A.user_id = B.receptor_id GROUP BY B.donor_id;
Hope this will solve your problem now.
Try below query(note that you will have to pass userid at 2 places):
SELECT SUM(share_data) as total_donated, sum(used_data) as total_used FROM tablea
LEFT JOIN tableB
ON tableA.user_id = tableB.donor_id
WHERE user_id IN (select receptor_id as id
from (select * from tableb
order by donor_id, receptor_id) u_sorted,
(select #pv := '1') initialisation
where find_in_set(donor_id, #pv) > 0
and #pv := concat(#pv, ',', receptor_id)) OR user_id = 1;

MySQL Query which require multiple joins

I have a system that is used to log kids' their behavior. If a child is naughty it is logged as negative and if it has a well behaviour it is logged as positive.
For instance - if a child is rude it gets a 'Rude' negative and this is logged in the system with minus x points.
My structure can be seen in this sqlfiddle - http://sqlfiddle.com/#!9/46904
In the users_rewards_logged table, the reward_id column is a foreign key linked to either the deductions OR achievements table depending on the type of column.
If type is 1 is a deduction reward, if the type value is 2 is a achievement reward.
I basically want a query to list out something like this:
+------------------------------+
| reward | points | count |
+------------------------------+
| Good Work | 100 | 1 |
| Rude | -50 | 2 |
+------------------------------+
So it tallys up the figures and matches the reward depending on type (1 is a deduction, 2 is a achievement)
What is a good way to do this, based on the sqlfiddle?
Here's a query that gets the above desired results:
SELECT COALESCE(ua.name, ud.name) AS reward,
SUM(url.points) AS points, COUNT(url.logged_id) AS count
FROM users_rewards_logged url
LEFT JOIN users_deductions ud
ON ud.deduction_id = url.reward_id
AND url.type = 1
LEFT JOIN users_achievements ua
ON ua.achievement_id = url.reward_id
AND url.type = 2
GROUP BY url.reward_id, url.type
Your SQLFiddle had the order of points and type in the wrong order for the table users_rewards_logged.
Here's the fixed SQLFiddle with the result:
reward points count
Good Work 100 1
Rude -50 2
Although eggyal is correct--this is rather bad design for your data--what you ask can be done, but requires a UNION clause:
SELECT users_achievements.name, users_rewards_logged.points, COUNT(*)
FROM users_rewards_logged
INNER JOIN users_achievements ON users_achievements.achievement_id = users_rewards_logged.reward_id
WHERE users_rewards_logged.type = 2
UNION
SELECT users_deductions.name, users_rewards_logged.points, COUNT(*)
FROM users_rewards_logged
INNER JOIN users_deductions ON users_deductions.deduction_id = users_rewards_logged.reward_id
WHERE users_rewards_logged.type = 1
GROUP BY 1, 2
There's no reason NOT to combine the achievements and deductions tables and just use non-conflicting codes. If you combined the tables, then you would no longer need the UNION clause--your query would be MUCH simpler.
I noticed that you have two tables (users_deductions and users_achievements) that defines the type of reward. As #eggyal stated, you are violating the principle of orthogonal design, which causes the lack of normalization of your schema.
So, I have combined the tables users_deductions and users_achievements in one table called reward_type.
The result is in this fiddle: http://sqlfiddle.com/#!9/813d5/6

SQL-query with sorting

Please help by writing a SQL-script that will collate data.
A key difficulty - need to create an additional column on which sorting will take place.
I tried to describe the situation as detailed as possible.
Let's get started. There is a table of the following form:
We will receive a user ID and return data, only those who do not have he, but there are others.
Next step: sort by artificially created column.
Next, I'll step by step.
So what do I mean by artificial column:
This column will contain the difference between the estimates. So to get it - you need to first perform a number of actions:
According to the information which is like set the user and at other user to calculate the difference in assessment, and get an average score.
The following two pictures show the same data and then the calculation itself, it seems to me - it's pretty simple.
Calculation of this column is as follows:
User with 2nd id:
1: 5 - 1 = 4;
2: 2 - 9 = -7;
3: next data what is in user 1 - absent in user 2, and we ease pass it;
User with 3rd id:
1: 3 - 1 = 2;
2: the next data's is absent in user with 3rt id;
3: 8 – 9 = -1;
4: 6 – 2 = 4;
5: passed;
End in the end:
User_2 will have new mark = -1.5
User_3 will have new mark = 1.66666
And in the end I need to return the table:
But that's not all. Often, the data will be duplicated and I'd like to get average results from the data obtained. Please look at the following example:
And this is the end. I really need your help, experts. I teach sql code myself, but it is very difficult for me.
Had the idea of ​​making the script as follows:
SELECT d.data, (d.mark + myCount(d.user, 1)) newOrder
FROM info d
WHERE -- data from user_1 NOT equal data from other users
ORDER BY newOrder;
But the script will execute a lot of time, because it uses its own function that could do with a query to each user, and not to record. I hope someone will be able to cope with this task.
Following your steps:
First, we need to isolate the data from the selected user (let's assume it's 1):
CREATE TEMP TABLE sel_user AS
SELECT data, mark FROM info d WHERE user = 1;
Now, we calculate the mark for every other user (again, the selected user is 1):
SELECT d.user user, d.mark - s.mark mark
FROM info d JOIN sel_user s USING (data)
WHERE d.user <> 1;
Result:
user mark
---------- ----------
2 4
2 -7
3 2
3 -1
3 4
We can query just the average:
SELECT d.user user, AVG(d.mark - s.mark) mark
FROM info d JOIN sel_user s USING (data)
WHERE d.user <> 1 GROUP BY user;
user mark
---------- ----------
2 -1.5
3 1.66666666
But you still want to do calculations with the marks that do not correspond to user 1:
SELECT d.user user, mark FROM info d
WHERE d.user <> 1 AND d.data NOT IN (SELECT data FROM sel_user);
user mark
---------- ----------
2 4
3 3
3 10
Specifically, you want to add the previously calculated average to each row:
SELECT d.user user, d.data, d.mark + d2.mark AS neworder FROM info d JOIN (
SELECT d.user user, AVG(d.mark - s.mark) mark
FROM info d JOIN sel_user s USING (data)
WHERE d.user <> 1 GROUP BY user
) d2 USING (user)
WHERE d.data NOT IN (SELECT data FROM sel_user)
ORDER BY neworder DESC;
user data neworder
---------- ---------- ----------------
3 6 11.6666666666667
3 3 4.66666666666667
2 5 2.5
And your last request is to get the average for each data:
SELECT data, AVG(neworder) final FROM (
SELECT d.user user, d.data, d.mark + d2.mark AS neworder FROM info d JOIN (
SELECT d.user user, AVG(d.mark - s.mark) mark
FROM info d JOIN sel_user s USING (data)
WHERE d.user <> 1 GROUP BY user
) d2 USING (user)
WHERE d.data NOT IN (SELECT data FROM sel_user)
)
GROUP BY data
ORDER BY final DESC;
data final
---------- ----------------
6 11.6666666666667
3 4.66666666666667
5 2.5

mysql follow and retweet-like functionality

This is a bit challenging but fun question.
Consider having these tables
tweets
tweet_id | retweet_of_id | user_id
follow
user_id | followed_user_id
So we store each "retweet as a separate tweet" pointing to the original tweet's id (retweet_of_id). This is because I want to have comments under each one separately.
If something is not a retweet then retweet_of_id will be 0.
How do I retrieve the following using MySQL efficiently?
My own tweets
All the original tweets (from users that I follow)
And the first retweet (by a user I follow) of a tweet (from a user that I don't follow)
And that the result should a combination of both (in order) just like how twitter does it.
Please consider that there may be 1,000,000 tweets and we only need the most recent ones (e.g.: 10).
Here is an example (I'm user 1 and I follow user 2 & 3)
tweet_id | retweet_of_id | user_id
----------------------------------
1 0 4 <- EXCLUDE (I don't follow user 4)
2 0 2 <- INCLUDE (I follow user 2)
3 0 3 <- INCLUDE (I follow user 3)
4 1 2 <- INCLUDE (I follow user 2 & first RT)
5 1 3 <- EXCLUDE (I already have the first RT)
6 2 3 <- EXCLUDE (I already have the orignal)
7 0 1 <- INCLUDE (My own tweet)
So the final order should be these tweets: 7, 4, 3, 2 (starting with the most recent)
Here's how I solved it
(both of these assume that the tweets are ordered by their tweet_id ASC)
Solution 1 (correct, runs fast)
SELECT tweet_id,
FROM tweets
WHERE user = 1 OR user IN (2,3)
GROUP BY IF(retweet_of_id = 0, tweet_id, retweet_of_id)
ORDER BY tweet_id DESC
Solution 2 (gives correct results, but it's dog slow for 1,000,000 tweets)
SELECT p1.tweet_id FROM tweets p1
LEFT JOIN tweets p2
ON p2.user IN (2,3)
AND p1.tweet_id > p2.tweet_id
AND (p1.retweet_of_id = p2.tweet_id
OR p1.retweet_of_id AND p1.retweet_of_id = p2.retweet_of_id )
WHERE p2.tweet_id IS NULL
AND (p1.user = 1 OR p1.user IN (2,3))
ORDER BY p1.tweet_id DESC
all the original tweets (from users that I follow)
1 users that i follow:
select user_id from follow where followed_user_id= MyOwnID
2 all the original tweets:
select * from tweets where retweed_of_id=0
both combined:
select * from tweets where retweed_of_id=0 and
user_id in (select user_id from follow where followed_user_id= MyOwnID)
that should be it - or did i miss something?