weird issue when sorting MySQL `date` results - mysql

I'm getting some weird results when I query on of my tables to show upcoming birthdays (schema and query below), and then sort by date with the upcoming dates first. The type for the dob (date of birth) field is date with the format 0000-00-00
I'm using the below schema:
People:
+------------+-------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+-------------------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| fname | varchar(32) | NO | | NULL | |
| lname | varchar(32) | NO | | NULL | |
| dob | date | NO | | 0000-00-00 | |
| license_no | varchar(24) | NO | | NULL | |
| date_added | timestamp | NO | | CURRENT_TIMESTAMP | |
| status | varchar(8) | NO | | Allow | |
+------------+-------------+------+-----+-------------------+----------------+
and here's my query, as it's called from PHP:
<?php
/* This will give us upcoming dobs for the next 2 weeks */
$con = connect_db();
// grab any dobs coming up in the next week and sort them by what's
//coming up first
//if they are born on Feb. 29th, it will fall on March 1st
$query = 'select p.lname, p.fname, u.number, p.dob ' .
'from people p, units u where p.id = u.resident and ' .
'DAYOFYEAR(curdate()) <= DAYOFYEAR(DATE_ADD(dob, INTERVAL ' .
'(YEAR(NOW()) - YEAR(dob)) YEAR)) AND DAYOFYEAR(curdate()) +30 >= ' .
'dayofyear(`dob`) order by dayofyear(dob) limit 7;';
$result = mysqli_query($con, $query);
if (!empty($result)) {
while($row = mysqli_fetch_array($result)) {
$fname = $row['fname'];
$lname = $row['lname'];
$number = $row['number'];
$dob = date("m-d", strtotime($row['dob']));
if($dob == date("m-d")) {
$dob = 'Today!';
printf('%s, %s</br>Unit: %s</br>%s</br></br>', $lname,
$fname, $number, $dob);
} else {
printf('%s, %s</br>Unit: %s</br>Date: %s</br></br>', $lname,
$fname, $number, $dob);
}
}
}
?>
here's an example of the returned query:
Name, Name
Unit: 110
Date: 09-11
Name2, Name2
Unit: 434
Date: 09-10
As you can see, the order is wrong!
EDIT - Now I've noticed that the records in question (the two dates above) are not ordered correctly ever in MySQL!
One of the full dates is: 1950-09-11
and the other is: 1956-09-10
I've looked through those two records and haven't found any mangled data, so I'm pretty confused as to why this is happening

ORDER BY dayofyear(dob) ASC
Good luck

Related

How to optimize mysql query to get count from json column which have huge data

