laravel recursion display referred users by level/depth - php

So I'm working with affiliate and doing fine with registration and saving who referred to a user,
now I'm struggling in showing those users with referrals by Level.
Level is not save in the database, I'm thinking of it as incrementing in the logic area?
users table structure
id | name | sponsor_id |
1 | John | NULL |
2 | Jane | 1 |
3 | Jess | 1 |
4 | Joe | 2 |
so the output I want should be like this:
I am John, level 1 are who's sponsor_id is mine, and level 2 are who's sponsor_id is id's of whom I invited.
ID | Name | Level |
2 | Jane | 1 |
3 | Jess | 1 |
4 | Joe | 2 |
where Jane & Jess's sponsor_id is mine, and Joe's sponsor_id is Jess
User has sponsor
public function sponsor()
{
return $this->belongsTo('App\User', 'sponsor_id');
}
User has referrals
public function referrals()
{
return $this->hasMany('App\User', 'sponsor_id');
}

If MySQL 8 (very advised with hierarchical data like yours), you can do this with a recursive CTE :
WITH RECURSIVE CTE_users_levels(id, name, level) AS
(
SELECT id, name, 0 as level
FROM users
WHERE sponsor_id IS NULL
UNION ALL
SELECT u.id, u.name, cte.level+1
FROM CTE_users_levels cte
JOIN users u ON cte.id=u.sponsor_id
)
-- To output all users' levels:
SELECT * FROM CTE_users_levels ORDER BY level;
Now you have virtual table containing the level column for all your users and you can query it
| id | name | level |
| --- | ---- | ----- |
| 1 | John | 0 |
| 2 | Jane | 1 |
| 3 | Jess | 1 |
| 4 | Joe | 2 |
DBFiddle
GOing further...
Make a view out of your CTE
CREATE VIEW VIEW_User_Levels AS
(
WITH RECURSIVE CTE_users_levels(id, name, level) AS
(
SELECT id, name, 0 as level
FROM users
WHERE sponsor_id IS NULL
UNION ALL
SELECT u.id, u.name, cte.level+1
FROM CTE_users_levels cte
JOIN users u ON cte.id=u.sponsor_id
)
SELECT * FROM CTE_users_levels ORDER BY level
)
Now it's easy to get all your users levels (no need to have the CTE statement) :
SELECT * FROM VIEW_User_Levels
Then, if you have a lot of users, it's a bit ovekilling to recompute the whole tree all the time (which the VIEW does)
Then you can fix the level of your users in a new column.
ALTER TABLE users ADD level INT;
And populate that column for all users with the help of your view:
UPDATE users u, VIEW_User_Levels v
SET u.level=v.level
WHERE u.id=v.id;
Which gives
SELECT * FROM users
id name sponsor_id level
1 John null 0
2 Jane 1 1
3 Jess 1 1
4 Joe 2 2
DBFiddle
If you have a lot of users, and your aim is to query the level column a lot, INDEX IT.
Note that you can also update the level value of only one user (for instance if its sponsor_id changes, or if the user is new)
UPDATE users u, VIEW_User_Levels v
SET u.level=v.level
WHERE u.id=v.id AND u.name='Jane';

Related

MySQL Select to create a rank based on number of entries from a table...how to do it?

