INSERT: new rows
In short: INSERT INTO adds new rows to a table, one or many at once or from the result of a SELECT, and RETURNING gives back columns of the new row, such as its id.
Problem
A new customer
So far you have only read from the database. But new things go into it every day. Today Vesela Raykova from Pleven signed up on the website.
The website has to write a new row into customers. Nobody opens the table by hand: the program sends a query to the database.
The idea
INSERT INTO
INSERT INTO says which table and which columns, and VALUES gives the values in the same order:
The column id is not in the list. It is a primary key of the kind INTEGER PRIMARY KEY, so SQLite itself gives it a number one higher than the largest: 21. A column that is not in the list gets its default value (for stock in the shop it is 0), and if there is none, NULL.
A field can hold several statements. They run one after another, and the SELECT at the end shows what happened.
Each field has its own database and keeps it between runs. Run the query again: the second time the database refuses, because Vesela is already in with that email. The “Fresh database” button puts the database back as it was at the start.
See it
Without the list of columns
The list of columns can be left out. Then the values must cover all the columns, in the order they have in the table:
Leave one out, and the database refuses:
Here id is missing. Without a list, the database cannot tell which value is for which column. So always list the columns. That way you can see what goes where, and the query does not break when the table gets a new column.
Try it
A new product
The shop starts selling ayran. Add the product “Ayran” in category 4 (Dairy), at 1.30 €, with 30 in stock, added today (2026-10-25).
Solution
INSERT INTO products (name, category_id, price, stock, added_at)
VALUES ('Ayran', 4, 1.3, 30, '2026-10-25');INSERT INTO products (name, category_id, price, stock, added_at) VALUES ('Ayran', 4, 1.3, 30, '2026-10-25'). Its number is 26, and the database gives it itself.
The idea
RETURNING
Yavor Penev places his first order. The website saves the order and then has to save the products in it. For that it needs the number of the new order, and the database hands that out.
RETURNING at the end of an INSERT gives back columns of the row that was just saved:
The order is number 61. Without RETURNING you would need a second query to find out. RETURNING takes any columns: RETURNING id, ordered_at or RETURNING *.
More precisely, if you are curious
RETURNING exists in SQLite (since version 3.35) and in PostgreSQL. In SQL Server the same thing is written with OUTPUT. MySQL does not have it: there the number is read with LAST_INSERT_ID() after the insert. SQLite has a similar function, last_insert_rowid().
The idea
Many rows at once
Yavor's order has coffee and two cartons of fresh milk. Several rows go in with one INSERT: after VALUES come several sets of brackets, separated by commas. In this database order 61 already exists:
The rows can also come from a query. Maria clicks “Order again” on her last order (60). The new order is 61, and the products come from a SELECT instead of VALUES:
This is INSERT … SELECT: whatever the SELECT returns is saved. The columns of the SELECT must match the list after INSERT INTO. The price is taken from products, not from the old order, because it may have changed since then.
See it
The database says no
The shop's tables have rules. Break one, and the database refuses the row and says which rule was broken.
A customer's email must be unique (UNIQUE):
The name cannot be missing (NOT NULL):
The price cannot be negative (CHECK):
An order must belong to a customer who exists. customer_id is a foreign key to customers, and there is no customer 99:
Each time the row is not saved and nothing in the database changes. That is a good thing: a bug in the program cannot fill the database with orders that belong to nobody.
These rules are written into the table itself when it is created. In the next module you will write them yourself.
Try it
A review without text
Simona Yordanova (customer 17) gives fresh milk (product 5) 5 stars today, 2026-10-25, without writing any text. Save the review.
Solution
INSERT INTO reviews (product_id, customer_id, rating, created_at)
VALUES (5, 17, 5, '2026-10-25');INSERT INTO reviews (product_id, customer_id, rating, created_at) VALUES (5, 17, 5, '2026-10-25'). The column body is not in the list, so it stays NULL. It may be empty, but rating may not: try it without.
Exercise
Two tasks
1. The shop is expanding its “Home” department (category 2). Add two subcategories with one query: “Garden” and “Bathroom”.
The two new categories in “Home”:
Solution
INSERT INTO categories (name, parent_id)
VALUES ('Garden', 2),
('Bathroom', 2);INSERT INTO categories (name, parent_id) VALUES ('Garden', 2), ('Bathroom', 2). parent_id is 2 because they are part of “Home”. Their numbers are 9 and 10.
2. Ognyan Slavov from Varna signs up today with the email ognyan.s@example.com. The website wants to show him his profile straight away, with the number the database gave him. Save him and return the whole new row with the same query.
The new customer, the whole row:
Solution
INSERT INTO customers (name, email, city, joined_at)
VALUES ('Ognyan Slavov', 'ognyan.s@example.com', 'Varna', '2026-10-25')
RETURNING *;INSERT INTO customers (name, email, city, joined_at) VALUES ('Ognyan Slavov', 'ognyan.s@example.com', 'Varna', '2026-10-25') RETURNING *. He is customer 21.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: WHERE: filtering rows · NULL: no value · Primary key and foreign key · JOIN: two tables in one query
- Continues in: UPDATE: changing rows · DELETE: deleting rows · UPSERT: insert or update · The database: from creating it to DROP
- Tasks on this topic: Changing data