00:00

MySQL last() Function

Last updated: 4 min read

MySQL has no LAST() function; calling it is an error. What you usually want is the latest or most recent row: the last reading of each weather station, a customer's latest order, the newest log line. This tutorial shows the reliable ways to get it, including a trap in the LAST_VALUE() window function that catches almost everyone.

1. LAST() Doesn't Exist

query.sql
SELECT LAST(temp_c) FROM readings;
Output
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(temp_c) FROM readings' at line 1
Don't rely on "the last inserted row" being the last row a query returns. Without ORDER BY, row order is not guaranteed.

Table: readings (7 rows)

idstationread_ontemp_crain_mm
1PUNE2026-03-0118.00.0
2PUNE2026-03-0221.00.0
3PUNE2026-03-0319.02.5
4PUNE2026-03-0424.00.0
5KOCHI2026-03-0127.512.0
6KOCHI2026-03-0228.18.4
7KOCHI2026-03-0326.920.2

2. Last Row: ORDER BY … DESC LIMIT 1

query.sql
SELECT station, read_on, temp_c FROM readings ORDER BY read_on DESC, id DESC LIMIT 1;

Most recent reading overall

stationread_ontemp_c
PUNE2026-03-0424.0
query.sql
SELECT station, read_on, temp_c FROM readings WHERE station = 'PUNE' ORDER BY read_on DESC LIMIT 1;

Latest Pune reading

stationread_ontemp_c
PUNE2026-03-0424.0

3. Latest Value Only: MAX()

query.sql
SELECT station, MAX(read_on) AS last_reading_date FROM readings GROUP BY station;

Result: 2 rows

stationlast_reading_date
PUNE2026-03-04
KOCHI2026-03-03

4. Latest Row per Group

latest-per-group.sql
SELECT station, read_on, temp_c, rain_mm
FROM (
  SELECT r.*, ROW_NUMBER() OVER (PARTITION BY station ORDER BY read_on DESC, id DESC) AS rn
  FROM readings r
) t
WHERE rn = 1
ORDER BY station;

Result: 2 rows

stationread_ontemp_crain_mm
KOCHI2026-03-0326.920.2
PUNE2026-03-0424.00.0

An index on (station, read_on) makes this fast on big tables.

5. LAST_VALUE() and the Frame Trap

Why LAST_VALUE needs a full window frameWith ORDER BY in OVER, the default frame ends at the current row, so LAST_VALUE returns the current row's own value. With ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, every row sees the true last value of the partition.Mar 01: 18Mar 01: 18Mar 02: 21Mar 02: 21Mar 03: 19Mar 03: 19Mar 04: 24Mar 04: 24default frame for the Mar 02 row: ends here → LAST_VALUE = 21 ✘ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING → LAST_VALUE = 24 ✔default framefull frame
Figure 1: LAST_VALUE() looks at the window frame; widen the frame to the whole partition to get the real last value.

Look what happens with the default frame:

default-frame.sql
SELECT station, read_on, temp_c,
       LAST_VALUE(temp_c) OVER (PARTITION BY station ORDER BY read_on) AS last_temp_wrong
FROM readings
ORDER BY station, read_on;

Result: 7 rows

stationread_ontemp_clast_temp_wrong
KOCHI2026-03-0127.527.5
KOCHI2026-03-0228.128.1
KOCHI2026-03-0326.926.9
PUNE2026-03-0118.018.0
PUNE2026-03-0221.021.0
PUNE2026-03-0319.019.0
PUNE2026-03-0424.024.0

Each row just shows its own temperature, because the default frame stops at the current row. Make the frame cover the whole partition:

full-frame.sql
SELECT station, read_on, temp_c,
       LAST_VALUE(temp_c) OVER (PARTITION BY station ORDER BY read_on
                                ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_temp
FROM readings
ORDER BY station, read_on;

Result: 7 rows

stationread_ontemp_clast_temp
KOCHI2026-03-0127.526.9
KOCHI2026-03-0228.126.9
KOCHI2026-03-0326.926.9
PUNE2026-03-0118.024.0
PUNE2026-03-0221.024.0
PUNE2026-03-0319.024.0
PUNE2026-03-0424.024.0
An easy alternative: FIRST_VALUE(temp_c) OVER (PARTITION BY station ORDER BY read_on DESC) gives the latest value without any frame clause.

6. Previous and Next Rows: LAG and LEAD

Often "last" means "the previous reading". LAG() looks back, LEAD() looks ahead:

lag-lead.sql
SELECT station, read_on, temp_c,
       LAG(temp_c)  OVER (PARTITION BY station ORDER BY read_on) AS previous_temp,
       temp_c - LAG(temp_c) OVER (PARTITION BY station ORDER BY read_on) AS change_c,
       LEAD(read_on) OVER (PARTITION BY station ORDER BY read_on) AS next_reading
FROM readings
ORDER BY station, read_on;

Result: 7 rows

stationread_ontemp_cprevious_tempchange_cnext_reading
KOCHI2026-03-0127.5NULLNULL2026-03-02
KOCHI2026-03-0228.127.50.62026-03-03
KOCHI2026-03-0326.928.1-1.2NULL
PUNE2026-03-0118.0NULLNULL2026-03-02
PUNE2026-03-0221.018.03.02026-03-03
PUNE2026-03-0319.021.0-2.02026-03-04
PUNE2026-03-0424.019.05.0NULL

7. The Last Inserted ID

To get the ID your own session just inserted, use LAST_INSERT_ID(), never MAX(id), which can return another session's row:

last-insert-id.sql
INSERT INTO readings (station, read_on, temp_c, rain_mm) VALUES ('KOCHI', '2026-03-04', 29.0, 0);
SELECT LAST_INSERT_ID() AS my_new_id;

Result: 1 row

my_new_id
8

8. Which Technique to Use

NeedUse
Latest row overallORDER BY col DESC LIMIT 1
Latest value onlyMAX(col)
Latest row in each groupROW_NUMBER() OVER (PARTITION BY … ORDER BY … DESC) = 1
Group's last value on every rowLAST_VALUE(…) OVER (… ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
Previous / next rowLAG() / LEAD()
ID you just insertedLAST_INSERT_ID()

9. Summary

  • MySQL has no LAST(); "last" needs an ORDER BY.
  • Use ORDER BY … DESC LIMIT 1, MAX(), or ROW_NUMBER() per group.
  • LAST_VALUE() needs a frame of UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
  • LAG/LEAD reach the previous/next row; LAST_INSERT_ID() returns your session's new ID.