Laravel Query Builder Join clause is ignoring orderBy() - mysql

I'm using Laravel query builder and I would like to ask how specify which row to use for the left join if many rows are returned?
I have code like that:
$builder->leftJoin('table2', function (JoinClause $join) {
$join->on('table2.id', '=', "table1.id")
->on('table2.region', '=', "table1.region")
->orderBy('table2.updatedAt', 'DESC') // This is being ignored.
->limit(1);
});
The part $join->on()->on() returns multiple rows so I need to specify which one to use for the left join. I though I can order it by table2.updatedAt and then it will use the first row for the left join. But it is completly ignoring the orderBy. So I dont know how to get the one specific row.
Edit:
Also its possible that some of the rows from table2 have exactly same table2.updatedAt. In that case it does not matter which of the ones with same table2.updatedAt will be selected.

Checking the query in laravel, I've found that applying ORDER BY doesn't apply any clause to the query.
Reading your requirements, you want to achieve the record with the largest updatedAt field for the criteria specified. I would personally opt for a MAX() query in a view if permitted, but views can be restricted or not used by organisation, and its good to know how to do it in line with your question, plus it means you dont have to maintain a view. Optionally, you can do a MAX() in a subquery, but that requires a bit more setup.
First, you inner join on the fields you require. Second, you do a left join to look for values within that join that are bigger than the one you want the MAX for. So we want the most recent updatedAt field. Finally, you WHERE the largest updatedAt value has no value bigger than it, so its the true max: where that value is NULL.
Code is remixed without the leftJoin clause object, Im passing your builder everywhere. I cant see the rest of your query, but I think this should drop straight in since were only up to 2 tables ie table2. If you supply 1 table.id in the WHERE clause, you will get 1 row back.
Update: You need to do the same with Id, so if you have 2 datetimes with the same timestamp, you pick the one with the higher ID:
$sql->join('table2', function ($sql) {
$sql->on('table2.id', '=', "table1.id")
->on('table2.region', '=', "table1.region");
});
$builder->leftJoin('table2 AS biggerThanUpdatedAt', function ($builder) {
$builder->on('table2.id', '=', "biggerThanUpdatedAt.id")
->on('table2.region', '=', "biggerThanUpdatedAt.region")
->on('table2.updatedAt', '<', 'biggerThanUpdatedAt.updatedAt');
});
$builder->leftJoin('table2 AS biggerThanId', function ($builder) {
$builder->on('table2.id', '<', "biggerThanId.id") //<-notice the greater at is here this time.
->on('table2.region', '=', "biggerThanId.region")
->on('table2.updatedAt', '=', 'biggerThanId.updatedAt');
});
$builder->whereNull('biggerThanUpdatedAt.updatedAt');
$builder->whereNull('biggerThanId.id');

Related

Is there a way to sort the whole database table according to a calculated data

I have a very large table called user and it looks like this
id
events (array)
... (extra columns)
1
[]
...
2
[]
...
...
...
...
When I query the table, I will pass two extra parameters, no_per_page and page so that only the data I want will be retrieved.
And before returning the data, an extra column called 'total_event_hours' will be calculated using the 'events' column from the query. However, because it is also a column and will be presented in a table of a front-end app, I want it also to be sortable.
The naive way I could think of will be to query the whole table and sort it after the calculation, but since it is very large, it will slow down the process.
Is there a way to sort the table using the calculated column and keep the pagination at the same time? Any input will be appreciated.
Edit:
$no_per_page = $param['no_per_page']
$start = $param['page'] > 1 ? ($param['page'] - 1) * $no_per_page : 0;
$query = "SELECT * FROM user LIMIT :start, :no_per_page";
$get_query = $this->db->prepare($query);
$get_query->bindParam(':start', $start);
$get_query->bindParam(':no_per_page', $no_per_page);
$get_query->execute();
$data = $get_query->fetchAll(PDO::FETCH_ASSOC);
foreach ($data as $_data) {
$_data['total_event_hours'] = $this->_getTotalEventHours($_data['events'])
// I want to sort by this column but it is not presented in the table
}
return data;
To answer your question
Is there a way to sort the table using the calculated column and keep the pagination at the same time?
The DBMS cannot sort data by something it does not know. To sort by a calculated value, there are several possibilities, e. g.
sometimes the result of the calculation is in the same order as a column. Then you can sort by this column, of course.
you can rewrite your query to include a calculated column. That is, you must perform the calculation with SQL/a user defined function. You can then sort by the calculated column. Note that it's sometimes possible to get the same order with a simpler calculation.
approach #2 most likely prevents the DBMS from using an index for sorting. To speed things up, you could modify your table to contain the calculated column, and then update its value using triggers. If you have this column, you can also index it.
As always, there's a trade-off, so it depends on the specific circumstances/use case which option to choose.

