| Lesson 8 | Using the INSERT statement |
| Objective | How to Use SQL INSERT Statement to add Information |
The statement opens with the keywords INSERT INTO followed immediately by the name of the table that will receive the new row. INSERT is the command; INTO is required by the SQL standard; the table name identifies the target:
INSERT INTO BasicTable
This tells the engine: "I want to add a row to BasicTable." The engine verifies that BasicTable exists and that you have INSERT permission before proceeding.
After the table name, an optional parenthesized list specifies which columns the INSERT will populate. Columns not listed will receive their default value — NULL for NULL-able columns, the column's DEFAULT value if one was defined, or a constraint violation if the column is NOT NULL with no default:
INSERT INTO BasicTable
(Lastname, Firstname)
Providing the column list is strongly recommended. It makes the INSERT statement self-documenting, protects against errors if the table's column order changes in the future, and allows you to omit columns intentionally.
The VALUES keyword introduces the list of data values to insert. The values must correspond positionally to the columns listed — the first value goes into the first listed column, the second into the second, and so on. String and date values are enclosed in single quotes; numeric values are inserted without quotes:
VALUES ('Gauss', 'Carl')
INSERT INTO BasicTable
(Lastname, Firstname)
VALUES ('Gauss', 'Carl');
This inserts one row into BasicTable with Lastname = 'Gauss' and Firstname = 'Carl'. The statement ends with a semicolon — the standard SQL statement terminator.
INSERT INTO BasicTable
(Lastname, Firstname)
VALUES ('Gauss', 'Carl');
The resulting row reflects the values provided for the named columns and NULL for the omitted column:
| Firstname | Lastname | PhoneNumber |
|---|---|---|
| Carl | Gauss | (null) |
The column list can be omitted if the VALUES list provides a value for every column in the table, in the exact order the columns were defined in CREATE TABLE. For BasicTable — defined as Firstname, Lastname, PhoneNumber — the column-list-free form requires all three values:
INSERT INTO BasicTable
VALUES ('Carl', 'Gauss', '555-0192');
This form is more fragile than the column list form. If the table's column order is ever changed with ALTER TABLE, or if a new column is added, column-list-free INSERTs will break or silently insert values into the wrong columns. The column list form is preferred for all production code.
Providing fewer values than the table has columns — without a column list to specify which columns receive values — produces a constraint error. The engine cannot determine which columns to leave at their defaults and which to populate:
-- This will fail: BasicTable has 3 columns, only 2 values provided
INSERT INTO BasicTable
VALUES ('Carl', 'Gauss');
The error message varies by engine but conveys the same problem — the number of values does not match the number of columns:
-- Generic SQL error (wording varies by engine):
-- ERROR: INSERT has more target columns than expressions
-- or: column name or number of supplied values does not match table definition
The fix is either to add the missing value, or to use the column list form and specify only the columns being populated.
INSERT INTO BasicTable (Lastname, Firstname)
VALUES ('Riemann', 'Bernhard');
This is correct, portable SQL that works in PostgreSQL, MySQL, SQL Server, Oracle, and SQLite without modification.
-- Double quotes around an identifier that would otherwise be a reserved word
SELECT "order", "date" FROM BasicTable;
Using double quotes around string values like "Gauss" is a common mistake. Some engines — MySQL with its default settings — accept double-quoted strings, but this is non-standard behavior. PostgreSQL treats double-quoted strings as identifier references and will produce an error or return unexpected results. Write single-quoted strings from the beginning and the code will be portable across all major database engines.
ANSI_QUOTES SQL mode is enabled. SQL Server accepts both single and double quotes for strings in some contexts. PostgreSQL strictly follows the standard: double quotes are identifiers only. To write portable SQL that works without modification across engines, always use single quotes for string literals.
INSERT INTO BasicTable (Lastname, Firstname, PhoneNumber)
VALUES ('Gauss', 'Carl', '555-0192');
INSERT INTO BasicTable (Lastname, Firstname, PhoneNumber)
VALUES ('Riemann', 'Bernhard', '555-0317');
INSERT INTO BasicTable (Lastname, Firstname, PhoneNumber)
VALUES ('Hilbert', 'David', '555-0437');
Each statement inserts one row and triggers one update to each index defined on BasicTable — as covered in lesson 7's write overhead discussion.
INSERT INTO BasicTable (Lastname, Firstname, PhoneNumber)
VALUES
('Gauss', 'Carl', '555-0192'),
('Riemann', 'Bernhard', '555-0317'),
('Hilbert', 'David', '555-0437');
This single statement inserts all three rows atomically — either all three are inserted successfully or none are, if a constraint violation occurs on any row. Multi-row INSERT is supported in PostgreSQL, MySQL/MariaDB, SQL Server (2008+), and SQLite. Oracle requires a different syntax using multiple SELECT clauses or a different INSERT ALL form.
CREATE TABLE Contacts (
ContactID INT NOT NULL AUTO_INCREMENT,
Firstname CHAR(50) NULL,
Lastname CHAR(50) NOT NULL,
PhoneNumber CHAR(10) NULL,
PRIMARY KEY (ContactID)
);
When inserting into this table, omit ContactID from the column list. The engine assigns the next available integer value automatically — you never need to track or supply the primary key value for auto-generated keys:
INSERT INTO Contacts (Lastname, Firstname, PhoneNumber)
VALUES ('Gauss', 'Carl', '555-0192');
INSERT INTO Contacts (Lastname, Firstname, PhoneNumber)
VALUES ('Riemann', 'Bernhard', '555-0317');
INSERT INTO Contacts (Lastname, Firstname, PhoneNumber)
VALUES ('Hilbert', 'David', '555-0437');
After these three INSERTs, ContactID values 1, 2, and 3 are assigned automatically.
AUTO_INCREMENT. PostgreSQL uses SERIAL (shorthand for an integer column with a sequence) or the SQL standard GENERATED ALWAYS AS IDENTITY syntax introduced in PostgreSQL 10. SQL Server uses IDENTITY(1,1). The INSERT syntax for omitting the column is the same across all engines — only the CREATE TABLE definition differs.
-- PhoneNumber receives NULL because it is omitted
INSERT INTO BasicTable (Lastname, Firstname)
VALUES ('Euler', 'Leonhard');
-- This fails: Lastname is NOT NULL with no default
INSERT INTO Contacts (Firstname, PhoneNumber)
VALUES ('Leonhard', '555-0512');
The error confirms that the constraint is enforced at the database level, independent of any application logic.
INSERT INTO BasicTable (Lastname, Firstname, PhoneNumber)
VALUES ('Euler', 'Leonhard', NULL);
-- If ContactID 1 already exists, this INSERT fails
INSERT INTO Contacts (ContactID, Lastname, Firstname)
VALUES (1, 'Euler', 'Leonhard');
-- ERROR: duplicate key value violates unique constraint "pk_contacts"
-- If idx_unique_email exists and 'gauss@example.com' already has a row:
INSERT INTO users (email, username)
VALUES ('gauss@example.com', 'cg1777');
-- ERROR: duplicate key value violates unique constraint "idx_unique_email"
-- If ContactID 99 does not exist in Contacts:
INSERT INTO Orders (OrderID, ContactID, OrderDate)
VALUES (1001, 99, '2026-04-16');
-- ERROR: insert or update on table "orders" violates
-- foreign key constraint "fk_orders_contactid"
-- Copy all contacts with a phone number into a PhoneDirectory table
INSERT INTO PhoneDirectory (Lastname, Firstname, PhoneNumber)
SELECT Lastname, Firstname, PhoneNumber
FROM Contacts
WHERE PhoneNumber IS NOT NULL;
The SELECT can include WHERE conditions, JOINs, expressions, and any other valid SQL — the INSERT receives whatever rows the SELECT produces. The column count and compatible data types between the SELECT output columns and the INSERT target columns must match.
SELECT * FROM BasicTable;
The asterisk (*) is shorthand for "all columns." This is a useful verification tool during development and testing. In production queries, specifying column names explicitly is preferred over SELECT * — it makes the query's intent clear, avoids returning unnecessary data, and protects against unexpected results if the table's column list changes.