I have a query to find the count of rejected serialNos for different reasons. I need to find each reason count within a date limit.I have 3 tables say:
+------------------+--------------+------+-----+-------------------+
| Field | Type | Null | Key | Default |
+------------------+--------------+------+-----+-------------------+
| id | int(11) | NO | PRI | NULL |
| client_id | int(11) |YES | MUL | NULL |
| tc_date | datetime | YES | | NULL |
+------------------+--------------+------+-----+--------------------
mysql> desc job_order_finish_product_serial_no;
+------------------------------+--------------+------+-----+-----+
| Field | Type | Null | Key | Default|
+------------------------------+--------------+------+-----+-----|
id | int(11) | NO | PRI | NULL |
| serial_no | varchar(255) | YES | MUL | NULL |
| specification | json | YES | | NULL |
| client_id | int(11) |YES | MUL | NULL |
| tc_id | int(11) | YES | MUL | NULL |
| job_order_finish_products_id | int(11) | YES | MUL | NULL |
---------------------------------+-------------+-------+----+-------
In specification column my sample data looks like
"Leakage":{
"time":"2021-09-20 10:00:00",
"status":"completed",
"rework":[],
"user":{
"name":"xyz",
"id":1
}
},
"Thickness":{
"time":"2021-09-20 10:00:00",
"status":"rejected",
"rework":[],
"user":{
"name":"xyz",
"id":1
}
},
"Diameter":{
"time":null,
"status":"pending",
"rework":[],
"user":{
}
},
"Bung":{
"time":null,
"status":"pending",
"rework":[],
"user":{
}
}
}
For each serial_no in job_order_finish_product_serial_no, the specification should look like the above snippet. I need to get a count of each reason rejected serial_nos count within a date range. job_order_finish_product_serial_no row count was 2251543 rows.My count query is
select tc.tc_date,
(
SELECT count(serial_no)
from nucleus.job_order_finish_product_serial_no jfps
where jfps.tc_id=tc.id
and specification ->> "$.Thickness.status" = "rejected"
and client_id = 154
) as rejected_thickness,
(
SELECT count(serial_no)
from nucleus.job_order_finish_product_serial_no jfps
where jfps.tc_id=tc.id
and specification ->> "$.Leakage.status" = "rejected"
and client_id = 154
) as rejected_leakage,
(
SELECT count(serial_no)
from nucleus.job_order_finish_product_serial_no jfps
where jfps.tc_id=tc.id
and specification ->> "$.Bung.status" = "rejected"
and client_id = 154
) as rejected_bung
from nucleus.tc_details tc
inner join nucleus.job_order_finish_product_serial_no jfps
ON jfps.tc_id=tc.id
inner join nucleus.job_order_finish_products jofp
ON jofp.id=jfps.job_order_finish_products_id
where tc.tc_date between '2021-09-18 00:00:00'
AND '2021-09-22 23:59:59'
and tc.client_id=154
and jofp.client_id=154
and jfps.client_id=154
group by job_order_finish_product_id,tc.tc_date;
Output:
data rejected_thickness rejected_bung rejected_leakage rejected_diameter
21-09-2021 2 10 23 3
with the above query each subquery taking 2 min to give the result and the entire query taking almost taking 10min. Is there any way to optimize the query? Thank you!
Indexes that may help:
jfps, jofp: INDEX(tc_id, client_id) -- or in the opposite order
tc: INDEX(client_id, tc_date, id)
I prefer this way to write date range tests:
date >= '2021-09-18'
AND date < '2021-09-18' + INTERVAL 4 DAY
JOIN with a 'derived' table might be faster than 3 subqueries
unless count(serial_no) is needed for excluding NULL values of serial_no, use COUNT(*).
Something like
SELECT ...
SUM(jfps.specification ->> "$.Leakage.status" = "rejected")
as rejected_leakage,
SUM(...) as ...,
SUM(...) as ...,
...
JOIN nucleus.job_order_finish_product_serial_no AS jfps
ON jfps.tc_id = tc.id
WHERE ...
AND jfps.client_id = 154

How To: Insert into column where the value is on another table

