AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

11 Modifying data / Lesson 1 of 5

INSERT — adding rows

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

How do you insert data into a table in SQL?

Use INSERT INTO table (col1, col2) VALUES (v1, v2);. Name the columns explicitly so the statement survives schema changes; columns you leave out get their DEFAULT, NULL or the next AUTO_INCREMENT value. One statement can insert many rows with several parenthesised value lists, and INSERT INTO ... SELECT copies rows from a query instead of literal values.

Everything in the earlier modules read data. From here on the statements change it, and the first one is INSERT: it adds new rows to a table. This lesson covers the single-row and multi-row forms, the column list and why it matters, what happens to the columns you leave out (defaults, NULL and AUTO_INCREMENT), how to read back a generated id, and INSERT INTO ... SELECT, which inserts the result of a query. The examples use MySQL 8 with InnoDB tables; the differences that come up in PostgreSQL interviews are called out where they matter.

The basic form

INSERT INTO products (id, name, category, price, in_stock)
VALUES (101, 'Desk lamp', 'lighting', 24.99, 12);

The statement names the table, then a column list in parentheses, then VALUES with one value per listed column, in the same order. Strings and dates are quoted with single quotes ('2024-03-01'), numbers are not, and NULL is written bare. A single quote inside a string is doubled: 'O''Brien'.

You may leave out the column list:

INSERT INTO products VALUES (102, 'Floor lamp', 'lighting', 79.00, 4);

This only works when you supply a value for every column in the order the table declares them, and it is a habit to avoid. The day someone adds a column, the statement either fails ("Column count doesn't match value count") or quietly puts values into the wrong columns if the types happen to fit. Always write the column list; it doubles as documentation.

Many rows in one statement

INSERT INTO products (id, name, category, price, in_stock)
VALUES
  (103, 'Bulb 9W',   'lighting', 3.50, 200),
  (104, 'Bulb 12W',  'lighting', 4.25, 150),
  (105, 'Extension', 'power',   15.00,  30);

Several value lists separated by commas insert several rows. It is faster than one statement per row (one parse, one round trip, one log flush under autocommit) and it is atomic: if row three violates a constraint, none of the three are inserted. Bulk loads of thousands of rows are usually batched this way, a few hundred or a thousand rows per statement.

The columns you leave out

A column missing from the column list gets, in order of preference:

  1. its DEFAULT value, if the table declares one;
  2. the next number, if it is an AUTO_INCREMENT column;
  3. NULL, if the column allows it;
  4. otherwise an error in MySQL's default strict mode ("Field 'x' doesn't have a default value").
CREATE TABLE tickets (
  id       INT AUTO_INCREMENT PRIMARY KEY,
  subject  VARCHAR(100) NOT NULL,
  status   VARCHAR(10)  NOT NULL DEFAULT 'open',
  assignee VARCHAR(40)
);

INSERT INTO tickets (subject) VALUES ('Printer jams'), ('VPN drops');
SELECT id, subject, status, assignee FROM tickets ORDER BY id;
idsubjectstatusassignee
1Printer jamsopenNULL
2VPN dropsopenNULL

The id came from AUTO_INCREMENT, status from its default, assignee is NULL. Writing NULL explicitly for a column with a default does not give you the default — you asked for NULL, and a NOT NULL column refuses it. To ask for the default by name, MySQL accepts the keyword DEFAULT in a value list (VALUES (DEFAULT, 'x')); leaving the column out of the list is the portable way.

Generated ids and LAST_INSERT_ID

When the database numbers the rows, the application usually needs the number it just got — to insert the order's lines, say. In MySQL:

INSERT INTO orders (customer_id, placed_on) VALUES (42, '2024-05-01');
SELECT LAST_INSERT_ID();          -- the id generated for that row

LAST_INSERT_ID() is per connection, so two sessions inserting at once each see their own value — never compute "the next id" with SELECT MAX(id) + 1, which two sessions can read at the same moment. After a multi-row insert it returns the id of the first row. In PostgreSQL the idiom is INSERT ... RETURNING id, which returns the generated columns as a result set; MySQL has no RETURNING.

Two facts about InnoDB's counter that interviewers like: ids are not guaranteed to be gap-free (a rolled-back insert still consumes its number), and a deleted id is never handed out again — the counter only moves forward. Treat surrogate ids as labels, not as a count of rows.

Copying rows with INSERT ... SELECT

Instead of literal values, an INSERT can take the rows of a query:

