I am trying to join 2 tables and get the latest unique results using whereRaw() and leftJoin() with Laravel eloquent.
I have 2 tables:-
skills table (has timestamps):-
| id| name | icon |
| 1 | skill 1 | skill1.png |
| 2 | skill 2 | skill2.png |
| 3 | skill 3 | skill3.png |
scores table (has timestamps):-
| id| player_id | skill_id | score |
| 1 | 1 | 1 | 1 |
| 2 | 1 | 2 | 1 |
| 3 | 1 | 3 | 1 |
| 4 | 1 | 2 | 2 |
I would like to return all skills but only the latest entry(by id), so for the above snippet I should get:-
| id| player_id | name | skill_id | score |
| 1 | 1 | skill 1 | 1 | 1 |
| 3 | 1 | skill 3 | 1 | 1 |
| 4 | 1 | skill 2 | 2 | 2 |
I can get the latest unique records by using:
return SkillScores::where('player_id', $this->id)
->whereRaw('id in (select max(id) from skills group by (name))')
->get();
I can get the skill names by using:-
return SkillScores::where('player_id', $this->id)
->leftJoin('skills', 'skill_scores.skill_id', '=', 'skills.id')
->get();
but when I combine them I get an SQLSTATE[23000] error
return SkillScores::where('player_id', $this->id)
->whereRaw('id in (select max(id) from skills group by (name))')
->leftJoin('skills', 'skill_scores.skill_id', '=', 'skills.id')
->get();
Can anyone help me figure out what is going wrong?
EDIT:-
It turns out that the SQLSTATE[23000] error is occuring because I have an id column in both tables and I havent told it which one I am referencing, the below fixes the issue and gives me the correct result.
return SkillScores::where('player_id', $this->id)
->whereRaw('skill_scores.id in (select max(skill_scores.id) from skill_scores group by (skill_id))')
->leftJoin('skills', 'skill_scores.skill_id', '=', 'skills.id')
->get();
I think there is a minor problem on your expected result(id and name is not matching) but i made it work as following;
The query;
SELECT scores.*, skills.*
FROM scores
INNER JOIN (SELECT skill_id, max(id) AS maxId
FROM scores
WHERE player_id = 1
GROUP BY skill_id) AS sub ON sub.maxId = scores.id
INNER JOIN skills ON skills.id = scores.skill_id;
The eloquent version (You may replace it with DB::table() if you want)
$subQuery = Score::where('player_id', DB::raw($this->id))
->groupBy('skill_id')
->select('skill_id', DB::raw('MAX(id) as maxId'));
return Score::join(DB::raw('(' . $subQuery->toSql() . ') as subQuery'), 'subQuery.maxId', '=', 'scores.id')
->join('skills', 'skills.id', '=', 'scores.skill_id')
->get(['scores.*', 'skills.*']);
Related
Imagine this is my table:
----------------------------------------------------
| id | user_id | amount_1 | amount_2 | amount_3 |
----------------------------------------------------
| 1 | 1 | 2 | 3 | 4 |
----------------------------------------------------
| 2 | 2 | 2 | 1 | 1 |
----------------------------------------------------
| 3 | 2 | 1 | 2 | 2 |
----------------------------------------------------
| 4 | 3 | 2 | 1 | 4 |
----------------------------------------------------
I need a query that gives me one result set for every entry that belongs to my current user, and then returns everything else as a single combined row with the amounts summed.
So in this case if I am user 1, I should get the following rows back:
---------------------------------------
| id | amount_1 | amount_2 | amount_3 |
---------------------------------------
| 1 | 2 | 3 | 4 | my own amounts
---------------------------------------
| 2 | 5 | 4 | 7 | everyone else's amounts
---------------------------------------
Any tips?
I've considered it might be a better idea to just filter the data in the code (php). Please help i'm starting to hate myself
You could use a UNION in sql
select 1 id, amount_1, amount_2, amount_3
from my_table
where user_id = 1
union
select 2 , sum(amount_1) , sum(amount_2), sum(amount_3 )
from my_table
where user_id <> 1
You can do with one query using union:
SELECT user_id, amount_1, amount_2, amount_3
FROM table
WHERE user_id = YOUR_USER_ID
UNION
SELECT -1, SUM(amount_1) AS amount_1, SUM(amount_2) AS amount_2, SUM(amount_3) AS amount_3
FROM table
WHERE user_id != YOUR_USER_ID
You can use aggregation in one fell swoop:
select (case when user_id = 1 then id end) as my_user_or_not,
sum(amount_1), sum(amount_2), sum(amount_3)
from t
group by my_user_or_not;
The null values in the first column indicate another user. You have labelled the column id, which is a bit problematic if you were -- for instance -- to choose user_id = 2 in your example. NULL seems safer for this purpose.
User table:
| id | name | age |
|----|------------|-----|
| 1 | Apple | 22 |
| 2 | Strawberry | 23 |
| 3 | Orange | 50 |
| 4 | Mango | 30 |
Memberships table:
| id | user_id | expire_at |
|----|---------|----------------------|
| 1 | 1 | 2019-08-17T11:19:30Z |
| 2 | 1 | 2019-08-10T11:20:10Z |
| 3 | 2 | 2019-08-29T11:20:19Z |
| 4 | 3 | 2019-08-02T11:20:30Z |
| 5 | 3 | 2019-08-28T11:20:40Z |
Problom
I want select users with the latest 'expire_at'.
After reference: https://stackoverflow.com/a/2111420/5588637,
I tried the following:
SELECT
u.*,
m1.*
FROM
users u
INNER JOIN memberships m1 ON u.id = m1.user_id
LEFT JOIN memberships m2 ON u.id = m2.user_id
AND (
m1.expire_at < m2.expire_at
OR m1.expire_at = m2.expire_at
AND m1.id < m2.id
)
WHERE
m2.id IS NULL;
Result
The id will appear twice because I used to join.
| id | name | age | id | user_id | expire_at |
|----|------------|-----|----|---------|----------------------|
| 1 | Apple | 22 | 1 | 1 | 2019-08-17T11:19:30Z |
| 2 | Strawberry | 23 | 3 | 2 | 2019-08-29T11:20:19Z |
| 3 | Orange | 50 | 5 | 3 | 2019-08-28T11:20:40Z |
After change m1.* to m1.expire_at. I got the result I want.
| id | name | age | expire_at |
|----|------------|-----|----------------------|
| 1 | Apple | 22 | 2019-08-17T11:19:30Z|
| 2 | Strawberry | 23 | 2019-08-29T11:20:19Z |
| 3 | Orange | 50 | 2019-08-28T11:20:40Z |
online try: http://sqlfiddle.com/#!9/27fa22/4
Implement in Lavavel
Laravel Framework version: 5.6.39
I am trying to convert the above SQL into Laravel using Database: Query Builder.
$users = DB::table('users as u')
->select('u.*', 'm1.*')
->join('memberships as m1','u.id','=','m1.user_id')
->leftJoin('memberships as m2', function($join){
$join->on('u.id', '=', 'm2.user_id')
->where(function ($query) {
$query->where('m1.expire_at','<','m2.expire_at')
->orWhere('m1.expire_at','=','m2.expire_at')
->where('m1.id','<','m2.id');
});
})
->whereNull('m2.id')
->toSQL();
I'm using toSql(). This will convert it to SQL first to make sure it's same of above SQL.
SELECT
`u`.*,
`m1`.*
FROM
`users` AS `u`
INNER JOIN `memberships` AS `m1` ON `u`.`id` = `m1`.`user_id`
LEFT JOIN `memberships` AS `m2` ON `u`.`id` = `m2`.`user_id`
AND (
`m1`.`expire_at` < ?
OR `m1`.`expire_at` = ?
AND `m1`.`id` < ?
)
WHERE
`m2`.`id` IS NULL
? seems to be the characteristic of laravel, I believe it is same of above SQL.
when i change toSQL() to get(), the result following:
Collection { ▼
#items: []
}
The above result is wrong, so i tried remove
whereNull('m2.id') in Laravel code (WHERE m2.id IS NULL in SQL), let’s see what happened.
Laravel result
Collection { ▼
#items: array:5 [▼
0 => { ▼
+"id": 1
+"name": "Apple"
+"age": "Eric Yiu SL"
+"user_id": 1
+"expire_at": "2019-08-10T11:20:10Z"
}
...
]
Ideal result
| id | name | age | id | user_id | expire_at |
|----|------------|-----|----|---------|----------------------|
| 1 | Apple | 22 | 2 | 1 | 2019-08-10T11:20:10Z |
| 3 | Orange | 50 | 4 | 3 | 2019-08-02T11:20:30Z |
| 1 | Apple | 22 | 1 | 1 | 2019-08-17T11:19:30Z |
| 2 | Strawberry | 23 | 3 | 2 | 2019-08-29T11:20:19Z |
| 3 | Orange | 50 | 5 | 3 | 2019-08-28T11:20:40Z |
Comparing results, Laravel result missing second id which is memberships table id, i guess this is the reason of incorrect results.
I have searched the Internet, seems is this problem.
https://github.com/laravel/framework/issues/4962
But I failed after various attempts...
You cannot select two rows with the same name in Laravel. The second one will override the first one. Use an alias instead.
$users = DB::table('users as u')
->select('u.*', 'm1.id as membership_id')
->join('memberships as m1','u.id','=','m1.user_id')
->leftJoin('memberships as m2', function($join){
$join->on('u.id', '=', 'm2.user_id')
->where(function ($query) {
$query->whereColumn('m1.expire_at','<','m2.expire_at')
->orWhere(function ($query) {
$query->whereColumn('m1.expire_at','=','m2.expire_at')
->whereColumn('m1.id','<','m2.id');
});
});
})
->whereNull('m2.id')
->get();
Note: I also encapsulated the orWhere() in the join to avoid confusion about the order of AND/OR.
What also works is using a different order in the select. You can for example use the following:
$query->select([
'm1.*',
'm1.id as membership_id',
'u.*'
])
It will return all columns of both tables plus the new membership_id column. But if there is a column on the users table which is named similarly to a column on the memberships table, only the users table column is returned (e.g. created_at). What comes last in your list is returned.
EDIT:
As #Namoshek mentioned, you should not select everything because you have a duplicate key problem in your SQL query. I modified my answer so that it would match #RaymondNijland answer. And by the way, even for the table user, you should select exactly what you need. And not only for a duplicate key problem but also for the speed of your SQL query. We don't think about it enough but it can quickly make the difference on a big set of results.
Less data to send from the database to your PHP server = faster
You should try this one :
DB::table('users as u')
->select('u.*', 'm1.id as membership_id')
->join('memberships as m1','u.id','=','m1.user_id')
->leftJoin('memberships as m2', function ($join) {
$join->on('u.id', '=', 'm2.user_id')
->on(function($join) {
$join->on('m1.id', '<', 'm2.id')
->on(function($join) {
$join->on('m1.expire_at', '<', 'm2.expire_at')
->orOn('m1.expire_at', '=', 'm2.expire_at');
});
});
})
->whereNull('m2.id')
->toSQL()
As mentioned in Laravel's documentation on this page: https://laravel.com/api/5.8/Illuminate/Database/Query/JoinClause.html#method_on
You can pass a closure to the on() method and there is the orOn() method that you can use in this closure.
I tested it and it gives the same result as your SQL query.
I have two tables - a students table and a products table.
When i make a list of the students in a table, i need to see the total amount (sum) of payments that has been made, unfortunately it seems like the result is the correct sum but multiplied by the amount of rows.
Students table:
+----+----------+
| id | name |
+----+----------+
| 1 | Jonathan |
| 2 | Bob |
+----+----------+
Products table:
+----+------------+-------+----------+
| id | student_id | money | quantity |
+----+------------+-------+----------+
| 1 | 1 | 1000 | 2 |
| 2 | 1 | 2000 | 1 |
| 3 | 2 | 500 | 5 |
| 4 | 2 | 3000 | 1 |
+----+------------+-------+----------+
Payments table:
+----+-------+------------+
| id | money | student_id |
+----+-------+------------+
| 1 | 5000 | 1 |
| 2 | 2000 | 1 |
| 3 | 2500 | 2 |
| 4 | 2500 | 2 |
+----+-------+------------+
In theory, the output of my query should be:
+-------------+----------+----------------+----------------+
| id | name | payments_total | products_total |
+-------------+----------+----------------+----------------+
| 1 | Jonathan | 4000 | 7000 |
| 2 | Bob | 5500 | 10000 |
+-------------+----------+----------------+----------------+
What i have tried:
$teamStudents = DB::table('students')->where('students.team', $team)->orderBy('first_name', 'ASC')
->join('products', 'students.id', '=', 'products.student_id')
->join('payments', 'students.id', '=', 'payments.student_id')
->select('students.first_name AS first_name', 'students.last_name AS last_name', 'students.created_at AS created_at', DB::raw('SUM(products.money * products.amount) AS products_total'), DB::raw('SUM(payments.money) AS payments_total'), 'students.id AS id')
->groupBy('students.id')
->get();
It returns no error except for the fact that the returned "payments_total" is inaccurate and multiplied by the amount of rows for some reason.
So my question is:
How do i get around this and what have i done wrong? I've been googling for an hour with no result.
Is my query an issue or the way i've set it up, if so, what would the correct solution be?
With your edit I was able to solve the problem that you have, but in your edit you use couple of things for which I don't have data, such as the $team, first_name and last_name of the students. But anyway, here is a solution for your problem, you have to use subqueries in order to solve this:
$teamStudents = DB::table('students')->orderBy('name', 'ASC')
->leftJoin(DB::raw('(select SUM(products.money * products.quantity) AS products_total, student_id from products group by student_id) products'), 'students.id', '=', 'products.student_id')
->leftJoin(DB::raw('(select sum(payments.money) as payments_total, student_id from payments group by student_id) payments'), 'students.id', '=',
'payments.student_id')
->select('students.name', 'payments.payments_total', 'products.products_total', 'students.id AS id')
->groupBy('students.id')
->get();
I am not sure if technically I will be correct, but the problem is because you use multiple joins, so that's why the results are doubled, if you don't use subqueries.
There's no need to join in this case, you don't use it anyways.
$teamStudents = DB::table('students')
->select('id, name')
->selectRaw('select sum(money) from payments where student_id = students.id as payments_total')
->selectRaw('select sum(money) from products where student_id = students.id as products_total')
->orderBy('name')
->get();
I have 2 tables, category and service
category_table
+----+--------------+
| id | category |
+----+--------------+
| 1 | category_1 |
| 2 | category_2 |
+----+--------------+
service_table
+----+--------------+-----------+
| id | service | cat_id |
+----+--------------+-----------+
| 1 | service_a | 1 |
| 2 | service_b | 1 |
| 3 | service_c | 1 |
| 4 | service_d | 2 |
+----+--------------+-----------+
I want to get the category count from the service table and display the result using the category name and limit it to 5 most used category
The desired result:
category name | category count
category_1 | 3
category_2 | 1
Seems like i have to join or leftjoin these table, but it is pretty confusing for me
You are right, you must use join's. You can rewrtie it with Eloquent, or use DB::raw() Try this:
SELECT c.category, COUNT(*) AS category_count FROM category_table c LEFT JOIN service_table s ON(c.id = s.cat_id) GROUP BY s.cat_id ORDER BY category_count DESC LIMIT 5;
P.S. I don't check it, but must work. Good tutorial about joins
DB::table('category_table')
->leftJoin('service_table', 'category_table.id', '=', 'service_table.cat_id')
->select(DB::raw('category_table.category,count(service_table.cat_id) as category_count'))
->groupBy('service_table.cat_id')
->order_by('category_count', 'desc')
->take(5)
->get();
I am trying to get some statistics for an online game I maintain. I am searching for an SQL statement to get the result on the bottom.
There are three tables:
A table with teams, each having a unique identifier.
table teams
---------------------
| teamid | teamname |
|--------|----------|
| 1 | team_a |
| 2 | team_x |
---------------------
A table with players, each having a unique identifier and optionally an affiliation to one team by it's unique teamid.
table players
--------------------------------
| playerid | teamid | username |
|----------|--------|----------|
| 1 | 1 | user_a |
| 2 | | user_b |
| 3 | 2 | user_c |
| 4 | 2 | user_d |
| 5 | 1 | user_e |
--------------------------------
Finally a table with events. The event (duration in seconds) is related to one of the players through their playerid.
table events.
-----------------------
| playerid | duration |
|----------|----------|
| 1 | 2 |
| 2 | 5 |
| 3 | 3 |
| 4 | 8 |
| 5 | 12 |
| 3 | 4 |
-----------------------
I am trying to get a result where the durations of all team members is summed up.
result
--------------------------
| teamid | SUM(duration) |
|--------|---------------|
| 1 | 14 | (2+12)
| 2 | 15 | (3+8+4)
--------------------------
I tried several combinations of UNION, WHERE IN, JOIN and GROUP but could not get it right. I am using PostgreSQL and PHP. Can anyone help me?
Just use sum with group by:
select t.teamid, sum(e.duration)
from team t
join players p on t.teamid = p.teamid
join events e on p.playerid = e.playerid
group by t.teamid
If you need all teams to be returned even if they don't have events, then use an outer join instead.
Try this
SELECT teamid, Sum(duration),
AS LineItemAmount, AccountDescription
FROM teams
JOIN teams ON teams.teamid = players.teamid
JOIN events ON players.playersid = events.playersid
JOIN GLAccounts ON InvoiceLineItems.AccountNo = GLAccounts.AccountNo
GROUP BY teamid
http://www.w3computing.com/sqlserver/inner-joins-join-two-tables/