Summarize the Problem:
I want to write a booking code. I had a little problem, that I wanted to insert a value into the booked table that is in another table called house_info.
The detail of booked & house_info database is below:
Booked Table
ID_Booking | House_Name | House_ID | House_No | House_Qty | House_Price |
1 | Rose House | 1 | RH01 | 1 | |
2 | Rose House | 1 | RH02 | 1 | |
House Info Table
House_ID | HouseState_Name | House_Qty | House_Price |
1 | Garden City | 8 | 40000|
2 | Electronic City | 10 | 1000000|
I want to insert the House_Price value on the house_info table into the House_Price column on booked table every time users input on the Booking Form.
Background you've already tried:
I already tried this using a trigger on booked table like below:
Trigger on Booked Table (Before Insert)
IF NEW.House_ID= '1' THEN SET
NEW.House_Price = 40000;
ELSEIF NEW.House_ID= '2' THEN SET
NEW.House_Price = 1000000;
But I realize this is not dynamic because when the company want to change the price of each HouseState_Name he needs to change it from the trigger. So I think what I needed is a query from PHP that can calls the value of each HouseState_Name and hold it on an array and place it or insert it when the Book Query passed (I hope my logic is true, I'm sorry if it's false).
I already tried to search too for the query's to use. But I didn't know how am I going to use the query.
Some Codes:
Booking.php
require 'Connection.php';
//Check Connection
if ($conn->connect_error){
die("Connection Failed: ". $conn->connect_error);
}
//Check for Submit
if (filter_has_var(INPUT_POST, 'Submit')) {
//Get Form Data
$CustomerEmail= htmlspecialchars($_POST["Email"], ENT_QUOTES);
$House_Name= htmlspecialchars($_POST["HouseName"], ENT_QUOTES);
$House_ID = htmlspecialchars($_POST["HouseID "], ENT_QUOTES);
$House_No = htmlspecialchars($_POST["HouseNo "], ENT_QUOTES);
//Validate the data fields
if (!empty($CustomerEmail) && !empty($House_Name)) {
//Passed
if (filter_var($CustomerEmail, FILTER_VALIDATE_EMAIL) === false) {
//Failed
$msg = 'Please use a valid email';
header("location: ../GardenCity.php?error=PleaseUseValidEmail");
} else {
//Passed
echo "Creating a Booking.<br>";
//Inserting the Booking Data into Database
$sql = "INSERT INTO `booked`(`ID_Booking`, `CustomerEmail`, `House_Name`, `House_ID`, `House_No`)
VALUES (NULL, '$CustomerEmail', '$House_Name', '$House_ID ', '$House_No', '', '')";
if ($conn->query($sql) === TRUE) {
header("location: ../GardenCity.php");
} else {
echo "Error: " . $sql . "<br><br>" . $conn->error;
}
}
} else {
header("location: ../GardenCity.php?error=EmptyFields");
}
}
$conn -> close();
Expected Results:
Before Update the price
Database Looks
ID_Booking | House_Name | House_ID | House_No | House_Qty | House_Price |
1 | Rose House | 1 | RH01 | 1 | |
2 | Rose House | 1 | RH02 | 1 | |
3 | Rose House | 1 | RH03 | 1 | 40000|
House_ID | HouseState_Name | House_Qty | House_Price |
1 | Garden City | 7 | 40000|
2 | Electronic City | 10 | 1000000|
After Update the price
Database Looks
ID_Booking | House_Name | House_ID | House_No | House_Qty | House_Price |
1 | Rose House | 1 | RH01 | 1 | |
2 | Rose House | 1 | RH02 | 1 | |
3 | Rose House | 1 | RH03 | 1 | 40000|
4 | Rose House | 1 | RH04 | 1 | 200000|
House_ID | HouseState_Name | House_Qty | House_Price |
1 | Garden City | 6 | 200000|
2 | Electronic City | 10 | 1000000|
I hope this is well explained. Please let me know if there's any confusing statements or questions. I will say many thanks to you all if this is answered because I'm so stuck at this and my brain won't work.
I think this could work, basically using a subquery just to fetch the price, that should achieve the same result as your insert trigger, but without using fixed prices.
INSERT INTO `booked` (
`ID_Booking`,
`CustomerEmail`,
`House_Name`,
`House_ID`,
`House_No`,
`House_Qty`,
`House_Price`
) VALUES (
NULL,
'$CustomerEmail',
'$House_Name',
'$House_ID',
'$House_No',
'1',
(SELECT House_Price FROM house_info WHERE House_ID = '$House_ID')
)
Edit: I set House_Qty at 1, change it according to your needs :)
Maybe you can use the same subquery in your trigger directly instead (haven't tested it) :
SET NEW.House_Price =
(SELECT House_Price FROM house_info WHERE House_ID = NEW.House_id);
Assuming your House_ID are unique :)
I would expect to see a schema more or less like this:
houses(house_id*,name)
house_prices(house_id*,price_start_date*,price)
bookings(booking_id*,customer_id,total)
booking_detail(booking_id*,house_id*,start_date,end_date)
* = (component of) PRIMARY KEY
After some reflection, it should be apparent that your present concerns evaporate with this design.
Insert Into booked_table (ID_Booking, House_Name, House_Id, House_No, House_Qty, House_Price)
Select 1, House_Name, House_ID, 'RHXX', House_Qty, (SELECT House_Price FROM house_info WHERE House_ID = MM1.House_ID) From booked_table MM1
Where
NOT EXISTS(
SELECT *
FROM booked_table MM2
WHERE MM2.ID_Booking > MM1.ID_Booking
);
Fiddle: https://www.db-fiddle.com/f/7Bt3ZTQqbjs1jKzJe34qSF/0
I dont included the increment of the ID_Booking and House_No.
If you want to increase the House_Price, just do that with another query.
You can construct such an UPDATE statement with INNER JOINs one of which's in the subquery as a self-JOIN for booked table, put after your existing INSERT statement :
update house_info h join (
select b1.House_Price, b2.House_ID
from booked b1
join ( select House_ID,max(ID_Booking) as ID_Booking
from booked
group by House_ID
) b2
on b1.House_ID = b2.House_ID and b1.ID_Booking = b2.ID_Booking
) bb on bb.House_ID = h.House_ID
set h.House_Price = bb.House_Price;
but I should admit that your tables' design is not good, because of repeating columns they hold the same information in each.
Demo