What I Have:
Table 1 : USERS (autoID, name, etc)
Table 2 : TROPHIES (autoID, name, etc)
Table 3 : VIEWS (userID, timestamp, etc)
Table 4 : CANDIDATES (userID, trophyID, etc)
What I Know:
USERS.autoID & TROPHIES.autoID
How I Do It:
I have this TROPHIES table where I store different categories users can be nomitated to.
Each User can be nominated for 1,2 or more trophies from TROPHIES table.
In the VIEWS table I store each view of the profiles for each individual user with USERS.autoID, timestamp and other data.
In the CANDIDATES table I store the TROPHIES.autoID and USERS.autoID - this way I know which User is nominated for which Trophy.
What I Need to Know
Knowing USERS.autoID & TROPHIES.autoID I want to make a TOP based on the number of entries in the last 3 days for example in VIEWS table of all USERS that are listed in CANDIDATES table for that specific trophy and find out the POSITION on that top of a specific user.
So let's say the user with the autoID 1 is nominated to the TROPHY with the autoID 10 and has 100 entries in the VIEWS table on the last 3 days but there are other 3 users nominated to the TROPHY with the autoID 10 who have more than 100 entries in the last 3 days so...I need a select that would return the number 4.
My Questions:
Can I do that with 1 single SELECT query? If yes...how? If no...how could I make this query to spend as little resources as possible.
Thanks!
[EDIT]
Here is some data
TABLE 1 - USERS
+--------+-------+
| autoID | name |
+--------+-------+
| 1 | user1 |
| 2 | user2 |
| 3 | user3 |
+--------+-------+
TABLE 2 - TROPHIES
+--------+------------+
| autoID | name |
+--------+------------+
| 1 | Baseball |
| 2 | Basketball |
| 3 | Boxing |
+--------+------------+
TABLE 3 - VIEWS
+--------+--------+------------+
| autoID | userID | timestamp |
+--------+--------+------------+
| 1 | 2 | 1551632970 |
| 2 | 2 | 1551632971 |
| 3 | 3 | 1551632972 |
| 4 | 1 | 1551632973 |
| 5 | 2 | 1551632974 |
| 6 | 1 | 1551632975 |
| 7 | 3 | 1551632976 |
| 8 | 1 | 1551632977 |
| 9 | 2 | 1551632978 |
| 10 | 3 | 1551632979 |
| 11 | 3 | 1551632980 |
| 12 | 3 | 1551632981 |
+--------+--------+------------+
TABLE 4 - CANDIDATES
+--------+--------+----------+
| autoID | userID | trophyID |
+--------+--------+----------+
| 1 | 2 | 1 |
| 2 | 3 | 3 |
| 3 | 1 | 2 |
| 4 | 1 | 1 |
+--------+--------+----------+
In the end I want to be able to know on which position a User is for a specific trophy based on the entries from the VIEWS table.
Let's say I want to check the position of the USER with the autoID = 1 for Baseball (trophy which has autoID = 1) after timestamp 1551632972.
So...First we have to see which users are listed in this trophy so we can ignore the entries from the table VIEWS for the other users. Trophy with the autoID 1 (Baseball) has only two users listed - user1 and user2.
Now I want to see how many entries both have so I can be able to find out which is the position of the user1 on this top.
So if we select and count all the entries from the table VIEWS for user1 where timestamp is equal or bigger than 1551632972 we will get number 3 and if we do the same thing for the user2 we will get 2 and since 3 is bigger than 2, user1 will be on 1st place and user2 will be on the 2nd place.
I am searching for a way to get the place in the TOP for a specific user inside a specific sport using a single MySQL query (if possible) or finding the best solution to do so...
I found the solution...I will just leave it here in case someone else will need it.
SELECT
U.autoId,
U.name,
U1.position
FROM USERS U
JOIN (SELECT
#rownum := #rownum + 1 AS position,
U.autoId,
U.name,
COUNT(V.autoID) as "Nr"
FROM USERS U
JOIN VIEWS V ON V.userID= U.autoID
JOIN CANDIDATESC ON C.userID= U.autoID
JOIN (SELECT #rownum := 0) R
WHERE C.trophyID= 'id_of_trophy_wanted' GROUP BY U.autoID ORDER BY Nr DESC) as U1 ON U1.autoID =
U.autoID
WHERE U.autoID = 'id_of_user_wanted'
Thanks to the ones who tried to help!

Issue with order by and group by with two foreign keys in same table

I need to develop chat application in php for this i have created two tables likes users and message. every user details will be stored in users table and every message will be stored in messages table.I have done storing part it is working fine. Now i need to display messages.So as per my requirement.
when any user logs into his portal he/she will be able to see latest messaged users list.And if he want to message any of other users ,he just clicks on there profile pic than a message panel will be opened .Untill here i completed everything.
But my issue is i need to display
latest messaged users list,
in this i need to show user first name, profile picture,last message, last message date.
And one more condition is i need to display the list like latest messaged user first.
I have tried in many ways but i got users list with first message i don't want like that i need last message for that user
My tables are
Users table
uid | firstname | email | mobile
---------------------------------------------
1 | kumar | kumar#gmail.com | 9876543210
----------------------------------------------
2 | jack | jack#gmail.com | 8876543216
----------------------------------------------
3 | rams | rams#gmail.com | 7876543215
----------------------------------------------
4 | devid | devid#gmail.com | 9876543220
----------------------------------------------
5 | joe | joe#gmail.com | 8876543212
----------------------------------------------
messages table
mid| from_id | to_id | message | created_at
----------------------------------------------------------------
1 | 1 | 2 | hello jack | 2017-02-03 09:00:52
----------------------------------------------------------------
2 | 2 | 1 | hi kumar | 2017-02-03 09:10:30
----------------------------------------------------------------
3 | 2 | 3 | ram where are you | 2017-02-03 09:15:02
----------------------------------------------------------------
4 | 3 | 2 | at home | 2017-02-03 09:35:20
----------------------------------------------------------------
5 | 1 | 2 | hello how are you | 2017-02-03 09:42:55
----------------------------------------------------------------
6 | 4 | 2 | good morning | 2017-02-03 09:50:45
----------------------------------------------------------------
8 | 1 | 3 | hi | 2017-02-03 09:54:22
----------------------------------------------------------------
7 | 3 | 1 | hello kumar | 2017-02-03 09:58:38
----------------------------------------------------------------
For example i have logged in as kumar(uid=1)
Expected output:
firstname | message | mid | uid
-----------------------------------------
rams | hello kumar | 7 | 3
-----------------------------------------
jack | hello how are you | 5 | 2
-----------------------------------------
I have tried like this :
SELECT DISTINCT
`u`.`firstname`,
`u`.`profile_photo`,
`u`.`uid`,
`u2`.`firstname`,
`u2`.`profile_photo`,
`u2`.`uid`,
`message`,
`messages`.`created_at`,
`messages`.`from_id`,
`messages`.`to_id`,
`messages`.`mid`
FROM
`messages`
INNER JOIN
`users` AS `u` ON `u`.`uid` = `messages`.`from_id`
INNER JOIN
`users` AS `u2` ON `u2`.`uid` = `messages`.`to_id`
WHERE
(from_id = 1 OR to_id = 1)
GROUP BY
`u`.`uid`,
`u2`.`uid`
ORDER BY
`messages`.`mid` DESC
But got output like this
firstname | message | mid | uid
-----------------------------------------
jack | hello jack | 1 | 2
-----------------------------------------
rams | hi | 5 | 2
-----------------------------------------
Thanks in advance
try this way
SELECT DISTINCT `u`.`firstname`,`u`.`profile_photo`, `u`.`uid`, `u2`.`firstname`,`u2`.`profile_photo`,`u2`.`uid`, `message`,`messages`.`created_at`, `messages`.`from_id`,`messages`.`to_id`,`messages`.`mid`
FROM `messages`
INNER JOIN `users` AS `u` ON `u`.`uid` = `messages`.`from_id`
INNER JOIN `users` AS `u2` ON `u2`.`uid` = `messages`.`to_id`
WHERE (from_id = 1 OR to_id = 1)
GROUP BY `u`.`uid`, `u2`.`uid`
ORDER BY `messages`.`created_at` DESC
It appears that you want to put messages in the same group if they're between the same two users, regardless of the direction. To do this, change your GROUP BY to:
GROUP BY GREATEST(u.uid, u2.uid), LEAST(u.uid, u2.uid)
Use this along with the solutions in SQL Select only rows with Max Value on a Column to get the first or last message in each conversation found using this grouping.
You should also give aliases to the columns from u and u2 in the SELECT clause, so you can distinguish the sender and receiver information in the result.
SELECT u.firstname AS sender_name, u.profile_photo AS sender_photo, ...
Or since one of the users is always kumar, you could just select only the information about the other user:
SELECT IF(from_id = 1, u2.firstname, u1.firstname) AS firstname,
IF(from_id = 1, u2.profile_photo, u1.profile_photo) AS profile_photo,
...

Get room for each user in chat room application considering users access level

I have two tables: room and with_access_room
id | name | access_level | image_id --- room
user_id | room_id | token --- with_access_room
There are 3 type of rooms:
Public: everyone can see it (doesn't matter registered or not)
Private: only registered users see it
Private with access: only registered user can create and nobody can see it except creator which gets special token to invite other users.
Now in main page I have to show my rooms. Consider I have a user with id = 1. So he can see all public, private (without access token needed) and private rooms (with access) created by him.
In access_level column 0 is public and 1 is private.
For example:
room
id | name | access_level | image_id
1 | aaa | 1 | 1
2 | bbb | 0 | 2
3 | ccc | 1 | 3
4 | ddd | 1 | 4
with_access_room
user_id | room_id | token
1 | 3 | xyz
2 | 4 | zyx
So my user with id = 1 must see these "table"
ID | name | access_level | image_id | token
1 | aaa | 1 | 1 | NULL
2 | bbb | 0 | 2 | NULL
3 | ccc | 1 | 3 | xyz
I am working with Yii 2
Which SQL query or Yii 2 model method with params can give me desired result ?
I don't think you have described enough information to represent the problem. Missing information:
How is "Private with access" represented in the data?
Where is the "creator" represented?
How do we know who is registered?
I can guess reasonable answers to these questions. In the end, the query will look something like this:
select r.*
from room r
where access_level = 0 or
(access_level = 1 and
1 in (select user_id from users u where is_registered = 1)
) or
(access_level = 2 and
1 in (select user_id from with_access_room war where war.room_id = r.id)
);
This assumes that the creator has a row in with_access_room.
This query should work:
select `id`, `name`, `access_level`, `image_id`, `token` from `room`
left join `with_access_room` on `id`=`room_id`
where `access_level`=0
or `user_id`=1
or `id` not in (select `room_id` from `with_access_room`)
order by `id`

MySQL and PHP One to Many Return Data in Single Row, Multiple Columns

I have been learning PHP and MySQL for a project and I am struggling with this part. For simplicity sake, I will only list the relevant fields (actually many more in real db) let's say I have 3 tables.
Table1
---------------------
Index | Name | email
1 | Rob | rob#email.com
2 | Kevin| kevin#email.com
3 | Amy | amy#email.com
Table2
------------------------
id | Info | Submitted
1 | Blah | 0
2 | Yada | 1
Table 3
-------------------------
id | Goal |Submitted
1 | 1 | 1
1 | 2 | 1
1 | 3 | 1
1 | 4 | 0
1 | 5 | 0
3 | 1 | 0
3 | 3 | 1
3 | 4 | 1
So, Table1 holds user information and is kinda the main table
Table2 the user inputs some data in a field and then submits for approval when ready. (I will be using the value of Submitted for functions later)
If the user has not submitted the info, there is not record. This is a 1 to 1 with Table1
Table3 The user inputs information for 5 goals. At any given time there could be 0 to max 5 goals entered for a user. The Submitted is the same for later processing. This table is Many to one with Table1. The Goal field literally shows the number 1 through 5, there is a separate field that holds the goal text, just not needed in this example.
Desired output is HTML table
Name | email | info |Goal1|Goal2|Goal3|Goal4|Goal5|
Rob | rob#email.com | Blah | 1 | 2 | 3 | 4 | 5 |
Kevin | kevin#email.com | Yada | | | | | |
Amy | amy#email.com | | 1 | | 3 | 4 | |
Not sure if the blanks are considered NULL or something else as they do not exist in the DB. I would like to put something in the field like an *. Basically the Submitted will be used for code to make the fields hyper links, so they need to be part of the query, just not in the table display, but if it would help, it can be displayed.
Name | email | info |Goal1|Goal2|Goal3|Goal4|Goal5|
Rob | rob#email.com | Blah | 1 | 2 | 3 | 4 | 5 |
Kevin | kevin#email.com | Yada | * | * | * | * | * |
Amy | amy#email.com | * | 1 | * | 3 | 4 | * |
I am using a query with left joins and group_concat, but that is not working well with the non existent data, and I cannot figure out how to include the Submitted field without doing some crazy concatenation, then pulling in all apart to put in the HTML fields.
I can include some code, but it might be hard to follow as there are lots of variables being used.
The best I have gotten out using only table1 and table3:
Rob 1,2,3,4,5
Kevin
Amy 1,3,4
With the records that have not been entered not be accounted for, it makes it near impossible to turn the data string into a table. If I can get something showing for every position, even if it does not exist yet, I do know how to make it into the html table.
I hope this makes sense and someone can help me with this.
As you stated that you have at most 5 goals. The best possible option is to use following query.
SELECT t1.Name,t1.email
,CASE WHEN t2.Info IS NULL THEN * ELSE t2.Info END as Info
,CASE WHEN g1.Goal IS NULL THEN * ELSE g1.Goal END as Goal1
,CASE WHEN g2.Goal IS NULL THEN * ELSE g2.Goal END as Goal2
,CASE WHEN g3.Goal IS NULL THEN * ELSE g3.Goal END as Goal3
,CASE WHEN g4.Goal IS NULL THEN * ELSE g4.Goal END as Goal4
,CASE WHEN g5.Goal IS NULL THEN * ELSE g5.Goal END as Goal5
FROM Table1 as t1
LEFT JOIN Table2 as t2 ON t2.id = t1.Index
LEFT JOIN Table3 as g1 ON g1.id = t1.Index AND g1.Goal=1
LEFT JOIN Table3 as g2 ON g2.id = t1.Index AND g2.Goal=2
LEFT JOIN Table3 as g3 ON g3.id = t1.Index AND g3.Goal=3
LEFT JOIN Table3 as g4 ON g4.id = t1.Index AND g4.Goal=4
LEFT JOIN Table3 as g5 ON g5.id = t1.Index AND g5.Goal=5
You will want to JOIN each of the tables together, and GROUP BY the user.
One way of handling the goals would be to display it as one column, containing all goals.
This should help you get started:
SELECT name,
email,
info,
<LOGIC> as goals
FROM table1
JOIN table2 ON table2.user_id = table1.id
JOIN table3 ON table3.user_id = table1.id
GROUP BY name
The logic that you use for the goals column could be created with a mixture of CASE and CONCAT (if a goal is defined, concatenate it into a string, and display that string as the final value of goals).

Distinct on only one column - only display FIRST duplicate row

I'm using an SQL query in PHP to display 5 columns from my "users" table. Here is my code for that:
SELECT DISTINCT account_id, full_name, email, login, phone, updated_at, last_request_at, unconfirmed_email FROM $table WHERE id < '300'
Basically, this table has 2 types of user - admin accounts and what we'll call "subusers" - they're still users but they don't have admin privileges, and they were created by their parent admin accounts. Because of this, the parent admin account and its subusers all share the same "account_id" Here's an example (sorry it's rubbish):
| account_id | full_name | Superhero? |
| 1 | Batman | 1 |
| 1 | Robin | 1 |
| 1 | Magneto | 0 |
| 2 | Spiderman | 1 |
| 2 | The Hulk | 1 |
| 2 | Wolverine | 1 |
| 3 | Professor X | 1 |
| 4 | Cyclops | 1 |
| 4 | Shrek | 0 |
| 4 | Superman | 1 |
| 4 | Bob | 0 |
So you can pretend that Spiderman made The Hulk's and Wolverine's accounts. This tells me that Spiderman has an admin account because he's the first instance of the account_id "2".
So as you can see, while full_name is unique, there are many duplicate account_id's, I would like to refine it so that that it only displays the first instance of each ID - there are only 4 different account_id's so it should only show 4 entries, like so:
| account_id | full_name | Superhero? |
| 1 | Batman | 1 |
| 2 | Spiderman | 1 |
| 3 | Professor X | 1 |
| 4 | Cyclops | 1 |
How can I achieve this?
You should probably add another column. Now it is possible to get distinct record for every account_id using GROUP BY clause but results of all nonagreggated columns can be ambigius. You have to have some order you approve or indicator inside group to determine which record for every accout_id is "first". With column marking which record in each group is first query is simple. Without it you have to accept some order telling query which record is "first". On example alphabetical order of full_name:
SELECT account_id,
full_name,
email,
login,
phone,
updated_at,
last_request_at,
unconfirmed_email
FROM table1 WHERE full_name IN (
SELECT MIN(full_name)
FROM table1
GROUP BY account_id
WHERE id < '300'
)
Here is the solution to what you asked for,
SELECT account_id,full_name,Superhero
FROM(
SELECT p.*,(
CASE account_id
WHEN #curType
THEN #curRow := #curRow + 1
ELSE #curRow := 1 AND #curType := account_id END
) + 1 AS rank
FROM test p, (SELECT #curRow := 0, #curType := '', #curRank := 0) r) A
WHERE rank=2;
Sql_Demo
Output:
ACCOUNT_ID FULL_NAME SUPERHERO
1 Batman 1
2 Spiderman 1
3 Professor X 1
4 Cyclops 1
try this
select * from table1
group by account_id
demo here
this will give you the first of every account_id

Categories