Query Builder, where compare date shifted by N days

In my laravel app I have a table with 2 relevant fields:
updated_at timestamp of last review
n number of days until next review.
I am trying to write a query that returns records which are due for review. In PHP, I would do something like this:
updated_at->addDays(n) > Carbon::now()
Looking at the query builder docs, I am thinking something like the subquery where clause would be helpful, but am unsure how to approach it. Particularly as I need to use a field from the table in both sides of the where.
->where('updated_at', '>', function ($query) {
//
})

Select last row from a MySQL query

I have a query that returns some dates which are not in any order. I need to select the last row from the sub query. The problem is all the solutions I can find online uses something like
ORDER BY qry_doc_dates.arrival_date DESC LIMIT 1
Select qry_doc_dates.arrival_date
FROM (qry_doc_date) AS qry_doc_dates
ORDER BY qry_doc_dates.arrival_date DESC
LIMIT 1
which will not serve my purpose because it first orders the dates as DESC(or ASC).
Suppose the qry_doc_date returns :
"2019-05-27",
"2019-05-13",
"2019-05-20",
"2019-05-22",
"2019-07-12",
"2019-05-22",
"2019-07-16",
"2019-05-22"
As we can see that the returned values are not in order. If I use
ORDER BY qry_doc_dates.arrival_date DESC LIMIT 1
then it returns "2019-07-16" But I need "2019-05-22" which is the last row.
EDIT 1:
I am trying to convert this VBA query to MYSQL.
DLast("arrival_date", "qry_doc_date", "[package_id] = " & Me!lstPackage)
I suppose I misunderstood what the VBA query wants to return. Another issue is I do not have means to run this VBA query and check the result myself.
Your question doesn't make too much sense according to the SQL standard. In the absense of an ORDER BY clause the database engine is free to return the rows in any order. This order may even change over time.
So essentially you are requesting the "last random row" the query returns. If this is the case, why don't you get the "first random row"? It doesn't make any difference, does it?
The only way of getting the last random row is to get them all and discard all of them except for the last one.
Now, if you just need one random row, I would suggest you just get the first random row, and problem solved.
In response to the additional information from your edit:
EDIT 1: I am trying to convert this VBA query to MYSQL.
DLast("arrival_date", "qry_doc_date", "[package_id] = " & Me!lstPackage)
I suppose I misunderstood what the VBA query wants to return. Another
issue is I do not have means to run this VBA query and check the
result myself.
Unless your dataset qry_doc_date is ordered by means of an order by clause, the DFirst or DLast domain aggregate functions will return essentially a random record.
This is stated in the MS Access Documentation for these two functions:
You can use the DFirst and DLast functions to return a random record from a particular field in a table or query when you simply need any value from that field.
[ ... ]
If you want to return the first or last record in a set of records (a domain), you should create a query sorted as either ascending or descending and set the TopValues property to 1. For more information, see the TopValues property topic. From a Visual Basic for Applications (VBA) module, you can also create an ADO Recordset object and use the MoveFirst or MoveLast method to return the first or last record in a set of records.
What you need is to in qry_doc_date to include a sequential row number.
Then you can use something like this:
ORDER BY qry_doc_dates.row_number DESC LIMIT 1

How to Sum the aggregates of a child group