MySQL/MariaDB/SQLite: DISTINCT CONCAT

This question isn't necessarily just Laravel related, but I'm trying to fetch records, which are distinct by concatenated fields. I need this to work with both MySQL/MariaDB as well as SQLite for testing purposes.
While doing my research, I've found out that SQLite does not have CONCAT function - instead you're using || operator to concatenate items. MySQL on the other hand will not interpret || the same way, but I can always use the conditional statement just to cover both cases.
However, I still cannot get records I'm after - my table consists of:
| id | tagable_id | tagable_type | name | title | description | url | image | hits |
| 1 | 1 | App\Models\Article | a.. | A.. | A.. descr.. | https://localhost | https://localhost... | 0 |
| 2 | 1 | App\Models\Article | b.. | B.. | B.. descr.. | https://localhost | https://localhost... | 2 |
| 3 | 1 | App\Models\Article | c.. | C.. | C.. descr.. | https://localhost | https://localhost... | 3 |
| 4 | 1 | App\Models\Page | a.. | A.. | C.. descr.. | https://localhost | https://localhost... | 0 |
I need get only 4 records that are sorted ASC by number of hits and which are unique using CONCAT(table_id, tagable_type).
What the statement should return in this case would be records with id 1 and 4 - because 2 and 3 have the same tagable_id and tagable_type as record with id 1, which has lowest number of hits - effectively only returning only 2 records:
| id | tagable_id | tagable_type | name | title | description | url | image | hits |
| 1 | 1 | App\Models\Article | a.. | A.. | A.. descr.. | https://localhost | https://localhost... | 0 |
| 4 | 1 | App\Models\Page | a.. | A.. | C.. descr.. | https://localhost | https://localhost... | 0 |
I tried already:
DB::table('tags')
->selectRaw("DISTINCT CONCAT(`tagable_id`, '-', `tagable_type`), `id`, `name`, `title`, `description`, `url`, `image`")
->whereIn('name', $tags->toArray())
->orderBy('hits');
This however does not return distinct records - it will return all records regardless of the distinct concatenation - that is in MySQL / MariaDB - in SQLite it will tell me no such function: CONCAT.
I also tried:
DB::table('tags')
->selectRaw("CONCAT(`tagable_id`, '-', `tagable_type`) as `identifier`, `id`, `name`, `title`, `description`, `url`, `image`")
->whereIn('name', $tags->toArray())
->groupBy('identifier')
->orderBy('hits');
This time MySQL/MariaDB tells me that I need to include other fields in the group by as well tags.id' isn't in GROUP BY, but when I use it with SQLite and replace CONCAT function with (tagable_id || '-' || tagable_type) as identifier - it seem to work.
So at this stage I'm: MySQL: 0 | SQLite: 1
Any help would be much appreciated.
UPDATE
After hours of trying to get it resolved I've decided to add a new column
identifier to the table to overcome issue of the non available concat function - my code now looks like this:
Tag::with('tagable')->whereIn('id', function($query) use ($tags) {
$query->selectRaw('min(`id`) from `tags`')
->whereIn('name', $tags->toArray())
->groupBy('identifier');
})
->orderBy('hits')
->take(4)
->get();
This is still not quite what I'm after as it relies on the lowest id min(id) of the given identifier and if the record with the lowest id for the same identifier has higher number of hits then its sibling then the sibling will not be returned.
You can use DISTINCT to get distinct combinations, but not whole rows:
DB::table('tags')->distinct()->get(['tagable_id', 'tagable_type']);
You have to use a more complex query for that:
$join = DB::table('tags')
->select('tagable_id', 'tagable_type')->selectRaw('MIN(hits) as hits')
->whereIn('name', $tags->toArray())
->groupBy('tagable_id', 'tagable_type');
$sql = '(' . $join->toSql() . ') as grouped';
$tags = DB::table('tags')
->join(DB::raw($sql), function($join) {
$join->on('tags.tagable_id', '=', 'grouped.tagable_id')
->on('tags.tagable_type', '=', 'grouped.tagable_type')
->on('tags.hits', '=', 'grouped.hits');
})->mergeBindings($join)
->whereIn('name', $tags->toArray())
->get();
A solution that guarantees one record per unique combination:
$join = DB::table('tags')
->select('tagable_id', 'tagable_type')->selectRaw('MIN(hits) as hits')
->whereIn('name', $tags->toArray())
->groupBy('tagable_id', 'tagable_type');
$sql = '(' . $join->toSql() . ') as grouped';
$ids = DB::table('tags')
->selectRaw('MIN(id) as id')
->join(DB::raw($sql), function($join) {
$join->on('tags.tagable_id', '=', 'grouped.tagable_id')
->on('tags.tagable_type', '=', 'grouped.tagable_type')
->on('tags.hits', '=', 'grouped.hits');
})->mergeBindings($join)
->whereIn('name', $tags->toArray())
->groupBy('tags.tagable_id', 'tags.tagable_type')
->pluck('id');
$tags = DB::table('tags')->whereIn('id', $ids)->get();

