SQL example

How to get the ID of an inserted row in SQL

7 min read Updated Oct 2026 Runs in an isolated runtime
Quick answer

Add RETURNING id to the insert: INSERT INTO users (email) VALUES ('[email protected]') RETURNING id; returns the new id as a result row, in SQLite 3.35+ and PostgreSQL. Elsewhere, read it on the same connection right after the insert: last_insert_rowid() in SQLite, LAST_INSERT_ID() in MySQL, SCOPE_IDENTITY() (or OUTPUT INSERTED.id) in SQL Server.

When the database numbers new rows for you, the id doesn't exist until the insert has run, so you have to ask for it. The cleanest way is to have the insert hand it back, with RETURNING. The older way is a function that reports the last id generated on your connection. Both are below, along with how SQLite picks the number, what happens after a multi-row insert, and how to use the new id to insert child rows. The examples run on SQLite; the table in section 5 gives the PostgreSQL, MySQL and SQL Server forms. Hit Run on any of them, then edit the code and run it again.

1INSERT … RETURNING idRecommended

A RETURNING clause at the end of an INSERT makes the statement return the rows it just wrote, like a SELECT. List the columns you want: the generated id, and anything else the database filled in, such as a default value. With a multi-row insert (see insert multiple rows) you get one result row per inserted row, so all the new ids come back from one statement.

returning.sql

Output

The first insert returns id 1. The second returns 2 together with plan = free, the default the table supplied, so you don't need a second query to see it. The multi-row insert returns ids 3 and 4. When you match returned rows back to the data you sent, match on a unique column such as email rather than on position. RETURNING needs SQLite 3.35 or later (the examples run on 3.53); it also works on UPDATE and DELETE. PostgreSQL has the same clause, MySQL doesn't, and SQL Server's version is OUTPUT INSERTED.id.

2last_insert_rowid(): the last id on this connection

Before RETURNING, SQLite code read the id afterwards with last_insert_rowid(). It reports the rowid of the most recent successful insert on the current connection. That makes it safe when other connections insert at the same time, but it is one value per connection, not per table, and it only remembers the last row.

last-insert-rowid.sql

Output

Ada gets 1. After the three-row insert it reports 4, the id of Margaret, the last row; the ids of Grace and Linus (2 and 3) aren't available from it, which is why RETURNING is the better tool for multi-row inserts. The insert into logins replaces the value with 100, even though it is a different table. The ignored duplicate inserts nothing (changes() is 0), so the value stays 100: always check that a row was actually inserted before trusting it. Your driver usually exposes the same number, for example cursor.lastrowid in Python or lastInsertRowid in Node.

3INTEGER PRIMARY KEY, the rowid and AUTOINCREMENT

Every ordinary SQLite table has a hidden integer key called rowid. A column declared INTEGER PRIMARY KEY becomes another name for it, which is why leaving the id out of an insert fills it in. The new value is normally one more than the largest id in the table, so if you delete the newest row, its id can be handed out again. Adding AUTOINCREMENT stops that: SQLite remembers the highest id it ever used and never goes back.

autoincrement.sql

Output

In plain, the new row d gets id 3 again, the id of the row that was just deleted. In auto it gets 4, because AUTOINCREMENT keeps the high-water mark in the internal sqlite_sequence table. In both, rowid and id match. Use AUTOINCREMENT only when a reused id would cause trouble (an old link or log entry pointing at a new row); without it SQLite does a little less work per insert. The last query shows a common mistake: INT PRIMARY KEY is not an alias for the rowid, so id comes back NULL while the hidden rowid is 1. Spell it INTEGER.

4Use the new id for child rows

The usual reason to want the id is to insert rows that point at it: the items of a new order, the addresses of a new customer. In application code, run the parent insert with RETURNING id, read the value, and pass it as a parameter to the child inserts. In a plain SQL script, be careful with last_insert_rowid(): every child row you insert changes it.

child-rows.sql

Output

The first order is created with id 100 so the bug is easy to see. Only pen gets 100: once it is inserted, last_insert_rowid() is the item's own id, so ink points at order 1 and pad at order 2, neither of which exists (the NULL refs). SQLite accepted them because foreign keys are off by default; after PRAGMA foreign_keys = ON the same insert fails with FOREIGN KEY constraint failed. Copying the id into a temp table first fixes it (both items of A-101 get 101), and so does looking the parent up by a unique column (A-102). SQLite doesn't allow an INSERT … RETURNING inside WITH; PostgreSQL does, which lets it insert the parent and its children in one statement (see the next table).