INSERT INTO orders_archive (id, customer, total, placed_on)
SELECT id, customer, total, placed_on
FROM orders
WHERE placed_on < '2023-01-01';

The SELECT can be anything — joins, GROUP BY, DISTINCT — as long as it returns one column per listed column. The match is by position, not by name: the SELECT's third column goes into the INSERT's third column whatever either is called, so keep the two lists in the same order. This is how you archive, fill a summary table, or move rows from a staging table after an import:

INSERT INTO daily_sales (sale_date, orders, revenue)
SELECT placed_on, COUNT(*), SUM(total)
FROM orders
GROUP BY placed_on;

A close cousin creates the table and fills it in one go: CREATE TABLE top_customers AS SELECT .... It copies column names and types from the query but not keys, indexes or defaults, so it suits scratch and reporting tables rather than real schema (the schema-design module returns to it).

When an INSERT fails

An insert that breaks a rule is refused and changes nothing:

  • a duplicate value in a PRIMARY KEY or UNIQUE column — "Duplicate entry '7' for key 'PRIMARY'";
  • NULL (or a missing value with no default) in a NOT NULL column;
  • a value that fails a CHECK constraint (MySQL 8.0.16 and later);
  • in strict mode, a string too long for its VARCHAR, or a value of the wrong type.

What to do with duplicates on purpose — skip them, overwrite them or update them — is the subject of the upserts lesson. One environment note: the practice engine here is MySQL syntax on SQLite, which does not truncate or reject an over-long string the way strict MySQL does, so the exercises never rely on that.

Pitfalls

  • An INSERT without a column list, which breaks when the table changes.
  • Expecting NULL to mean "use the default".
  • Computing ids with MAX(id) + 1 instead of AUTO_INCREMENT.
  • An INSERT ... SELECT whose two column lists are in different orders — the types often match, so nothing complains.
  • Thousands of single-row inserts under autocommit, each a separate transaction and log flush; batch them.

Key takeaways

  • INSERT INTO t (cols) VALUES (...), (...); — always the column list; many rows per statement.
  • Omitted columns take their DEFAULT, an AUTO_INCREMENT number or NULL; explicit NULL is not the default.
  • LAST_INSERT_ID() (MySQL) or RETURNING (PostgreSQL) for the generated id; never MAX(id) + 1.
  • INSERT INTO ... SELECT copies query results, matched by position.
  • A constraint violation rejects the whole statement.

Common questions

Why should I always list the columns in an INSERT?

Without a column list the values must match every column of the table in its declared order. Add a column later and every such INSERT breaks or, worse, puts a value in the wrong column. A column list documents intent, lets defaults fill the rest and keeps working when the table grows.

How do I insert multiple rows at once in MySQL?

Put several value lists after VALUES, separated by commas: INSERT INTO t (a, b) VALUES (1, 'x'), (2, 'y'), (3, 'z');. It is one statement, so it is parsed once, sent in one round trip and, under InnoDB, either all rows go in or none do if one fails.

How do I get the id of the row I just inserted in MySQL?

Call LAST_INSERT_ID() right after the INSERT on the same connection. It returns the first AUTO_INCREMENT value generated by the most recent INSERT in that session, so it is safe under concurrency. PostgreSQL uses INSERT ... RETURNING id instead, and drivers expose the value directly.

What is INSERT INTO SELECT used for?

It inserts the rows a query returns rather than literal values: archiving old rows, filling a summary table, copying from a staging table after an import. The SELECT's columns are matched to the INSERT's column list by position, not by name, so list both in the same order.

Mark as readSign in to keep your place.

SQL INSERT INTO Explained: Single, Multi-Row and INSERT SELECT

INSERT INTO adds rows to a table. Name the columns, insert many rows at once, let defaults and auto-increment fill the rest, or copy rows with a SELECT.

  • Course: SQL study plan
  • Module: Modifying data
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: MySQL 8 syntax on SQLite 3.49

How do you insert data into a table in SQL?

Use INSERT INTO table (col1, col2) VALUES (v1, v2);. Name the columns explicitly so the statement survives schema changes; columns you leave out get their DEFAULT, NULL or the next AUTO_INCREMENT value. One statement can insert many rows with several parenthesised value lists, and INSERT INTO ... SELECT copies rows from a query instead of literal values.

Lesson

Everything in the earlier modules read data. From here on the statements change it, and the first one is INSERT: it adds new rows to a table. This lesson covers the single-row and multi-row forms, the column list and why it matters, what happens to the columns you leave out (defaults, NULL and AUTO_INCREMENT), how to read back a generated id, and INSERT INTO ... SELECT, which inserts the result of a query. The examples use MySQL 8 with InnoDB tables; the differences that come up in PostgreSQL interviews are called out where they matter.

