Fix it now
Error 208 is “Invalid object name” and it very rarely means the table has been dropped. Far more often the session is in a different database than you think, the caller’s default schema is not dbo, the collation makes names case-sensitive, or the caller cannot see the object at all. Qualifying names with the schema removes most of it in one move.
SELECT DB_NAME() AS CurrentDb, SCHEMA_NAME() AS DefaultSchema,
SUSER_SNAME() AS Login, USER_NAME() AS DbUser;
SELECT OBJECT_ID('dbo.YourTable') AS ResolvesTo;
SELECT s.name AS SchemaName, o.name AS ObjectName, o.type_desc
FROM sys.objects AS o
JOIN sys.schemas AS s ON s.schema_id = o.schema_id
WHERE o.name LIKE '%YourTable%';
- Read the first result. If
DB_NAME()is master or tempdb, that is the whole answer and aUSEor a corrected initial catalog fixes it. - A NULL from
OBJECT_IDmeans this session cannot resolve the name – which is the same conclusion the query reached, and includes the case where the caller has no permission on it. - Qualify every reference with its schema.
dbo.YourTableresolves identically for every caller; an unqualified name does not. - If the object is in another database, use three parts. Across servers, use four and check the linked server exists before blaming the name.
- If it is a temporary table, confirm it was created in the same session and has not gone out of scope.
Run this as the account that failed, using EXECUTE AS USER = 'AppUser'; and REVERT; if you cannot log in as it. Metadata is filtered by permission, so your own session is not a fair test.
If the name resolves for the account that failed, you are done. If not, the next section explains how the engine turns a name into an object.
Why it happens
An unqualified name is not a complete address. When the engine sees YourTable, it looks first in the default schema of the database user running the statement, and then in dbo. That means the same query can succeed for one login and fail for another purely because their users were created with different default schemas. The failure is deterministic and repeatable, which is what makes it so confusing when only some people report it.
Resolution is also scoped to the current database. A job step, a connection string or an application that opens on master and never issues a USE fails on every unqualified name, even though the objects plainly exist somewhere on the instance. The tool you are testing in almost always has the right database selected, which is why the same statement works interactively and fails in production.
Then there is visibility. Microsoft documents metadata visibility as limited to securables a user owns or has been granted some permission on: a catalogue query for an object the caller has no rights to returns an empty result set rather than an error. The same rule reaches the built-in functions, and the OBJECT_ID documentation says so explicitly – a metadata-emitting function such as OBJECT_ID can return NULL if the user has no permission on the object. That is why a diagnosis run as sysadmin proves nothing about what the application account can see.
Finally, procedures behave differently from ad hoc batches. Microsoft’s CREATE PROCEDURE reference states that a procedure can reference tables that do not yet exist, that only syntax checking is performed at creation time, and that objects are resolved when the procedure is first executed. So a procedure that references a missing table is created quite happily and fails later – by design, so that procedures can be created in any order.
The caller has a different default schema
You have this one if The identical unqualified query works for a sysadmin and fails for an application login.
- Check it:
SELECT name, default_schema_name FROM sys.database_principals WHERE type IN ('S','U','G'); - Set it if it is wrong:
ALTER USER AppUser WITH DEFAULT_SCHEMA = dbo; - Better, qualify the names in the code so the default schema stops mattering at all.
An EXECUTE AS block changes the effective user, and therefore the default schema, for everything inside it. That is useful for testing and surprising when it happens in production code.
The session is in the wrong database
You have this one if SELECT DB_NAME(); returns master, tempdb or something unrelated.
- Add
USE YourDatabase;at the top of the batch, or set the correct database as the initial catalog in the connection string. - For an Agent job step, set the database on the step itself rather than relying on the service account’s default.
- Check the login’s default database:
SELECT name, default_database_name FROM sys.server_principals;
The database collation is case-sensitive
You have this one if SELECT * FROM dbo.Customers fails while dbo.customers works, or the reverse.
- Check it:
SELECT name, collation_name FROM sys.databases;A collation containing CS is case-sensitive, and object names follow it. - Match the case exactly as the object was created, which you can read from
sys.objects. - Do not change the database collation to work around this. It affects comparisons, sorting and every column created afterwards.
The caller cannot see the object
You have this one if The object exists and the name is right, but the failure happens only for one account, and granting a permission makes it disappear.
- Test what the caller can see:
EXECUTE AS USER = 'AppUser'; SELECT OBJECT_ID('dbo.YourTable'); REVERT;A NULL there is the answer. - Grant the minimum needed, on the schema rather than object by object where that fits:
GRANT SELECT ON SCHEMA::dbo TO AppUser; - Check for a DENY higher up. A DENY at any level beats a GRANT.
Metadata visibility is deliberate: telling an unauthorised caller that an object exists is itself a disclosure. It does mean your own session is a misleading place to diagnose from.
It is a temporary object, a synonym or a view over something that moved
You have this one if The failure is intermittent, or the object is reached through a synonym or a view rather than directly.
- A local temporary table lives only in the session that created it. A new session, or a batch after the creating one ended, will not find it.
- Check synonyms for a target that no longer exists:
SELECT name, base_object_name FROM sys.synonyms; - For a view whose underlying table changed, run
EXEC sp_refreshview 'dbo.YourView';. It updates the persisted metadata for a view not created with SCHEMABINDING.
Full reference
Symptom to cause
| What you see | Where to look |
|---|---|
| Works for you, fails for one application account | That user’s default schema, or its permissions |
| Fails everywhere, object visible in Object Explorer | The connection is in the wrong database |
| Fails only when the case of the name differs | A case-sensitive database collation |
| Fails inside a procedure that was created without complaint | Deferred name resolution: the name is checked at first execution |
| 207 rather than 208 | The object resolved but a column name did not |
| 4104 rather than 208 | A multi-part identifier could not be bound: usually an alias or prefix not in scope |
| 209 | More than one source in the query exposes that column name |
| 1087 | A table variable was referenced without being declared in the same batch |
| 2714 | The mirror image: an object of that name already exists |
The four-part address, in full
| Form | Resolves how |
|---|---|
YourTable |
Default schema of the current database user, then dbo, in the current database |
dbo.YourTable |
Named schema, current database. Identical for every caller |
OtherDb.dbo.YourTable |
Named schema in a named database on this instance |
SALESSRV.OtherDb.dbo.YourTable |
Through a linked server registered in sys.servers |
#Temp |
tempdb, scoped to the session that created it |
##Temp |
tempdb, global, and gone when the last session referencing it ends |
Testing as somebody else
EXECUTE AS USER = 'AppUser';
SELECT DB_NAME() AS CurrentDb, SCHEMA_NAME() AS DefaultSchema, USER_NAME() AS DbUser;
SELECT OBJECT_ID('dbo.YourTable') AS ResolvesTo;
SELECT TOP 1 * FROM dbo.YourTable;
REVERT;
Because metadata is filtered by permission, this block is the difference between guessing and knowing. A NULL from OBJECT_ID inside the impersonation, where your own session returns a value, tells you the account cannot see the object – and the fix is a grant, not a rename. Remember EXECUTE AS also switches the default schema to that user’s, which is convenient here and a trap in production code that assumes otherwise.
Deferred name resolution, precisely
Microsoft’s wording is worth having exactly: a procedure can reference tables that do not yet exist; at creation time only syntax checking is performed; the procedure is not compiled until it is executed for the first time; and only during compilation are all objects referenced in the procedure resolved. So a syntactically correct procedure referencing a missing table is created successfully and fails at execution time. That is by design, so procedures can be created in any order, and it is why a deployment can look clean and break on first use.
Why qualifying names is worth doing everywhere
- It removes the default schema question entirely, so behaviour stops depending on who is connected.
- It makes the statement mean the same thing in a job step, an application and your query window.
- It removes an ambiguity the engine would otherwise resolve per user, which helps plan reuse.
- It makes 208 mean what it says: the object genuinely is not there, rather than not there for you.
- It costs four characters.
When it really is missing
Once schema, database, collation and permission are all ruled out, search the instance rather than the database: SELECT s.name, o.name, o.type_desc FROM sys.objects AS o JOIN sys.schemas AS s ON s.schema_id = o.schema_id WHERE o.name LIKE '%YourTable%'; run in each candidate database. If nothing turns up anywhere, check whether a deployment dropped it, whether a synonym is pointing at something that has gone, and whether the object was ever created outside a transaction that was later rolled back.
Every code this article covers
| Code | What it points at | Source |
|---|---|---|
208 |
Invalid object name: the name could not be resolved in the current database and schema scope, or the caller cannot see it | Microsoft Learn |
207 |
Invalid column name: the object resolved but a column in the statement did not | Microsoft Learn |
4104 |
The multi-part identifier could not be bound, usually a table prefix or alias that is not in scope | Microsoft Learn |
209 |
Ambiguous column name: more than one source in the query exposes that column | Microsoft Learn |
1087 |
Must declare the table variable: it was referenced without being declared in the same batch | Microsoft Learn |
2714 |
There is already an object of that name in the database, which is the mirror image of 208 | Microsoft Learn |
Confirm the fix worked
SELECT OBJECT_ID('dbo.YourTable');returns a value in the failing session, not just in yours.- The original statement runs under the account that failed, using
EXECUTE AS USERif you cannot log in as it. SELECT DB_NAME();returns the intended database in the application connection, not only in your query window.- Every reference in the changed code is schema-qualified.
- If the object is reached through a view or synonym, that view has been refreshed and the synonym’s target exists.
Questions people ask about this
Should I always write the schema name?
Yes. Qualifying with the schema removes the default schema question entirely and makes the statement mean the same thing for every caller, in every context. It also helps plan reuse, because the engine no longer has to resolve the name per user.
Why does a procedure compile and then fail at run time?
Deferred name resolution. Microsoft’s CREATE PROCEDURE reference states that only syntax checking happens at creation, that the procedure is not compiled until first executed, and that objects are resolved only during that compilation. It is by design so procedures can be created in any order.
Why do I get 208 instead of a permission denied message?
Metadata visibility is filtered by permission: a caller with no rights on an object gets an empty result from the catalogue views, and OBJECT_ID can return NULL for them. Test with EXECUTE AS USER rather than from your own session, which sees everything.
Does it cost anything to fix?
No. This is a naming, context or permission problem and it behaves identically on every edition. There is nothing to buy.
My temporary table disappeared. Why?
A local temporary table lives only in the session that created it. A different session, or a batch after the creating one has ended, will not find it. If you need it across batches, use a global temporary table or a permanent one in a working schema.
