What it means
A multi-part identifier is a name with dots in it: c.id, customers.name, dbo.customers.name.
SQL Server reads everything before the last dot as a table, or a table’s alias, and looks for it
among the tables the statement can see at that point. Error 4104 means there’s no table by that name
there, so SQL Server can’t tie (“bind”) the column to anything. It finds this while compiling the
batch, so nothing in the batch runs.
Msg 4104, Level 16, State 1, Server 7732422b7f56, Line 1
The multi-part identifier "customers.name" could not be bound.
The table almost always exists. What’s wrong is the name you used for it in that place: once you give
a table an alias, the alias is its only name in that query. If the table part is right and the column
is wrong (c.nmae), you get error 207 instead.
Common causes
- The table’s own name after you gave it an alias.
FROM dbo.customers AS chides the namecustomers:customers.nameanddbo.customers.nameboth fail,c.nameworks. This includesORDER BY customers.nameat the end of an aliased query. - A misspelt alias, or one left over from another query:
ON o.customer_id = cust.idwhen the alias isc. - A table that isn’t in the statement at all. Typical in an
UPDATEwritten the way other databases allow:UPDATE orders SET … WHERE customers.name = 'Ada' AND orders.customer_id = customers.id. T-SQL needscustomersin aFROMclause. - A table used in an
ONbefore it’s joined. EachONsees only the tables joined so far, so in… JOIN payments AS p ON p.order_id = o.id JOIN orders AS o ON …the firstONcan’t seeo. - Comma joins mixed with
JOIN. InFROM customers AS c, orders AS o JOIN payments AS p ON p.customer_id = c.idtheJOINbinds first, so itsONseesoandpbut notc. - A subquery in
FROMthat refers to the outer query. A derived table can’t see the other tables in the sameFROM.
How to fix it
Use the alias everywhere
Give each table a short alias and qualify every column with it, in SELECT, ON, WHERE,
GROUP BY and ORDER BY:
SELECT c.name, o.total
FROM dbo.customers AS c
JOIN dbo.orders AS o ON o.customer_id = c.id
ORDER BY c.name;
Qualifying every column also prevents ambiguous column names when two tables share one.
Give UPDATE and DELETE a FROM
T-SQL updates through a join with UPDATE <alias> … FROM, naming the alias of the table you’re
changing:
UPDATE o
SET status = 'vip'
FROM dbo.orders AS o
JOIN dbo.customers AS c ON c.id = o.customer_id
WHERE c.name = N'Ada';
DELETE works the same way: DELETE o FROM dbo.orders AS o JOIN dbo.customers AS c ON … WHERE ….
When you only need a filter, a subquery avoids the join:
UPDATE dbo.orders
SET status = 'vip'
WHERE customer_id IN (SELECT id FROM dbo.customers WHERE name = N'Ada');
Join in order, and drop comma joins
Write every join as JOIN … ON, and put each condition in the ON of the last table it mentions:
SELECT c.name, o.total, p.amount
FROM dbo.customers AS c
JOIN dbo.orders AS o ON o.customer_id = c.id
JOIN dbo.payments AS p ON p.order_id = o.id AND p.customer_id = c.id;
Use APPLY when a subquery needs the outer row
CROSS APPLY (or OUTER APPLY, to keep rows with no match) runs the subquery once per outer row, so
it can refer to c:
SELECT c.name, x.orders
FROM dbo.customers AS c
CROSS APPLY (SELECT COUNT(*) AS orders FROM dbo.orders AS o WHERE o.customer_id = c.id) AS x;
Reproduce it
On SQL Server 2022 (16.0.4295.3) with sqlcmd, a scratch schema with customers and orders, each
statement in its own batch:
SELECT customers.name FROM seo_err_mssql.customers AS c;
SELECT c.name, o.total FROM seo_err_mssql.customers AS c
JOIN seo_err_mssql.orders AS o ON o.customer_id = cust.id;
UPDATE seo_err_mssql.orders SET status = 'vip'
WHERE customers.name = N'Ada' AND orders.customer_id = customers.id;
Msg 4104, Level 16, State 1, Server 7732422b7f56, Line 1
The multi-part identifier "customers.name" could not be bound.
Msg 4104, Level 16, State 1, Server 7732422b7f56, Line 1
The multi-part identifier "cust.id" could not be bound.
Msg 4104, Level 16, State 1, Server 7732422b7f56, Line 2
The multi-part identifier "customers.name" could not be bound.
Msg 4104, Level 16, State 1, Server 7732422b7f56, Line 2
The multi-part identifier "customers.id" could not be bound.
The UPDATE reported both unbound names. SELECT seo_err_mssql.customers.name FROM seo_err_mssql.customers
worked, and failed with "seo_err_mssql.customers.name" once the table had the alias c. A comma join
followed by a JOIN whose ON used c.id, and an ON that used o.id before orders was joined:
Msg 4104, Level 16, State 1, Server 7732422b7f56, Line 3
The multi-part identifier "c.id" could not be bound.
Msg 4104, Level 16, State 1, Server 7732422b7f56, Line 3
The multi-part identifier "o.id" could not be bound.
Rewritten with every join in order, the query returned its row. A derived table counting
WHERE o.customer_id = c.id failed on "c.id"; the same subquery in CROSS APPLY returned 2, 1
and 0 orders for the three customers. UPDATE o … FROM … JOIN changed Ada’s two orders, inside a
transaction that was rolled back. SQL Server 2019 and 2025 printed the same messages.
In Inlet
The query editor completes table and column names from the live schema. When SQL Server rejects the
batch, Inlet shows its
message with SQL Server error 4104, state 1, severity 16 and links to this page. With your own
Anthropic API key, Ask Claude (Pro, ⌘L) can fix a failed statement; it sends the schema and the SQL,
never rows.