Count and select all dates for a specific field in MySQL

i have a data format like this:
+----+--------+---------------------+
| ID | utente | data |
+----+--------+---------------------+
| 1 | Man1 | 2014-02-10 12:12:00 |
+----+--------+---------------------+
| 2 | Women1 | 2015-02-10 12:12:00 |
+----+--------+---------------------+
| 3 | Man2 | 2016-02-10 12:12:00 |
+----+--------+---------------------+
| 4 | Women1 | 2014-03-10 12:12:00 |
+----+--------+---------------------+
| 5 | Man1 | 2014-04-10 12:12:00 |
+----+--------+---------------------+
| 6 | Women1 | 2014-02-10 12:12:00 |
+----+--------+---------------------+
I want to make a report that organise the ouptout in way like this:
+---------+--------+-------+---------------------+---------------------+---------------------+
| IDs | utente | count | data1 | data2 | data3 |
+---------+--------+-------+---------------------+---------------------+---------------------+
| 1, 5 | Man1 | 2 | 2014-02-10 12:12:00 | 2014-04-10 12:12:00 | |
+---------+--------+-------+---------------------+---------------------+---------------------+
| 2, 4, 6 | Women1 | 3 | 2015-02-10 12:12:00 | 2014-03-10 12:12:00 | 2014-05-10 12:12:00 |
+---------+--------+-------+---------------------+---------------------+---------------------+
All the row thath include the same user (utente) more than one time will be included in one row with all the dates and the count of records.
Thanks
While it's certainly possible to write a query that returns the data in the format you want, I would suggest you to use a GROUP BY query and two GROUP_CONCAT aggregate functions:
SELECT
GROUP_CONCAT(ID) as IDs,
utente,
COUNT(*) as cnt,
GROUP_CONCAT(data ORDER BY data) AS Dates
FROM
tablename
GROUP BY
utente
then at the application level you can split your Dates field to multiple columns.
Looks like a fairly standard "Breaking" report, complicated only by the fact that your dates extend horizontally instead of down...
SELECT * FROM t ORDER BY utente, data
$lastutente = $lastdata = '';
echo "<table>\n";
while ($row = fetch()) {
if ($lastutente != $row['utente']) {
if ($lastutente != '') {
/****
* THIS SECTION REF'D BELOW
***/
echo "<td>$cnt</td>\n";
foreach ($datelst[] as $d)
echo "<td>$row[data]</td>\n";
for ($i = count($datelst); $i < $NumberOfDateCells; $i++)
echo "<td> </td>\n";
echo "</tr>\n";
/****
* END OF SECTION REF'D BELOW
***/
}
echo "<tr><td>$row[utente]</td>\n"; // start a new row - you probably want to print other stuff too
$datelst = array();
$cnt = 0;
}
if ($lastdata != $row['data']) {
datelst[] = $row['data'];
}
$cnt += $row['cnt']; // or $cnt++ if it's one per row
}
print the end of the last row - see SECTION REF'D ABOVE
echo "</table>\n";
You could add a GROUP BY utente, data to your query above to put a little more load on mysql and a little less on your code - then you should have SUM(cnt) as cnt or COUNT(*) as cnt.

Converting Previous Row Subquery into a Join in MySQL

