12.1 Single and Multi-Row INSERT Syntax
Let’s get one thing straight: you’re going to insert data. A lot of it. And while inserting one row at a time is like carefully placing individual bricks, we’re building a cathedral here. We need to move with purpose. The INSERT statement is your workhorse, and understanding its full syntax is the difference between being a casual user and a power user who doesn’t waste cycles.
The basic, single-row INSERT is straightforward. You’re telling the database exactly what to put where.
INSERT INTO users (id, username, email)
VALUES (1, 'admin', 'admin@example.com');
Simple, right? The column list (id, username, email) is your target. The VALUES clause is your payload. But here’s the first “gotcha”: the column list is optional. If you omit it, you’re betting that your VALUES clause will have exactly the right number of values in exactly the right order as the table definition. This is a spectacularly bad bet for anything but a quick throwaway script. If someone adds a created_at column with a default to that table, your insert suddenly breaks. Always specify the columns. It’s self-documenting and robust. Don’t be lazy.
Multi-Row Insert: Your New Best Friend
Now, let’s say you have ten users to add. The absolute worst thing you can do is fire off ten individual INSERT statements. The network overhead alone is a crime. This is where multi-row insert comes in, and it’s the first major performance win you’ll get.
INSERT INTO users (id, username, email)
VALUES
(2, 'jane_doe', 'jane@example.com'),
(3, 'john_smith', 'john@example.com'),
(4, 'alice_wonder', 'alice@example.com');
See that? One statement, three rows. The database can optimize the heck out of this single transaction. It’s one round trip to the server, one round of parsing, and it’s vastly more efficient. There’s a practical limit to how many rows you can shove into a single statement (it depends on max_allowed_packet in MySQL or work_mem in PostgreSQL, among other things), but for batches of thousands of rows, it’s a game-changer.
Inserting Data From Another Query (The Power Move)
Sometimes the data you need to insert already lives in another table. Maybe you’re archiving old records or creating a summary table. You could write a script to select the data, process it, and generate a giant VALUES list. Or, you could be smart and let the database do it for you in a single, elegant operation.
INSERT INTO user_archives (user_id, username, archived_at)
SELECT id, username, NOW()
FROM users
WHERE created_at < '2020-01-01';
This is incredibly powerful. The SELECT statement can be as complex as you need—with joins, filters, transformations, you name it. The database will execute the select and feed the results directly into the insert. It’s the most efficient way to move data around within the database itself. There’s no marshaling data back to your application only to send it right back.
The RETURNING Clause: A Glimpse Into the Machine
Here’s a feature that feels like a minor superpower, especially if you’re coming from other databases that make you jump through hoops to get this info. The RETURNING clause (a PostgreSQL gem) gives you immediate feedback on what was actually inserted. This is crucial for getting autogenerated values like SERIAL or BIGSERIAL IDs back without making a separate query.
INSERT INTO products (name, price)
VALUES ('Mind-Blowing Technical Book', 49.99)
RETURNING id, created_at;
This statement doesn’t just insert the row; it returns the resulting row, or the specific columns you ask for. So in one go, you insert the product and immediately get its newly generated id and the timestamp created_at that the database assigned. It eliminates a whole class of “insert-then-select” logic from your code, making it cleaner and faster. It’s the database saying, “Here, I handled that for you. You’re welcome.”