And the output...
...confirms that I could create a proc referencing a non-existent table. But now let's create the table first but reference a non-existent column:
USE tempdb
CREATE TABLE FOO (colA varchar(100))
GO
CREATE PROC BAR AS SELECT ID FROM Foo
GO
SELECT name, type FROM sysobjects WHERE name = 'BAR' and type = 'P'
GO
DROP TABLE FOO
GO
DROP PROC BAR
GO
And the screenshot shows that we failed.
It seems ridiculous to me that I can create a proc referencing a non-existent table, but can't reference a table with a non-existent column. You may be saying, "So what? Why would I ever want to reference a non-existent column?"
Code Reusability and Temp Tables
...that's why. It's good OO programming design to not copy/paste code everywhere. Unfortunately in TSQL we often have to do that if we expect performance to be acceptable. Many people take their copy/paste logic and insert into into a scalar UDF. That won't work if you want peformant code since the scalar UDF is processed in RBAR (row by agonizing row) fashion. An inline table valued function will work, the optimizer will treat the TVF as a kind of "macro" and will expand the code in a performant manner. But there are limitations to this approach that make it's use very limited.
The best approach, by far, in my opinion, is to pass around temp tables into stored procs that are in essence the TVF code, or code you don't want to copy/paste everywhere. So, let's assume you have a complex calculation that you run over a series of table ids in various procedures that you would like to reuse in a performant manner.
The whole process should look something like this:
USE tempdb
GO
CREATE PROC Utility AS
BEGIN
UPDATE #tblUtility SET calc = round(PI(),4)* ID --you're calculation logic goes here
WHERE ID % 2 = 0
END;
GO
CREATE PROC Caller AS
BEGIN
--I'm building my list of IDs that I need into my temp table
CREATE TABLE #tblUtility (ID int, calc decimal(13,4))
INSERT INTO #tblUtility SELECT TOP 5 object_id, NULL FROM master.sys.objects
EXEC Utility
SELECT * from #tblUtility
END
GO
EXEC Caller
GO
DROP Proc Utility
GO
DROP Proc Caller
GO
Very simple and elegant. The problem comes in where I want to do a lot of calculations in my Utility procedure, hence I need a lot of columns, let's assume we need 20 columns declared. We now have to copy/paste those 20 column declarations into every Caller procedure. That's redundant code. The simple solution is to only declare the basic table in Caller with just the ID col, then ALTER the table in the Utility procedure to ADD the calculation columns. That's much less copy/paste code. And much less work when we need to change or add new cols in the future. We don't need 20 new columns to see how the improvement would work. Using our example, this should be much easier to maintain:
USE tempdb
GO
CREATE PROC Utility AS
BEGIN
ALTER TABLE #tblUtility ADD calc decimal(13,4) --note that any calc cols are created here
UPDATE #tblUtility SET calc = round(PI(),4)* ID --you're calculation logic goes here
WHERE ID % 2 = 0
END;
GO
CREATE PROC Caller AS
BEGIN
--I'm building my list of IDs that I need into my temp table
CREATE TABLE #tblUtility (ID int) --note only the ID col is needed here
INSERT INTO #tblUtility SELECT TOP 5 object_id FROM master.sys.objects
EXEC Utility
SELECT * from #tblUtility
END
GO
EXEC Caller
GO
DROP Proc Utility
GO
DROP Proc Caller
GO
But it doesn't work, thanks to our old friend Deferred Name Resolution.
Essentially this is telling us that the UPDATE cannot occur because a column does not exist in an existing table. Two solutions come to mind:
Assuming we go with Option 2, this is what the new Code Reusability Pattern would look like:
USE tempdb
GO
CREATE PROC Utility AS
BEGIN
ALTER TABLE #tblUtility ADD calc decimal(13,4) --note that any calc cols are created here, in the first level util proc
EXEC Utility_SubProc_Calculations
END;
GO
CREATE PROC Utility_SubProc_Calculations AS
BEGIN
UPDATE #tblUtility SET calc = round(PI(),4)* ID --you're calculation logic goes here, in the subproc
WHERE ID % 2 = 0
END;
GO
CREATE PROC Caller AS
BEGIN
--I'm building my list of IDs that I need into my temp table
CREATE TABLE #tblUtility (ID int) --note only the ID col is needed here
INSERT INTO #tblUtility SELECT TOP 5 object_id FROM master.sys.objects
EXEC Utility
SELECT * from #tblUtility
END
GO
EXEC Caller
GO
DROP Proc Utility
GO
DROP Proc Utility_SubProc_Calculations
GO
DROP Proc Caller
GO
Pattern Summary
The key to performant TSQL code reusability, in my opinion, is utility procedures. To get the most benefit we need to create an "extra" layer of abstraction to handle the god-awful Deferred Name Resolution implementation of TSQL. You need to :
With this simple API pattern we overcome both DNR issues and Performant TSQL Code Reuse. [[Tips on Writing Better Stored Procedures|More Tips on Writing Better Stored Procedures]]
Dave Wentzel CONTENT
sql server data architecture