The basic form

INSERT INTO products (id, name, category, price, in_stock)
VALUES (101, 'Desk lamp', 'lighting', 24.99, 12);

The statement names the table, then a column list in parentheses, then VALUES with one value per listed column, in the same order. Strings and dates are quoted with single quotes ('2024-03-01'), numbers are not, and NULL is written bare. A single quote inside a string is doubled: 'O''Brien'.

You may leave out the column list:

INSERT INTO products VALUES (102, 'Floor lamp', 'lighting', 79.00, 4);

This only works when you supply a value for every column in the order the table declares them, and it is a habit to avoid. The day someone adds a column, the statement either fails ("Column count doesn't match value count") or quietly puts values into the wrong columns if the types happen to fit. Always write the column list; it doubles as documentation.

Many rows in one statement

INSERT INTO products (id, name, category, price, in_stock)
VALUES
  (103, 'Bulb 9W',   'lighting', 3.50, 200),
  (104, 'Bulb 12W',  'lighting', 4.25, 150),
  (105, 'Extension', 'power',   15.00,  30);

Several value lists separated by commas insert several rows. It is faster than one statement per row (one parse, one round trip, one log flush under autocommit) and it is atomic: if row three violates a constraint, none of the three are inserted. Bulk loads of thousands of rows are usually batched this way, a few hundred or a thousand rows per statement.

The columns you leave out

A column missing from the column list gets, in order of preference:

  1. its DEFAULT value, if the table declares one;
  2. the next number, if it is an AUTO_INCREMENT column;
  3. NULL, if the column allows it;
  4. otherwise an error in MySQL's default strict mode ("Field 'x' doesn't have a default value").
CREATE TABLE tickets (
  id       INT AUTO_INCREMENT PRIMARY KEY,
  subject  VARCHAR(100) NOT NULL,
  status   VARCHAR(10)  NOT NULL DEFAULT 'open',
  assignee VARCHAR(40)
);

INSERT INTO tickets (subject) VALUES ('Printer jams'), ('VPN drops');
SELECT id, subject, status, assignee FROM tickets ORDER BY id;
idsubjectstatusassignee
1Printer jamsopenNULL
2VPN dropsopenNULL

The id came from AUTO_INCREMENT, status from its default, assignee is NULL. Writing NULL explicitly for a column with a default does not give you the default — you asked for NULL, and a NOT NULL column refuses it. To ask for the default by name, MySQL accepts the keyword DEFAULT in a value list (VALUES (DEFAULT, 'x')); leaving the column out of the list is the portable way.

Generated ids and LAST_INSERT_ID

When the database numbers the rows, the application usually needs the number it just got — to insert the order's lines, say. In MySQL:

INSERT INTO orders (customer_id, placed_on) VALUES (42, '2024-05-01');
SELECT LAST_INSERT_ID();          -- the id generated for that row

LAST_INSERT_ID() is per connection, so two sessions inserting at once each see their own value — never compute "the next id" with SELECT MAX(id) + 1, which two sessions can read at the same moment. After a multi-row insert it returns the id of the first row. In PostgreSQL the idiom is INSERT ... RETURNING id, which returns the generated columns as a result set; MySQL has no RETURNING.

Two facts about InnoDB's counter that interviewers like: ids are not guaranteed to be gap-free (a rolled-back insert still consumes its number), and a deleted id is never handed out again — the counter only moves forward. Treat surrogate ids as labels, not as a count of rows.

Copying rows with INSERT ... SELECT

Instead of literal values, an INSERT can take the rows of a query:

INSERT INTO orders_archive (id, customer, total, placed_on)
SELECT id, customer, total, placed_on
FROM orders
WHERE placed_on < '2023-01-01';

The SELECT can be anything — joins, GROUP BY, DISTINCT — as long as it returns one column per listed column. The match is by position, not by name: the SELECT's third column goes into the INSERT's third column whatever either is called, so keep the two lists in the same order. This is how you archive, fill a summary table, or move rows from a staging table after an import:

INSERT INTO daily_sales (sale_date, orders, revenue)
SELECT placed_on, COUNT(*), SUM(total)
FROM orders
GROUP BY placed_on;

A close cousin creates the table and fills it in one go: CREATE TABLE top_customers AS SELECT .... It copies column names and types from the query but not keys, indexes or defaults, so it suits scratch and reporting tables rather than real schema (the schema-design module returns to it).

