Laravel 5.7 - multiply two columns and add to final sum - php

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();

Related

Combine whereRaw() with leftjoin() using eloquent

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.*']);

Laravel Implement greatest-n-per-group

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.

codeigniter query: how to take all the record from a column in a table when i use sum Select function to get data from another table

the case is i have this table called journal_type
id_type | account_no | description |
---------------------------------------------
1 |
1.1 |
bank
2 |
1.2 |
bank & cash
3 |
1.3 |
sale
4 |
1.4 |
stock
5 |
1.5 |
office tools
6 |
1.6 | payroll
and the second table called journal is something like this
id_transaction | id_type | debit | date_of_transaction
---------------------------------------------
1 |
1 |
50.00 | 08-22-2018
2 |
3 |
90.00 | 08-21-2018
3 |
1 |
80.00 | 08-17-2018
what i do so far is sum the data from second table's debit column based on month(in this case is august) and i want to group it by the id_type. so it would be something like id_type1 = 130.00 and id_type3 = 90.00
but i want the output is printing all the record from the id_type column in first table and let the other column null when it don't used. maybe the result i want is something like this
id_type | account_no | description | debit
---------------------------------------------
1 |
1.1 |
bank | 130
2 |
1.2 |
bank & cash| 0
3 |
1.3 |
sale | 90
4 |
1.4 |
stock | 0
5 |
1.5 |
office tools |0
6 |
1.6 | payroll | 0
public function get_finance_debitkredit($periode1, $periode2){
$query = $this->db->select('sum(debit) as debit, journal_type.account_no, journal_type.description, journal.id_type')
->from('journal_type')
->join('journal', 'journal.id_type = journal_type.id_type', 'left outer')
->group_by('journal.id_type')
->where('date_of_transaction >=', $periode1)
->where('date_of_transaction <=', $periode2)
->order_by('journal_type.account_no')
->get();
return $query;
}
but so far what I get is that it's only printed the record data that have the transaction (in this case is the id_type 1 and id_type_3) so only 2 row that appear. can i make all the 6 row of journal_type appear like what i want with some change of the query.
that code is from my model in codeigniter
You first need to create seperate table for that records and then need to left join with that table. Use following query it will give the result you want
public function get_finance_debitkredit($periode1, $periode2){
$query = $this->db->select('finl_tbl.debit, journal_type.account_no, journal_type.description, journal_type.id_type')
->from('journal_type')
->join('(SELECT sum(debit) as debit,id_type FROM journal WHERE date_of_transaction >='.$periode1.' AND date_of_transaction <='.$periode2.' group by id_type) as finl_tbl', 'finl_tbl.id_type = journal_type.id_type', 'left')
->order_by('journal_type.account_no')
->get();
return $query;
}

Laravel 5 Join table and select Popular Category

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();

rank query in laravel from one table data

At the moment it gets the races times in order but i want to combined the user_id column together, but to make sure when it groups it together it, the fastest recorded_time is kept.
Example of what i have so far from the query
User_id | race_id | recorded_time | stroke |
__________________________________________
2 | 2 | 10.03 | fly |
____________________________________________
1 | 3 | 12.98 | fly |
____________________________________________
2 3 13.58 | fly |
Here is the code that gives me that result
$query->where('race_history.stroke', '=', $stroke)
->orderBy('recorded_time', 'ASC')->get();
This is what i want it to return
User_id | race_id | recorded_time | stroke |
__________________________________________
2 | 2 | 10.03 | fly |
____________________________________________
1 | 3 | 12.98 | fly |
____________________________________________
I have tried the code bellow but it doesn't take the fastest record time
$query->where('stroke', '=', $stroke)
->orderBy('recorded_time', 'ASC')
->groupBy('user_id')->get();
But Instead produces this
User_id | race_id | recorded_time | stroke |
__________________________________________
1 | 3 | 12.98 | fly |
____________________________________________
2 | 3 | 13.58 | fly |
____________________________________________
So its grouping it by user_id BUT not taking it fastest recorded time
I have tried so many ways anyone can help this is a raw query to get what i want i would use this but the AND distance doesnt like this..
$total = DB::select(DB::raw('
SELECT *, min(recorded_time) as time
FROM race_history
WHERE stroke = "' . $stroke . '" AND distance="' . $distance . '"
GROUP BY user_id
ORDER BY min(recorded_time) ASC
'));
Here's raw query as I don't know laravel syntax
SELECT `user_id`, `race_id`, min(`recorded_time`) AS `recorded_time`, `stroke`
FROM `race_history`
WHERE `stroke` = 'fly'
GROUP BY `user_id`
ORDER BY `recorded_time` ASC

Categories