| Lesson 8 | Inserting data from values in another table |
| Objective | Construct INSERT Transact-SQL statements that retrieve data from another table. |
Inserting Data from Another Table
The previous lesson covered SQL Server's punctuation rules for literal values: which datatypes need quotes, and how to escape an apostrophe inside a string. This lesson moves from values you type yourself to values that already live somewhere else in your database. Instead of writing out a VALUES list by hand, you will construct INSERT statements that pull their data directly from another table's rows.
Why Pull Data from Another Table?
Copying rows from one table into another is one of the most common tasks in day-to-day database work. A few typical scenarios:
- Archiving: moving completed orders out of an active table and into a history table.
- Auditing: recording a "before" snapshot of a row just before it changes, so you can see what it used to look like.
- Staging and reporting: populating a summary or reporting table from detailed transactional data.
- Migration: copying selected rows from one system's table into another's during a data migration.
In each case, the source data already exists; you just need to get it into a new location, sometimes transforming it slightly along the way.
The INSERT INTO ... SELECT Statement
The core tool for this job is
INSERT INTO ... SELECT:
INSERT INTO target_table (column1, column2, ...)
SELECT column1, column2, ...
FROM source_table
WHERE search_condition;
A few rules govern how this works:
- The
SELECT list must line up with the INSERT column list, in the same order, with matching (or implicitly convertible) data types.
- The
WHERE clause filters the source table, not the target: it decides which source rows get copied.
- You can pull static or calculated values into the mix alongside columns, as you will see below.
target_table must already exist; if it doesn't, see SELECT ... INTO further down.
Worked Example: Widget Sales Service
Suppose Widget Sales Service (WSS) wants to track salary history for its employees. Every time an employee receives a raise, WSS needs to record that employee's salary immediately before the raise, along with the date the raise took effect. WSS already has a SalaryHistory table for this purpose:
| LastName | FirstName | PriorSalary | RaiseDate |
| Smith | John | 50000 | 2025-01-01 |
| Jones | Fred | 60000 | 2025-06-15 |
The values WSS needs to insert, last name, first name, and current (soon-to-be-prior) salary, already live in the company's Employee table:
| LastName | FirstName | Dept | CurrentSalary |
| Duck | Donald | ENG | 10000 |
| Mouse | Mickey | MFG | 20000 |
| Mouse | Minnie | RES | 30000 |
Mickey Gets a Raise
Mickey Mouse has earned a raise, from $20,000 to $25,000. Before WSS applies that raise in the
Employee table, it needs a
SalaryHistory record capturing his prior salary and the date of the change. Both the salary and the row-selection values come straight from
Employee, so a single
INSERT ... SELECT statement handles it:
INSERT INTO SalaryHistory (LastName, FirstName, PriorSalary, RaiseDate)
SELECT LastName, FirstName, CurrentSalary, GETDATE()
FROM Employee
WHERE LastName = N'Mouse'
AND FirstName = N'Mickey';
Notice that
RaiseDate isn't pulled from
Employee at all, since
Employee has no such column; it's generated on the spot with
GETDATE(). This is a good illustration of the point above: a
SELECT list can freely mix columns copied from the source table with literal or calculated values, as long as the total column count and order still match the
INSERT list. After this statement runs,
SalaryHistory looks like this:
| LastName | FirstName | PriorSalary | RaiseDate |
| Smith | John | 50000 | 2025-01-01 |
| Jones | Fred | 60000 | 2025-06-15 |
| Mouse | Mickey | 20000 | (today's date) |
With the history recorded, a separate UPDATE statement (covered in an earlier lesson) would then raise Mickey's actual salary in the Employee table. If you want a higher-precision timestamp than GETDATE() provides, SYSDATETIME() returns a datetime2 value accurate to 100 nanoseconds; either works here, and both remain fully current in SQL Server 2025.
Matching Columns and Handling IDENTITY Columns
The column-matching rule above has one common wrinkle:
IDENTITY columns. If your target table has an
IDENTITY column (an auto-incrementing primary key, for example), leave it out of both the
INSERT column list and the
SELECT list. SQL Server generates the next value automatically:
INSERT INTO SalaryHistory (LastName, FirstName, PriorSalary, RaiseDate)
SELECT LastName, FirstName, CurrentSalary, GETDATE()
FROM Employee
WHERE LastName = N'Mouse' AND FirstName = N'Mickey';
-- SalaryHistoryID (an IDENTITY column) is generated automatically, not supplied here
Occasionally you need to preserve specific ID values from the source table instead, for example when migrating historical records from an old system where those IDs are still referenced elsewhere. Suppose WSS is migrating rows from a legacy
OldSalaryHistoryArchive table that already has its own
SalaryHistoryID values assigned. SQL Server allows this, but only with
IDENTITY_INSERT turned on explicitly for the duration of the statement:
SET IDENTITY_INSERT SalaryHistory ON;
INSERT INTO SalaryHistory (SalaryHistoryID, LastName, FirstName, PriorSalary, RaiseDate)
SELECT SalaryHistoryID, LastName, FirstName, PriorSalary, RaiseDate
FROM OldSalaryHistoryArchive;
SET IDENTITY_INSERT SalaryHistory OFF;
Only one table per session can have
IDENTITY_INSERT on at a time, so always turn it back off once you're done.
Pulling from More Than One Table
The SELECT in an INSERT ... SELECT statement is a normal SELECT, which means it can join multiple source tables together, not just read from one. Suppose WSS also keeps a small Department lookup table translating the department codes on Employee into full names:
| DeptCode | DeptName |
| ENG | Engineering |
| MFG | Manufacturing |
| RES | Research |
To build an
EmployeeDirectory table that lists each employee alongside their full department name, rather than the bare code, join
Employee to
Department inside the
SELECT:
INSERT INTO EmployeeDirectory (LastName, FirstName, DeptName)
SELECT e.LastName, e.FirstName, d.DeptName
FROM Employee AS e
JOIN Department AS d
ON e.Dept = d.DeptCode;
Table aliases (
e,
d) keep the column references unambiguous once more than one table is in play. Everything from the syntax rules above still applies: the
SELECT list, now assembled from two tables instead of one, still has to line up column-for-column with the
INSERT list.
Creating a Brand-New Table: SELECT ... INTO
Everything so far assumes
SalaryHistory already exists. If you instead needed to spin up a brand-new table, say, a one-off snapshot of every WSS employee earning over $25,000,
SELECT ... INTO builds the table and populates it in a single step:
SELECT LastName, FirstName, Dept, CurrentSalary
INTO HighEarners
FROM Employee
WHERE CurrentSalary >= 25000;
HighEarners doesn't need to exist beforehand; SQL Server creates it with column names and data types inferred from the
SELECT list. There's an important limitation to keep in mind, though:
SELECT ... INTO copies column names, data types, and nullability, but it does
not copy primary keys, foreign keys, indexes, triggers, or default constraints from the source table. Those have to be added separately with their own
ALTER TABLE or
CREATE INDEX statements if the new table needs them.
Duplicate Rows and a Preview of MERGE
INSERT ... SELECT has no built-in awareness of what's already in the target table. Suppose WSS accidentally runs the statement for Mickey Mouse a second time:
INSERT INTO SalaryHistory (LastName, FirstName, PriorSalary, RaiseDate)
SELECT LastName, FirstName, CurrentSalary, GETDATE()
FROM Employee
WHERE LastName = N'Mouse' AND FirstName = N'Mickey';
-- Run a second time by mistake: SalaryHistory now has TWO Mickey Mouse rows
Nothing about the statement itself prevents this. It succeeds both times and quietly leaves a duplicate
SalaryHistory row behind, which is exactly the kind of trap a multi-step "insert the salary, then insert the salary again with a date added" approach could fall into, so it is worth watching for whenever a table doesn't have a constraint stopping it.
For situations where you need to insert only the rows that are genuinely new, while updating the ones that already exist, SQL Server offers the
MERGE statement, an "upsert" (update-or-insert) in a single pass:
MERGE INTO SalaryHistory AS Target
USING (SELECT LastName, FirstName, CurrentSalary, GETDATE() AS RaiseDate
FROM Employee
WHERE LastName = N'Mouse' AND FirstName = N'Mickey') AS Source
ON Target.LastName = Source.LastName AND Target.FirstName = Source.FirstName
WHEN NOT MATCHED THEN
INSERT (LastName, FirstName, PriorSalary, RaiseDate)
VALUES (Source.LastName, Source.FirstName, Source.CurrentSalary, Source.RaiseDate);
Run this twice and the second run inserts nothing, since
WHEN NOT MATCHED only fires when no matching row already exists.
MERGE has its own full syntax and its own set of caveats, which this module covers in more depth separately; for now, it's worth knowing the tool exists so you reach for it deliberately rather than layering repeated
INSERT ... SELECT statements and hoping duplicates don't happen.
Capturing a Stored Procedure's Results: INSERT INTO ... EXEC
Sometimes the data you want to insert isn't sitting in a plain table at all, it's the output of a stored procedure.
INSERT INTO ... EXEC routes that procedure's result set directly into a table:
INSERT INTO MonthlySalesSummary (Region, TotalSales, OrderCount)
EXEC usp_GetMonthlyMetrics @Year = 2025, @Month = 10;
The procedure's result set must match the target column list the same way a
SELECT list would. This is a convenient way to capture and store the output of reporting procedures, or dynamic SQL batches, without writing a separate query to recreate that same logic.
Performance and Auditing Notes
Inserting Data Values - Exercise
Ready to practice? The exercise below gives you another scenario for inserting data pulled from one table into another, so you can work through the column-matching and
WHERE-filtering rules on your own.
Inserting Data Values - Exercise
In the next lesson, you will learn how to update data using values from another table.