When an INSERT fails

An insert that breaks a rule is refused and changes nothing:

  • a duplicate value in a PRIMARY KEY or UNIQUE column — "Duplicate entry '7' for key 'PRIMARY'";
  • NULL (or a missing value with no default) in a NOT NULL column;
  • a value that fails a CHECK constraint (MySQL 8.0.16 and later);
  • in strict mode, a string too long for its VARCHAR, or a value of the wrong type.

What to do with duplicates on purpose — skip them, overwrite them or update them — is the subject of the upserts lesson. One environment note: the practice engine here is MySQL syntax on SQLite, which does not truncate or reject an over-long string the way strict MySQL does, so the exercises never rely on that.

Pitfalls

  • An INSERT without a column list, which breaks when the table changes.
  • Expecting NULL to mean "use the default".
  • Computing ids with MAX(id) + 1 instead of AUTO_INCREMENT.
  • An INSERT ... SELECT whose two column lists are in different orders — the types often match, so nothing complains.
  • Thousands of single-row inserts under autocommit, each a separate transaction and log flush; batch them.

Key takeaways

  • INSERT INTO t (cols) VALUES (...), (...); — always the column list; many rows per statement.
  • Omitted columns take their DEFAULT, an AUTO_INCREMENT number or NULL; explicit NULL is not the default.
  • LAST_INSERT_ID() (MySQL) or RETURNING (PostgreSQL) for the generated id; never MAX(id) + 1.
  • INSERT INTO ... SELECT copies query results, matched by position.
  • A constraint violation rejects the whole statement.

Common questions

Why should I always list the columns in an INSERT?

Without a column list the values must match every column of the table in its declared order. Add a column later and every such INSERT breaks or, worse, puts a value in the wrong column. A column list documents intent, lets defaults fill the rest and keeps working when the table grows.

How do I insert multiple rows at once in MySQL?

Put several value lists after VALUES, separated by commas: INSERT INTO t (a, b) VALUES (1, 'x'), (2, 'y'), (3, 'z');. It is one statement, so it is parsed once, sent in one round trip and, under InnoDB, either all rows go in or none do if one fails.

How do I get the id of the row I just inserted in MySQL?

Call LAST_INSERT_ID() right after the INSERT on the same connection. It returns the first AUTO_INCREMENT value generated by the most recent INSERT in that session, so it is safe under concurrency. PostgreSQL uses INSERT ... RETURNING id instead, and drivers expose the value directly.

What is INSERT INTO SELECT used for?

It inserts the rows a query returns rather than literal values: archiving old rows, filling a summary table, copying from a staging table after an import. The SELECT's columns are matched to the INSERT's column list by position, not by name, so list both in the same order.

Exercises

Stock a new product line

The shop is adding three products. Insert them into products, always with an explicit column list — the first two in one multi-row INSERT:

idnamecategorypricein_stock
201USB-C cablecables9.9940
202HDMI cablecables12.5025
203Cable tiesaccessories3.00(not given — use the column's default)

Leave in_stock out of the third product's column list so the table's DEFAULT applies — don't write the default's value yourself (it is not always 0). Then show every product: columns id, name, price, in_stock, ordered by id.

The table already holds:

idnamecategorypricein_stock
101Desk lamplighting24.9912
102Floor lamplighting79.004

The judge prints numbers without trailing zeros (79.00 as 79, 12.50 as 12.5).

id | name | price | in_stock
101 | Desk lamp | 24.99 | 12
102 | Floor lamp | 79 | 4
201 | USB-C cable | 9.99 | 40
202 | HDMI cable | 12.5 | 25
203 | Cable ties | 3 | 0

Archive last year's orders

Copy every order placed before 2024-01-01 from orders into orders_archive with one INSERT ... SELECT, setting archived_on to '2024-06-30' for each copied row. Do not change orders. Then show the archive: columns id, customer, total, archived_on, ordered by id.

orders:

idcustomertotalplaced_on
1Asha120.002023-11-02
2Ben45.502023-12-31
3Chen80.002024-01-01
4Asha15.252024-02-14
5Dev60.002023-07-19

orders_archive has the same columns plus archived_on DATE, and starts empty here (it may already hold rows — keep them).

id | customer | total | archived_on
1 | Asha | 120 | 2024-06-30
2 | Ben | 45.5 | 2024-06-30
5 | Dev | 60 | 2024-06-30

In this module: Modifying data

← Checkpoint — Window functions · UPDATE — changing rows →