I have policy information in a policy table. Each row represents the policy status at a certain time (the time is stored in an updated_on column). Each row belongs to a policy iteration (multiple policy rows can belong to a single policy iteration). I want to look at status changes from row to row within a policy iteration.
The policy table:
CREATE TABLE `policy` (
`policy_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`policy_iteration_id` int(10) unsigned NOT NULL,
`policy_status_id` tinyint(3) unsigned NOT NULL,
`updated_on` datetime NOT NULL,
PRIMARY KEY (`policy_id`),
KEY `policy_iteration_idx` (`policy_iteration_id`),
KEY `policy_status_updated_idx` (`policy_status_id`,`updated_on`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
I want to be able to pass a date range and a "from" status and a "to" status and return the policy data for the "to" row. So in pseudo code, I need to group by policy iteration, find rows that satisfy the data range and the "to" status, then look at the previous row within that policy iteration to see if it has the "from" status. If so, return the "to" row's information.
This is the query I came up with:
SELECT
pto.policy_iteration_id,
pto.policy_id,
pto.updated_on,
FROM
policy AS pto
WHERE
pto.updated_on >= $from_date AND
pto.updated_on <= $to_date AND
pto.policy_status_id = $to_status_id AND
$from_status_id =
(SELECT
pfrom.policy_status_id
FROM
policy AS pfrom
WHERE
pfrom.policy_iteration_id = pto.policy_iteration_id AND
pfrom.policy_id < pto.policy_id
ORDER BY
pfrom.policy_id DESC
LIMIT
1);
This query works but is very inefficient because of the subquery having to be executed for each row. I'd like to make it more efficient by using subquery join(s) but I can't figure out how.
Any help would be appreciated. Thanks!
UPDATE #1
To help explain what I'm trying to do, here is an example data set:
+-----------+---------------------+------------------+---------------------+
| policy_id | policy_iteration_id | policy_status_id | updated_on |
+-----------+---------------------+------------------+---------------------+
| 323705 | 27230 | 6 | 2014-08-01 10:27:11 |
| 325028 | 27230 | 2 | 2014-08-01 17:12:28 |
| 323999 | 27591 | 2 | 2014-08-01 12:07:31 |
| 324008 | 27591 | 6 | 2014-08-01 12:10:23 |
| 325909 | 27591 | 2 | 2014-08-02 14:59:12 |
| 327116 | 29083 | 6 | 2014-08-04 12:09:16 |
| 327142 | 29083 | 6 | 2014-08-04 12:19:00 |
| 328067 | 29083 | 2 | 2014-08-04 17:58:41 |
| 327740 | 29666 | 3 | 2014-08-04 16:16:55 |
| 327749 | 29666 | 3 | 2014-08-04 16:19:01 |
+-----------+---------------------+------------------+---------------------+
Now if I run the query where from_date = '2014-08-02 00:00:00', to_date = '2014-08-05 00:00:00', from_status = 6 and to_status = 2, the result should be:
+-----------+---------------------+------------------+---------------------+
| policy_id | policy_iteration_id | policy_status_id | updated_on |
+-----------+---------------------+------------------+---------------------+
| 325909 | 27591 | 2 | 2014-08-02 14:59:12 |
| 328067 | 29083 | 2 | 2014-08-04 17:58:41 |
+-----------+---------------------+------------------+---------------------+
Those two rows have a row with the selected "to_status" of 2 within the stated time period and have their previous row with the "from_status" of 6.
I don't believe joining a MAX policy id with a GROUP BY of policy_iteration_id will do the job since that would return the rows that are most recent, not the row that is previous to the row with the "to_status".
Any further help would be appreciated. Thanks!
You can use use max from.policy_id where from.policy_id < to.policy_id to help get the previous row as a set.
select
p.policy_iteration_id,
p.policy_id,
p.updated_on
from
policy f
inner join (
select
p.policy_iteration_id,
p.policy_id,
p.updated_on,
max(f.policy_id) as prev_policy_id
from
policy p
inner join
policy f
on f.policy_iteration_id = p.policy_iteration_id and
f.policy_id < p.policy_id
where
p.updated_on >= $from_date and
p.updated_on <= $to_date and
p.policy_status_id = $to_status_id
group by
p.policy_iteration_id,
p.policy_id,
p.updated_on
) p
on p.prev_policy_id = f.policy_id
where
f.policy_status_id = $from_status_id
In a database with window functions there are simpler ways of achieving this.
Example SQLFiddle