This should be easy, but I am stuck.
I have a table listing some figures about Qualifications - to achieve which a dataset that is essentially a row per Student is being grouped on Qualification with a Parent Grouping on "Measure" (which is just a bucket of qualifications).
One of the columns is trying to work out the number of students (well, more properly the number of students with a value in a particular field, weighted by another field) in each Measure/Qualification. In the screenshot below, it's the "Pred. Avg" column on the right hand side.
So for the Qualification Row Grouping, that column is calculated by:
=CountDistinct(Iif(IsNothing(Fields!AVG_PTS.Value) = False, Fields!Learner_ID.Value, Nothing), "Qual") * Lookup(Fields!Qual_Code.Value, Fields!Qual_Code.Value, Fields!size.Value, "DS_KS5Nationals_LKP")
This works fine - the values of 35 and 11.5 in that rightmost column are correct for those rows. What the top row should be doing is simply adding up the values in the other rows to give me the number of students in this Measure, in this case to give 46.5. To do that the expression I am using is:
=Sum(CountDistinct(Iif(IsNothing(Fields!AVG_PTS.Value) = False, Fields!Learner_ID.Value, Nothing), "Qual") * Lookup(Fields!Qual_Code.Value, Fields!Qual_Code.Value, Fields!size.Value, "DS_KS5Nationals_LKP"), "Measure")
However as you can see in the screenshot, this returns 2917 instead.
So my question is; Why doesn't that work, and given that it doesn't work how can I, within a parent group, aggregate the results of aggregates inside a child group?
EDIT:
OK so, I have determined that the following works correctly:
=Sum(CountDistinct(Iif(IsNothing(Fields!AVG_PTS.Value) = False, Fields!Learner_ID.Value, Nothing), "Qual"), "Measure")
The problem there is that the Qual row that returns 11.5 is weighted to 0.5. I.E. it actually returns 23, and the Lookup(Fields!Qual_Code.Value, Fields!Qual_Code.Value, Fields!size.Value, "DS_KS5Nationals_LKP") is for that row returning 0.5 and altering it to 11.5...so the question becomes; "how do I force that ...*Lookup(Fields!Qual_Code.Value, Fields!Qual_Code.Value, Fields!size.Value, "DS_KS5Nationals_LKP") into the "Qual" scope, like the CountDistinct() is already in?
The issue here is that you're trying to aggregate values using that Lookup function which only returns one value. There are a couple ways you could go about doing this. One option would be to use the LookupSet function to get the applicable weightings. An even better option is to combine the data in your dataset so that the weighting is available without using a lookup. That way the function can recalculate an any grouping level without you having to force a scope on it. Also, CountDistinct ignores "Nothing" so you can do without the extra IIf statement. Hope that helps.

How to get a single field in a VIEW to use in WHERE

Hello dear stack overflow friends,
I want to use a single field located in an other view as a parameter for a WHERE clause (in an other view). However, I can't find the correct way to get this done.
I already used the FIRST() function, but this gives a syntax error.
This probably has to do with the fact that it selects the first row in its whole, but I want to get only a single field in this row. (actually this whole view only has one field)
/*this first view contains the value to use for the second view, called average */
CREATE OR REPLACE VIEW getAverage AS
SELECT AVG(todoitem.completiondate-todoitem.creationdate) as average from todolist
inner join todoitem on todoitem.id=todolist.id;
/*this view has a WHERE clause that should use a value in the getAverage VIEW */
CREATE OR REPLACE VIEW aboveAverage AS
SELECT * FROM todolist
inner join todoitem on todoitem.id=todolist.id
WHERE (todoitem.completiondate-todoitem.creationdate > FIRST(getAverage.average));
If somebody could help me I would be very grateful.
Is this what you want?
CREATE OR REPLACE VIEW aboveAverage AS
SELECT *
FROM todolist inner join
todoitem
on todoitem.id=todolist.id
WHERE (todoitem.completiondate-todoitem.creationdate > (SELECT average FROM getAverage);
MySQL does allow subqueries in the where and select clauses. You wouldn't need the second view for this.