5Other databases

Each database has its own function for "the id I just generated", and they disagree about multi-row inserts: MySQL reports the first new id, SQLite and SQL Server the last. Every form below was run on PostgreSQL 18, MySQL 8.4 and SQL Server's T-SQL (through Babelfish). Oracle uses RETURNING id INTO a variable in PL/SQL.

SyntaxDatabaseNotes
INSERT … RETURNING idPostgreSQL, SQLite 3.35+One row per inserted row, any columns. MySQL 8.4 rejects it with syntax error 1064
WITH o AS (INSERT … RETURNING id) INSERT … SELECT … FROM oPostgreSQLParent and child rows in one statement; SQLite reports a syntax error
currval(pg_get_serial_sequence('users', 'id'))PostgreSQLThe last value this session took from that table's sequence
lastval()PostgreSQLThe last value from any sequence in this session, so an insert into another table changes it
LAST_INSERT_ID()MySQLPer connection. After a multi-row insert it is the FIRST new id (2 for rows 2, 3, 4)
SET @order_id = LAST_INSERT_ID()MySQLSave it in a variable before inserting the child rows
OUTPUT INSERTED.idSQL ServerGoes before VALUES; returns one row per inserted row, like RETURNING
OUTPUT INSERTED.id INTO @newSQL ServerCollects the ids in a table variable for child inserts; required when the table has triggers
SCOPE_IDENTITY()SQL ServerLast identity in this session and scope; after a multi-row insert, the last row
@@IDENTITYSQL ServerIncludes triggers: returned a trigger's audit-table id (503) where SCOPE_IDENTITY() gave 4
IDENT_CURRENT('users')SQL ServerThe table's last identity from any session; not safe for "my" row

Frequently asked questions

How do I get the ID of the row I just inserted?

Add RETURNING id to the insert in SQLite 3.35+ or PostgreSQL: INSERT INTO users (email) VALUES ('[email protected]') RETURNING id; returns the id as a result row. In SQL Server put OUTPUT INSERTED.id before VALUES. Otherwise read it on the same connection straight after the insert: last_insert_rowid() in SQLite, LAST_INSERT_ID() in MySQL, SCOPE_IDENTITY() in SQL Server.

What is the difference between SCOPE_IDENTITY() and @@IDENTITY?

Both report the last identity value generated in your session, but @@IDENTITY also counts inserts made by triggers. With an AFTER INSERT trigger that writes to an audit table, SCOPE_IDENTITY() returned the new user id (4) while @@IDENTITY returned the audit row id (503). Use SCOPE_IDENTITY(), or OUTPUT INSERTED.id. IDENT_CURRENT('users') is different again: it looks at every session, so another user's insert can change it.

How do I get all the IDs from a multi-row insert?

Use RETURNING id (SQLite 3.35+, PostgreSQL) or OUTPUT INSERTED.id (SQL Server), which return one row per inserted row. The single-value functions report only one id: after inserting three rows numbered 2 to 4, SQLite's last_insert_rowid() returned 4, the last, while MySQL's LAST_INSERT_ID() returned 2, the first.

Is last_insert_rowid() or LAST_INSERT_ID() safe when many users insert at once?

Yes, both are per connection, so inserts on other connections don't change your value. What does change it is your own next insert on the same connection, into any table, including the child rows you insert next. Read it immediately, or save it (a temp table in SQLite, SET @id = LAST_INSERT_ID() in MySQL). With a connection pool, run the insert and the read on the same connection.

Why is my SQLite id column NULL after an insert?

The column is probably declared INT PRIMARY KEY or BIGINT PRIMARY KEY. Only the exact type name INTEGER PRIMARY KEY makes the column an alias for the rowid that SQLite fills in. With id INT PRIMARY KEY, an insert without an id returned NULL for id while the hidden rowid was 1.

Does SQLite reuse IDs after a row is deleted?

It can. Without AUTOINCREMENT, a new row gets one more than the current largest id, so deleting the newest row (id 3) and inserting again gave id 3 a second time. With INTEGER PRIMARY KEY AUTOINCREMENT the same steps gave 4, because SQLite records the highest id ever used in sqlite_sequence.

Run it yourself

Open any of these in the full SQLite editor to tweak, run and share.

SQLite playground