Introduction
SQL is an old language — almost 50 years! The first commercially-available version of SQL was released by Oracle way back in 1969. In its specifications and general, “standard” appearance, it resembles the other leading language of the day, COBOL. Language theory and computer languages have evolved considerably since then. Modern concepts such as object-oriented programming and functional programming are not well-represented in SQL. That’s a pity. It can lead to copying code which almost always increases maintenance costs and code fragility.
One thing that I try to adhere to — in every language I use — is the Don’t Repeat Yourself, or DRY, principle. In other articles and blogs, I’ve discussed using CROSS APPLY as a way to eliminate repeated expressions. In this one, I want to explore another way to make your code DRYer.
Too many arguments!
One principle from the functional programming world is that a function should have at most 3 or 4 arguments. Any more and the function is probably trying to do too much. In the SQL world, this would apply to both functions and stored procedures. Nevertheless, I see this principle violated with alarming regularity. What can we do about it? Let’s borrow another technique from the functional programming world: partial functions.
The idea sounds simple. Let’s look at an example in Python. (Don’t worry if you don’t know Python. It’s rightly called “the language that’s easy to learn and hard to forget!”). I’ll define a simple function:
1 2 |
def myfunc(a, b, c): print(a, b, c) |
OK, that’s not too exciting. However, imagine that, in some section of code, I call myfunc 10 times and for all ten calls, only argument c changes. I’d have to repeat arguments a and b 10 times. What if I could somehow encapsulate them in a custom function so that I only needed to specify argument c? I can do it like this:
def partial_a_b(a, b):
def f(c):
myfunc(a, b, c)
return f
1 2 3 4 5 6 7 8 9 10 |
def partial_a_b(a, b): def f(c): myfunc(a, b, c) return f # Create new function, using variables a and b myfunc_c = partial_a_b("The Answer", "is") # Now, only specify argument c myfunc_c(42) |
Here, I create a function called partial_a_b that creates a new function each time it is called. The new function encapsulates the values of arguments a and b, just leaving argument c exposed, then returns the function. Then, I use partial_a_b to create another function I call myfunc_c. This snippet yields:
The Answer is 42
In Python, functions are first-class. They can be passed around like other objects and returned from other functions, as above. The function partial_a_b encapsulates arguments a and b. We can also say that the function closes over those arguments and that the partial_a_b is a closure. Closures are a typical way to create partial functions, since they close over some of the arguments while leaving the others exposed.
Writing partial functions is easy in Python, but they are not easy to write in all languages. For example, in an article by Jon Skeet, a .NET guru, he shows how to write them in C#, which is not a functional language. While possible, it is a little complicated. See the references section for a link to that article if you want to know more.
ANSI SQL is another language where writing partial functions are challenging and are dependent on the dialect. Since I’m talking about SQL Server, that means the T-SQL dialect. Functions are not first-class objects in ANSI SQL or T-SQL but we can achieve the same goal using dynamic SQL. Let’s look at an example from a real project I’m working with.
A logging example
Before learning about Partial stored procedures in SQL Server, in case you want to walk through the basics of Stored procedures and understand how they are different from functions, I would recommend you to read these informative articles:
One system that I help support uses a logging system that is invoked with a stored procedure call. (Note that I’m not arguing the merits of doing logging this way. That could be a subject for another article.) The logger proc has 10 parameters, breaking the rule about 3 or 4 that is typical in functional languages. Furthermore, for a given calling program on a typical run, which may be another stored procedure, 5 or more of those 10 parameters will not change whether the logger is called once or a hundred times. It would be great to be able to build a “partial” procedure that closes over those five, fixed arguments and exposes the rest.
Let’s see how to do that with something simpler than my ten-parameter logger:
1 2 3 4 5 6 7 8 9 10 11 |
CREATE PROC logger @jobid int, @jobname varchar(50), @jobdate date, @logmsgid int, @logmsg varchar(255) AS BEGIN PRINT CONCAT(@jobid, '/', @jobname, '/', @jobdate, '/', @logmsgid, '/', @logmsg); END EXEC logger 1, 'The Ultimate Question','20120601', 42, 'The Answer' |
The EXEC call produces:
No surprise there.
This little logger doesn’t do much, I’ll admit, but it’s enough to see how to build a partial procedure in T-SQL. Suppose this is called from another stored procedure that needs to log its progress for later analysis. Assume that, for the calling procedure, the first three arguments do not change for a given run. Further, assume that the calling proc has ten places where it wants to call the logger. A typical call might look like:
1 2 3 4 5 6 |
EXEC logger @jobid=@jobid, @jobname = @jobname, @jobdate = @jobdate, @logmsgid = 42, @logmsg = 'The Answer'; |
The first three parameters are variables set elsewhere in the calling proc. Only the last two vary from call to call. What I want is to be able to call something like:
1 |
EXEC mylogger @logmsgid = 42, @logmsg = 'The answer'; |
If I can do that, I’ll avoid repeating myself. That is, the code will be DRYer. What I need to do is build a helper proc, which I’ll call GetLogger, that will take in all the arguments that the logger proc has and creates a new, temporary stored procedure called #mylogger. Using a temporary stored procedure makes sense since it is only exposed to the current session, which is the session having the repeated arguments. Here’s the signature of GetLogger:
1 2 3 4 5 6 7 |
CREATE PROC GetLogger @logger sysname = '#logger', @jobid int = NULL, @jobname varchar(50) = NULL, @jobdate date = NULL, @logmsgid int= NULL, @logmsg varchar(255) = NULL |
The sharp-eyed will notice that I’ve added one additional parameter and set the rest to have default values of NULL. I’ll use the @logger parameter to name the partial proc I’ll create and check the NULL values to determine which parameters to close over and which to expose. In the calling proc, I’ll call GetLogger like this:
1 2 3 4 5 |
EXEC GetLogger @logger=N'#mylogger', @jobid=@jobid, @jobname=@jobname, @jobdate=@jobdate |
GetLogger will then create a session-level, temporary procedure called #mylogger, closing over the three non-null arguments and exposing the other two. Let’s see how that works:
1 2 3 4 5 6 7 8 9 10 11 |
DECLARE @closed NVARCHAR(MAX) = N''; DECLARE @exposed NVARCHAR(MAX) = N''; DECLARE @params NVARCHAR(MAX) = N''; -- Check @jobid parameter IF @jobid is not null SET @closed += CONCAT(N'@jobid = ', @jobid, N', ') ELSE BEGIN SET @exposed += N'@jobid int = NULL, '; SET @passed += N'@jobid = @jobid, '; END |
All this does is see if the @jobid parameter is specified or not (that is, not NULL). If so, it is appended to a variable called @closed; if not, it is appended to the @exposed variable. A third variable, @passed, is also built up to use in the call to the original logger procedure defined above and pass the exposed parameters to it.
GetLogger does the same processing for all 5 parameters, then builds a CREATE PROC statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-- Assemble dynamic SQL to create the partial stored procedure DECLARE @stmt NVARCHAR(MAX) = CONCAT( N'CREATE PROC ', @logger, N' ', LEFT(@exposed, LEN(@exposed)-1), N' ', N'AS EXEC logger ', @closed, @passed ); -- Change trailing comma to a semicolon SET @stmt = LEFT(@stmt, len(@stmt)-1) + N';' -- Create temporary partial stored procedure, dropping any existing one DECLARE @dropExisting NVARCHAR(MAX) = CONCAT(N'DROP PROCEDURE IF EXISTS ', @logger); EXEC sp_executesql @stmt = @dropExisting; PRINT @stmt; EXEC sp_executesql @stmt; |
Now, let’s test this! I call the GetLogger procedure as shown above, which produces one line of output (shown wrapped here for easier reading):
Now let’s call the partial function:
1 |
EXEC #mylogger 1, 'The Ultimate Question' |
This yields:
This is the same in the original call. The temporary, partial stored procedure works!
Summary
This article shows an approach for building partially stored procedures in T-SQL. This basic approach can be enhanced to suit your needs. For example, in my actual implementation, I add an @debug parameter, which causes the GetLogger proc to create a print statement instead of calling the original procedure. I’ll leave that detail as an exercise for the reader!
You may be wondering if the same technique can be applied to T-SQL functions. In short, it can, with care. Since there is no such thing as a temporary function in T-SQL, you would need to find a way to avoid collisions, in case two sessions tried to create two partial functions at the same time with different parameter values. You see, you have to use a regular function name. You could generate a name for the partial function at runtime, perhaps using the NEWID() function. But then, any place you call your partial function would need to be dynamic SQL, an extra complication (and a second exercise for the reader!)
Others may wonder if I could write a generic stored procedure that would create a partially stored procedure from any other procedure. That would entail some way to call a stored procedure with arbitrary arguments. In pure T-SQL, this is not possible. The system stored procedure I used, sp_executesql, does do this, however. That’s because it is written in CLR code (e.g. C++, C#, etc.) and such functions can indeed take arbitrary parameters.
- Snapshot Isolation in SQL Server - August 5, 2019
- Shrinking your database using DBCC SHRINKFILE - August 16, 2018
- Partial stored procedures in SQL Server - June 8, 2018