SQL Server – Adding Column to View Corrupts Table-Valued Function

set-returning-functionssql serversql server 2014view

I have a TVP myTVP implemented as select val.* from vw_MyView val. The other day, I modified vw_MyView, replacing:
select Foo,Bar from DB with
select Foo, Bash, Bar from DB.

This had a strange side effect: Calling select Bar from myTVP() returned a column named Bar populated with the contents of Bash (despite the fact that Bar and Bash don't even have the same type).

Anonymized plan demonstrating the problem: https://www.brentozar.com/pastetheplan/?id=ryJLJmqdl

Note that Object2 and Object1 are identical, but the output list of the left-most Nested Loop is completely different between the two plans.

Notes:

  • There are no plan hints in use. No trace flags are set.
  • CheckDB found no errors.
  • Using option(recompile) did not fix the issue.
  • Running right-click alter on the TVP with no changes fixed the issue.
  • Running right-click alter on the TVP with a name change fixed the issue, but the new TVP was identical to the previous other than the name.
  • Using SQL 2014 Standard Edition.

Questions:

  1. Is it normal for adding a new column to a view to change which column
    of the view is used in by other references to that view?
  2. Under what circumstances will adding a column to a view cause such behavior?

Best Answer

Yes, this is expected. Functions and views that use SELECT * end up storing part of the metadata themselves relating to the underlying columns, so it is very easy for them to get confused. I talk about this problem in the following post from 2009:

And also in my recent GroupBy presentation:

How do you avoid this problem?

  1. Don't use SELECT * in objects.
  2. Make your functions and views WITH SCHEMABINDING, which means you can't modify the underlying objects without also modifying the objects that reference them (this also has the side effect of prevent * so win-win).
  3. In this specific case, you can fix the object so that Bar returns the correct data by using:

    EXEC sys.sp_refreshsqlmodule @name = N'dbo.myTVP';
    

Incidentally, the documentation for sys.sp_refreshsqlmodule describes this exact scenario:

Updates the metadata for the specified non-schema-bound stored procedure, user-defined function, view, DML trigger, database-level DDL trigger, or server-level DDL trigger in the current database. Persistent metadata for these objects, such as data types of parameters, can become outdated because of changes to their underlying objects.

As an aside, you should never use FROM myTVP() but instead always specify the schema, e.g. FROM dbo.myTVP(). I talk about that in an old post, too.