Transact-SQL User-Defined Functions For MSSQL Server PDF
Transact-SQL User-Defined Functions For MSSQL Server PDF
User-Defined
Functions
Andrew Novick
Novick, Andrew N.
Transact-SQL user-defined functions / by Andrew Novick.
p. cm.
Includes bibliographical references and index.
ISBN 1-55622-079-0 (pbk.)
1. SQL server. 2. Database management. I. Title.
QA76.9.D3 N695 2003
005.75'85--dc22 2003020942
CIP
ISBN 1-55622-079-0
10 9 8 7 6 5 4 3 2 1
0309
Crystal Reports is a registered trademark of Crystal Decisions, Inc. in the United States and/or other countries.
Names of Crystal Decisions products referenced herein are trademarks or registered trademarks of Crystal Decisions or
its
Transact-SQL is a trademark of Sybase, Inc. or its subsidiaries.
SQL Server is a trademark of Microsoft Corporation in the United States and/or other countries.
All brand names and product names mentioned in this book are trademarks or service marks of their respective companies.
Any omission or misuse (of any kind) of service marks or trademarks should not be regarded as intent to infringe on the
property of others. The publisher recognizes and respects all marks used by companies, manufacturers, and developers as
a means to distinguish their products.
This book is sold as is, without warranty of any kind, either express or implied, respecting the contents of this book and any
disks or programs that may accompany it, including but not limited to implied warranties for the books quality,
performance, merchantability, or fitness for any particular purpose. Neither Wordware Publishing, Inc. nor its dealers or
distributors shall be liable to the purchaser or any other person or entity with respect to any liability, loss, or damage caused
or alleged to have been caused directly or indirectly by this book.
All inquiries for volume purchases of this book should be addressed to Wordware Publishing,
Inc., at the above address. Telephone inquiries may be made by calling:
(972) 423-0090
To my parents, Toni and Larry Novick
This page intentionally left blank.
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . xv
2 Scalar UDFs . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Creating, Dropping, and Altering Scalar UDFs. . . . . . . . . . . . . 25
Permissions to Use CREATE/DROP/ALTER FUNCTION . . . . 25
Using the CREATE FUNCTION Statement . . . . . . . . . . . . 28
The Function Body . . . . . . . . . . . . . . . . . . . . . . . . . 31
Declaring Local Variables (Including TABLEs) . . . . . . . . . 31
Control-of-flow Statements and Cursors . . . . . . . . . . . . . 33
Using SQL DML in Scalar UDFs. . . . . . . . . . . . . . . . . 34
Adding the WITH Clause . . . . . . . . . . . . . . . . . . . . . . 36
Specifying WITH ENCRYPTION . . . . . . . . . . . . . . . . 36
Specifying WITH SCHEMABINDING. . . . . . . . . . . . . . 39
Using Scalar UDFs . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Granting Permission to Use Scalar UDFs. . . . . . . . . . . . . . 44
Using Scalar UDFs in SQL DML . . . . . . . . . . . . . . . . . . 45
Using Scalar UDFs in the Select List . . . . . . . . . . . . . . 45
Using Scalar UDFs in the WHERE and ORDER BY Clauses . . 47
Using Scalar UDFs in the ON Clause of a JOIN . . . . . . . . . 49
Using Scalar UDFs in INSERT, UPDATE, and DELETE
Statements . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Using Scalar UDFs in SET Statements . . . . . . . . . . . . . 50
Using Scalar UDFs in EXECUTE and PRINT Statements . . . 51
v
Contents
* History. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
* Copyright . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Whats Not in the Header . . . . . . . . . . . . . . . . . . . . . 122
* Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . 122
* Algorithm and Formulas. . . . . . . . . . . . . . . . . . . . 123
Naming Conventions . . . . . . . . . . . . . . . . . . . . . . . . . 123
Naming User-Defined Functions . . . . . . . . . . . . . . . . . . 123
Naming Columns . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Domain Names . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Naming Function Parameters and Local Variables . . . . . . . . . 131
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
sp_helptext . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
sp_rename . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
sp_depends . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Retrieving Metadata about UDFs . . . . . . . . . . . . . . . . . . . 183
Finding out about UDFs in INFORMATION_SCHEMA . . . . . 183
INFORMATION_SCHEMA.ROUTINES. . . . . . . . . . . . 184
INFORMATION_SCHEMA.ROUTINE_COLUMNS . . . . . 185
INFORMATION_SCHEMA.PARAMETERS . . . . . . . . . 185
Built-in Metadata Functions . . . . . . . . . . . . . . . . . . . . 186
Information about UDFs in System Tables . . . . . . . . . . . . 187
Metadata UDFs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Function Information . . . . . . . . . . . . . . . . . . . . . . . . 189
What Are the Columns Returned by a UDF? . . . . . . . . . . . 190
What Are the Parameters Used When Invoking a UDF? . . . . . 192
Metadata Functions that Work on All Objects . . . . . . . . . . . 193
SQL-DMO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
viii
Contents
fn_chariswhitespace . . . . . . . . . . . . . . . . . . . . . . . . 392
fn_dblog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
fn_mssharedversion . . . . . . . . . . . . . . . . . . . . . . . . 396
fn_replinttobitstring . . . . . . . . . . . . . . . . . . . . . . . . 396
fn_replbitstringtoint . . . . . . . . . . . . . . . . . . . . . . . . 399
fn_replmakestringliteral . . . . . . . . . . . . . . . . . . . . . . 403
fn_replquotename . . . . . . . . . . . . . . . . . . . . . . . . . 406
fn_varbintohexstr. . . . . . . . . . . . . . . . . . . . . . . . . . 409
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
Appendix A
Deterministic and Nondeterministic Functions . . . . . . . 421
Appendix B
Keyboard Shortcuts for Query Analyzer Debugging . . . . 427
Appendix C
Implementation Problems in SQL Server 2000
Involving UDFs . . . . . . . . . . . . . . . . . . . . . . . 429
Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
xi
This page intentionally left blank.
Acknowledgments
This book is the product of a lot of work on my part, which was enabled by
the direct and indirect support of many other people. Id like to acknowl-
edge their support and thank them for their help.
My family, especially my wife, Ulli, has been very supportive through-
out the year that its taken to create this book. Theres no way it would
have been completed without her help and encouragement.
Phil Denoncourt did a great job as technical editor. His quick turn-
around of the chapters with both corrections and useful suggestions for
improvements made completing the final version a satisfying experience.
And it was on time!
The staff at Wordware has been very helpful, and I appreciate all their
efforts. Wes Beckwith and Beth Kohler moved the book through the edit-
ing and production process effectively. Special thanks to Jim Hill for taking
a chance on a SQL Server book and a fledgling author.
The technical background that made this book possible is the product
of 32 years of computer programming and computer science education.
Many people helped my education along, particularly Dan Ian at New
Rochelle High School who got me started programming PDP-8s and Andy
Van Dam at Brown.
Experience over the last 23 years has enabled me to build the knowl-
edge of how to create practical computer software, which I hope is
reflected throughout the book. For that Im thankful to a handful of entre-
preneurs who gave me the opportunities to build programs that are used
by large numbers of people for important purposes: Larry Garrett, Alan
Treffler, Lance Neumann, and Peter Lemay. Along with them Id like to
acknowledge the work of some of my technical colleagues without whom
the work would never have been as successful or enjoyable: Bill Guiffree,
Steve Pax, Dmitry Gurenich, Nick Vlahos, Allan Marshall, Carolyn
Boettner, Victor Khatutsky, Marty Solomon, Kevin Caravella, Vlad Viner,
Elaine Ray, Andy Martin, and many others.
Books dont just happen. They take a lot of work, and the people
acknowledged here had a hand in making this book possible.
xiii
This page intentionally left blank.
Introduction
User-defined functions (UDFs) are new in SQL Server 2000. This book
examines the various types of UDFs and describes just about everything
that youll ever want to know in order to make the most of this new fea-
ture. In particular, well cover:
n The different types of UDFs
n Creating UDFs
n Using UDFs
n Debugging UDFs
n Documenting UDFs
Along the way, dozens of useful UDFs are created. Theyre available for
you to use in future projects or to reference when you want to see an
example of a particular technique.
I first encountered UDFs in SQL Server when I discovered that SQL
Server didnt have them. That was a disappointment. It was also a problem
because I had completed work on a database design that depended on the
ability to create my own functions.
xv
Introduction
Bill Guiffre, the lead programmer, and I, as project manager, began the
design for the database and user interface. Using T-SQL wouldnt be much
of a problem. After all, between the two of us, wed developed similar
applications with Oracle, Access, Watcom SQL (now Sybase SQL Any-
where), Informix, and a few other RDBMSs.
Then we made one unfortunate assumption. We assumed that T-SQL
would have a way to create user-defined functions. After all, all the other
RDBMSs that wed used had such a facility. Oops!
For the pavement management system, we had planned to use UDFs
for converting pavement measurements that were stored in the metric
system into the imperial (aka U.S. standard) system of measurement for
use in the applications user interface.
When the database arrived, Bill and I were plenty happy. I really liked
the product and its integration into Windows. I started trying out a few of
the tools and getting used to T-SQL. For a while, everything looked great.
The surprise came when I decided to code the unit conversion func-
tions. CREATE FUNCTION worked in Oracles PL/SQL, so I assumed that it
would work in T-SQL as well. Of course it didnt work, and the documen-
tation wasnt any help. We even put in a call to technical support to be
sure we werent missing something, but we werent. T-SQL didnt include
CREATE FUNCTION or any alternative.
In the grand scheme of things, our problem wasnt very difficult to
overcome. We just did a little redesign. We added a couple dozen stored
procedures and learned a few new techniques in PowerBuilder, the UI
development tool, to make them work. All-in-all, the lack of UDFs set us
back only two or three days. In a nine-month project, that was pretty easy
to overcome. Chapter 12 has more about unit conversions and shows
some of the alternative ways to code them now that SQL Server supports
UDFs.
As SQL Server became my primary development database in the late
1990s, I waited a long time for the availability of UDFs. Finally, SQL
Server 2000 made them available. Ive used them on a couple of SQL
Server 2000 projects since it was released. For the most part, Im pretty
happy with them. They do the job even if they have a few idiosyncrasies.
As I built a library of UDFs, I realized the need for more information
than the Books Online provides. It just doesnt tell you very much except
the basics of the syntax and some of the rules about what you cant do.
Other T-SQL-oriented books werent much help either. They pretty much
stuck to the basics with not much more information than youll find in the
first half of Chapter 1 in this book.
xvi
Introduction
Since theres so much more to UDFs than just the syntax of the
CREATE FUNCTION statement, I decided the world needed a book on the sub-
ject. When my opportunity arose to write a book on UDFs, I decided to go
for it; youre reading the product of that effort.
xvii
Introduction
Chapter 3, Working with UDFs in the SQL Server Tools, shows you
how to use the principal SQL Server client tools: Query Analyzer, SQL
Profiler, and Enterprise Manager. This isnt an introduction to the tools; I
assume youre already familiar with them. The chapter sticks to the fea-
tures that are particularly relevant to UDFs.
UDFs are different from stored procedures in several ways. Some of
the most important differences are the restrictions placed on the T-SQL
statements that can be used in a UDF. The restrictions are detailed in
Chapter 4, You Cant Do That in a UDF.
I sometimes find that almost half of my code is devoted to error han-
dling. Chapter 5, Handling Run-time Errors in UDFs, shows you what
you can and cannot do in a UDF to handle an error. Sometimes its less
than youd like, and the chapter discusses how to live within SQL Servers
limitations.
There are many styles used to write T-SQL codealmost as many
styles as there are programmers. Chapter 6, Documentation, Formatting,
and Naming Conventions, shows you aspects of my style and discusses
why the conventions that I show are helpful when creating and maintain-
ing UDFs.
Chapter 7, Inline UDFs, and Chapter 8, Multistatement UDFs,
cover the two types of UDFs that havent previously been given detailed
treatment. Each of these chapters shows how these types of UDFs can be
used to solve particular problems faced by the database designer and
programmer.
The SQL Server GUI tools Query Analyzer and Enterprise Manager
are great for handling individual UDFs. But anyone responsible for main-
taining a database with a lot of UDFs ultimately needs to manage them
with T-SQL scripts. Chapter 9, Metadata about UDFs, describes SQL
Servers system stored procedures, functions, and views that can be used
to get information about UDFs.
Extended stored procedures are compiled code that can be invoked
from T-SQL scripts. Fortunately, SQL Server allows the use of extended
stored procedures in UDFs, as long as they dont return rowsets. Chapter
10, Using Extended Stored Procedures in UDFs, shows which of these
procedures can be used and how to use them. The most important of the
extended stored procedures are those that allow the use of COM objects
from a T-SQL script. The chapter creates an object with Visual Basic 6 and
shows how it can be used and debugged.
Speaking of bugs, we cant escape testing. Chapter 11, Testing UDFs
for Correctness and Performance, gets into the details of writing tests
and test scripts for UDFs. Most importantly, it discusses testing UDFs for
performance, and demonstrates how much a UDF can slow a query.
xviii
Introduction
With the discussion of how to create, manage, use, and test UDFs
complete, Chapter 12, Converting between Unit Systems, and Chapter
13, Currency Conversion, tackle two common problems that can be
solved with UDFs. The unit conversion problem is what first motivated
me to want UDFs in SQL Server. It should be simple, right? In a sense, it
is simple, but it provides an opportunity to examine the problems of
numeric precision and a way to illustrate alternative methods for solving
one problem. Currency conversion is similar in many ways to converting
between unit systems with the exception that the variability of the con-
version rate forces us to store the rate in a table and handle issues such as
missing data and interpolation.
Microsoft used the availability of UDFs as part of its implementation
of SQL Server 2000. It went beyond just extending the syntax of T-SQL to
add a special class of system UDFs. Part II of the book is six chapters
about the system UDFs and how to use them. It starts with Chapter 14,
Introduction to System UDFs, which gives you an overview of what sys-
tem UDFs are available and the differences between system UDFs and
the ordinary UDFs that you and I create. It covers four of the ten docu-
mented system UDFs, including the new fn_get_sql function that wasnt
available before Service Pack 3.
Chapter 15, Documenting DB Objects with fn_listextendedproperty,
discusses how to create and retrieve SQL Servers extended properties.
This is a new feature in the 2000 version that can be used to document a
database or store other information related to database objects.
The amount of input/output (I/O) required of SQL Server is a key
determinant of its performance. Chapter 16, Using fn_virtualfilestats to
Analyze I/O Performance, shows you that system UDF and how to slice
and dice the statistics it generates to narrow down performance problems.
The SQL Profiler is a great tool for analyzing the performance of
UDFs as well as other T-SQL code. Behind the scenes, it uses a set of
system stored procedures for creating traces. Chapter 17, fn_trace_* and
How to Create and Monitor System Traces, shows you those system
stored procedures and a group of four system UDFs that help you retrieve
information about active traces in your SQL Server instance.
In addition to the ten system UDFs that are documented in the Books
Online, there are a few dozen more undocumented UDFs declared in mas-
ter. Some of these are true system UDFs and possess that special status.
Others are just ordinary UDFs that are located in master and owned by
dbo. Chapter 18, Undocumented System UDFs, lists all of these UDFs.
It also has a detailed treatment of several undocumented system UDFs
that you might want to use.
xix
Introduction
The special status of system UDFs lets you create them in just one
place, master, but use them in every database in the SQL Server instance.
Chapter 19, Creating a System UDF, shows you how to make your own.
Most importantly, it shows how data access from a system UDF is differ-
ent when the UDF has system status.
Three appendices wrap up the book. Appendix A is a complete list of
the built-in functions in SQL Server 2000 along with an indication of
whether the function is deterministic or nondeterministic. Appendix B has
a chart with the keyboard shortcuts for the T-SQL debugger in SQL Query
Analyzer. Finally, Appendix C describes some of the problems that I dis-
covered in SQL Server 2000 during the course of writing this book.
xx
Introduction
that you want to work with, then use the context menu from the Object
Browser and select the menu command Script Object to New Window As
Create or Alter. You can then work with the UDF in a new Query Ana-
lyzer connection.
Most chapters have a file named Chapter X Listing 0 Short
Queries.sql. This file has the short queries that illustrate how UDFs are
used and various aspects of T-SQL. I started out creating these files so
that I could be sure to verify each of the batches and so I could easily go
back and retest each one. As I worked with the files, I said to myself, If I
was reading this book, Id like to have this file so I could execute every
query without opening a different file for each one. So Ive included the
Listing 0 file for each chapter in the chapters directory.
Please dont run all the queries in the Listing 0 files all at once. Each
query should be executed one at a time. To better show you how to do
this, Ive bracketed each query between a comment line that describes it
and a GO command. Youll find the following query in the file Introduction
Listing 0 Short Queries.sql in the directory Book_Introduction of the
download tree. Once youve attached the TSQLUDFS database, open a
Query Analyzer window and try it:
(Results)
---------------------------------------------
Just a sample to show how the Listing 0 works
To run just the second query, select from the start of the comment to the
GO command and either press F5 or Ctrl+E, or use your mouse to select
the green Execute arrow on Query Analyzers toolbar. Figure I.1 shows
the Query Analyzer screen just after I used the F5 key to run the query.
There are various other files in the chapter download tree. They are
explained in the chapters.
xxii
Introduction
On to the Book
Thats it for the preliminaries. However you choose to read this book, I
hope it brings you the information that youre looking for. User-defined
functions are a worthwhile tool for SQL Server programmers and DBAs
to add to their arsenal.
If you have comments about the book, Id like to hear from you.
Please send your comments and questions directly to me at:
anovick@NovickSoftware.com. Thanks.
xxiii
This page intentionally left blank.
This page intentionally left blank.
1
Overview of
User-Defined
Functions
SQL Server 2000 introduced three forms of user-defined functions
(UDFs), each of which can be a great addition to your SQL repertoire.
UDFs are SQL code subroutines that can be created in a database and
invoked in many situations. The types are:
n Scalar UDFs
n Inline table-valued UDFs
n Multistatement table-valued UDFs
The initial sections of this chapter describe each type of UDF and show
how to use them. An example or two accompanies each type of UDF. The
intent is to give you a quick overview of the different types of functions
and how to use them.
Once you get the overall idea of UDFs, the second part of this chapter
discusses why you would use them. There isnt really anything that can be
done in a UDF that couldnt be done in some other way. Their advantage is
that theyre a method for packaging T-SQL code in a way thats more reus-
able, understandable, and maintainable. The UDF is a technique for
improving the development process.
As we consider why to use UDFs, we should also consider potential
reasons not to use them. UDFs have disadvantages because of perfor-
mance degradation and loss of portability, and we shouldnt ignore them.
In particular, UDFs have the potential for introducing significant perfor-
mance problems. The trick in using UDFs wisely is knowing when the
potential problems are going to be real problems. Performance is a topic
that comes up throughout this book.
Now lets get down to the meat. If you want to execute the queries
in this chapter, theyre all in the file Chapter 1 Listing 0 Short Queries.sql,
3
4 Part I
Chapter 1: Overview of User-Defined Functions
which youll find in the download directory for this chapter. I strongly sug-
gest that you try it by starting SQL Query Analyzer and opening the file.
Introduction to UDFs
All three types of UDFs start with the CREATE FUNCTION keywords, a func-
tion name, and a parameter list. The rest of the definition depends on the
type of UDF. The SQL Server Books Online does an adequate job of
describing the CREATE FUNCTION statement, so I wont reproduce the docu-
mentation that you already have. The next sections each concentrate on
one type of UDF and show how to create and use them through examples.
Scalar UDFs are up first.
Scalar UDFs
Scalar UDFs are similar to functions in other procedural languages. They
take zero or more parameters and return a single value. To accomplish
their objective, they can execute multiple T-SQL statements that could
involve anything from very simple calculations to very complex queries on
tables in multiple databases.
That last capability, the ability to query databases as part of determin-
ing the result of the function, sets SQL Server UDFs apart from functions
in mathematics and other programming languages.
Listing 1.1 has an example of a simple scalar UDF, udf_Name_Full. If
you store names as separate first name, middle name, and last name, this
function puts them together to form the full name as it might appear on a
report or check.
Before we use udf_Name_Full in a query, permission to execute it must
be granted to the users and groups that need it. The database owner (dbo)
has permission to execute the function without any additional GRANT state-
ments, so you may not run into this problem until someone else tries to
use a UDF that you created. For any user or group other than dbo, you
must grant EXECUTE permission before they may use the function. At the
bottom of Listing 1.1, the GRANT statement gives EXECUTE permission to the
PUBLIC group, which includes everyone. There is more on the topic of per-
missions for scalar UDFs in Chapter 2. That chapter also deals with the
permissions required to create, alter, and delete UDFs. As dbo, you have
all the permissions necessary to work with all your database objects.
Other users require that you grant permission explicitly.
Part I 5
Chapter 1: Overview of User-Defined Functions
END
GO
The Authors table in pubs stores separate first and last names in the col-
umns au_fname and au_lname, respectively. Heres how you might use
udf_Name_Full in a select list to combine them:
Author Title
------------------- --------------------------------------------------------
Cheryl Carson But Is It User Friendly?
Stearns MacFeather Computer Phobic AND Non-Phobic Individuals: Behavior Var
Livia Karsen Computer Phobic AND Non-Phobic Individuals: Behavior Var
Michael O'Leary Cooking with Computers: Surreptitious Balance Sheets
Stearns MacFeather Cooking with Computers: Surreptitious Balance Sheets
(Results)
In addition to T-SQL logic and calculations, a scalar UDF can read data
USE Northwind
GO
-- These options should be set this way before creating any UDF
Set Quoted_Identifier ON
Set ANSI_Warnings ON
GO
RETURN @Territories
END
GO
(Results)
The select list isnt the only place where a UDF can be invoked. The next
example uses udf_EmpTerritoryCOUNT in the WHERE clause and the ORDER BY
clause in addition to the select list:
8 Part I
Chapter 1: Overview of User-Defined Functions
(Results)
USE Northwind
GO
-- These options should be set this way before creating any UDF
SET Quoted_Identifier ON
SET ANSI_Warnings ON
GO
) RETURNS TABLE
/*
* Returns a table of information about the territories assigned
* to an employee.
*
* Example:
select * FROM udf_EmpTerritoriesTAB(2, 2, 3)
****************************************************************/
AS RETURN
GO
(Results)
SELECT r.RegionID
, r.RegionDescription as [Region]
FROM udf_EmpTerritoriesTAB (@EmpID) t
LEFT OUTER JOIN Region r
ON t.RegionID = r.RegionID
GROUP BY r.RegionID, r.RegionDescription
GO
(Results)
RegionID Region
----------- --------------------------------------------------
4 Southern
There are ten columns in the output. I only use a few of them in any one
query. As a general-purpose function, udf_DT_MonthsTAB contains all the
columns that might be used by different programmers in different situa-
tions. For instance, there are several different ways to represent the end
of the month: EndDT, End_SOD_DT, EndJulian, and NextMonStartDT. Any one of
these could be used to group datetime values into the month in which they
belong. Well see one possible method in the example that follows.
Now that we have a table of months, we need date-based data to com-
bine it with in order to make our report. We can use the Northwind Orders
and [Order Details] tables to produce rows of items shipped by date. The
UnitPrice, Quantity, and Discount columns are combined to produce the
Revenue for each item. Heres the query with its first five rows of output:
Part I 15
Chapter 1: Overview of User-Defined Functions
-- Months will be joined with this subquery in another example that follows
GO
Next, combine the two queries to produce a report of revenue per month
for the first six months of 1998:
(Results)
The GROUP BY clause and the SUM function aggregate the ShippedItems by
month. The CAST function is used to produce two decimal places to the
right of the decimal. The warning is issued because the SUM aggregate
function is aggregating no rows in the month of June 1998. Thats an
important point because one of the reasons for using udf_DT_MonthsTAB and
similar UDFs for reporting on months is that periods with no activity
show up in the results instead of disappearing. The next chapter takes this
point a little further when it discusses the pros and cons of using UDFs.
There are many other ways to use multistatement UDFs. Some, like
udf_DT_MonthsTAB, are general-purpose functions that can be used with any
database. There are also many uses that are tied to an individual database.
Chapter 8 has much more information about multistatement UDFs and
various ways to put them to work.
Now that youve been introduced to UDFs, why would you use them?
Let me say something again that I said before: Its the economic benefits
of creating UDFs, not the technical ones, that count the most. The next
section discusses the economic arguments for and against using UDFs.
costs and benefits associated with a database. UDFs have a part to play in
Reuse
Software reuse has been the Holy Grail of programming methodologies as
long as I can remember. I recall Modular Programming, Structured Pro-
gramming, Object-Oriented Programming, and, more recently, Component
Design. All these methodologies have reuse as one of their primary objec-
tives and as a major selling point.
Why not? If you can reuse code, you dont have to write it. With the
high cost of developing systems, reuse looks like the way to go. While the
history of software reuse is less spectacular than the hype that precedes
each new technology, there have been some successes. The oldest model,
and maybe the most successful, is the function library.
Function libraries have been a way to package tested code and reuse
it since the days of FORTRAN. The function library paradigm of reuse
comes from the age of Modular Programming. Programmers have been
creating and reusing function libraries for many years, and UDFs fit most
closely into this model.
The basic idea of the function library is that once written, a gen-
eral-purpose subroutine can be used over and over again. As a consultant
developing SQL, VB, ASP, and .NET applications, I carry around a library
18 Part I
Chapter 1: Overview of User-Defined Functions
of a few hundred subroutines that Ive developed over the years. Since
SQL Server 2000 came on the scene, Ive added a library of UDFs.
My UDF library has a lot of text- and date-handling functions. Listing
1.3 shows one of them, udf_Txt_CharIndexRev. The SQL Server built-in
function CHARINDEX searches for a string from the front. udf_Txt_Char-
IndexRev searches from the back.
RETURN @WorkingVariable
END
GO
(Results)
Extension
----------------
txt
Now that its written, I dont have to write it again. Neither do you. Youll
find many other reusable UDFs in this book and in the many SQL script
libraries on the web.
Of course, I could have written the function in such a way that it
wouldnt be very reusable. For example, I could have written the function
to work with one specific column of one specific table. That would work
just as well the first time I needed it.
The database that comes with this book contains over 100 functions.
Youll find many that you can reuse. The rest are examples with reusable
techniques.
Ease of Coding
A good coder is a lazy coder. I dont mean knocking off work and heading
to the beach or sitting at your desk playing Solitaire instead of working. I
mean that a good coder looks for ways to do more with less: fewer lines of
code and less effort. The point is to find easy ways to deliver what the cus-
tomer needs so the programmer can move on to project completion.
We could live without the UDF udf_EmpTerritoriesTAB that was
defined in the section on inline UDFs. Instead of:
SELECT et.TerritoryID
, t.TerritoryDescription as [Territory]
FROM EmployeeTerritories et
LEFT OUTER JOIN Territories t
ON et.TerritoryID = t.TerritoryID
WHERE et.EmployeeID = = @EmpID
ORDER BY t.TerritoryDescription
Performance
When considering the execution cost, we should always ask ourselves,
Does it matter?
Purists may say, Performance always matters!
I disagree. As far as Im concerned, performance matters when it has
an economic impact on the construction of a system. When your system
has the available resources, use them. Other economic benefits of using
UDFs and simplifying code usually outweigh performance concerns.
In the case of UDFs, the economic benefit to using them comes from
more rapid software development. If used well, the three factors men-
tioned earlierreuse, organization, and ease of codinghave a large
impact on the development process.
The performance penalty comes in execution speed. The SQL Server
database engine is optimized for relational operations, and it does a great
job at performing them quickly. When SQL Server executes a UDF in a
SELECT statement that uses a column name as a parameter, it has to run
the function once for every row in the input. It sets up a loop, calls the
UDF, and follows its logic to produce a result. The loop is essentially a
cursor, and thats why there are many comparisons of UDFs to cursors.
Cursors have a justified reputation for slowing SQL Servers execution
speed. When overused, UDFs can have the same effect on performance as
a cursor. But like a cursor, its often the fastest way to write the code.
Over the last 17 years, Ive managed many projects. Ive got a pretty
good record of completing them on time, and Ive evolved the following
philosophy about writing efficient code:
22 Part I
Chapter 1: Overview of User-Defined Functions
Summary
This chapter has served as an introduction to the three types of UDFs:
n Scalar UDFs
n Inline table-valued UDFs
n Multistatement table-valued UDFs
Chapters 2, 7, and 8 go into detail about each type.
By now you should have an understanding of what UDFs are and how
theyre used. Theyre a great tool. But as discussed in this chapter, there
can be performance problems with UDFs. In fact, they can slow a query by
a hundredfold. That can be quite a price to pay.
The choice to use a UDF is really a balancing act between the benefits
and the costs associated with them. Most of the time, the benefits in
reduced development time, improved organization, and ease of mainte-
nance are worth both the effort involved and any performance penalty.
Now that youve seen an overview of UDFs and have heard my ideas
about why to use them, its time to drill down into the details of creating,
debugging, using, and managing UDFs. The next chapter goes into depth
on scalar UDFs. That is followed by several chapters that deal with the
really practical issues of using the SQL Server tools, documentation,
debugging, naming, and handling run-time errors. After those chapters,
we get back to inline and multistatement UDFs in Chapters 7 and 8,
respectively.
2
Scalar UDFs
Scalar:
3. <programming> Any data type that stores a single value
(e.g., a number or Boolean), as opposed to an aggregate
data type that has many elements. A string is regarded as a
scalar in some languages (e.g., Perl) and a vector of
characters in others (e.g., C).
The Free On-line Dictionary of Computing
I think thats a pretty good definition of the term scalar for programming.
By the way, a string is a scalar in T-SQL.
Scalar UDFs return one and only one scalar value. Table 2.1 lists data
types that can be returned by a scalar UDF. The types text, ntext, and
image arent on the list.
Table 2.1: Data types that can be returned by a UDF
23
24 Part I
Chapter 2: Scalar UDFs
1 My reference, A Guide to the SQL Standard 4th Edition, by C.J. Date with Hugh
Darwin, (Addison Wesley), doesnt make such a distinction. It puts statements like
IF and WHILE in the Persistent Stored Module appendix because theyre not yet
part of the standard. Ive placed IF and WHILE with the DML statements for clarity.
Part I 25
Chapter 2: Scalar UDFs
LimitedDBA can now create any type of function, not just scalar UDFs.
If a user without CREATE FUNCTION permission tries to create a func-
tion, he will get the following message:
26 Part I
Chapter 2: Scalar UDFs
(Results)
(Results)
can manipulate, the DENY statement can be used to remove permissions for
(Result)
(Results)
Logged in as
------------------------------------------------------------
LimitedDBA
The Books Online gives definitions for each of the elements of the func-
tion declaration. The remarks that follow are my comments. Chapter 6 has
additional information about how to format the CREATE FUNCTION script for
maximum usefulness.
owner_name While SQL Server allows each user to have his or her
own version of a function, I find that having database objects owned by any
user other than dbo leads to errors. I suggest that you always use dbo for
the owner name. If you dont specify the owner name explicitly, the func-
tion is created with the current user as the owner. But you do not have to
be the dbo to create a function owned by dbo.
function_name You may have noticed that I use a naming convention
for functions. They always begin with the characters udf_. Most have a
Part I 29
Chapter 2: Scalar UDFs
function group name, such as Txt for character string processing func-
Note:
I usually encourage the use of UDTs. They add consistency to the defi-
nition of the database. However, in general purpose functions that are
intended to be added to many databases, they complicate the process
of distributing the function because the UDT must be distributed with
any function that references it. Another complication is that UDTs cant
be used when the WITH SCHEMABINDING option is used to create a UDF.
For these reasons, you may decide to create the functions without
referring to UDTs.
caller as possible. This keeps the amount of comments that are needed to
a minimum. Chapter 6 is all about how to use naming conventions, com-
ments, and function structure to make a function better, including an
extensive discussion of the comment block.
Listing 2.1 is the CREATE FUNCTION script for the very simple string
handling function udf_Example_Palindrome. It returns 1 when the parame-
ter is the same from front to back as it is from back to front. To keep the
function simple, it doesnt ignore punctuation or spaces as a human might.
You can see that the function uses most of the parts of the syntax but has
no WITH clause or function body, just a RETURN statement. Most functions
have a function body, and its contents are the subject of the next section.
Part I 31
Chapter 2: Scalar UDFs
CREATE TABLE. TABLE variables are visible only within the function body of
the function in which they are declared.
Unlike temporary tables, TABLE variables have very limited scope.
They cant be passed to stored procedures or triggers. They also cant be
accessed by functions other than the one in which the TABLE is declared. In
addition to their use in scalar functions, we also saw in Chapter 1 that
theyre created in the RETURNS statement of a multistatement UDF.
TABLE variables have many of the same features, such as constraints
and computed columns, as database tables. But most TABLE variables that
Ive ever created are pretty simple. This DECLARE statement for the @Emp
TABLE variable demonstrates some of the available constraints.
There are PRIMARY KEY, NOT NULL, NULL, UNIQUE, and CHECK constraints on the
table and its columns. Theres also an example computed column,
FullName.
TABLE variables dont have storage related options such as ON file-
group or fillfactors. Also, they may not have foreign key constraints nor
may indexes be created for them, other than indexes created implicitly for
primary keys and unique constraints.
Once created, a TABLE variable is manipulated with the usual SQL
DML statements: INSERT, UPDATE, DELETE, and SELECT. There is an example
of using these statements in the section Using SQL DML in Scalar
UDFs.
Note:
Many programmers have the misconception that TABLE variables are
stored only in memory. Thats incorrect. While they may be cached in
memory, TABLE variables are objects in tempdb and they are written to
disk. However, unlike temporary tables, they are not entered into the
system tables of tempdb. Therefore, using TABLE variables does not
exhaust memory nor are they limited to available memory. They are
limited only by the size of tempdb.
Part I 33
Chapter 2: Scalar UDFs
@Num INT
) RETURNS bit -- 1 if @Num is prime, otherwise 0
/* Returns 1 if @Num is prime. Uses a loop to check every odd
* number up to the square root of the number.
*
* Example:
SELECT dbo.udf_Num_IsPrime (31) [Is Prime]
, dbo.udf_Num_IsPrime (49) [Not Prime]
****************************************************************/
AS BEGIN
SET @Divisor=3
SET @N = SQRT(@Num)
RETURN @resultBIT
END
GO
Youll find many other SET statements in the UDFs in this book. However,
when several SET statements are used one after the other, its a good idea
to rewrite them into a single SELECT statement. This is because the over-
head of a single SELECT is slightly less than the overhead for the individual
SET statements and there are fewer statements to work with when debug-
ging the function or tracing it with the SQL Profiler.
Part I 35
Chapter 2: Scalar UDFs
TABLE variables are very similar to database tables, and the INSERT,
UPDATE @Emp
SET AgeAtHire = DATEDIFF (y, Birthdate, HireDATE)
RETURN @MinAge
END
GO
The last statement available for use in a scalar UDF is the EXEC statement.
Most forms of the EXEC statement are prohibited in UDFs. The only
allowed form is using EXEC on an extended stored procedure that doesnt
return any rows. Using extended stored procedures in UDFs is the subject
of Chapter 10.
The CREATE FUNCTION statement has two options that can be specified
using the WITH clause. They each modify how SQL Server creates the
UDF.
END
GO
(Results)
(Results)
/*** Encrypted object is not transferable, and script cannot be generated. ***/
Of course, SQL Server still tries to help you use the UDF so you can still
see the parameter list in Query Analyzer, as shown in Figure 2.1. But
Query Analyzer and all the other SQL Server tools wont show the defini-
tion of the function.
38 Part I
Chapter 2: Scalar UDFs
The text tool for examining the definition of a UDF is sp_helptext. Youll
learn more about it in Chapter 9. It wont reveal the text of the function, as
shown by this attempt:
(Results)
The refusal of SQL Server to generate the script of an object created using
WITH ENCRYPTION makes source code control difficult. In fact, if youre plan-
ning on using it, be sure that you have some other mechanism for storing
the source code of your UDFs. I suggest a tool like Visual SourceSafe.
So long as all youre looking for is protection from the casual observer,
WITH ENCRYPTION can protect your source code. The best protection
remains a scheme that restricts access to the database and only grants
permission to use tools such as sp_helptext to trusted users.
WITH ENCRYPTION protects the source code of your functions. The other
option on the CREATE FUNCTION statement is WITH SCHEMABINDING. It protects
a UDF from modification in the results that it returns due to changes in
the database objects that it references. It also provides that type of protec-
tion to any object that references your function.
Part I 39
Chapter 2: Scalar UDFs
(Results)
)
GO
(Results)
(Results)
@DateTime datetime
) RETURNS varchar(8)
/*
* Returns the time portion of a datetime as a character string
* in the form HH:MM:SS
* Example:
SELECT dbo.udf_DT_TimePart (Getdate())
****************************************************************/
AS BEGIN
END
GO
GRANT EXEC, REFERENCES on dbo.udf_DT_TimePart to [PUBLIC]
GO
(Results)
Now try to create a view that uses the UDF. It must be created using the
WITH SCHEMABINDING option in order to be indexed:
(Results)
Ah ha! The rule that requires that all views and UDFs be referenced by a
schemabound object hasnt been followed. Lets go back and modify
udf_DT_TimePart so it qualifies:
42 Part I
Chapter 2: Scalar UDFs
@DateTime datetime
) RETURNS varchar(8)
WITH SCHEMABINDING
/*
* Returns the time portion of a datetime as a character string
* in the form HH:MM:SS
****************************************************************/
AS BEGIN
END
GO
(Results)
(Result)
Our view is created. Right now its an ordinary view. Can it be indexed?
There are many restrictions on what views can be indexed, and I had
to be pretty careful about constructing both udf_DT_TimePart and
CalendarByLocation so that they qualify. Because almost all the queries on
the view are for a single date for all locations, Ive put EventDate first in
the index. Heres the script to create the index on the view:
Part I 43
Chapter 2: Scalar UDFs
(Results)
When permission to execute a UDF is granted to a user, the user can also
Once LimitedDBA has the right to grant EXEC and REFERENCES to other users,
the user can do so and can also pass on the right to pass it on. This is dif-
ferent from the statement permissions required to create and manage
UDFs, which cant be passed on by those who hold them.
REVOKE and DENY are used for the EXECUTE and REFERENCES permissions
in the same way that they are used for all other permissions. Check the
Books Online if you need to know any details. The Listing 0 file also has
an example of REVOKE.
Now that permission to use our UDFs has been granted, lets move
on to show you how they can be invoked.
(Results)
-- Get the bill date, grace period of FRANK's 1st 4 orders in 1998
SELECT TOP 4
OrderID, ShippedDate
, dbo.udf_DT_NthDayInMon (YEAR(DATEADD(Month, 1, ShippedDate))
, MONTH(DATEADD(Month, 1, ShippedDate))
, 2, 'TUESDAY') as [Billing Date]
, dbo.udf_DT_WeekdaysBtwn (ShippedDate,
dbo.udf_DT_NthDayInMon (YEAR(DATEADD(Month, 1, ShippedDate))
, MONTH(DATEADD(Month, 1, ShippedDate))
, 2, 'TUESDAY')
) as [Grace Period]
FROM Orders
WHERE ShippedDate >= '1998-01-01' and ShippedDate < '1999-01-01'
AND CustomerID = 'FRANK'
ORDER BY ShippedDate
GO
(Results)
udf_DT_NthDayInMon is used twice in this query. The first time, its used to
-- Get the # of orders and average grace period for FRANK in 1998
SELECT COUNT(*) as [Number of Orders]
, AVG(dbo.udf_DT_WeekdaysBtwn (ShippedDate,
dbo.udf_DT_NthDayInMon (YEAR(DATEADD(Month, 1, ShippedDate))
, MONTH(DATEADD(Month, 1, ShippedDate))
, 2, 'TUESDAY')
)
) as [Average Grace Period in Weekdays]
FROM Orders
WHERE ShippedDate >= '1998-01-01' and ShippedDATE < '1999-01-01'
and CustomerID = 'FRANK'
GO
(Results)
The rule of thumb holds. A UDF is an expression that can be used where
an expression can be used. Next, lets extend that to other clauses: WHERE,
ORDER BY, and SANTA. (Oh, I know. I cant fool you. There is no SANTA
clause.)
(Results)
The WHERE clause has four expressions combined with AND operators. By
placing the three simple comparisons first, the expression that uses
udf_DT_WeekdaysBtwn and udf_DT_NthDayInMon to calculate the grace period
is only evaluated for rows that satisfy the first three conditions. This is the
smallest number of rows possible.
The ORDER BY clause in the query also uses a method to limit the num-
ber of invocations of the two UDFs. Ordering can be done either by using
the expressions to be ordered or by giving an integer that represents the
column in the select list to be used for ordering. In this case, column 3 in
the select list is our grace period calculation.
Normally I dont favor using column numbers in the ORDER BY clause.
Its too easy to make a mistake by, for example, adding a column to the
select list and forgetting to change the column number in the ORDER BY
clause. However, because of the overhead of executing UDFs, I make an
exception in this case. Using the column number eliminates the need for
the query engine to execute the UDF again.
Part I 49
Chapter 2: Scalar UDFs
There are other places that scalar UDFs can be used. The next sec-
(Results)
The query now has a non-NULL result for June 1998 because there are bill-
ings in that month even if there are no shipments. The ON clause that uses
udf_DT_NthDayInMon is shown in the second shaded area. Listing 0 has both
50 Part I
Chapter 2: Scalar UDFs
the original query from Chapter 1 and the refined query joined on the bill-
ing date.
The first shaded area is kind of interesting. It shows how the dates in
the WHERE clause of the inline SELECT must to be modified. Since billing
takes place one month after the shipment, the dates in the inline SELECT
had to be moved up a month to the range '1997-12-01' through
'1998-06-01'.
The WHERE clause in the inline SELECT isnt really necessary. We would
get the same results without it because the LEFT OUTER JOIN statement
limits the output rows to the months produced by the rowset returned by
udf_DT_MonthsTAB, January through June 1998. However, there would be a
performance penalty to pay.
What would have happened without that additional WHERE clause?
Without it, the inner SELECT returns rows for all orders instead of just for
orders in the time period of interest. Thats 2155 rows instead of 763
rows, and udf_DT_NthDayInMon is evaluated for all of them. Thats the
potential performance penalty that was avoided by having the WHERE
clause.
SELECT isnt the only DML statement in which you can use a UDF. The
next section shows how to use them in INSERT, UPDATE, and DELETE
statements.
The assignment type of SET changes the value of a local variable. It has the
This use of SET was discussed previously in this chapter in the section
Using the CREATE FUNCTION Statement. You wont be surprised to
hear that the expression on the right-hand side of the equals sign can
invoke a scalar UDF.
The second type of SET is used to change the value of a database
option for the current connection. This type of SET doesnt take any
expressions in any part of its syntax, and so there is no way to use a UDF
in these SET statements. What is important and interesting is the way that
the database options that are normally modified with a SET statement work
when a UDF is executing. This topic is covered in Chapter 4, which dis-
cusses some of the subtleties of the execution environment of UDFs.
EXEC and PRINT are the last DML statements that can invoke a UDF.
EXEC has a few flavors. All of them are tasted in the next section.
-- Invoking a UDF from an EXEC statement to find the first Mon in Oct.
DECLARE @1stMonInOct smalldatetime
EXEC @1stMonInOct = dbo.udf_DT_NthDayInMon 2003, 10, 1, 'MONDAY'
PRINT '2003''s U.S. Supreme Court session begins on '
+ CONVERT(varchar(11), @1stMonInOct, 100)
GO
(Results)
This use of a UDF is mentioned in the first paragraph of the Books Online
page about CREATE FUNCTION and nowhere else in that document. It seems
to be derived from a UDFs similarity to a stored procedure. Notice that
there are no parentheses around the arguments to the function. This form
of EXEC can be used in a stored procedure or a batch but not from within a
UDF.
52 Part I
Chapter 2: Scalar UDFs
The last place to invoke a UDF with EXEC is from within a dynamic
SQL statement. Theres no restriction on using a UDF in a string exe-
cuted dynamically. Heres an example of dynamic SQL that uses a UDF in
a PRINT statement:
I had forgotten about the PRINT statement. PRINT is just another example of
a UDF being used where an expression can be used.
That wraps up the places that Im aware of for using a UDF in SQL
DML. DML covers the SQL used to manipulate data from batches, stored
procedures, and triggers. All of these uses are governed by the EXEC per-
mission on the UDF. The remaining uses of scalar UDFs are in SQL DDL
statements that define tables, indexes, and views. Theyre the subject of
the remainder of this chapter.
RETURN @sum
END
The NOCHECK clause on the first line of the ALTER TABLE statement tells SQL
Server not to apply this rule to rows that are already in the database. If the
NOCHECK clause had been omitted, existing rows would have failed the test
and the constraint would not have been created.
The CHECK constraint is removed with another ALTER TABLE statement.
This batch removes the constraint that was just created:
CHECK constraints may also use UDFs that select data from the database in
order to decide if a column value is okay. This feature allows them to
Part I 55
Chapter 2: Scalar UDFs
bypass the usual restriction that CHECK constraints only work on data in the
-- CHECK constraint to limit shipping when the order doesn't total 100
ALTER TABLE NWOrders WITH NOCHECK
ADD CONSTRAINT Ship_Only_Orders_Over_100
CHECK (ShippedDate is NULL
OR dbo.udf_Order_Amount (OrderID) >= 100
)
GO
To test that the constraint works, well need to find an order with a NULL
ShippedDate that has an amount less than 100. We do it with this query:
(Result)
The first order wont satisfy the constraint, so this next script uses it in an
attempted update:
(Results)
The constraint prevented the updating of order 11019 with the shipping
date. In a real-world application, its up to the application to recognize that
56 Part I
Chapter 2: Scalar UDFs
the update failed and handle the failure. Thats why its rarely enough to
only enforce business rules at the database level. The application must be
aware of the rules and must communicate them to the user effectively.
Had the restriction been enforced in a trigger, it would have been possible
to raise a custom error, which can be more specific and easier for a user or
program to handle.
When a UDF is used in a constraint, SQL Server doesnt allow any
alterations to the function. Its almost as if it is schemabound to the com-
puted column. This script attempts to alter udf_Order_Amount:
(Results)
Price] column synchronized becomes quite a job for the user of the data-
(Results)
Thats all there really is to using a UDF in a computed column. The UDF
is an expression, and it can be used in a computed column on its own or as
part of an expression that combines its result with other expressions.
As we saw with a constraint, using a UDF in a computed column binds
the UDF to the column, and SQL Server prohibits any alteration to the
function. An attempt to alter udf_DT_NthDayInMon receives this error
message:
The computed column would have to be dropped from the table before the
UDF could be changed.
Computed columns extend a table by adding a new column. One inter-
esting aspect of computed columns is that they can be indexed. That
includes computed columns based on UDFs. This is where determinism
really becomes important.
(Results abridged)
(Results)
It wouldnt be that hard for the SQL engine to create the index. After all,
it can calculate the result of the expression very easily. However, what
would happen in the next instant? The value of the expression might
change at any time as the month anniversary is passed. In this case the
order of the rows wouldnt really change, but sometimes rows would have
60 Part I
Chapter 2: Scalar UDFs
the same age in months and at other times they would not. Every time the
SQL engine went to read the index, it would have to recalculate all the val-
ues. What youd have wouldnt be any help as an index. It would be more
like a view.
There is another set of requirements for the proper use of views on
computed columns. A group of session options, listed in Table 2.2, must be
set to consistent values. These options affect the results of evaluating
some expressions. If theyre not consistent, the results of any UDF might
change.
Table 2.2: Session options that must be set to use an index on a computed column
Option Setting in a UDF Description
ANSI_NULLS ON Governs = and <> comparisons to
NULL
ANSI_PADDING ON Governs right side padding of char-
acter strings
ANSI_WARNINGS ON Governs the use of SQL-92 behavior
for conditions like arithmetic overflow
and divide-by-zero
ARITHABORT ON Governs whether a query is termi-
nated when a divide-by-zero or
arithmetic overflow occurs
CONCAT_NULL_YIELDS_NULL ON Governs whether string concatenation
with NULL yields a NULL
QUOTED_IDENTIFIER ON Governs the treatment of double quo-
tation marks for identifiers
NUMERIC_ROUNDABORT OFF Governs whether an error is raised if
a numeric expression loses precision
These options should be set when the index is created and in any session
that hopes to use the index. If the session options are not set correctly
when a query is made on the table, SQL Server ignores the index. Cre-
ating and maintaining it become a waste of effort.
For UDFs to be used in computed columns that are indexed, particu-
lar attention has to be paid to two of these options: QUOTED_IDENTIFIER and
ANSI_NULLS. These options can only be set when the function is created or
altered. Their run-time setting has no effect when the UDF is executed.
Therefore, all UDFs should be created with these options ON. Thats why
youll see this batch at the beginning of CREATE FUNCTION scripts:
SET ANSI_WARNINGS ON
SET QUOTED_IDENTIERS ON
GO
The other five session options must be set to the correct value at run
time. Fortunately, ADO (OLE DB) and ODBC set every option except
Part I 61
Chapter 2: Scalar UDFs
Summary
Scalar UDFs are only one of the three types of functions, but they repre-
sent the most common type of UDF and the only one that doesnt return a
rowset. This chapter has covered how to create and use them.
It is important to understand the permissions that govern the right to
create, alter, and delete UDFs. The creation of all types of UDFs is gov-
erned by the statement permission CREATE FUNCTION. That permission is
given to members of the db_ddladmin fixed database role, which includes
the database owner. It can also be passed on to other users as needed.
However, the ALTER FUNCTION and DROP FUNCTION permissions cannot be
given to users who are not part of db_ddladmin.
Two permissions, EXECUTE and REFERENCES, govern the use of scalar
UDFs in the two major parts of the SQL language: SQL DML and SQL
DDL. EXECUTE permission governs the use of UDFs in SQL DML, the part
of SQL that changes data. REFERENCES permission governs the use of UDFs
in SQL DDL, the part of SQL that defines tables and other database
objects.
Once permissions are granted, a simple rule of thumb applies: A sca-
lar UDF can be used wherever an expression can be used. We saw how
this works in the select list and in the WHERE and ORDER BY clauses. Other
spots for using scalar UDFs include the ON clause of a JOIN and the right-
hand side of a SET clause of the UPDATE statement.
If the table owner has REFERENCES permission on a scalar UDF, it can
also be used in CHECK constraints and computed columns, including com-
puted columns that are indexed. Using a UDF, a CHECK constraint or
computed column allows calculations to be made on data thats not in the
62 Part I
Chapter 2: Scalar UDFs
row being checked. This extends the power of these two features. Of
course, the same functionality can also be achieved using triggers.
The creation of indexes on computed columns creates a new require-
ment for determinism of any functions used in the computed column
definitions. This includes a prohibition on indexing a computed column
that uses any scalar UDF that references data. Indexes cant be main-
tained without determinism, so its a restriction that must be obeyed.
Scalar UDFs are the first of the three types of UDFs to get their own
chapter. Inline UDFs and multistatement UDFs are covered in Chapters 7
and 8, respectively. Before we get to them, Chapters 3 through 6 cover
topics that affect all functions. Lets move on to Chapter 3, which shows
you how to use the SQL Server tools to work with UDFs.
3
63
64 Part I
Chapter 3: Working with UDFs in the SQL Server Tools
Query Analyzer
SQL Query Analyzer, or just Query Analyzer, is Microsofts GUI tool for
executing and analyzing SQL scripts. Its not the only tool available for the
task. Microsoft provides two similar command-line toolsOSQL and the
rather ancient ISQL. Theyre discussed in the final section of this chapter.
In addition, there are third-party tools, some with excellent reputations.
This section concentrates on Query Analyzer because thats the GUI tool
available to everyone with SQL Server.
Those of you using MSDE may be more interested in alternative
tools. Three ways to work with functions come from Microsoft: Access,
Visual Studio, and Visual Studio .NET. All have some capability to work
with UDFs. As of the summer of 2003, Access 2002 and Visual Studio
.NET are the best of the Microsoft tools. Other companies have their own
tools, but I rarely get to use them.
Lets start with the basics. Figure 3.1 shows the Query Analyzer
window. The function list for the TSQLUDFS database is expanded and
highlighted with the label A . It shows the functions already defined in the
database.
Debugging UDFs
Since it isnt possible to debug UDFs directly in SQL Server 2000, they
have to be debugged by creating a stored procedure that invokes the UDF
and then stepping into the UDF during the debugging session. This isnt
very difficult.
All three types of UDFs can be debugged. The scalar and multi-
statement UDFs are the most interesting because they can have more
than one statement, loops, conditional execution, and other debuggable
features. Inline UDFs are only worth debugging for the sake of stepping
into other UDFs that they might invoke. Well start with scalar UDFs, and
Ill also show an example of debugging a multistatement UDF.
The first step in the debugging process is to create the stored proce-
dure for debugging. I always name the stored procedure by starting with
the characters DEBUG_ followed by the name of the UDF. Youll find several
DEBUG_ stored procedures in TSQLUDFS. For an example, lets use
DEBUG_udf_DT_NthDayInMon, which is shown in Listing 3.1.
The procedure has two test cases. The first one is the test taken from the
function header. The second is a harder case that I made up for debugging.
Like the tests that I embed in function headers, DEBUG_ procedures
should be self-documenting. Thats why each test case has two SELECT
statements. The first is used to step into the function and get the answer.
The second checks the answer and selects the result as part of a rowset
for the user to see. Its important to return the results to the caller so he
or she can see exactly whats returned. The SQL debugger doesnt high-
light the return value as well as I would like. The second SELECT in each
test shows the answer to the caller and checks the functions response for
correctness.
The DEBUG_ procedure should test the functions result for correctness
and tell the caller if it is right or wrong. Reporting the correctness of the
result, rather than the result itself, makes life easier both for the person
who writes the test in the first place and for anyone maintaining the UDF
after the first few days of creation. Dont forget Alzheimers law, which is
something about how easy it is to forget something, like the code you
wrote a few weeks ago, but I dont remember.
Theres no GRANT statement shown for the DEBUG_ procedures. These
procedures arent for public consumption.
Select the stored procedure, right-click on the procedure name, and
use the Debug menu item on the procedures context menu to start
debugging. Figure 3.2 shows the context menu as Debug is being selected.
The debugger stops on the first executable statement of the DEBUG_ proc.
The yellow arrow in the left border points into the code window to show
68 Part I
Chapter 3: Working with UDFs in the SQL Server Tools
the next statement to be executed. Below the code window are the Locals
window, the Globals window, and the Callstack.
The row of icons above the code window control execution of code
during debugging. Each of the icons has a keyboard equivalent. A chart of
keyboard shortcuts is given in Appendix B.
At this point, press F11 or use the Step Into icon to start the
function that we want to debug. If there were problems doing type conver-
sion on the parameters to the function, an error would be raised before
stepping into the function. Assuming that there are no such problems, the
T-SQL Debugger steps to the first executable statement of udf_DT_Nth-
DayInMon. Figure 3.5 shows what it looks like after Ive adjusted the size of
some of the windows.
Theres almost never enough space on the screen for everything youd
like to see while using the T-SQL Debugger. Adjusting the windows helps
to see the key facts. I like to make the Locals window pretty big and keep
the Callstack in view. The Globals window isnt very useful for debugging
UDFs, so I make it very narrow.
Step through the UDF with either the Step Into icon (F11) or Step
Over icon (F10). If the statement doesnt invoke any more UDFs, there is
no difference between the two ways to step. If the statement calls a UDF
and you dont want to step into it, use Step Over. Figure 3.6 shows the
debug window after Ive stepped over several statements.
Part I 69
Chapter 3: Working with UDFs in the SQL Server Tools
Ive enlarged the Locals window so I can see several of the local variables.
Youll still have to scroll through the window to see them all. Either that
or do what I did and purchase a very large monitor, or two, and a high-res-
olution, multi-monitor display card.
One of the most important aspects of the Locals window is that you
can change the value of a local variable. This allows you to fix your inter-
mediate results so you can limit the number of debugging trials needed to
resolve an issue. This is the only way that you can change how the UDF
executes as you debug it. Unlike some other debuggers, you cannot alter
the order of execution of statements and you cannot change any of the
statements themselves.
Once youve decided on a change, its time to go back to the ALTER
script for the function. Since the debugger places a lock on the functions
definition, you must close the debugger window before you can alter the
UDF. Once youve altered the function, you can debug the DEBUG_ proc
again. Unless youve changed the number or meaning of the parameters,
theres no need to change the stored procedure.
After I was satisfied that I didnt need to make any changes to
udf_DT_NthDayInMon, I pressed F5 and let the execution of the function and
the stored procedure complete. Figure 3.7 shows the debugger in a com-
pleted state. The bottom panel has been enlarged to show the output of
DEBUG_udf_DT_NthDayInMon.
70 Part I
Chapter 3: Working with UDFs in the SQL Server Tools
The code window, Locals window, and Callstack remain as they were when
I pressed F5 to complete execution. The gray shade of the Locals window
indicates that you may no longer change the values it displays.
Note:
There is another available user interface for debugging UDFs:
Visual Studio .NET. It has a direct interface to debugging and doesnt
require setting up a stored procedure. I find the stored procedure
method preferable because it always supplies parameters and tells me
if the results were correct.
The T-SQL Debugger is a great tool that I use often. Im looking forward
to improvements in future releases of SQL Server.
Another great feature of Query Analyzer is the ability to create UDFs
from templates. This is useful because you can create your own templates.
The next section shows you how.
Figure 3.8 shows the Query Analyzer as the TSQL UDFS Create Sca-
lar Function.tql template is about to be loaded.
Once the Open menu item is selected, the window opens and the
template is inserted, without modification, into the window. Theres no
need to show that because its the same as Listing 3.2.
The next step is to select the Edit Replace Template Parameters
menu item. This brings up the Replace Template Parameters dialog box
that lets you enter your substitution text for each of the parameters in the
template. Figure 3.9 shows the Replace Template Parameters dialog with
the parameters that Im interested in filled in just before I press the
Replace All button.
Part I 73
Chapter 3: Working with UDFs in the SQL Server Tools
The template parameters are replaced and your query window is left open,
as shown in Figure 3.10, after I closed the Object Browser window.
Of course, theres still work to do to complete the UDF. For starters, since
theres only one parameter to udf_DT_2Julian, @p2 and @p3 must be
removed from the UDF. Also, theres the comment block to write.
Templates are just a way to get a start on writing the UDF.
74 Part I
Chapter 3: Working with UDFs in the SQL Server Tools
SQL Profiler
SQL Profiler is a great tool thats been improved in SQL Server 2000. Im
not going to show you everything about how to use SQL Profiler. For that,
I suggest you take a quick look at the Books Online and then start using it
on a test system. This section concentrates on a few features of the SQL
Profiler that are relevant to UDFs.
For the purpose of event tracing, UDFs are treated as stored proce-
dures. Most but not all of the trace events in the stored procedure
category are applicable to UDFs. Table 3.1 lists the stored procedure
events and describes how they apply to UDFs.
Table 3.1: SQL Profiler events for stored procedures
Event Description
RPC:Output Parameter Not available for UDFs
RPC:Complete Not available for UDFs
RPC:Starting Not available for UDFs
SP:CacheHit One event each time the UDF is found in the proce-
dure cache
SP:CacheInsert One event each time the UDF is compiled and
inserted into the procedure cache
SP:CacheMiss When a UDF is not found in the procedure cache
SP:CacheRemove When a UDF is removed from the procedure cache
SP:Completed When a UDF completes. TextData shows the state-
ment that invoked the UDF.
SP:ExecContextHit The execution version of a UDF has been found in
the procedure cache. (Recompile not necessary)
SP:Recompile A UDF was recompiled. This doesnt happen often
but can be made to happen when an index on a
table referenced by the UDF is dropped or created.
SP:Starting Each time the UDF is started. TextData shows the
statement that invoked the UDF.
SP:StmtCompleted At the end of each executable statement within the
UDF. DECLARE and comments are not executable.
SP:StmtStarting At the start of each executable statement within the
UDF. DECLARE and comments are not executable.
Part I 75
Chapter 3: Working with UDFs in the SQL Server Tools
If you turn on all these events, youll see either a CacheHit or a Cache-
(Results)
udf_SESSION_OptionsTAB udf_DT_age
---------------------- -----------
837578022 1141579105
Well put these object IDs into the trace filter. For a variety of reasons,
theres a good chance that your object IDs will be different from the two
above. Be sure to use the ones from the query that you run when we need
them.
Start SQL Profiler and start a trace. Figure 3.11 shows the Events tab
with the stored procedures events that I suggest you try. I usually use
either SP:StmtStarting or SP:StmtCompleted but not both.
You can add all of the SP events if you like. The RPC events are not fired
for UDFs.
Next, move over to the Data Columns tab. Figure 3.12 depicts this
window after I added the ObjectID column to the Selected data list.
Finally, navigate to the Filters tab and down to the ObjectID tree node of
the Trace event criteria tree and open it up. I suggest that you add the
object IDs of udf_DT_Age to the filter. It isnt really necessary for tracing
during this chapters scripts; its just a way to exercise a useful technique.
Filtering the trace on the object ID(s) of the UDFs that youre interested
in eliminates extraneous events that can be distracting. Figure 3.13 shows
the Filters tab as the object ID is being added.
To filter on a UDF, you should use ObjectID and not ObjectName. The
(Results)
Andy's Age
-----------
47
Figure 3.14 shows the events that were traced during this script. Every
statement in the UDF that was executed caused the SP:StmtCompleted
event.
(Results)
------------------------------------------------------
2001-02-14 00:00:00
The only event you should see is the SQL:BatchCompleted event for the
SELECT statement. That is assuming that you didnt remove SQL:Batch-
Completed from the Events tab of the Trace Properties dialog box. Filters
78 Part I
Chapter 3: Working with UDFs in the SQL Server Tools
on the column dont filter out events that dont have a value for a data col-
umn, such as ObjectID. Thats why SQL:BatchComplete shows up in the
trace.
Now lets try to force a SP:Recompile event. Before you run the
script, stop the trace. Open the properties window and remove the filter
on ObjectID. Then restart the trace and run this script:
(Results)
RETURN @RC
END
80 Part I
Chapter 3: Working with UDFs in the SQL Server Tools
(Result)
Its unfortunate that it doesnt work. We can always hope that Microsoft
fixes it in the next service pack.
SQL Profiler can help solve all sorts of problems. Enterprise Manager
and the other SQL tools are for more routine maintenance.
Summary
83
84 Part I
Chapter 4: You Cant Do That in a UDF
Restriction on Invoking
Nondeterministic Functions
UDFs may not call a nondeterministic built-in function! Well, thats part of
the story because while there are certain functions that are clearly deter-
ministic and others that are clearly nondeterministic, there is a group in
between that can be called by a UDF. However, doing so makes the UDF
nondeterministic.
Appendix A has a list of nondeterministic functions. Ive tried to make
that list as complete as possible by breaking out groups that are all non-
deterministic based on the list from the Books Online.
Attempts to call any of the nondeterministic functions, such as
GETDATE, in a UDF are rejected by the T-SQL parser, and the UDF is never
created. Heres an example:
(Results)
Youll find the same message if you try to use any of the other built-in
functions in the list of always nondeterministic functions.
The Books Online lists two other groups in the article Deterministic
and Nondeterministic Functions. These are functions that are always
deterministic and functions that are sometimes deterministic. The lists in
Books Online are complete, so I wont show anything similar here.
What Books Online omits is a discussion of a group of functions that
return the same result most of the time and may be used in UDFs. But
use of these functions mark the calling function as nondeterministic.
These functions include:
n DATEPART
n DATENAME
Why do they make a UDF nondeterministic? Its because a change in
@@DATEFIRST changes the answer that they return. Any caller could change
@@DATEFIRST before using a UDF that used one of the sensitive functions
Part I 85
Chapter 4: You Cant Do That in a UDF
and then might get different results when the UDF is invoked with the
-- Use udf_DT_SecSince
SELECT dbo.udf_DT_SecSince ('1964-02-09 20:05:00', getdate())
as [Seconds since the Beatles appeared on Ed Sullivan]
GO
Supplying the second argument isnt convenient, and it makes the caller
do the work. UDFs should make the callers job easier, not harder. The
86 Part I
Chapter 4: You Cant Do That in a UDF
AS
SELECT getdate() as [GetDate]
GO
-- Use udf_DT_CurrTime
SELECT dbo.udf_DT_CurrTime() as [The time is now:]
GO
(Results)
Obviously, its time for lunch. Ill go eat before discussing restrictions on
access to data.
88 Part I
Chapter 4: You Cant Do That in a UDF
If you executed it, youll find a message in the Windows application event
log and in the SQL Server log. xp_logevent can be used inside UDFs for
reporting errors. That use is covered more fully in Chapters 5 and 10.
Part I 89
Chapter 4: You Cant Do That in a UDF
AS BEGIN
RETURN @@DATEFIRST
END
GO
(Results)
Changing the value of DATEFIRST doesnt make any code more nondeter-
ministic than it already is, since using functions that are sensitive to
DATEFIRST, such as DATEPART, already make a UDF nondeterministic. In any
92 Part I
Chapter 4: You Cant Do That in a UDF
case, you cant SET DATEFIRST or use any SET command to change an option
from within a UDF.
SET commands that have been executed before the UDF runs can
change how the UDF executes, so its important that they be set consis-
tently. The next subsection demonstrates this feature, which turns out to
be important for maintaining determinism along with the exceptions to the
rule.
Listing 4.3 has the creation script of two UDFs that can be used to demon-
END
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_NULLS ON
GO
END
GO
GRANT EXEC on dbo.udf_Test_Quoted_Identifier_On to [PUBLIC]
GO
The most important thing to notice about Listing 4.3 is that the setting for
QUOTED_IDENTIFIER is different for the two UDFs. Its off when creating
94 Part I
Chapter 4: You Cant Do That in a UDF
SELECT OBJECTPROPERTY
(OBJECT_ID('dbo.udf_test_quoted_identifier_off')
, 'ExecIsQuotedIdentOn') as [Off]
, OBJECTPROPERTY
(OBJECT_ID('dbo.udf_test_quoted_identifier_on')
, 'ExecIsQuotedIdentOn') as [On]
GO
-- End execution of the script here.
(Results)
With QUOTED_IDENTIFIER ON
Off On
---- ----
0 1
change the behavior inside a UDF that was created with QUOTED_IDENTI-
****************************************************************/
AS BEGIN
. . .
-- SET some options and then see the output of DBCC USEROPTIONS and
-- SELECT * FROM udf_Session_OptionsTAB()
SET DATEFIRST 4
SET TEXTSIZE 200000000
SET ARITHABORT OFF
SET ARITHIGNORE ON
SET NOCOUNT OFF
SET QUOTED_IDENTIFIER OFF
SET CURSOR_CLOSE_ON_COMMIT ON
SET ANSI_WARNINGS OFF
SET ANSI_PADDING OFF
SET ANSI_NULL_DFLT_ON OFF
SET NUMERIC_ROUNDABORT ON
SET XACT_ABORT ON
FROM DBCC
Set Option Value
------------------------ --------------------
textsize 200000000
language us_english
dateformat mdy
datefirst 4
arithignore SET
numeric_roundabort SET
xact_abort SET
disable_def_cnst_chk SET
cursor_close_on_commit SET
ansi_nulls SET
concat_null_yields_null SET
DBCC execution completed. If DBCC printed error messages, contact your system
administrator.
FROM udf_Session_OptionsTAB
Set Option Value
-------------------------------- -----------------
DISABLE_DEF_CNST_CHK ON
IMPLICIT_TRANSACTIONS OFF
CURSOR_CLOSE_ON_COMMIT ON
ANSI_WARNINGS OFF
ANSI_PADDING OFF
ARITHABORT OFF
ARITHABORT SESSIONPROPERTY OFF
ARITHIGNORE ON
NOCOUNT OFF
98 Part I
Chapter 4: You Cant Do That in a UDF
ANSI_NULL_DFLT_ON OFF
ANSI_NULL_DFLT_OFF OFF
CONCAT_NULL_YIELDS_NULL ON
NUMERIC_ROUNDABORT ON
XACT_ABORT ON
@@DATEFIRST 4
@@LOCK_TIMEOUT -1
@@TEXTSIZE -1
@@LANGUAGE us_english
Its a good idea to close the Query Analyzer session right now. The data-
base options have been mixed up, and you wouldnt want to use it any
more.
The restriction on SET is the last of the restrictions that Im aware of.
Dont be surprised if you encounter one or two others in obscure situa-
tions. Ive tried to be thorough, but there may be more restrictions that I
just havent run across.
Summary
Microsoft has gone to great lengths to restrict user-designed functions so
that they observe strict design principles that prevent side effects and
ensure determinism to the maximum extent possible. UDFs were created
with two principles in mind:
n Functions shouldnt have side effects.
n Functions should be deterministic whenever possible.
These principles have lead to the many restrictions placed on UDFs. If
you keep them in mind as you code your UDFs, you wont be quite so sur-
prised when a CREATE FUNCTION script is rejected by the T-SQL parser. By
observing the principles, UDFs can be used in computed columns that are
indexed and in indexed views.
The restrictions on UDFs that were discussed in this chapter raise
another issue: How should a UDF report error conditions? The UDF cant
use RAISEERROR or the PRINT statement, and other strategies are off-limits.
What should it do? Thats the subject of Chapter 5.
5
Handling Run-time
Errors in UDFs
Due to all the restrictions discussed in the previous chapter, there are
very limited choices for how to handle errors inside a UDF. Beyond the
prohibitions on executing stored procedures, PRINT statements, RAISERROR,
etc., SQL Server behaves differently when executing UDFs than when
executing other types of T-SQL code. In particular, theres no opportunity
to handle run-time errors.
Without the usual error handling mechanism, the potential solutions
to handling run-time errors that occur in UDFs are:
n Detect errors before they happen and handle them on your own.
n Let them happen and rely on the code that called the UDF to handle
the errors.
Youll never detect every possible error condition, although its possible to
detect the most obvious errors and do something that is more meaningful
than allowing the error to be raised. Whats more meaningful than the
error? When youre working with scalar UDFs, about the only meaningful
action you can take is to set the return value of the function. One of the
options to discuss is using either NULL or a special numeric value as the
return value for the function.
This chapter starts by showing how the handling of errors inside
UDFs is different from the handling of errors in other types of T-SQL such
as stored procedures. That is done by first setting up a demonstration of
how error handling works in most T-SQL and then creating examples of
how it works in the three types of UDF.
99
100 Part I
Chapter 5: Handling Run-time Errors in UDFs
In the search for better ways to handle errors inside UDFs, Ive tried
various solutions. Unfortunately, only a few of the normal solutions are
available to the coder of UDFs. As shown in Chapter 4, SQL Server wont
let you use the RAISERROR statement inside a function. It is possible to
cause an unrelated error, such as divide-by-zero, to stop execution of the
program. But thats a messy solution and would confuse anyone who came
along and used the function without knowledge of this unusual behavior. I
dont recommend it, but I have experimented with it as a possible solu-
tion. Ill go into more details in the section Causing Unrelated Errors in
UDFs.
Lets start by showing how SQL Server treats errors that occur in a
UDF differently than it treats other errors. Understanding this is very
important when writing non-trivial UDFs.
SET NOCOUNT ON
SET XACT_ABORT OFF
IF @myError = 0
UPDATE @SampleTable
SET [Error] = @myError, [RowCount] = @myRowCount
WHERE [ID] = @myID
ELSE
PRINT 'No Update after first insert due to error '
+ CONVERT(varchar(10), @myError)
-- ENIDF
Before running this query in Query Analyzer, press Ctrl+T to turn on the
Query Results in Text menu item. Because of the mixing of messages
and result sets, text is the best way to view the output of this batch. Now,
run the procedure and see what happens:
(Results)
A message about the second insert failing is returned to the caller, in this
case Query Analyzer. Thats followed by the message that comes from the
PRINT statement about the error. The fact that this message gets returned
at all is proof that execution of the stored procedure continued after the
error was raised. Next, the resultset that comes from the SELECT * FROM
@SampleTable statement is returned with one row. Finally, the PRINT state-
ment in the batch shows us that the return code from the procedure is 0.
You might have noticed the SET XACT_ABORT OFF statement in the
stored procedure usp_Example_Runtime_Error. The behavior of the proce-
dure depends on this setting. If XACT_ABORT is ON, a stored procedure
terminates as soon as any error is encountered, and the current transac-
tion is rolled back. SQL Servers behavior inside a UDF is similar to but
not exactly the same as if XACT_ABORT was set ON while it was executed.
The best way to see exactly whats happening in the procedure is to
debug it. Thats hard to show you in a book, so Ill leave it for you to try on
your own.
Listing 5.2 shows udf_Example_Runtime_Error, which is as similar to
usp_Example_Runtime_Error as I could make it. UDFs cant have PRINT
statements so I had to remove them. Also UDFs can only return rowsets
or scalar results, not both. I chose returning the scalar result.
RETURN @myError
END
(Results)
The fact that the @@ERROR value is available inside the UDF and not
available to the statement returned after the UDF is executed means that
its almost impossible to handle errors generated by UDFs in any intelli-
gent way. Ive listed this in Appendix C as a bug along with a small number
of other issues that Ive found with the implementation of UDFs. This is
not an insurmountable problem. The error message is sent back to the
calling application and will eventually be discovered. But its inconsistent
and makes it impossible to write good error handling code in T-SQL.
There are a couple of other things to notice. @RC is NULL. It never gets
set. Also, take a look at @Var2. Its in the SELECT statement to illustrate that
when the UDF is terminated, other parts of the SELECT are not executed.
Error handling in multistatement UDFs is similar to error handling in
scalars. The TSQLUDFS database has udf_Example_Runtime_Error_Multi-
statement that you can use to demonstrate how it works. The procedure
DEBUG_udf_Example_Runtime_Error_Multistatement has a reasonable demon-
stration and can be used to debug the UDF.
Error handling in inline UDFs is different from the other two types of
UDFs. When an error occurs during the execution of an inline UDF, the
statement stops running. However, the rows that have already been cre-
ated are returned to the caller and @@ERROR is set to the error code that
caused the problem.
udf_Example_Runtime_Error_Inline, shown in Listing 5.3, illustrates
what happens by causing a divide-by-zero error when the column expres-
sion 100/Num is evaluated for the third row.
) RETURNS TABLE
/*
* Example UDF to demonstrate what happens when an error is
* raised by a SQL statement. This is an inline UDF.
*
* Example:
SELECT * from dbo.udf_Example_Runtime_Error_Inline()
****************************************************************/
AS RETURN
SELECT Num
, 100 / Num as [100 divided by Num]
FROM ( SELECT 1 as Num
UNION ALL SELECT 2
UNION ALL SELECT 0 -- Will cause divide by 0
UNION ALL SELECT 4
) as NumberList
Part I 105
Chapter 5: Handling Run-time Errors in UDFs
(Results)
(3 row(s) affected)
Only two rows really get returned, although Query Analyzer seems to
think that there are three rows in the result. @@ERROR is set after the state-
ment terminates to the correct error code.
I suggest that you experiment with UDF error handling further. To
make that easy, Ive included DEBUG stored procedures for all three
Example_Runtime_Error UDFs. You also have the scripts in this section.
Errors inside UDFs are handled in a way thats much different from
the way theyre handled in other T-SQL scripts. There is no opportunity
for examining @@ERROR so that the UDF can decide how to proceed after an
error occurs. SQL Server has decided that the UDF terminates and the
code that calls the UDF is responsible for handling the error. That leads us
to the ways that we might avoid these problems in the first place.
106 Part I
Chapter 5: Handling Run-time Errors in UDFs
IF @n IS NULL OR @n <= 0 OR
@Base IS NULL OR @Base <= 0 OR @Base = 1
RETURN NULL
I dont expect anyone to be doing Markov models in SQL. Its the wrong
place for that type of code. However, a database is a great place to store
model results and generate reports, and using the special negative values
may afford a way to communicate problems in a way thats more detailed
than just returning NULL.
108 Part I
Chapter 5: Handling Run-time Errors in UDFs
IF @bCauseError = 1
SET @TestVar = 1 / 0
RETURN 0
END
The following queries illustrate what happens when you execute a SELECT
that calls this function using three variations:
(Results)
(Results)
-- But if the right session options are set there is no error raised
SET ANSI_WARNINGS OFF
SET ARITHABORT OFF
SET ARITHIGNORE ON
SELECT TOP 2 au_id, au_fname, au_lname
, dbo.udf_Test_ConditionalDivideBy0(1) [Request Divide by Zero]
FROM pubs..authors
GO
(Results)
The first query doesnt have any code that raises an error. The second
query calls udf_Test_ConditionalDivideBy0 with a parameter 1 that causes
the divide-by-zero result to be raised and the error message to be
returned to the caller with no results. The final query has three calls to
SET that change SQL Servers handling of divide-by-zero errors, and no
error is raised.
To reiterate, I dont advocate causing divide-by-zero errors in the mid-
dle of queries. This section is here to illustrate that it could be done and
how errors are handled. But as the last query shows, if certain SET options
are left in unexpected settings, no error gets raised.
This is technique should be used with a great deal of caution but can
be a lifesaver. The best use that Ive found for it is for writing messages
about conditions that you thought were impossible or nearly impossible
but that seem to be occurring. You can test for these conditions in your
UDF code, write a message, and keep going if you want.
Listing 5.6 shows udf_SQL_LogMsgBIT, which is a UDF that calls
xp_logevent. I prefer to use this intermediate UDF instead of putting calls
to xp_logevent in my other code, but there may not be any real advantage
to doing so. If you read the functions comments, youll notice that it men-
tions a little trick: When defining a view, you can add a column that
invokes this UDF. It would cause a message to be written to the SQL log
every time the view was used.
IF @sSeverity is NULL
EXEC @WorkingVariable = master..xp_logevent @nMessageNumber
, @sMessage
ELSE
EXEC @WorkingVariable = master..xp_logevent @nMessageNumber
, @sMessage
, @sSeverity
END
-- xp_logevent has it backwards
RETURN CASE WHEN @WorkingVariable=1 THEN 0 ELSE 1 END
END
Part I 111
Chapter 5: Handling Run-time Errors in UDFs
You can view the SQL Server message log with Enterprise Manager. Fig-
ure 5.1 shows what the bottom of the log looks like just after I ran the
script.
Figure 5.1: Enterprise Manager showing the SQL Server message log
Summary
This chapter has shown how SQL Server handles errors that occur in
UDFs differently than it handles errors that occur in other T-SQL. This
can be a real problem and one that must be dealt with.
Ive shown you a variety of techniques for dealing with the error han-
dling issue in your UDF code. These techniques boil down to:
n Let the error happen and make the caller responsible for handling it.
n Find errors by careful checking of parameters and intermediate val-
ues, and return NULL for invalid values.
n Return a special value for the function that tells the caller what type
of error occurred.
112 Part I
Chapter 5: Handling Run-time Errors in UDFs
Ive also shown two techniques that should be reserved for desperate
times:
n Generating an unrelated error, such as divide-by-zero, from within a
UDF
n Sending a message to the SQL message log and NT event log
Neither should be used casually. Rather, they should be reserved for when
you really need them.
Whatever your choice when writing each UDF, error handling remains
your responsibility, and it shouldnt be ignored. To be successful at it, its
important to remain aware of what SQL Server is doing with each poten-
tial error and how your code expects to handle it.
Im sure that youve noticed the comment blocks that I put at the top
of every UDF. I think theyre pretty important even though theyre time
consuming to write. You might have noticed that I format T-SQL with sep-
arators at the beginning of lines instead of at the end of lines. Also, what
about the names that I give to UDFs? They follow a pretty specific pat-
tern. The next chapter is about the best ways for writing and maintaining
code. The choices you make for documentation, naming, and formatting
have a big effect on the long-term usability and maintainability of UDFs.
6
Documentation,
Formatting, and
Naming
Conventions
What does this next function do?
Take a minute and try to figure it out. Better yet, try to use it. Youll find
the script in the Chapter 6 Listing 0 Short Queries.sql file in the chapters
download directory. The procedure has already been added to the TSQL-
UDFS database. If you want to run the script, youll have to do it in
another database or drop the function first.
If youre like me, you copied the text into Query Analyzer and refor-
matted it so it was easier to read or copied just the function declaration
into Query Analyzer and added a SELECT statement that executed it.
Its been a long time since Ive seen a professional programmer try to
use a function that is as badly formatted as the one above, but it makes a
point: The presentation of a function has a lot to do with its usability. This
chapter is about how to make functions more useful through the way they
are formatted, named, and documented.
113
114 Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
IF LEN(ltrim(rtrim(@sMiddleName))) > 0
SET @sTemp = @sTemp
+ N' '
+ UPPER(left(ltrim(@sMiddleName), 1))
+ N'.'
IF LEN(ltrim(rtrim(@sSuffixName))) > 0
SET @sTemp = @sTemp + N' ' + ltrim(rtrim(@sSuffixName))
RETURN @sTemp
END
GO
For starters, each subclause (FROM, WHERE, ORDER BY) of the SELECT state-
ment starts a new line indented one tab stop from the start of the SELECT.
The first entry in the list goes on the line with the subclause. Thats a
compromise to keep the code slightly more compact.
In most circumstances, every entry in a list after the first goes on its
own line. There will be exceptions. For one, the arguments to a function
call rarely belong on their own line. Therefore, the lines:
, dbo.udf_NameFullWithComma(au_fname, null
, au_lname, null) as Name -- so it's sortable.
are broken only when it was necessary to do so for line wrapping in this
book. Sometimes space considerations are more important and several
lines are combined. For example, this line has two fields on it because
theyre covered by the same comment:
My overriding decision criteria for layout is to make the code more read-
Notice that I almost always use a table alias. I also use column aliases for
every expression but avoid them when the column is not an expression.
Throughout this book Ive tried to put SQL keywords in uppercase.
Ive done that so that they are visually distinct. When writing SQL for any
purpose except publication, I type in all lowercase (except variable names
that have uppercase letters embedded). The coloring that Query Analyzer
uses is sufficient to differentiate the parts of SQL. Besides, typing all
those uppercase characters is harder on the fingers.
You may discover almost as many formatting conventions as there are
programmers. SFF works for me because it makes the SQL more readable
and easier to change. For example, to add another column to the end of the
select list, all that is necessary is to put in the new line. Theres no need
to change the line before the new one to add a comma after the expres-
sion. The same works in reverse. By putting the separators at the start of
a line, its only necessary to put a double dash at the start of a line to elim-
inate it. Theres no need to go to the previous line and adjust the commas.
For example, to eliminate the second conditional expression from the
WHERE clause, just add a double dash, as in:
The second set of double dashes that delimited the original comment is
ignored.
In a function creation script, SFF comes into play in the parameter
list, as you can see from this function declaration:
Header Comments
The first place to find information about the function is in the header. Its
often the only place that any information is available. While it would be
useful to have complete program documentation for all functions, I find
that thats rarely in the project plan. The benefits to good documentation
are just too far in the future for many project managers (myself included)
to decide that theyre worthwhile. Thats why I devote some attention to
the header.
The place to start a function is with a template. A template is a text
file that can be used as the starting point for a SQL script. It has the
extension .TQL. SQL Server ships with templates for Query Analyzer that
can be used to get your function creation process started. Ive enhanced
these templates by adding the comment header and more formatting to
create my own templates that I use in place of the ones from SQL Server.
Listing 6.2 shows the function template for starting a function that returns
a scalar value. Youll find it in the companion materials in the Templates
directory (\Templates\Create Function\Create Scalar Function.tql) along
with two other templates for creating UDFs that return tables. Using tem-
plates was discussed in Chapter 3 in the Query Analyzer section.
Part I 119
Chapter 6: Documentation, Formatting, and Naming Conventions
RETURN @Result
END
GO
* description
The function is described here. A simple description of what the function
does will suffice. If the function has any unusual behavior, it should be
mentioned. For example, the types of errors in the input that cause the
function to return NULL might be important.
Some UDFs are simple enough that they can be replaced by an
expression. Doing so usually makes the SQL code more complex, but it
will always execute faster. This section gives the template that can be
used to replace an invocation of the function. For an example, see udf_DT_
2Julian in the TSQLUDFS database. The function user might use the
replacement in a performance-sensitive situation.
* Related Functions
Here we discuss any functions that are frequently used together with this
function and, most importantly, why the other function is related. Its a
heads-up to the user of the function and the maintenance programmer.
* Attribution
This section tells something about how the function was created. If it was
copied from the net or if it was based on someone elses idea, that would
be stated here. I generally only consider this section when Im writing for
publication. Of course, I do that a lot these days. Between this book and
the UDF of the Week Newsletter, Ive written several hundred UDFs in
the past year.
* Maintenance Notes
These are notes to any programmer who is going to maintain the function.
It might be something about where to look to get the algorithm for the
function or which other functions use the same algorithm and should be
changed in synch with this one.
* Example
This gives a simple example of how the function might be used. The line
with Example starts with an asterisk, but the lines of the example that
follows do not. Thats so the example can be selected and executed within
Query Analyzer without changing any of the text or having to remove the
asterisk. Heres a sample section:
Part I 121
Chapter 6: Documentation, Formatting, and Naming Conventions
* Example:
To execute the sample, select the line or lines with the SELECT statement
and use the F5 key or green execution arrow to run the query. In the
interest of space, the example section is typically left on a single line. SFF
formatting used elsewhere is skipped. Given the simple nature of the
example, this rarely presents a problem. Of course, no harm would come if
you chose to reformat the examples.
It may be more difficult to provide a meaningful example for inline and
multistatement UDFs than it is for scalar UDFs. The two types that
return tables usually depend on the state of the database, while scalars
dont.
* Test
A few simple tests go here. They are not intended to be a comprehensive
test that proves that the function worksjust some simple tests that can
verify that the function isnt screwed up after a simple change has been
made.
The test should check its own result. It shouldnt just print the result and
leave it to the programmer to do the checking. That would put an addi-
tional burden on the programmer executing the test, wasting his or her
time. After all, whoever wrote the test knows the answer that the function
should produce. The results of executing the test are:
* Test Script
This gives the location of a comprehensive test of the function. The test
might be embedded in a stored procedure or a script file. Since the com-
prehensive tests are not published along with this book, this section is not
included in any of the functions published here. Chapter 11 is all about
testing and what to put into the test.
* History
As the function is modified, each programmer should leave a short
description of who made the change, when the change was made, and
what was changed. In the interest of saving space in the function headers,
the history sections are left out of the functions in this book.
* Copyright
If a copyright notice is needed, this is where it goes. Since you bought this
book, you have the right to use any of the functions published in it. You
can put them in a database and include them in software. The only right
that is reserved is the right of publication. You may not publish the func-
tions in an article without permission, which can be obtained by writing to
me.
Notice the copyright symbol () on the copyright line. Although its
not an ASCII symbol, it shows up in the fonts that are most likely to be
used for viewing script text.
Thats what I put in a header. There are two additional types of infor-
mation that Ive seen in function headers that I omit.
* Parameters
Its a common practice in many programming languages for programmers
to maintain a parameters section in the comment header of each function.
In the case of a T-SQL function, the parameters are in the declaration of
the function at the top of the file with their data type. It isnt necessary to
repeat them in the comment header. Doing so would require that the
Part I 123
Chapter 6: Documentation, Formatting, and Naming Conventions
Naming Conventions
There are many naming conventions. Throughout this book, youll see a
pretty consistent convention for naming functions, parameters, local vari-
ables, tables, and views that make up the database. My convention is a
little bit different, but theres a method behind the madness, and Ill
explain why here. First up are names for UDFs.
-- Null is OK
IF @sSuffixName is NULL RETURN 1
SET @bRC = CASE WHEN @sTemp in ('JR', 'SR', 'II', 'III', 'IV'
, '2ND', '3RD', '4TH', 'MD'
, 'ESQ', 'ESQRS'
, 'PHD', 'PH.D'
, 'DDS' -- dentist
, 'DVM' -- Veterinarian
)
THEN 1
ELSE 0
END
RETURN @bRC
END
GO
To see whats really happening, start the SQL Profiler and add these
events: SP:CacheHit, SP:CacheMiss, SP:StmtStarting, SP:Stmt-
Completed, and SP:ExecContextHit. Also include the ObjectID and
Reads in the data columns.
Now log in as a user who is not dbo but is a member of the
PUBLIC group. If youve created the two Limited user IDs,
LimitedUser would fit the bill. Execute the following batch:
-- You should log in as a user that is not dbo before running these queries
-- These generate different cache misses. Use SQL Profiler to watch them.
exec usp_ExampleSelectWithoutOwner
exec dbo.usp_ExampleSelectWithOwner
go
Naming Columns
Domain Names
However, when a column describes an entity from an application domain,
I sometimes use a domain name and associated data type. For example,
when working with stockbrokers, one domain is ExchangeCD, which stores
the codes for stock exchanges. When working with roads, there are two
domains that store measurements of kilometers: lengths of road segments
and markers that designate a position relative to the start of the road.
There is more on these domains in Chapter 12, Converting between Unit
Systems.
For ExchangeCD, I might create the user-defined type (UDT)
DomainExchangeCD with this script:
Id also use it to define parameters to UDFs. But thats where were going
to run into a problem. Heres a script that attempts to create a UDF that
uses the DomainExchangeCD UDT to define the data type of its parameter:
(Results)
SELECT B.BrokerID
, B.FirstName
, B.LastName
FROM dbo.Broker b
INNER JOIN dbo.ExchangeMembership EM
ON B.BrokerID = EM.BrokerID
WHERE EM.ExchangeCD = @ExchangeCD
Part I 131
Chapter 6: Documentation, Formatting, and Naming Conventions
Using the domain name tells the programmer something about what the
Yes, thats a little inconsistent. If you want to put the type at the end, I
wouldnt object. The goal is to put maximum explanatory power in the
variable or parameter name.
Summary
Almost any naming convention is better than no convention. Through the
conventions for naming, documenting, and formatting in this chapter, Ive
tried to show:
n A way to format SQL with separators at the start of a line for readabil-
ity and easier manipulation of the text.
n A header format for functions that provides the documentation needed
by the users of the function and those charged with maintaining it in
the future.
132 Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
Inline UDFs
Inline UDFs are a form of SQL view that accepts parameters. The param-
eters are usually used in the WHERE clause to restrict the rows returned,
but they can also be used in other parts of the UDFs SELECT statement.
Inline UDFs can be convenient, but they dont have all the features avail-
able to views.
Like views, inline UDFs can be updatable and can have INSERT, UPDATE,
and DELETE permissions granted to them. Unlike views, there is no WITH
CHECK OPTION clause to prevent insertion of rows that, given the parame-
ters, would not be returned by the UDF. This limits the usefulness of
updatable views in many situations.
Although its not part of standard SQL, views and inline UDFs can be
sorted with an ORDER BY clause if there is a TOP clause in the SELECT state-
ment. This technique can add value to inline UDFs at the cost of a
potential for duplicate sort operations.
One use of inline UDFs that Ive found particularly productive is to
create a pair of inline UDFs for paging web applications that display a
screens worth of data from a larger set of rows. A section of this chapter
shows how to use this technique and the trade-offs that seem to work
best.
Permissions are required both for creating and for using inline UDFs.
We cant do anything without them, so theyre the first subjects for this
chapter.
133
134 Part I
Chapter 7: Inline UDFs
adding the extra characters either makes a name too long or when the
This mixture of feature availability makes the inline UDF a possible sub-
stitute for views but not a sure thing. There are enough features that are
not available when using a UDF that you might decide that a view is pref-
erable, even if it doesnt have parameters. The code that uses the view
can qualify its SELECT statement to limit the rows returned and achieve the
same result achieved using the UDF. Youll have to examine the specific
circumstances to choose between these two alternatives.
The ability to create inline UDFs quickly is aided by the use of a
template file. The next section shows you a template that has several
modifications from the one provided with SQL Server.
/*
* description goes here
*
* Related Functions:
* Attribution: Based on xxx by yyy found in zzzzzzzzzzzzz
* Maintenance Notes:
* Example:
select * FROM dbo.<inline_function_name, sysname, udf_>
(<parm1_test_value, , 1>, <parm2_test_value, , 2>, <parm3_test_value, , 3>)
* Test:
* Test Script: TEST_<inline_function_name, sysname, udf_>
* History:
* When Who Description
* ------------- ------- -----------------------------------------
* <date created,smalldatetime, YYYY-MM-DD> <your initials,char(8), XXX>
Initial Coding
****************************************************************/
AS RETURN
SELECT
FROM
WHERE
GROUP BY
HAVING
ORDER BY
GO
There isnt much new to say about creating inline UDFs. Its a single
SELECT statement that uses its parameters to drive the query. Youll see
more examples throughout the chapter.
Once the inline UDF has been created, its time to use it. Using inline
UDFs is very much like using a regular view except with parameters. The
syntax is what youd expect: function-like.
As with all other objects, I suggest that dbo be the only user to own
(Results)
-- Combine rowsets.
SELECT cp.CategoryName, c.[Description], NumProducts
FROM dbo.udf_Category_ProductCountTAB (default) cp
inner join Northwind.dbo.Categories c
ON c.CategoryID = cp.CategoryID
ORDER BY cp.CategoryName
GO
Notice that without the alias, cp, the reference to the CategoryName column
in the select list would be ambiguous. That points out the need to use
aliases for all inline and multistatement UDF invocations.
Before anyone gets up in arms, Im aware that joining udf_Cate-
gory_ProductCountTAB with the Categories table is not the optimal way of
140 Part I
Chapter 7: Inline UDFs
getting the result that we desire. This query forces SQL Server to read
the Categories table twice: a table scan to produce the resultset for
udf_Category_ProductCountTAB and a seek using the clustered index on
each of the categories in the resultset to find the matching CategoryID so
that the Description column can be returned. A single query that included
the description field would be better, either as the body of the UDF or to
replace the query. When we realize this, were faced with a choice: Either
accept the suboptimal query or rewrite it.
To get the desired ordering, the previous two queries had ORDER BY
clauses. The SELECT in the inline UDF can also have an ORDER BY clause but
only if it has a TOP clause. By using both clauses, the results of the UDF
are sorted. This has advantages and disadvantages, which are the subject
of the next section.
League (NL) and American League (AL). Figure 7.1 shows the schema of
the BBTeams table in a SQL Server diagram.
The following series of batches demonstrate that unlike the limits placed
on INSERT statements into views that use WITH CHECK OPTION, the parame-
ters to the inline function have no bearing on what can be inserted into the
UDF. Start by listing the teams in the American League:
(Results)
(2 row(s) affected)
(Results)
(Results)
(1 row(s) affected)
The message indicates that one row is affected, confirming the success of
the statement. The next queries verify the contents of the BBTeams table:
(Results)
American League
ID Name League Manager
----------- ------------ ------ ------------
2 Yankees AL Yogi
7 Red Sox AL Zimmer
(2 row(s) affected)
National League
ID Name League Manager
----------- ------------ ------ ------------
1 Dodgers NL Walt
3 Cubs NL Joe
11 Mets NL Casey
(3 row(s) affected)
Updates and deletes are different. They work only on rows that are
returned by the UDF. It is as if the WHERE clause of the UDF was combined
with the WHERE clause of the UPDATE or DELETE statement thats being exe-
cuted. For example, these two statements dont modify the database as
intended:
Part I 145
Chapter 7: Inline UDFs
(Results)
(0 row(s) affected)
(0 row(s) affected)
(Results)
(1 row(s) affected)
This last batch gets rid of the Mets so that you can run the experiment
again some other time:
In my opinion, the lack of a WITH CHECK OPTION clause and the fact that the
UDF parameters dont limit INSERT statements make updatable views pref-
erable to updatable UDFs. At least with views, the limitations are explicit
and consistent.
146 Part I
Chapter 7: Inline UDFs
screen. Ive come to the conclusion that its best to keep pages short, at
How many rows should you retrieve? Since the numeric argument to the
TOP clause must be a constant, use the largest number that could fit on a
screen. The data transmission between the SQL Server and the web cre-
ation engine (ASP.NET, ASP, or some other) is usually over a fast
connection, and there is little benefit to trying to save a few bytes at the
potential cost of another database round-trip. If our typical screen fits 15
rows, TOP 15 should be added as the first clause in the SELECT statement.
The ability to retrieve rows after the first page is displayed is essen-
tial. To accomplish this, its necessary to save one or more columns that
identify where the user is in the paging process. The selection of the col-
umns to save depends on the ordering used in the query.
The columns used to identify position must uniquely identify the last
row shown. It may be necessary to add additional columns to the ORDER BY
clause to provide uniqueness. In fact, for our sample query, the UnitsIn-
Stock column doesnt provide uniqueness, and we must add an additional
column or columns. While there might be some benefit to using [Total
148 Part I
Chapter 7: Inline UDFs
Sales] as the second sort column, its a field that could actually change
between pages. Were better off using a combination of ProductName and
ProductID. Why two columns? Because ProductName isnt guaranteed to be
unique in the Products table of the Northwind database. Most of the time,
ProductName provides a very understandable and useful ordering. But in
the rare occasions where a page with two products with the same quantity
for UnitsInStock have the same name and fall on the exact end of a page,
we might produce an error if we dont also use the ProductID. The
ORDER BY clause in our query becomes:
In the web page generation code, well have to save three scalar values,
one for each of the sort variables: UnitsInStock, ProductName, and
ProductID. In ASP or ASP.NET, these values can safely be saved in the
SESSION object. Other web programming environments have their own way
to save session-related values. The values are used when the second and
subsequent pages are retrieved. The page generation code must then hand
the values from the end of the last page back to the paging UDF as argu-
ments, which can then be used in the WHERE clause. The declaration of the
parameters is:
-- Parameters identify the last row shown. Default for first page.
@LastUnitsInStock int = 20000000 -- Product.UnitsInStock
, @LastProductName nvarchar(40) = '' -- Product.ProductName
, @LastProductID int = 0 -- Product.ProductID
Each of them has a default value that is used to retrieve the first page.
Providing the defaults simplifies retrieval of the first page and relieves the
programmer of having to figure them out. Just use DEFAULT for each of the
function arguments.
Its possible to use a page number or starting line number to store the
users position. I find that its better to use values from the application to
define where pages start and end. The problem with page and line num-
bers is that the insertion of rows in the database while the user is paging
through the table can make the paging operation miss a row or a few rows.
That ends up being classified as a bug. Although it may take a little more
time, choosing a set of columns from the application is worth the effort.
The WHERE clause gets a little tricky. Of course, the P.ProductID =
S.ProductID condition must remain in the query, and the three parameters
must be compared to the corresponding columns in each of the rows so
that we start where we left off. My first instinct is to code these compari-
sons as:
Part I 149
Chapter 7: Inline UDFs
But thats wrong! The problem is that it only returns rows with Product-
Name columns that are greater than or equal to the last ProductName, even if
they have lower UnitsInStock values. The same problem holds for
ProductIDs. The correct coding of the WHERE conditions for positioning the
results is:
This retrieves rows that are after the last row shown. Using the less than
or equal (<=) and greater than or equal (>=) comparison operators gives
us one row of overlap between pages. Use just the less than (<) or greater
than (>) comparison operators to eliminate the overlap.
Listing 7.5 pulls these changes together to create udf_Paging_Product-
ByUnits_Forward, our forward paging UDF. The name is long, but theres a
method to creating it. After the usual udf_ designation, the second part of
the name is the group, which identifies the UDF as one used for paging.
The name ProductByUnits identifies the page that the function serves.
Finally, Forward tells the direction that were going.
-- Parameters identify the last row shown. Null for first page.
@LastUnitsInStock int = 20000000 -- Product.UnitsInStock
, @LastProductName nvarchar(40) = '' -- Product.ProductName
, @LastProductID int = 0 -- Product.ProductID
) RETURNS TABLE
/*
* Forward paging UDF for ASP page ProductByUnits
*
* Example:
SELECT *
FROM udf_Paging_ProductByUnits_Forward
(default, default, default) -- defaults for 1st Page
****************************************************************/
AS RETURN
SELECT TOP 15
P.ProductID
, P.ProductName
150 Part I
Chapter 7: Inline UDFs
, P.UnitsInStock
, S.[Total Sold]
, C.CategoryName
FROM Northwind.dbo.Categories C
INNER JOIN Northwind.dbo.Products p
ON C.CategoryID = P.CategoryID
INNER JOIN (SELECT ProductID
, SUM (Quantity) as [Total Sold]
FROM Northwind.dbo.[Order Details]
GROUP BY ProductID
) AS S
ON P.ProductID = S.ProductID
WHERE P.Discontinued <> 1
AND (P.UnitsInStock <= @LastUnitsInStock
OR (P.UnitsInStock = @LastUnitsInStock
AND P.ProductName >= @LastProductName)
OR (P.UnitsInStock = @LastUnitsInStock
AND P.ProductName = @LastProductName
AND P.ProductID >= @LastProductID)
)
ORDER BY P.UnitsInStock desc
, P.ProductName asc
, P.ProductID asc
GO
To retrieve rows for the first page, the SELECT statement is:
Parameter values are not supplied to retrieve the first page because
defaults can be used. To retrieve the rows for the second page, use this
SELECT statement:
-- Parameters identify the last row shown. default for last page.
@LastUnitsInStock int = -1 -- Product.UnitsInStock
, @LastProductName nvarchar(40) = 'zzzzzzzzzzzzzzz'
-- Product.ProductName
, @LastProductID int = 2000000000 -- Product.ProductID
) RETURNS TABLE
/*
* Forward paging UDF for ASP page ProductsByUnit
*
* Example:
SELECT *
FROM udf_Paging_ProductByUnits_Reverse
(default, default, default) -- defaults for last page
****************************************************************/
AS RETURN
By the way, to use the reverse paging function, the web page creation
Summary
This chapter has shown how inline UDFs are similar to views. The addi-
tion of parameters makes them more powerful. By using the parameters
in the SELECT statement, choices that the user of a similar view would nor-
mally have to make are coded into the function. This makes the UDF
simpler to use in the right situation.
While inline UDFs can be updatable, the differences between them
and updatable views may not be sufficient to make the switch worthwhile.
In particular, the absence of a WITH CHECK option and the inconsistent
behavior of not checking inserts but checking updates and deletes makes
me want to stick with updatable views rather than switching to updatable
UDFs.
Web site paging is one application of inline UDFs that has proved to
work well in practice. The capability to supply parameters and the use of
the TOP clause facilitates moving the SQL required for paging logic out of
the page generation script and into a compiled SQL object. By retrieving
the right number of rows, database and network resources are consumed
in proportion to the number of pages displayed.
Inline UDFs return a single rowset but can only contain one SELECT
statement. If you require more program logic to achieve the desired
results, a multistatement UDF may be what you need. The next chapter
takes a detailed look at them.
This page intentionally left blank.
8
Multistatement
UDFs
Among the confusing aspects of multistatement UDFs is that they go by
many different names. Some of the names youll see for multistatement
UDFs are:
n Multiline
n Multistatement
n Table-valued
n TABLE
n Table function
n Multistatement table-valued function
n TF (the type code used in sysobjects)
Ive used multistatement in the text of this book because its the name
that Books Online uses. SQL Servers code, such as the sp_help system
stored procedure, refers to them as table functions. Inline UDFs also
return tables, so I think the term is somewhat confusing.
No matter what the name, theyre a useful hybrid of a scalar and inline
UDF. They return a table that is constructed by the T-SQL script in the
body of the function. The table can be used in the FROM clause of any SQL
statement, and they join the ranks of the rowset returning functions like
OPENROWSET and OPENXML.
The logic in the body of the UDF can be extensive but must obey the
same limitations on side effects that were documented in Chapter 5. Most
importantly, multistatement UDFs cant execute stored procedures nor
can they create or reference temporary tables or generate any messages.
Their communication options are limited by design.
In this chapter, we examine them and concentrate on these topics:
n Permissions for multistatement UDFs
n Using cursors in UDFs
n Managing lists with UDFs
155
156 Part I
Chapter 8: Multistatement UDFs
Permissions for managing and using multistatement UDFs are very simi-
lar to permissions on the other types of UDFs. The mix is slightly
different due to the limits on what can be done with them.
SELECT @I = 1, @F = CONVERT(bigint, 1)
RETURN
END
(Results - abridged)
Number Factorial
----------- --------------------
1 1
2 2
3 6
4 24
...
18 6402373705728000
19 121645100408832000
20 2432902008176640000
(Results - abridged)
The scenario is a bit contrived. If you examine the two UDFs that
udf_Category_BigCategoryProductsTAB calls, its possible to combine them
into a single SQL statement and do away with the cursor. The point is to
illustrate some of the features of multistatement UDFs.
The results of the query are returnedsorted first by CategoryName
and then by ProductName. Thats due to the use of a primary key. The
table-level primary key declaration in a multistatement UDF uses only the
limited primary key syntax. It cant use the more elaborate CONSTRAINT
Part I 161
Chapter 8: Multistatement UDFs
syntax thats available when using CREATE TABLE. The key created is a
DECLARE
RETURN
END
GO
The template reflects choices that I usually make about what to include or
exclude from a multistatement UDF. Ive never granted REFERENCES per-
mission on a multistatement UDF, so I dont include it in the template. A
PRIMARY KEY clause is included in the template to encourage its use. If you
dont need it, comment it out or delete it.
Youre not limited to one template. If you have different styles of
UDFs, you may have several templates for any of the UDF types. You can
also include templates with partial code. The section that follows is about
using cursors; it includes a template that makes writing them a snap.
Part I 163
Chapter 8: Multistatement UDFs
-- Loop contents
List Management
Data doesnt always come in neat relational tables. Sometimes it comes in
delimiter-separated text. One of the most asked-for tasks that can be per-
formed with a multistatement UDF is to convert delimited text into a
table. Similarly, when reporting data, the neat relational table isnt always
the best way to show a list, particularly when it has few entries. Some-
times its best to combine the list with a delimiter prior to display.
Code tables are another type of list that many applications use for data
validation and drop-down data entry fields. Sometimes shipping a code
table as a UDF can simplify the installation process for an application.
This section shows how to handle delimited text and code tables with
multistatement UDFs. These are hardly the only tasks suitable for these
UDFs, but they are tasks that were previously more difficult in T-SQL.
Part I 165
Chapter 8: Multistatement UDFs
SELECT @Pos = 1
, @DelimLen = LEN(@Delimiter) -- usually 1
, @LenInput = LEN(@sInputList)
, @NextPos = CharIndex(@Delimiter, @sInputList, 1)
END -- IF
RETURN
END
This UDF is pretty easy to use. Just supply the delimited text and the
delimiter. This query demonstrates:
(Results)
->Item<-
----------------------------
->Kaleigh<-
->Phil IV<-
->Ben<-
->Kara<-
->Eric<-
->Tommy<-
->Christine<-
Part I 167
Chapter 8: Multistatement UDFs
Notice that the embedded space in the name Phil IV is preserved, but
-- The teams...
SELECT Top 2 * from BBTeams
GO
ID Name Players
----------- ------------ -----------------------------------------
1 Dodgers Eric, Nick, Patrick, David, Billy, Alex, Matt, Gaven,...
2 Yankees Ulli, Tommy, Christine, Rika, Violet, Ken, Pat, Kenny,...
(Results)
Im not sure what type of join could possibly make this work. In any case,
SQL Server doesnt have any way to let you specify this type of query.
There are several possible solutions. One solution is to write code that
creates a dynamic SQL string that UNIONs the result of parsing each row.
That solution is limited by the size of a string variable and would require
its own cursor. Another solution would be to concatenate the Players col-
umns and parse the result. Once again, thats limited by the size of a string
variable, and it stops working when the list gets to 8,000 characters. The
solution that were going to try here is to write a UDF that uses a cursor
to traverse the BBTeams table and split each list of players. Listing 8.6
168 Part I
Chapter 8: Multistatement UDFs
OPEN TeamCURSOR
FETCH TeamCURSOR INTO @Players
RETURN
END
(Results - abridged)
list. Thats the basic approach. Stay tuned for an alternative solution that
will follow shortly.
OPEN BookAuthors
FETCH BookAuthors INTO @lname
WHILE @@Fetch_status = 0 BEGIN
SET @sList = CASE WHEN LEN(@sList) > 0
THEN @sList + ', ' + @lname
ELSE @lname
END
FETCH BookAuthors INTO @lname
END
CLOSE BookAuthors
DEALLOCATE BookAuthors
RETURN @sList
END
Part I 171
Chapter 8: Multistatement UDFs
Using it is a cinch:
Title Authors
----------------------------------- -----------------------------
But Is It User Friendly? Carson
Computer Phobic AND Non-Phobic I... Karsen, MacFeather
Cooking with Computers: Surrepti... MacFeather, O'Leary
Secrets of Silicon Valley Dull, Hunter
Sushi, Anyone? Gringlesby, O'Leary, Yokomoto
It turns out that the cursor isnt necessary. In Listing 8.8, udf_Titles_
AuthorList2 has an alternate implementation that accomplishes the same
result. It does this by concatenating each rows au_lname to a local variable.
FROM pubs..Authors A
INNER JOIN pubs..titleAuthor ta
ON A.au_id = ta.au_id
WHERE ta.title_ID = @Title_ID
ORDER BY au_lname
RETURN @sList
END
172 Part I
Chapter 8: Multistatement UDFs
The shaded lines of Listing 8.8 show the key difference from the original
function. By concatenating each au_lname to the local variable @sList, we
achieve the same result as using a cursor. With the exception of the 2 at
the end of the UDF name, the query to invoke the UDF and the results
are the same as those for udf_Titles_AuthorList. I wont repeat them.
Although I havent tested it, my understanding is that because the
second query doesnt have a cursor, its much faster than the version with
the cursor. You might have a case where theres enough data where that
matters, but for the one to three authors found in most books, youll never
know the difference. Remember, because this UDF is used for display or
reporting purposes, youre unlikely to use it on more than a few thousand
rows.
Another use of a multistatement UDF to produce a list is the tech-
nique of shipping UDFs instead of code tables. This can make the software
update process somewhat simpler.
RETURN
END
Summary
Multistatement UDFs are the third and final type of UDF. Theyre very
much like a stored procedure that returns one resultset, with the differ-
ence being that theyre subject to all the restrictions on UDFs that
prevent side effects. Side effects are very common in stored procedures.
While they can get you out of a jam, they make the design of the code less
understandable and less maintainable.
This chapter has shown several sample UDFs and templates to aid in
creating them. These examples are fairly simple. Real-world code may be
longer because it has to tackle more complex problems.
Sometimes the complexity leads us to use cursors in our UDFs. Cur-
sors are well supported by SQL Server but can lead to slow-running code.
SQL Server is optimized for declarative programming. If you have to
resort to cursors, consider moving the code to another layer of the
application.
Now that youve seen how to create each type of UDF, the aim of the
next chapter is to advance your knowledge of techniques to manage your
UDFs. After that, well move on to topics like using extended stored pro-
cedures and techniques to extend the range of possibilities for what UDFs
can do for your application.
This page intentionally left blank.
9
Metadata about
UDFs
Metadata is data about data. This chapter discusses information about
user-defined functionsinformation that SQL Server provides in several
different forms. The first place to look is at a few system stored proce-
dures, which are written to provide information about all database objects,
including functions. In addition, SQL Server has several ways to give you
direct access to metadata in the form of:
n INFORMATION_SCHEMA views
n Built-in metadata functions
n System tables
This chapter shows you the best place to look for information about UDFs
in each of these sources.
Once the sources of information are defined, the information they pro-
vide can be combined into UDFs that reshape the information into formats
that are useful to DBAs and programmers. You may want to retrieve the
UDF metadata in a different format, but these functions give you a place to
start.
There is one more interface available for working with SQL Server
metadata: SQL-DMO. SQL-DMO is a Win32 COM library that is not usu-
ally used from inside T-SQL. The best way to work with SQL-DMO is
from a language that is good at COM automation such as Visual Basic or
VB Script. This book is about T-SQL, so there is just a short introduction
to SQL-DMO near the end of the chapter.
sp_help and many other system stored procedures provide informa-
tion about all types of database objects. UDFs are no exception. Since the
nature of a UDF is different from other database objects, the way the sys-
tem procedures treat a UDF is also a little different.
As with all the other chapters, the short queries that appear in the
chapter have been collected into the file Chapter 9 Listing 0 Short
Queries.sql. You can find it in this chapters download directory.
175
176 Part I
Chapter 9: Metadata about UDFs
sp_help
sp_help is a system stored procedure that provides a small amount of basic
information about any database object, including UDFs. The syntax of the
call is:
sp_help [ [ @objname] = name ]
The name can be the name of any database object. The resultset(s)
returned by sp_help differ depending on the object type. Each of the three
types of UDFs generate different combinations of resultsets. For
multistatement UDFs, the number of resultsets depends on whether the
UDF has a ROWGUID column or an IDENTITY column.
sp_help works fine for interactive use in Query Analyzer. It gives you
most of the basic information about the UDF. However, because of the
variable number of resultsets returned by sp_help, its difficult to use its
output in a program. This is especially true of report writers, which dont
handle a variable number of resultsets well. To get information about
UDFs into programs such as report writers, Ive created a group of UDFs
that return metadata about functions. Youll find them in the section
Metadata UDFs later in this chapter.
One important resultset that sp_help doesnt return is one that
describes the parameters of the UDF. Look for udf_Func_ParmsTAB in List-
ing 9.3. It lists the parameters.
Part I 177
Chapter 9: Metadata about UDFs
(Results)
The same table is also returned for inline and multistatement UDFs. The
Type column for those UDFs says inline table or table function for
multistatement UDFs. Additional recordsets are returned for these types
of UDFs.
RowGuidCol
-----------------------------------------------------------------
No rowguidcol column defined.
I find that I use sp_help only on rare occasions. Of course, the SQL Server
tools, such as Enterprise Manager, use it when they retrieve the informa-
tion that they show to you.
An item that sp_help doesnt return is the text that defines the UDF.
The SQL Server tools retrieve the function definition using sp_helptext.
You can also retrieve it directly from syscomments if you join with
sysobjects.
180 Part I
Chapter 9: Metadata about UDFs
sp_helptext
sp_helptext retrieves the textual definition of many types of database
objects including UDFs. The syntax of the call is:
sp_helptext [ @objname = ] 'name'
-- sp_helptext on udf_Num_LOGN
EXEC sp_helptext udf_Num_LOGN
GO
(Results)
Text
-----------------------------------------------------------------------
IF @n IS NULL OR @n <= 0 OR
@Base IS NULL OR @Base <= 0 OR @Base = 1
RETURN NULL
When using sp_helptext from Query Analyzer, be sure that youve set the
Maximum characters per column field on the Results tab of the Options
dialog to a size thats longer than your longest line of text. Otherwise, the
text is truncated on the right.
If this happens to you, use the Tools Options menu command,
select the Results tab, and set Maximum characters per line to 8192.
Thats the largest number the field allows.
Another way to get the function definition is by querying the ROUTINE_
DEFINITION column in INFORMATION_SCHEMA.ROUTINES. Theres more about
using INFORMATION_SCHEMA in this chapters Retrieving Metadata about
UDFs section.
Part I 181
Chapter 9: Metadata about UDFs
sp_rename
Text
-----------------------------------------------------------------------
CREATE FUNCTION dbo.udf_Test_RenameMe () returns int as begin return 1 end
What happens is that although an object is created with the new name
and the row in sysobjects is changed, the CREATE FUNCTION script in
syscomments is not changed. However, the renamed UDF works. Unfortu-
nately, if the database is ever converted to a script, the old name will
remain in the database and any code that invokes the UDF under the new
name doesnt compile because the UDF is recreated using the old name.
Thats the bug. If you try to edit the newly renamed UDF using Enter-
prise Manager, youll also run into the original script. If youre not careful,
youll recreate the UDF under its original name.
When analyzing the impact of changes to UDFs, you sometimes want
to know which database objects reference a UDF and which ones are ref-
erenced by it. That information is available from the system stored
procedure sp_depends.
182 Part I
Chapter 9: Metadata about UDFs
sp_depends
sp_depends returns information about the database objects referenced by a
UDF and the database objects that reference the UDF in separate
resultsets. Both sets of information can be useful. Heres a simple script
that retrieves dependency information for udf_Order_Amount, which was
created back in Chapter 2. Note that NWOrderDetails is a table in
TSQLUDFS:
(Results)
The two resultsets are returned only when they have rows. If neither
resultset has any rows, only a message is returned. If youre trying to
work with the results from sp_depends in a program, it could get a little
tricky.
Notice that the results include the column Updated, which is always
no when sp_depends is used on a UDF. The Selected column is only
yes when the column is in the select list. udf_Order_Amount uses the col-
umns shown in the query results in expressions or in the WHERE clause;
they arent directly used in the select list. Thats why theyre all no.
Some limitations of sp_depends are:
n References outside the current database are not reported.
n References to system tables are not reported.
n References to INFORMATION_SCHEMA views are not reported.
Youll have to work within these limitations of sp_depends.
The system stored procedures that have been discussed in this sec-
tion are adequate for many tasks but not always so easy to work with.
There are other ways to get metadata about UDFs. One way is to write
UDFs that query data from the system tables. The next section discusses
the best ways to get at that data.
Part I 183
Chapter 9: Metadata about UDFs
INFORMATION_SCHEMA.ROUTINES
INFORMATION_SCHEMA.ROUTINES has information for both stored procedures
and UDFs. Use the ROUTINE_TYPE column to distinguish between the two.
It equals 'FUNCTION' for all three types of UDF.
Unfortunately, nothing in ROUTINES tells you which type of UDF it is,
so you have to rely on other sources for that information. There is a
DATA_TYPE column that helps. For scalar UDFs, it gives the base type that
is returned. For inline and multistatement UDFs, it is 'TABLE'.
Heres a quick look at a few fields from ROUTINES:
(Results)
Functions may have been added to TSQLUDFS by the time you read this,
so you may see different results.
Part I 185
Chapter 9: Metadata about UDFs
INFORMATION_SCHEMA.ROUTINE_COLUMNS
ROUTINE_COLUMNS has one row for each column returned by either an inline
or a multistatement UDF. This script uses it to show the columns
returned by udf_DT_MonthsTAB:
(Results)
INFORMATION_SCHEMA.PARAMETERS
INFORMATION_SCHEMA.PARAMETERS has parameters for functions as well as
stored procedures. There is one entry for each parameter. For scalar
UDFs, there is a row for the result of the function that has an ORDINAL_
POSITION of 0. This script shows the parameters for udf_DT_Age, which is
scalar:
(Results)
Name Position Parm Name DATA_TYPE Mode IS_RESULT
---------- -------- ------------- --------- ---------- ----------
udf_DT_Age 0 int OUT YES
udf_DT_Age 1 @DateOfBirth datetime IN NO
udf_DT_Age 2 @AsOfDate datetime IN NO
Thats the last of the INFORMATION_SCHEMA views that has important infor-
mation about UDFs. The next information sources are two built-in
functions that give information about many object types, UDFs included.
-- try OBJECTPROPERTY
DECLARE @Func_ID int
SET @Func_ID = OBJECT_ID ('udf_DT_MonthsTAB')
SELECT OBJECTPROPERTY(@Func_ID, 'IsQuotedIdentOn') as IsQuotedIdentOn
, OBJECTPROPERTY(@Func_ID, 'IsTableFunction') as IsTable
, OBJECTPROPERTY(@Func_ID, 'IsScalarFunction') as IsScalar
GO
(Results)
IsQuotedIdentOn IsTable IsScalar
--------------- ----------- -----------
NULL 1 0
-- try COLUMNPROPERTY
DECLARE @Func_ID int
SET @Func_ID = OBJECT_ID ('udf_Example_Multistatement_WithComputedColumn')
SELECT COLUMN_NAME
, COLUMNPROPERTY (@Func_ID, COLUMN_NAME
, 'IsComputed') as [IsComputed]
, COLUMNPROPERTY (@Func_ID, COLUMN_NAME
, 'IsPrimaryKey') as [IsPrimaryKey]
FROM INFORMATION_SCHEMA.ROUTINE_COLUMNS
WHERE TABLE_NAME = 'udf_Example_Multistatement_WithComputedColumn'
ORDER BY ORDINAL_POSITION
GO
Part I 187
Chapter 9: Metadata about UDFs
(Results)
Books Online has the complete list of properties that the built-in metadata
functions can return. But the previous queries illustrate one of the limita-
tions of these functions: They dont always report the expected
information when working with UDFs. For example, IsPrimaryKey should
be 1 or 0 for all columns, but it returns NULL. IsQuotedIdentOn should also
be reported as 1 or 0 but returns NULL. Ive listed these as bugs in Appen-
dix C.
PERMISSIONS summarizes information that is stored in the
syspermissions and sysprotects system tables. By using PERMISSIONS it is
possible to check whether the current user has permissions to execute a
particular UDF. This query checks to see if the current user can run
udf_Order_Amount:
(Results)
The built-in metadata functions should remain the same as new versions
of SQL Server are released. That makes using them preferable to interro-
gating the system tables. However, sometimes the system tables are the
only place to get the answer you want.
Books Online has the details of the system tables and a complete list of
their columns. Table 9.3 lists system tables with the information most
important to UDFs.
Table 9.3: Important systems tables
System Table Information about UDFs
sysobjects One row for all database objects including each UDF.
syscolumns An entry for every column returned and every parameter.
sysdepends Rows for references by UDF and references to the UDF.
sysconstraints Only used when multistatement UDFs have constraints on
their table.
syscomments The CREATE FUNCTION script is stored here.
syspermissions Permissions granted on database objects including UDFs.
sysprotects Grants and denies to UDFs and other objects.
Every database object such as a table, view, stored procedure, or UDF has
an entry in sysobjects. Every object has a unique ID and a unique name.
The sysobjects.type column differentiates between the different object
types. The codes for the three different object types for UDFs are given in
Table 9.4.
Table 9.4: Object type codes in sysobjects for UDFs
Type Type of UDF
FN Scalar
IF Inline
TF Multistatement
Now that the sources of metadata have been defined, the next section is
devoted to creating some functions to package the information in the most
useful ways. Some of the functions that follow use the system tables but
only when the information isnt available in either INFORMATION_SCHEMA or a
built-in system function.
Metadata UDFs
This section explores some of the most useful functions that Ive created
for packaging metadata about UDFs. The functions here gather their infor-
mation from the sources described in the previous section.
I group metadata functions about UDFs under the group prefix
udf_Func_. Youll also find more general-purpose metadata UDFs in the
udf_Object_ group.
Part I 189
Chapter 9: Metadata about UDFs
Function Information
all functions. The caller is expected to put in any wildcard matching char-
acters for the LIKE expression. That way, the caller has full control to
request information about a single UDF or multiple UDFs. A naming con-
vention like the one used for functions in this book makes this type of
pattern search very convenient.
The UDFs in the udf_Func group can be listed with this query:
(Results You may see any some additional functions in your results)
The query shows a couple of other interesting UDFs that are investigated
next.
The fact that the parameter is a pattern that works with LIKE allows you to
request information for one or more UDFs in one query. That might be
useful when searching for particular column names. To get the columns
for one function, supply the UDF name without any wildcards, as in this
query that documents the columns returned by udf_Func_ColumnsTAB:
(Results)
INFORMATION_SCHEMA.PARAMETERS has a row for the return type, and Ive left
it in the results. It can always be filtered out of the results by adding a
POSITION!=0 expression in the WHERE clause to any query that doesnt need
the return type.
In keeping with the pattern of self reporting, this query gets the list of
parameters to all the udf_Func group functions:
Part I 193
Chapter 9: Metadata about UDFs
(Results)
As you can see, most of these functions take the same @Function_name_
pattern parameter. Any multistatement or inline UDF that doesnt have
any parameters wont show up in the results.
Thats the last of the functions that is specific to UDFs. The next
section discusses a couple of functions that work on all objects.
) RETURNS TABLE
/*
* Searches the text of SQL objects for the string @SearchFor.
* Returns the object type and name as a table.
*
* Example:
SELECT * from udf_Object_SearchTAB('xp_cmdshell', NULL)
****************************************************************/
AS RETURN
SELECT TOP 100 PERCENT WITH TIES -- TOP clause makes Order by OK
CASE xtype WHEN 'C' THEN 'Check Constraint'
WHEN 'D' THEN 'DEFAULT Constraint'
WHEN 'FN' THEN 'Function/Scalar'
WHEN 'IF' THEN 'Function/Inline'
WHEN 'P' THEN 'Stored Procedure'
WHEN 'R' THEN 'Rule'
WHEN 'TF' THEN 'Function/Table'
WHEN 'TR' THEN 'Trigger'
WHEN 'V' THEN 'View'
ELSE 'Unknown'
END as [Object Type]
, OBJECT_NAME(o.[id]) as [Name]
FROM syscomments c
INNER JOIN sysobjects o
ON c.[id] = o.[id]
WHERE (@Just4Type IS NULL
OR (o.xtype = @Just4Type
Part I 195
Chapter 9: Metadata about UDFs
Heres a query that searches for all functions that have a reference to
objects in the master database:
(Result)
Of course, this is a text search, and any function or other object that con-
tained the sentence A dog will always listen to its master also shows up
in the results. Text search is not a perfect technique, but its often the
fastest way to find references to objects.
Thats the last of the functions for this chapter. The TSQLUDFS data-
base has other metadata functions that work on other types of database
objects, such as tables, views, and stored procedures.
Throughout the book weve been using T-SQL to do all our work with
functions. Sometimes working in a compiled language makes the job of
coding a solution much easier. SQL-DMO is a COM library that facilitates
working with SQL Server objects including UDFs.
196 Part I
Chapter 9: Metadata about UDFs
SQL-DMO
SQL-DMO is a Win32 COM interface to SQL Server objects. The objects
can be used either from compiled programs written in C++, Visual Basic,
or any .NET language or from a scripting language such as VBScript.
Scripting languages are available in SQL Servers DTS, in ASP pages, and
in VBS files executed by Windows Scripting Host.
Enterprise Manager does all its work through SQL-DMO, so you
know it has to be pretty complete. A related COM library that is also used
by Enterprise Manager is the SQL Namespace library or SQL-NS. This
library has the dialog boxes and other user interface elements from Enter-
prise Manager.
If you want to base any programs on these COM libraries, check on
licensing issues. As far as I know SQL-DMO is redistributable with any of
your applications, but SQL-NS requires a SQL Server client access license
to use at run time, so I dont think that its okay to distribute it.
An explanation of how to use SQL-DMO to work with UDFs is
beyond the scope of this book. However, I thought that I would bring it to
your attention because of the robustness of its interface, and the ease of
working with COM objects make SQL-DMO a logical choice when trying
to program code to manipulate SQL Server objects.
Summary
SQL Server offers a variety of ways to retrieve information about UDFs.
This chapter has discussed these possibilities:
n System stored procedures such as sp_help, sp_depends, and
sp_helptext
n Querying INFORMATION_SCHEMA views
n From built-in metadata functions OBJECTPROPERTY and COLUMNPROPERTY
n Directly from system tables
n Through SQL-DOMs COM interface
All of these methods have their own advantages and disadvantages. Youll
have to match the method to your needs.
Along the way, several functions that package information about
UDFs into convenient forms were created. The UDFs package the infor-
mation in a form thats easy to use in a program, another SQL statement,
or a report writer. Thats their advantage. Now that you know where to
look for the information, you can write your own functions to retrieve the
information the way that you want to see it.
10
Using Extended
Stored Procedures
in UDFs
Extended stored procedures (xp_s) are functions written in C/C++ or
another language can create a DLL to use a specific interface that SQL
Server supports, named ODS. The names of most extended stored proce-
dures begin with the characters X and P, followed by an underscore.
Hence, they are often referred to as xp_s. As well see shortly, the xp_
prefix is a convention that even Microsoft breaks; its not a rule. xp_s
reside in DLL files.
SQL Server invokes extended stored procedures in the SQL Server
database engines process. This is the heart of SQL Server. Any untrapped
errors in an xp_ can destabilize SQL Server. They shouldnt be created
without careful testing. But because of the direct nature of the ODS call
interface, xp_s can be very fast. Plus, they have access to resources, such
as disk files, network interfaces, and devices that are inaccessible from
T-SQL. They can participate in the current connection or open a new one,
giving them a great deal of flexibility.
I wont describe how to write your own extended stored procedures.
If youre interested, I suggest that you take a look at Books Online and the
examples provided with SQL Server. If youd like to read more, the best
explanation of how to create extended stored procedures that Ive read is
in Ken Hendersons book The Gurus Guide to SQL Server Stored Proce-
dures, XML and HTML (Addison-Wesley, 2002).
xp_s cant be created with standard Visual Basic 6.0 because VB cant
export functions in the conventional Windows sense. Functions and object
interfaces exported by VB are exported through COM. As a programmer
whos used Visual Basic intensely for the last six years, I find this some-
what disappointing. However, theres another way: The sp_OA* procedures
allow the T-SQL programmer to create and manipulate COM objects
197
198 Part I
Chapter 10: Using Extended Stored Procedures in UDFs
That doesnt leave many options for working with the output of an xp_.
The only xp_s that can be used are those that return their results in
OUTPUT parameters and the return code. Table 10.1 lists all the xp_s docu-
mented in the BOL with a column that says whether they can be used in a
UDF and either the reason they cant be used in a UDF or a description.
Table 10.1: Documented extended stored procedures that can be used in UDFs
Extended Can It Be Used Summary or Why It Cant Be Used
Procedure in a UDF?
sp_OA* YES This is a group of extended stored procedures
that all begin with sp_OA. Although their names
begin with sp_, theyre extended stored proce-
dures. They create, destroy, and communicate
with COM objects. See the sections on OLE
automation later in this chapter.
xp_cmdshell NO Returns a resultset.
xp_deletemail YES Deletes an item from a SQL Mail inbox.
xp_enumgroups NO Returns a resultset.
xp_findnextmsg YES Possible. Returns a message ID, used with
sp_processmail.
xp_gettable_dblib NO This is an extended stored procedure that is
delivered in the sample code. It cant be used
because it returns a resultset. However,
OPENROWSET or OPENQUERY can achieve the
same functionality.
xp_grantlogin NO Although its name begins with xp_, its now a
stored procedure that is provided for backward
compatibility.
Part I 199
Chapter 10: Using Extended Stored Procedures in UDFs
xp_logevent
xp_logevent writes a user-defined message to both the SQL Server log file
and the Windows NT application event log. The SQL Server log in ques-
tion is the information log maintained by SQL Server, not its transaction
log. The syntax to the call is:
200 Part I
Chapter 10: Using Extended Stored Procedures in UDFs
(Results)
Return Code = 0
Note:
Before demonstrating xp_logevent, I used the sp_cycle_errorlog
stored procedure to create a new SQL Server log. It makes the dis-
plays that follow somewhat easier to read. Dont do this unless youre
sure its okay on your system.
Now take a look at whats produced in the SQL Server log. Figure 10.1
shows the log as it appears in Enterprise Manager.
The error log can also be queried using the undocumented extended
ERRORLOG
--------------------------------------------------------------------------------
2002-10-09 11:08:59.73 spid55 Microsoft SQL Server 2000 - 8.00.534 (Intel X8
Nov 19 2001 13:23:50
Copyright (c) 1988-2000 Microsoft Corporation
Developer Edition on Windows NT 5.0 (Build 2195: Service Pack 3)
2002-10-09 11:08:59.73 spid55 Copyright (C) 1988-2000 Microsoft Corporation.
2002-10-09 11:08:59.73 spid55 All rights reserved.
2002-10-09 11:08:59.73 spid55 Server Process ID is 708.
2002-10-09 11:08:59.73 spid55 Logging SQL Server messages in file 'C:\Program
2002-10-09 11:08:59.73 spid55 Errorlog has been reinitialized. See previous
2002-10-09 11:09:20.03 spid53 Error: 60000, Severity: 10, State: 1
2002-10-09 11:09:20.03 spid53 The quick brown fox jumped over the lazy dog..
The log grows quickly, so youll want to use xp_readerrorlog with care.
But its a valuable tool for seeing whats going on during development. Its
so valuable that I wrote a stored procedure, usp_SQL_MyLogRpt, that uses its
output to show any messages added by the current process within the last
60 minutes. The most recent messages are displayed first. Listing 10.1 on
the following page shows the stored procedure. It has to read the entire
SQL log into a temporary table, so use it with care on production servers
that may have very long log files. Heres a sample invocation:
(Results)
When Message
---------------------- --------------------------------------------------------
2002-10-09 11:09:20.03 The quick brown fox jumped over the lazy dog..
2002-10-09 11:09:20.03 Error: 60000, Severity: 10, State:
The output of xp_logevent is also written to the NT event log. Figure 10.2
shows the NT Event Viewers Application Log. It contains one message
for each invocation of xp_logevent. Our sample message is shown in detail
in Figure 10.3.
202 Part I
Chapter 10: Using Extended Stored Procedures in UDFs
Figure 10.3: The Event Properties window with the details of a message
SELECT
left(ErrorLog, 22) as [When]
, dbo.udf_TxtWrapDelimiters(SUBSTRING (ErrorLog, 34, 1000), 80, N' '
, N' ', NCHAR(10), 23, 23) as [Message]
FROM #ErrorLog
WHERE LEFT (ErrorLog, 1) <> CHAR(9) -- Lines don't start with a TAB
AND @spid = CAST (RTRIM(SUBSTRING (ErrorLog, 24, 9)) as varchar(9))
-- From my process
AND 1=ISDATE(LEFT(ErrorLog, 22)) -- Date found on the line
AND 61 > DATEDIFF (n, CASE WHEN 1=ISDATE(LEFT(ErrorLog, 22))
THEN CONVERT(datetime, LEFT(ErrorLog, 22))
ELSE 0
END
, GETDATE()) -- within the last 60 minutes
ORDER BY SequenceNumber DESC
END
IF @sSeverity is NULL
EXEC @WorkingVariable = master..xp_logevent @nMessageNumber
, @sMessage
ELSE
EXEC @WorkingVariable = master..xp_logevent @nMessageNumber
, @sMessage
, @sSeverity
-- use udf_SQL_LogMsgBIT
Declare @rcBIT BIT -- return code
SELECT @rcBIT = dbo.udf_SQL_LogMsgBIT (default,
'Now that''s what I call a message!', null)
PRINT 'Return Code is ' + convert(varchar, @rcBIT)
GO
(Result)
Return Code is 1
When Message
---------------------- --------------------------------------------------------
2002-10-09 11:32:31.92 Now that's what I call a message!.
2002-10-09 11:32:31.92 Error: 50001, Severity: 10, State:
2002-10-09 11:09:20.03 The quick brown fox jumped over the lazy dog..
2002-10-09 11:09:20.03 Error: 60000, Severity: 10, State:
Thats interesting enough. But where would you ever use the function?
After all, in almost all circumstances where you want to add a message to
the log, you can just use xp_logevent. The only circumstance that I know
of for using it is when you want to log an event in the middle of a query,
such as for each row in a query as the query is processed. This might
occur in the select list or in the middle of the WHERE clause. Of course, I
wouldnt want to do that in a production system, but Ive done it during
development as a debugging tool. The following view definition makes use
of the technique:
GO
Part I 205
Chapter 10: Using Extended Stored Procedures in UDFs
The view does a SELECT on the sample Cust table in the TSQLUDFS data-
(Results)
(Results)
When Message
---------------------- --------------------------------------------------------
2002-10-09 11:46:01.72 Executing for Customer Wordware.
2002-10-09 11:46:01.72 Error: 50001, Severity: 10, State:
2002-10-09 11:46:01.72 Executing for Customer Novick Software.
2002-10-09 11:46:01.72 Error: 50001, Severity: 10, State:
2002-10-09 11:32:31.92 Now that's what I call a message..
2002-10-09 11:32:31.92 Error: 50001, Severity: 10, State:
2002-10-09 11:09:20.03 The quick brown fox jumped over the lazy dog..
2002-10-09 11:09:20.03 Error: 60000, Severity: 10, State:
(Results)
(Results)
When Message
---------------------- ---------------------------------------------------------
2002-10-09 11:55:35.88 Message In WHERE Clause for Customer Wordware.
2002-10-09 11:55:35.88 Error: 65000, Severity: 10, State:
2002-10-09 11:46:01.72 Executing for Customer Wordware.
2002-10-09 11:46:01.72 Error: 50001, Severity: 10, State:
2002-10-09 11:46:01.72 Executing for Customer Novick Software.
2002-10-09 11:46:01.72 Error: 50001, Severity: 10, State:
2002-10-09 11:32:31.92 Now that's what I call a message!.
2002-10-09 11:32:31.92 Error: 50001, Severity: 10, State:
2002-10-09 11:09:20.03 The quick brown fox jumped over the lazy dog..
2002-10-09 11:09:20.03 Error: 60000, Severity: 10, State:
The Left (CompanyName, 1) > 'N' clause filters out Novick Software, so
Wordware is the only company left. Although two rows are processed by
the WHERE clause, there was only one invocation of udf_SQL_LogMsgBIT.
Thats because once the Novick Software row was excluded by the Left
(CompanyName, 1) > 'N' clause, there was no reason to execute the test on
the other side of the AND. This is called expression short-circuiting, and its
a technique that SQL Server uses to speed the evaluation of queries.
Switching the clause that invokes udf_SQL_LogMsgBIT to one that
always returns false doesnt help either. It would read:
However, I found that switching both the order of the clauses and using an
OR operator between them does the trick. Heres the query:
(Results - abridged)
When Message
---------------------- --------------------------------------------------------
2002-10-09 12:06:25.83 Message In WHERE Clause for Customer Wordware.
2002-10-09 12:06:25.83 Error: 65000, Severity: 10, State:
2002-10-09 12:06:25.82 Message In WHERE Clause for Customer Novick Software.
2002-10-09 12:06:25.82 Error: 65000, Severity: 10, State:
2002-10-09 11:55:35.88 Message In WHERE Clause for Customer Wordware.
2002-10-09 11:55:35.88 Error: 65000, Severity: 10, State:
2002-10-09 11:46:01.72 Executing for Customer Wordware.
2002-10-09 11:46:01.72 Error: 50001, Severity: 10, State:
That did it. The results are consistent. According to SQL Server Books
Online, they should be repeatable. In the article on search conditions,
Books Online says this about predicate order:
The order of precedence for the logical operators is NOT
(highest), followed by AND, followed by OR. The order of
evaluation at the same precedence level is from left to
right. Parentheses can be used to override this order in a
search condition.
Net has a similar behavior, and therefore its likely that future versions of
SQL Server will continue to short-circuit expressions.
Message text and severity levels in sysmessages are ignored by
xp_logevent and hence by udf_SQL_LogMsgBIT. To use one of the messages
in sysmessages, use the FORMATMESSAGE built-in function to create the mes-
sage before calling udf_SQL_LogMsgBIT.
Thats all for xp_logevent. As you can see from the log, its time for
lunch. Today its cucumber salad and leftover meatloaf.
208 Part I
Chapter 10: Using Extended Stored Procedures in UDFs
xp_sprintf
This extended stored procedure is similar to the C language function
sprintf. However, its a very limited version of that function because it
only supports the insertion of strings with the %s format. The syntax of
the call is:
xp_sprintf @Result OUTPUT
@Format
[, @Argument1 [,..n]]
@Result is a varchar variable to receive the output.
@Format is a varchar with the text of the output message and
embedded %s symbols where substitution should take place.
@Argument1, @Argument2 are varchar arguments to @Format.
There should be as many of these as %s insertion points in @Format.
As an extended stored procedure, xp_sprintf is used to format textual
messages by substituting several arguments into the correct position of a
format string. However, it suffers from two limitations:
n It can only be used in circumstances where an xp_ can be invoked.
n It only accepts strings for substitution.
These limitations can be overcome with the UDF shown in Listing 10.3.
Because its a UDF, it can be invoked in additional circumstances, such as
a select list, where xp_s couldnt be used. udf_Txt_Sprintf uses xp_sprintf
to perform the substitution but takes care of the conversion to character
string. Its limitations are that it only handles input and output strings up to
254 characters, and it always expects exactly three parameters.
EXEC master.dbo.xp_sprintf
@Result OUTPUT
, @Format
, @Parm1
, @Parm2
, @Parm3
RETURN @Result
END
Using the function is pretty simple. The easiest location to use it is in the
select list, as shown by this query:
-- Print authors and contract value, which has a BIT data type.
SELECT TOP 4
dbo.udf_Txt_Sprintf ('%s %s Contract Value = %s'
, au_fname, au_lname, contract)
as [Author and Contract]
FROM pubs..authors
GO
(Results)
The COM objects that you automate can be objects in an existing program
or in a new program that you create. There are no restrictions on what
they can do beyond the restrictions placed on the account that executes
SQL Server.
GRANT EXECUTE permission on the sp_OA* procedures to all users who will
Note:
Some of the scripts in this chapter produce long results that wrap over
several lines. To see them properly, you should send output to text by
using Query Analyzers Query Results in Text (Ctrl+T) menu com-
mand. In addition, set the maximum characters per column to 8192
in the Results tab of the Options dialog. You can reach it using the
Tools Options menu command.
GOTO BypassErrorHandling
Automation_Error:
ByPassErrorHandling:
PRINT 'done'
GO
(Results)
Drive Exists = 1
Done
to:
Drive Exists = 0
done
...
GO
Part I 215
Chapter 10: Using Extended Stored Procedures in UDFs
//
// Values are 32-bit values laid out as follows:
//
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
// +---+-+-+-----------------------+-------------------------------+
// |Sev|C|R| Facility | Code |
// +---+-+-+-----------------------+-------------------------------+
//
// where
//
// Sev - is the severity code
//
// 00 - Success
// 01 - Informational
// 10 - Warning
// 11 - Error
//
// C - is the Customer code flag
//
// R - is a reserved bit
//
// Facility - is the facility code
//
// Code - is the facility's status code
//
//
// Define the facility codes
//
#define FACILITY_WINDOWS 8
#define FACILITY_STORAGE 3
#define FACILITY_SSPI 9
#define FACILITY_SETUPAPI 15
216 Part I
Chapter 10: Using Extended Stored Procedures in UDFs
#define FACILITY_RPC 1
#define FACILITY_WIN32 7
#define FACILITY_CONTROL 10
#define FACILITY_NULL 0
#define FACILITY_MSMQ 14
#define FACILITY_MEDIASERVER 13
#define FACILITY_INTERNET 12
#define FACILITY_ITF 4
#define FACILITY_DISPATCH 2
#define FACILITY_CERT 11
The three parts of the code that are most useful are:
n The Severity Code in bits 31 and 30
n The Facility Code in bits 27-16
n The Facility Status Code in bits 15-0
The remainder of WINERROR.H lists error codes. The file is part of
Visual C++ and Visual Studio, so youll need one of those products to get
the complete file.
sp_displayoaerrorinfo uses sp_hexadecimal to display the error code
in hex. Its the hex codes that are listed in Books Online. The function
udf_BitS_FromInt shows the bits in full detail.
Note:
You should be running with SQL Server 2000 Service Pack 2 or above
to execute the next script. For security reasons, all SQL Servers should
have at least Service Pack 3.
(Results)
IF @HRESULTfromGetErrorInfo != 0 BEGIN
SELECT @ErrorMsg = 'Call to GetErrorInfo failed. Suggest stopping '
+ 'ODSOLE with sp_OAStop. '
+ ' HRESULT from sp_OAGetErrorInfo = '
+ master.dbo.fn_varbintohexstr(@HRESULTfromGetErrorInfo)
+ ' Original HRESULT = '
+ master.dbo.fn_varbintohexstr(@HRESULT)
RETURN @ErrorMsg
END -- end if
-- Extract the correct bits to get the facility code and error code
SELECT @FacilityCode = (@HRESULT & 0x0FFF0000) -- first mask
/ POWER(2, 16) -- then shift right 16 bits
, @Code = @HRESULT & 0X0000FFFF -- Just mask
RETURN @ErrorMsg
END
(Results)
Error Message
-------------------------------------------------------------------------------
HRESULT=0x800401f3 Facility:ITF Src:ODSOLE Extended Procedure
Desc:(PROGID or CLSID not registered) Invalid class string
That gives us a better error message. But if were going to use it inside a
function, we have to decide what to do with the message. If the function is
going to return a string, the error message could be used for the return
value. All functions dont return strings, so that doesnt always work.
Besides, it greatly complicates using the function. The next section has an
alternative solution thats pretty powerful.
RETURN @FullMsg
END
-- Log an error message to the SQL log and NT event log using udf_OA_LogError
DECLARE @HRESULT int, @hObject int
EXEC @HRESULT = sp_OACreate 'BADPROGID', @hObject OUTPUT, 5
SELECT dbo.udf_OA_LogError(@hObject, @HRESULT, 'Chapter 10 Listing 0'
, 'sp_OACreate', 'BADPRODID') as [Error Message]
EXEC @HRESULT = sp_OADestroy @hObject
GO
Part I 221
Chapter 10: Using Extended Stored Procedures in UDFs
(Results)
Error Message
--------------------------------------------------------------------------------
OA error in 'Chapter 10 Listing 0' Invoking:sp_OACreate (BADPRODID)
HRESULT=0x800401f3 Facility:ITF Src:ODSOLE Extended Procedure
Desc:(PROGID or CLSID not registered) Invalid class string
Warning:
The text for this script, along with all the others in the chapter, are in
the file Chapter 10 Listing 0 Short Queries.sql, which youll find in the
download directory. Because it logs a message to the SQL log and NT
event log on the server, dont execute it unless youre sure its okay on
the SQL Server (for example, if youre using a development server).
Now that we have a way to handle OLE Automation errors and a way to
log them efficiently, its time to create a UDF that does something useful.
The following example builds on a script shown above. Its a simple exam-
ple of what can be done.
IF @HRESULT != 0 BEGIN
SELECT @msg = dbo.udf_OA_LogError (
'udf_SYS_DriveExistsBIT'
, 'sp_OACreate'
, 'FileSystemObject'
, @HRESULT, @hFSO)
GOTO Function_Cleanup
END
IF @HRESULT != 0 BEGIN
SELECT @msg = dbo.udf_OA_LogError (
'udf_SYS_DriveExistsBIT'
, 'sp_OAMethod'
, 'DriveExists'
, @HRESULT, @hFSO)
SET @DriveExists = NULL -- In case it had been set by the call
END
Function_Cleanup:
RETURN @DriveExists
END
(Results)
Drive R Exists
--------------
0
Be careful how you interpret the results of the function. The UDF is run
on the computer thats running SQL Server. That might not be the same
computer that Query Analyzer is running on. The results returned are for
the server machine.
As you can see from Listing 10.6, every method call to an sp_OA*
extended stored procedure is accompanied by a group of statements that
handle and log the error, such as this code:
Part I 223
Chapter 10: Using Extended Stored Procedures in UDFs
IF @HRESULT != 0 BEGIN
Each message must identify exactly which call caused the problem so that
it can be tracked down efficiently. Without this kind of detail, youll be left
to speculate about the origin of the error.
Note:
If at all possible, use Visual Basic 6.0 and the latest service pack for
creating OLE objects. Versions of Visual Basic before Version 5.0 Ser-
vice Pack 3 shouldnt be used for creating COM objects to use with
SQL Server. VB .NET doesnt use COM by default. The way to create
an object with the .NET framework and use it from SQL Server is
through COM interop.
224 Part I
Chapter 10: Using Extended Stored Procedures in UDFs
myName = m_sMyName
End Property
m_sMyName = vNewValue
End Property
End Function
The object has one property, myName, and one method, sayHello. The exam-
ple script below does the following:
n Creates the object
n Sets the myName property to my name
n Invokes the sayHello method to produce our message
Before you try to execute the script, you must create and register the
DLL that has the OLE object. You can do that in one of two ways:
n Open the project TDSQLUDFVB with VB 6.0 and make the DLL on
your system.
n Register the DLL with the command-line utility regsvr32.exe. To do
that, open a command window and navigate to the directory with
TDSQLUDFVB.DLL. Then execute the command: regsvr32.exe
TDSQLUDFVB.DLL.
You should see a dialog box like the one shown in Figure 10.4.
Part I 225
Chapter 10: Using Extended Stored Procedures in UDFs
Be sure that you register the DLL or make it on the machine where SQL
Server is running. Thats the machine where the OLE object is going to
be executed.
Listing 10.8 shows udf_Example_OAhello, a short example UDF to
exercise the new OLE object by creating a Hello string. There are only
three OLE calls necessary to use the demonstration object:
n sp_OACreate To create the object
n sp_OASetProperty To set the name property of the object
n sp_OAMethod To execute the SayHello method of the object
IF @HRESULT != 0 BEGIN
SELECT @sMessage = dbo.udf_OA_LogError (
@hObject, @HRESULT
, 'udf_ExampleOAhello'
, 'sp_OACreate'
, 'TSQLUDFVB.cTSQLUDFVBDemo'
)
GOTO Function_Cleanup
END
226 Part I
Chapter 10: Using Extended Stored Procedures in UDFs
IF @HRESULT != 0 BEGIN
SELECT @sMessage = dbo.udf_OA_LogError (
@hObject, @HRESULT
, 'udf_ExampleOAhello'
, 'sp_OASetProperty'
, 'TSQLUDFVB.cTSQLUDFVBDemo'
)
GOTO Function_Cleanup
END
Function_Cleanup:
RETURN @sMessage
END
(Results)
Theres a useful technique for debugging the object that you ought to
know about. Its possible to debug your object while it is called from SQL
Server. If youre trying to track down a problem and cant reproduce the
problem any other way, this might work for you. However, dont use this
technique on any computer except a development machine thats not
being used by anyone else. To debug your object, do the following:
Part I 227
Chapter 10: Using Extended Stored Procedures in UDFs
Warning:
Just in case this isnt obvious, let me make this very clear: This is a
technique that could bring down a SQL Server. I recommend that it
only be used on dedicated development machines, such as a pro-
grammers computer with the SQL Server Developer edition installed.
The OLE object presented here is a trivial one. But there are many OLE
objects in the Windows world. Many of them can be used constructively
from within SQL Server. Some of them are suitable for use within a UDF.
A few suggestions are:
n The SQL-DMO interface to SQL Server
n The WMI interface to manage Windows objects
n Cryptography libraries
n Collaboration Data Objects for interacting with mail systems
n MSXML for manipulating XML documents
n The COM interface to Visual SourceSafe
The usefulness of OLE objects to you will depend on the tasks that you
seek to accomplish.
Summary
This chapter has discussed using extended stored procedures from UDFs.
SQL Server provides a long list of xp_s, both documented and undocu-
mented. Ive tried to show you the few that are most useful as a way to
illustrate what can be done.
The most powerful of the xp_s are those that enable OLE Automation.
COM is the backbone for most Windows programs created since the
mid-1990s. The ability to tap into that large resource of COM code is a
powerful but dangerous capability. I continue to recommend that it be used
only for administrative tasks. If you need to employ a COM object in a
high activity production application, it probably belongs in some other
application tier, such as the user interface or the application server.
Throughout this book Ive shown small tests of how one function or
another should work. Most UDFs in the listings have one or more tests in
the programs header. The next chapter is about testing UDFs with header
tests and more extensive test scripts.
11
229
230 Part I
Chapter 11: Testing UDFs for Correctness and Performance
These two- or three-line tests are much better than no test at all, but
theyre not the only testing that should be done on a UDF. Test scripts are
appropriate when the importance or complexity of the UDF warrant one.
That leaves a lot of room for judgment. Itll have to be up to the project
manager to decide. I like to put my complete test scripts into stored pro-
cedures. After discussing embedded tests, Ill show you a complete test
embedded in an SP.
Once the UDF works, you may or may not be done with testing it.
Performance evaluation is important to improving your applications. In
this regard, UDFs can pose a problem. Depending on how theyre used,
they can introduce noticeable performance problems. Its important that
you watch out for this. After discussing test scripts, well look at evaluat-
ing the speed of a UDF and comparing it to an equivalent SQL expression.
If you want to execute the scripts as you read the chapter, the short
queries used in this chapter are stored in the file Chapter 11 Listing 0
Short Queries.sql. Youll find it in this chapters download directory. The
UDFs and stored procedures used here are in the TSQLUDFS database.
* Test:
PRINT 'Test 1 year ' + case when DATEPART(yy, Getdate())
=dbo.udf_DT_dynamicDATEPART ('yy', GetDate())
THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 2 month ' + case when DATEPART(mm, Getdate())
=dbo.udf_DT_dynamicDATEPART ('mm', GetDate())
THEN 'Worked' ELSE 'ERROR' END
As shown in Figure 11.1, you select the code of the test in Query Analyzer
and use the F5 button, use the green execute arrow, or press Ctrl+E to
Part I 231
Chapter 11: Testing UDFs for Correctness and Performance
run the tests. The results print out in the results window, and you can
The day after you write the UDF code you may know what results to
expect. A few months later or when someone else has to test the code,
the tester has to spend time figuring out what to expect from the function.
The practice of printing only a confirmation message makes it nearly triv-
ial to run a quick test and know that the UDF is still, probably, okay.
The inline tests work really well. But unless the UDF is trivial, they
dont do a complete job of testing the functionality of the routine. That job
falls to the test script.
Test Scripts
There are many ways that tests can be written. They can be textual
scripts with instructions on what queries to run and what results to
expect. They can be programs written in a client-side development tool
such as VB .NET. They can also be a script in an automated testing tool. I
prefer to write tests for UDFs in a T-SQL stored procedure.
232 Part I
Chapter 11: Testing UDFs for Correctness and Performance
Stored procedures are backed up with the database. That makes them
your best bet for having the test around for the long term. Separate scripts
are too easy to lose. SPs also have the advantage of being in a program-
ming language that is certain to always be available when the time comes
to test. If you rely on having a VB compiler or even Windows Scripting
Host available in the field, you may be caught short in a critical situation.
By convention, I name my testing stored procedures with a prefix of
TEST_ followed by the UDFs name. Listing 11.1 shows TEST_udf_DT_
WeekdayNext. Youll find udf_DT_WeekdayNext in the TSQLUDFS database.
The first thing that you should do with the test procedure is run it. This
test is embedded in the comment block near the top of the procedure. Its
kept there so that its always accessible and difficult to lose:
-- Testing UDFs
DECLARE @AllWorked BIT, @RC int
EXEC @RC = TEST_udf_DT_WeekdayNext @AllWorked OUTPUT, 1
PRINT 'Test udf_DT_WeekdayNext @RC = ' + CONVERT(char(10), @RC)
GO
(Results - abridged)
The proc has a mixture of a few hard-coded tests and a double loop of tests
that test every day for a year under every possible value for @@DATEFIRST.
@@DATEFIRST governs the numbering of days. Changing it, with SET
DATEFIRST, changes the result from the DATEPART function when requesting
the day of the week.
Usually, its trivial for a human to decide whats the next weekday.
Under most circumstances, its also easy to code. However, udf_DT_Week-
dayNext is sensitive to the value of @@DATEFIRST. Thats the reason for the
double loop.
The most important part of any test procedure is that its correct. In
addition, if attention is paid to a few mechanical items, the long-term value
of the procedure is improved.
The parameter @AllWorked should be part of every test procedure. I
havent written it yet, but Im moving toward having a regression test pro-
cedure that runs all my testing stored procedures. That could be run a few
times a day during any heavy-duty development to be sure that nothing is
broken. Once development slows down, it could be run once a day or
when needed.
The parameter @PrintSuccessMsgs is there to suppress the 9,000 lines
of output from the procedure most of the time. While Im developing the
Part I 235
Chapter 11: Testing UDFs for Correctness and Performance
test, I want to see all the output. Once UDF is no longer in active develop-
Before we get to the experiment, lets try out a few simple cases with this
test query:
-- Demonstrate udf_Txt_CharIndexRev
SELECT dbo.udf_Txt_CharIndexRev('fdf', 'f123 asdasfdfdfddfjas ')
as [Middle]
, dbo.udf_Txt_CharIndexRev('C', 'C:\temp\ab.txt') as [start]
, dbo.udf_Txt_CharIndexRev('X', '123456789X') as [end]
, dbo.udf_Txt_CharIndexRev('AB', '12347') as [missing]
GO
(Results)
Middle start end missing
----------- ----------- ----------- -----------
13 1 10 0
CASE
WHEN CHARINDEX(<search_for, varchar(255), ''>
, <search_in, varchar(8000), ''>) > 0
THEN LEN(<search_in, varchar(8000), ''>)
- CHARINDEX(REVERSE(<search_for, varchar(255), ''>)
, REVERSE (<search_in, varchar(8000), ''>)) + 1
ELSE 0 END
Why bother having the UDF at all if it can be replaced with an expression?
As you may recall, my philosophy about writing efficient code is that cod-
ing is an economic activity with trade-offs between the cost of writing
code and the cost of running it. In my opinion, the best course of action is
to write good, easy-to-maintain code and use a basic concern for perfor-
mance to eliminate any obvious performance problem. The overhead of
using a UDF to parse the extension from one file name isnt ever going to
show up on the performance radar screen. Its when a UDF is used on
large numbers of rows that it becomes a problem. In situations with few
rows, having the UDF helps by making the code easier to write.
238 Part I
Chapter 11: Testing UDFs for Correctness and Performance
-- Query to translate
SELECT dbo.udf_Txt_CharIndexRev('fad', 'f123 asdasfdfdfddfjas ')
as [Middle]
GO
The first step is to replace the function call with the Equivalent Template,
and we get:
SELECT CASE
WHEN CHARINDEX(<search_for, varchar(255), ''>
, <search_in, varchar(8000), ''>) > 0
THEN LEN(<search_in, varchar(8000), ''>)
- CHARINDEX(REVERSE(<search_for, varchar(255), ''>)
, REVERSE (<search_in, varchar(8000), ''>)) + 1
ELSE 0 END
as [Middle]
Next we use the Edit Replace Template Parameters menu item and
enter the two parameters. Figure 11.2 shows the Replace Template
Parameters dialog box with the replacement text entered.
After pressing the Replace All button, the result looks like this:
(Results)
Middle
-----------
13
As you can see, replacing the call to the UDF with the equivalent expres-
sion results in a much longer and messier query. Simplification, with a
corresponding reduction in maintenance effort, is a benefit of using UDFs
that is lost when you replace them with complex expressions.
Now that we have a function and can replace it with an expression, we
need some data that helps show us the difference in performance between
the two. Pubs and Northwind are useful for learning but they dont contain
very much data; at best they have a few hundred rows in any one table. I
like to run tests on a million-row table. SQL Server doesnt come with any
tables that large, so well have to construct one.
The table hasnt been added to your TSQLUDFS database because there
is a stored procedure in the database to create the table and populate it
with plenty of rows. usp_CreateExampleNumberString is shown in Listing
240 Part I
Chapter 11: Testing UDFs for Correctness and Performance
11.3. Its parameter is the number of times to double the number of rows
in ExampleNumberString.
SELECT @LC = 0
WHILE @LC < @Loops BEGIN
UPDATE ExampleNumberString
SET NumberString = convert(varchar(128)
, convert(numeric(38,0), 9834311) * bignum)
-- Pin ExampleNumberString into memory. You must change the database name, if
-- you're not going to run it in TSQLUDFS
-- Be sure that you have enough memory available before you do
-- this. My SQL Server Process grew to 82 megabytes when I ran
-- this script.
DECLARE @db_id int, @tbl_id int
SET @db_id = DB_ID('TSQLUDFS') -- DB_ID('<your database name goes here>')
SET @tbl_id = OBJECT_ID('ExampleNumberString')
DBCC PINTABLE (@db_id, @tbl_id)
GO
(Results)
Pinning the table only tells SQL Server to never remove the tables pages
from the page cache. It doesnt read them into the cache. The next query
does:
-- Read all the rows to force the pages into the cache
SELECT * from ExampleNumberString
GO
With a million-row table in memory, the stage is set for comparing the
UDF and the equivalent template. Ladies and gentleman, place your bets.
Dont run them yet. I have a few more wrinkles to throw into the
experiment.
Any query that returns a million rows to SQL Query Analyzers
results window is going to do a lot of work on sending, receiving, and dis-
playing the results. To eliminate most of that work, using an aggregate
function, such as MAX, forces the UDF or expression to be evaluated with-
out returning much data as the result.
For comparison purposes, Ive also included a third query, labeled
Query #0, that just takes the MAX of the LEN function. This query repre-
sents the minimum time it might take to just read the million rows of data.
The SET STATISTICS TIME ON command tells SQL Server to measure
the time to parse and compile the query and the time to execute the query
and report them back with the query results.
Making these three changes to the queries gives us our experiment.
If you run them, be patient. Query #1 took over four minutes on my sys-
tem. The three queries with their results from my desktop development
system are:
Part I 243
Chapter 11: Testing UDFs for Correctness and Performance
-- Query #0: The minimum time for an operation that scans the whole table.
(Results)
SQL Server parse and compile time:
CPU time = 2 ms, elapsed time = 2 ms.Max Length
-----------
23
SQL Server Execution Times:
CPU time = 991 ms, elapsed time = 991 ms.
(Results)
SQL Server parse and compile time:
CPU time = 2 ms, elapsed time = 2 ms.
Right Most Position
-------------------
23
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.
(Results)
SQL Server parse and compile time:
CPU time = 3 ms, elapsed time = 3 ms.
Right Most Position
-------------------
23
SQL Server Execution Times:
CPU time = 2934 ms, elapsed time = 2954 ms.
Query #1, the UDF, didnt report its execution times correctly. It seems
that SET STATISTICS TIME is limited in the duration that it can measure. I
used the Windows Task Manager to watch what was happening on my sys-
tem, and the CPU time for Query #1 is very close to the elapsed time. For
our comparison, well have to use the elapsed time.
Table 11.1 has the comparison in run time of the queries. The Net col-
umn subtracts the time it took to run Query #0, the very simplest
expression on the same set of strings, from the other queries. I think that
the Net column has the numbers that should be compared because it iso-
lates just the effect of running the UDF or the replacement expression.
Table 11.1: Query timings for the experiment
Query Description Time (milliseconds) Net
#0 Simple expression 991 N/A
#1 UDF 278000 277009
#2 UDF replaced by expression 2954 1963
The difference in time is dramatic. The UDF takes about 140 times longer
to run as an equivalent expression. Wow! Four-plus minutes versus three
seconds is the kind of difference users really notice.
Of course, the difference isnt going to be perceivable when the query
is run on 1, 5, or even 100 rows. Only when the number of rows grows
into the thousands does the difference begin to be noticeable.
The lesson that I draw from this experiment is that UDFs must be
used with care in performance-sensitive situations. Theyre a great tool
for simplifying code and promoting code reuse, but they can have a dra-
matically negative effect on performance.
Summary
This chapter described issues about testing UDFs for correctness and
performance that didnt belong in the earlier chapters. I wanted to wait
until you had seen all the basic material before delving into these topics.
I know that few programmers really love to write test programs for
their production programs. In the case of UDFs, writing a program in the
form of a stored procedure appears to be the best way to test it and be able
to continue to test it over time. Like all other code writing, the amount of
testing applied depends on the economics of the situation. A partial solu-
tion that should be included with most scalar UDFs is the test inside the
comment block. Having a quick and easy-to-execute test available
increases the likelihood that at least some tests will be run after any
change to the function.
Part I 245
Chapter 11: Testing UDFs for Correctness and Performance
Converting
between Unit
Systems
The Introduction described how I originally encountered the lack of func-
tions in T-SQL back in 1996 while designing the Pavement Management
System (PMS) for the Mississippi Department of Transportation (MDOT).
The database used for the project was Sybase SQL Server System 11,
which also uses the Transact-SQL (T-SQL) dialect of SQL used by
Microsoft SQL Server. Of course, a solution was found that didnt require
UDFs, but I was never happy with the lack of functions in T-SQL. The
pain was particularly acute because the two other databases that I use
often, Oracle and Access, both have user-defined functions. My prayers
were answered when SQL Server 2000 arrived.
Converting distances and areas from the metric system to the system
of feet and miles that is commonly used in the United States is a problem
in the MDOT PMS that would have benefited from the availability of
UDFs. This chapter discusses UDFs that implement the conversion
between unit systems.
There are many ways that a conversion function can be constructed.
The variations all produce the same result, but they reflect choices about
what parameters to give to the function and when certain parameters,
such as the unit system of the result, must be specified. This chapter
shows three alternative methods for constructing the conversion UDF.
Each alternative represents a different balance between flexibility, com-
plexity, and performance. There are many other alternatives that you
might also construct based on the needs of your application.
Of particular importance in any system to convert between unit sys-
tems is maintaining numeric precision and showing the results to the
correct number of significant digits. This chapter reviews the use of SQL
Server built-in functions for rounding and data type conversion and shows
how theyre applied in the context of unit conversion.
247
248 Part I
Chapter 12: Converting between Unit Systems
interstate highways. I still see one lonely sign on I-95 between Boston and
-- A simple conversion
SELECT 1.27 * 1.609344 as [km]
GO
(Results)
Km
-------------
2.04386688
SQL Server returned eight digits to the right of the decimal. Multiplica-
tion by hand, as I was taught in third grade by Mrs. Eidelhoch, as
implemented by my desktop calculator, a TI-503SV, and as implemented
by Microsoft Excel, all return eight digits to the right of the decimal,
which shows meaningless precision. Therefore, precision must be handled
in ways that go beyond straightforward multiplication.
To manage numeric precision, SQL Server has the ROUND, CAST, and
CONVERT functions. ROUND changes a number to have the requested digits of
precision. This query shows how applying ROUND changes the previous cal-
culation. The expression requests two digits to the right of the decimal:
252 Part I
Chapter 12: Converting between Unit Systems
-- Demonstrate ROUND
SELECT ROUND(1.27 * 1.609344, 2) as Km
GO
(Results)
Km
-------------
2.04000000
(Results)
Km Cast to 2 digits
----------------------------------------
2.04
When the CAST function is applied, its argument is rounded using the same
algorithm used by ROUND.
Note:
CAST and CONVERT usually do the same job, but CAST is part of the
SQL-92 specification and CONVERT is not. If you want your SQL to be
portable between databases, use CAST when possible. The only differ-
ence between the two is that CAST doesnt accept the date conversion
parameter that CONVERT can use to format dates as strings.
(Result)
Round To Hundreds
-----------------
12300.00
-- Formula for the correct parameter to the ROUND function to preserve precision
SELECT 2 - ROUND(Log10 (1609.344), 0) as [Combined Rounding]
GO
(Results)
Combined Rounding
-----------------------------------------------------
-1.0
The answer, 1, says to round to one digit to the left of the decimal, the
tens place. In other words, if a measurement is known to an accuracy of
hundredths of miles, the measurement is known to tens of meters. Its not
a perfect answer. A hundredth of a mile is 52.8 feet and 10 meters is 32.8
feet, so were claiming somewhat increased precision. However, the alter-
native of claiming hundreds of meters of precision is further from the
truth. The TSQLUDFS database has the function udf_Unit_Rounding-
Precision that implements the algorithm. Listing 12.1 creates a similar
function, udf_Unit_Rounding4Factor, with just the scale computation. Its
used in the next section as an aid when writing conversion functions.
END
The issues about precision and scale are important. Without addressing
them, were liable to produce results that show more or less precision
than can be truly assigned to the data. This section has created tools to
handle these issues; the next section uses the tools to create unit conver-
sion UDFs in a variety of ways.
The function has been designed to execute quickly, but a few extra state-
ments have been left in for ease of editing. In the middle of the function at
line 32 is the line:
3. Press F5 to execute this code fragment, and youll get the adjustment
Figure 12.1: Getting the adjustment factor for a units conversion function
(Results)
When the conversion is requested, its done the same way as in udf_Unit_
mi2m. Notice that the clause COLLATE Latin1_General_CI_AI is used in case
the function is executed within a case-sensitive database and the caller
supplies a lowercase m.
END
The advantage to writing functions that include both the conversion and a
choice about whether a conversion is necessary is that simple SELECT
statements can be used to retrieve data from the database and supply the
users choice of unit system at run time. This batch illustrates how it
might have worked:
-- Using udf_km2Distance
DECLARE @UnitSystem char(1)
SET @UnitSystem = 'U' -- For US Standard system
(Results)
For the MDOT PMS, this would have been particularly useful because the
front end was built using Sybases PowerBuilder product. Its DataWindow
control makes good use of the SELECT statement, and it would have short-
ened the time needed to develop the application.
udf_Unit_Km2Distance is still pretty restrictive. It requires that the
input be in kilometers and assumes that the only choices for the result are
either kilometers or miles. That works well in a pavement management
system but not in other applications.
Anything-to-Anything Conversions
The conversion functions written so far translate from one particular unit
to another particular unit. This only works well when you know in
advance what type of conversion is required. The benefit of using them is
that the functions are short and efficient.
Sometimes you dont know what units need to be converted when the
code is written, so a function that can convert between any two units is
required. For example, suppose your database is in meters, but your users
might like to see the measurement in centimeters, meters, kilometers,
inches, feet, yards, or miles. A more flexible function is required in such a
situation.
Excels function for converting units, CONVERT, is an any-unit-to-any-
unit conversion function. Its part of Excels Analysis ToolPak add-in. Load
that add-in before using the function or trying the spreadsheet Unit Con-
versions.xls provided in this chapters download.
The function udf_Unit_CONVERT_Distance is similar to Excels CONVERT
but works only with units that measure distance. Its shown in Listing
12.4. One potential way to code this function is to use a large CASE state-
ment that has every possible combination of conversion. I considered it
but decided on a different approach.
Instead, a base unit for each system of units is chosen. For the metric sys-
tem, the meter is the base. For the U.S. Standard system, the foot is the
base. Then two conversion factors are looked up in CASE statements.
@fCvtToBase is the conversion factor from the measurement-to-base in the
unit system of the measurement. Next, @fCvtToTarget is looked up. Its the
conversion factor from the base-to-target units. The conversion factors for
base-to-target conversion are the reciprocals of the measurement-to-base
factors.
That works fine if the measurement and the target are from the same
unit system. However, if theyre from different unit systems, a third con-
version factor is used, @fCvtSystems. It converts between the base units of
the two systems.
udf_Unit_CONVERT_Distance returns a numeric (18,9) type. The type
was chosen so that rounding shows the amount of precision. However,
double precision floating-point computations (data type float) are used
internally. Returning numeric (18,9) limits the range of values that can be
used to 109. That range represents the most common real-world conver-
sions. While there might be a reason to convert miles to nanometers using
a factor of 1.609344e+12, that sort of high-magnitude conversion is the
exception. It is not handled by this function in favor of avoiding some
issues of numerical rounding. Of course, your application may need a
larger range of possible values in its result, and you might want to return
float or numeric (38,9) instead.
udf_Unit_CONVERT_Distance is the last of the three methods for con-
verting units. Each fits a slightly different situation, and I might choose
between them based on the application design. However, its worthwhile
to check their performance. How much might they slow the application?
Is it enough to make you want to switch conversion methods?
that define the section. In addition, there are columns for a variety of
All three methods produce the same results with slightly different calling
sequences. Heres a query that converts the begin_km markers to miles.
The conversion is performed first using an expression that doesnt require
a UDF and then using each of the three types of conversion functions:
264 Part I
Chapter 12: Converting between Unit Systems
All of the results are very close. The only differences are due to the
rounding performed by each of the functions.
To compare the time that it takes to execute each of the functions,
lets set up a simple experiment. The script that follows uses a technique
for measuring performance similar to the one used in Chapter 11. The
table in question is first pinned into memory. Then each of the functions
gets their chance to convert two of the columns in the test table. To pre-
vent the time needed to display thousands of rows of data from becoming a
factor in the experiment, the results are summed instead of displayed.
Heres the script:
-- Create variables to hold the start time and duration in ms for each query
DECLARE @Start_Expr datetime , @Start_Km2mi datetime
, @Start_Km2Dist datetime , @Start_CONVERT datetime
, @ms_Expr int, @ms_Km2mi int, @ms_Km2Dist int, @ms_CONVERT int
PRINT 'SUM of Begin_km column. Used to force all pages into memory'
SELECT SUM(begin_km) as [Sum in km]
FROM pms_analysis_Section
Its obvious that using a UDF has a substantial cost. Using the simplest
UDF takes almost six times as long as using the equivalent expression.
The difference between udf_Unit_Km2mi and udf_Unit_Km2Distance is
noticeable but pretty small. Theres a big jump when using udf_Unit_
CONVERT_Distance. That must be accounted for by the complexity of this
longer UDF.
There are other ways to write the functions, but theres no point to
endless variations. I suggest that after reading this chapter, you pick a way
that works well in your application. It might be one of the alternatives
shown here, but it may just as well be some other variation. It should be
efficient, easy to code, and easy to maintain. With UDFs theres always a
trade-off.
In the course of the conversion weve paid a lot of attention to
numeric precision and issues caused by rounding. One of the most impor-
tant of these issues occurs when numbers are compared. This is
addressed in the next section.
What Is Equal?
Listing 12.6 has a function, udf_Unit_lb2kg that illustrates another issue
about working with floating-point data in UDFs. If you execute the exam-
ple you see what can happen when SQL Server stores floating-point data:
(Results)
Weight lb to kg
-----------------------------------------------------
84.099999999999994
Part I 267
Chapter 12: Converting between Unit Systems
(Results)
Test 1 185.4 lb to kg Worked
-- Floating-point approximation
SELECT CAST (84.10 AS FLOAT) as [Show floating-point approximation]
GO
(Results)
Show floating-point approximation
-----------------------------------------------------
84.099999999999994
AS BEGIN
DECLARE @nRound2Digits int -- Digits to round
, @fConversionFactor float -- factor used for conversion
SET @fConversionFactor = 0.45359237
-- SELECT dbo.udf_UnitRounding4Factor (@fConversionFactor) As Adjustment
SET @nRound2Digits = @nDigits2RightOfDecimal + 0 -- < put adjustment here
RETURN ROUND(@fInput * @fConversionFactor, @nRound2Digits)
END
The issue of when two numbers are equal is very important. Making good
choices about how to compare numbers can make a big difference in the
number of bugs reports made about your application. This is especially
important when your application uses floating-point numbers or when
UDFs convert numbers to floating point.
(Results)
The result is equal because SQL Server uses numeric data types to per-
form the addition and comparison. Now CAST the numbers to float, and we
get a different answer:
RETURN @bEqual
END
-- Exercise udf_Unit_EqualFpBIT
SELECT dbo.udf_Unit_EqualFpBIT (1.1234567890, 1.1230000000, 1) as [To 1 Digit]
, dbo.udf_Unit_EqualFpBIT (1.1234567890, 1.1230000000, 2) as [To 2 Digits]
, dbo.udf_Unit_EqualFpBIT (1.1234567890, 1.1230000000, 3) as [To 3 Digits]
, dbo.udf_Unit_EqualFpBIT (1.1234567890, 1.1230000000, 4) as [To 4 Digits]
, dbo.udf_Unit_EqualFpBIT (1.1234567890, 1.1230000000, 5) as [To 5 Digits]
GO
To 1 Digit To 2 Digits To 3 Digits To 4 Digits To 5 Digits
---------- ----------- ----------- ----------- -----------
1 1 1 0 0
Everything looks okay there. What if the numbers are really close?
(Results)
Your Bill His Bill udf_Unit_EqualFpBit Says
-------------------- -------------------- ------------------------
1.13 1.12 1
Part I 271
Chapter 12: Converting between Unit Systems
So udf_Unit_EqualFpBIT says that the numbers are equal, but your bill is
one cent higher than his bill. Why are you charged more than him? The
answer is that even though the numbers are very close, they round to
different pennies. When compared as floating-point numbers by udf_Unit_
EqualFpBIT, theyre equal.
To a programmer, there are two groups of people to whom the inter-
pretation of number equality matters: the Software Quality Assurance
(SQA) team and the users of the application. Do they think a bill of $1.12
equals a bill of $1.13? Experience shows that they dont. When rounding
results in one-cent differences, both SQA and users report the discrep-
ancy as a bug. Both groups can be educated and might accept a reasonable
explanation when appropriate. But the importance of the one-cent differ-
ence is going to depend on the application in which it appears and on the
people involved. If hundreds, thousands, or millions of occurrences multi-
ply the one-cent difference, sooner or later its going to add up to enough
money to matter to someone. For some users, the one-cent difference is
always going to matter, regardless of the context and even if it never costs
anyone a single cent.
To avoid issues caused by rounding, its better to compare numbers in
exactly the same way that they are shown to the user. Doing this results
in fewer bug reports and arguably a better system. The function udf_Unit_
EqualNumBIT in Listing 12.8 checks numbers for equality by converting to a
numeric data type. Since this method of comparison uses numbers in the
way they are presented to the user, the functions answers are more
acceptable in most applications than other methods of comparison.
if @nDigits2RtOfDecimal > 0
SELECT @bEqual =
CASE @nDigits2RtOfDecimal
WHEN 1 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 1))
= CAST(@fArg2 AS NUMERIC (38, 1)) THEN 1 ELSE 0 END
WHEN 2 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 2))
= CAST(@fArg2 AS NUMERIC (38, 2)) THEN 1 ELSE 0 END
WHEN 3 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 3))
= CAST(@fArg2 AS NUMERIC (38, 3)) THEN 1 ELSE 0 END
WHEN 4 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 4))
= CAST(@fArg2 AS NUMERIC (38, 4)) THEN 1 ELSE 0 END
WHEN 5 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 5))
= CAST(@fArg2 AS NUMERIC (38, 5)) THEN 1 ELSE 0 END
WHEN 6 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 6))
= CAST(@fArg2 AS NUMERIC (38, 6)) THEN 1 ELSE 0 END
WHEN 7 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 7))
= CAST(@fArg2 AS NUMERIC (38, 7)) THEN 1 ELSE 0 END
WHEN 8 THEN CASE WHEN CAST(@fArg1 AS NUMERIC (38, 8))
= CAST(@fArg2 AS NUMERIC (38, 8)) THEN 1 ELSE 0 END
ELSE -- Only supports up to 9 digits of precision
CASE WHEN CAST(@fArg1 AS NUMERIC (38, 9))
= CAST(@fArg2 AS NUMERIC (38, 9)) THEN 1 ELSE 0 END
END
ELSE
-- Negative numbers of digits implies to the left of the decimal
-- ROUND takes a parameter for the length so we don't need a CASE.
-- After ROUNDing the numbers are CAST to INT to insure INT comparison.
SELECT @bEqual =
CASE WHEN CAST(ROUND (@fArg1, @nDigits2RtOfDecimal) AS INT)
= CAST(ROUND (@fArg2, @nDigits2RtOfDecimal) AS INT)
THEN 1 ELSE 0 END
-- ENDIF
RETURN @bEqual
END
(Results)
udf_Unit_EqualNumBIT tells us that the numbers are not equal, while the
(Results)
As you can see, when the comparison is done to one digit of precision,
udf_Unit_EqualNumBIT says theyre equal. When compared to two digits,
theyre not equal, but when compared to three and four digits theyre
equal again. Thats called a discontinuity in the result of the function.
Mathematicians dont like discontinuous functions. In this case, living with
the discontinuity is a choice that you might make to satisfy application
requirements.
In my book (oh, this is my book!), the choice of numeric comparison
methods depends on the application. For the pavement application, float-
ing point works well. Thats partially because the users of the application
are engineers who have an appreciation of measurement inaccuracies and
rounding errors. For other applications, I lean toward whichever method
the users consider correct, usually the CAST to numeric method as imple-
mented by udf_Unit_EqualNumBIT.
274 Part I
Chapter 12: Converting between Unit Systems
(Results)
sum
==================================
36.046700000000001
sum
========================================
36.03
cent as $36.05. When the data is rounded before being summed, the
answer is $36.03. Which one is correct? It depends on the context in
which the numbers are used. Rounding before summing makes the
numbers in the column add up to the total, and youll get fewer bug
reports. If there is an accounting system involved, the same rounding
system should be used when reporting, as when the charges are allo-
cated in the accounting system.
If you decide to do the sum before rounding, as in the first column,
and if you want to avoid the appearance of a discrepancy between the
data and the sum, it is necessary to show more than just two digits of
precision in the data points. It may be necessary to show four or five
digits (that is, change the report so it shows more of each value so the
reader will understand how the sum was derived).
Summary
This chapter has created T-SQL UDFs to solve the problem of converting
between unit systems. As it turned out, most of the chapter focused on
ways to maintain the proper amount of numeric precision as functions are
created.
Centralization of the conversion process in the database has some dis-
tinct advantages, of which the most important is consistency. If the
conversion process is moved to a higher level, such as the client applica-
tion, every type of client code becomes responsible for performing the
correct conversion. Particularly when working with report writers, this
can lead to inconsistency and error.
Key points to remember from this chapter are:
n Managing numeric precision is the responsibility of the database
designer and should be given careful consideration based on the
requirements of the application.
n Choice of data type and careful use of the CONVERT, CAST, and ROUND
functions are essential to accurate results and maintaining the correct
amount of precision.
n Care in comparing and aggregating of numeric data can cut down on
bug reports.
The chapter contains several functions that perform unit conversion in a
variety of ways. The sample database TSQLUDFS contains several more
unit conversion functions.
The next chapter addresses a similar problem, currency conversion.
The key difference is that conversion rates change frequentlyso fre-
quently that the conversion rates must be stored in a table.
This page intentionally left blank.
13
Currency
Conversion
Currency conversion has many similarities with unit conversion, but there
are some important differences. Its different on these counts:
n The precision of amounts and rates is well known and presents less of
an issue than the precision of measurements of length, volume, or
time.
n Its the conversion factor, or exchange rate, that creates the complex-
ity when changing money. The exchange rate changes over time,
almost continuously.
n There are different rates depending on the relationship of the parties
making the exchange.
Like the unit conversion functions, a currency conversion function should
return a scalar value. However, instead of being based on a fixed conver-
sion rate, the rate changes frequently and must be looked up in a table.
Well design a table to hold the rates and a few support tables to go along
with it.
Since the rate is stored in a table, the possibility that the rate is miss-
ing could come up. The example UDFs show two alternative approaches
for handling that situation.
Before getting into the design issues, its best to start with some
background material. I find that knowing a little bit about the history of
any subject makes it easier to understand design choices, some of which
may have historical origins.
277
278 Part I
Chapter 13: Currency Conversion
(Results)
well known. Theyre usually traded by the ounce, but the required preci-
RETURN @mResult
END
284 Part I
Chapter 13: Currency Conversion
(Results)
IF @LaterExchangeRate Is NULL
AND @EarlierExchangeRate is Not Null
AND @EarlierDaysDiff <= 30
RETURN @EarlierExchangeRate * @mAmount
Lets rerun the query on a dollar to euro exchange, adding a column that
(Results)
As you can see, from December 30 to 31, when the @AsOfDate is earlier
than the available rates, the nearest good rate is used. When a rate is
missing but there are rates on both sides of @AsOfDate (for instance, on
January 12), linear interpolation is performed to produce the most usable
rate.
Actually, you cant really see what happened by looking at the result.
When a conversion is made, you dont know if the rate was available in the
table or interpolated. Sometimes thats okay, and sometimes the user
really has to know how the rate was derived.
A scalar UDF cant return both the rate and a code to say how the rate
was derived. To make up for that, Ive written a companion function,
udf_Currency_DateStatus, that returns a status code indicating how the
result of udf_Currency_XlateNearDate is determined. The logic is very sim-
ilar to udf_Currency_XlateNearDate, and both functions must be maintained
in parallel. The UDF is in Listing 13.3. The most common use for it is to
288 Part I
Chapter 13: Currency Conversion
IF @LaterExchangeRate is NULL
AND @EarlierExchangeRate is not Null
AND @EarlierDaysDiff <= 30
RETURN -3 -- Use the rate from the last date in the table
Summary
This chapter has put T-SQL functions to work to solve the problem of
currency conversion. Along the way, weve run into new issues about
numeric precision and error handling. The key points to remember from
this chapter are:
n Managing numeric precision of currency conversion is more straight-
forward than with units of measure but still important.
n Because functions cant raise errors, designing a clever solution to
handling missing data and numeric errors is essential to good function
writing. The use of NULL works well in many situations. Returning a
special value or interpolation was also discussed.
Currency conversion requires that the ever-changing conversion rate be
stored in a table. This chapter demonstrates a group of tables to store
currency conversion information and several functions that perform the
currency conversion. They give you a place to start when you have to
code a conversion based on a table in your application. Most importantly,
they illustrate another type of problem that UDFs can solve.
Showing the two versions of the currency conversion function,
udf_Currency_XlateOnDate and udf_Currency_XlateNearDate, illustrates
another important dividing line. I can conceive of ways to get by without
udf_Currency_XlateOnDate. Any query that uses it could be modified to use
an outer join that coupled the proper rate with the amount instead. I cant
conceive of similar ways to get by without udf_Currency_XlateNearDate.
Even if the logic it contains could be rewritten in a declarative syntax, the
result would be so complex as to be unmaintainable. Writing new queries
that also handled the same logic would be time consuming and very error
prone. This isnt where I want my development time to go.
This chapter concludes Part I of the book, which was about the cre-
ation and use of UDFs. There is a group of UDFs that come with SQL
Server that expose real-time information from inside the SQL Server
database engine and can be very useful at times. Youll see these in Part II.
This page intentionally left blank.
Part II
System
User-Defined
Functions
293
This page intentionally left blank.
14
Introduction to
System UDFs
The addition of UDFs to SQL Server 2000 created the opportunity for the
SQL Server development team to use them to implement features of SQL
Server itself. Theyve taken advantage of that opportunity in a couple of
ways. This chapter and the next four are devoted to system UDFs.
System UDFs arent just normal UDFs that happened to be shipped
with SQL Server. Theyre a distinct entity that can run in any database and
reference the tables in that database instead of the tables in the database
in which they are defined. They can also use T-SQL syntax thats reserved
for them and for system stored procedures.
There are three groups of system UDFs to discuss:
n The ten system UDFs that are supplied with SQL Server and docu-
mented in Books Online. Well start the discussion of them in this
chapter. The next three chapters cover the most useful documented
system UDFs in depth.
n The numerous undocumented system UDFs. These are supplied as
source code and compiled into SQL Server during installation. Chap-
ter 18 documents several of the more interesting of these and shows
how to find them and view their source code.
n Your own system UDFs. Chapter 19 describes how to create your
own system UDFs as well as the pros and cons of doing so.
System UDFs are different from both normal UDFs, the ones we create in
a user database, and the functions that are built into the SQL Server
engine, such as DATEPART. The first task of this chapter is to define what
sets system UDFs apart from other functions.
295
296 Part II
Chapter 14: Introduction to System UDFs
Normal UDFs can have any name that follows the T-SQL rules for
identifiers including upper and lowercase characters, digits, and the spe-
cial characters underscore, ampersand (@), and pound sign (#). System
UDFs must follow narrower rules, which are detailed in the next
subsection.
System UDFs
the adage goes, What are the three most important factors in being a
system UDF?
name description
------------------------- -----------------------------------------------------
Albanian_BIN Albanian, binary sort
Albanian_CI_AI Albanian, case-insensitive, accent-insensitive, kanat
Albanian_CI_AI_WS Albanian, case-insensitive, accent-insensitive, kanat
...
The double colon is required to use the system UDFs that return tables.
Using the database.owner.functionname syntax doesnt work, as seen in
this attempt:
(Results)
Youll see the double colon used for all the documented system UDFs that
are discussed in this chapter.
There is no documented scalar system UDF. However, there are a few
undocumented scalar system UDFs, and its also possible to create your
own. Scalar system UDFs can be referenced without the database or
owner name qualification. There are also a few scalar non-system UDFs
defined in master. These are referenced with the three-part name
master.dbo.functionname since dbo is their owner.
Due to the special status of system UDFs, they can invoke special
syntax that you and I cant use in our T-SQL. Whats more, the text of the
system UDFs is hidden from view behind a little smoke and a flimsy cur-
tain (that is, until Toto pulls the curtain aside).
Part II 299
Chapter 14: Introduction to System UDFs
(Results)
Server: Msg 15009, Level 16, State 1, Procedure sp_helptext, Line 53
The object 'fn_helpcollations' does not exist in database 'master'.
Server: Msg 15009, Level 16, State 1, Procedure sp_helptext, Line 53
The object 'system_function_schema.fn_helpcollations' does not exist in database
'master'.
The way to see the text of the documented system UDFs is to query
master..syscomments. Its a table that stores the text of all stored proce-
dures and UDFs.
Be sure you set the Maximum characters per column field on the
System UDFs
Results tab of the Tools Options menu command statement to 8192 so
long output from Query Analyzer isnt truncated. Heres a query that
retrieves the text of fn_helpcollations:
USE master
GO
-- Retrieve the text of fn_helpcollations
SELECT text
FROM syscomments
WHERE text like '%system_function_schema.fn_helpcollations%'
GO
(Results)
text
------------------------------------------------------------------------------
create function system_function_schema.fn_helpcollations
(
)
returns @tab table(name sysname NOT NULL,
description nvarchar(1000) NOT NULL)
as
begin
insert @tab
select * from OpenRowset(collations)
return
end -- fn_helpcollations
300 Part II
Chapter 14: Introduction to System UDFs
USE TSQLFUDFS
GO
(Results)
This special syntax is reserved for system UDFs and system stored pro-
cedures. The rowset collations is created inside the SQL Server engine.
Other system UDFs use similar undocumented syntax.
SQL Server knows to allow the reserved syntax based on the require-
ments for system UDFs that weve been discussing in this chapter: the
naming of the function with fn_ followed by lowercase characters and the
location of the function in master.system_function_schema.
Weve seen one system UDF, fn_helpcollations. There are nine more
documented system UDFs, which are introduced in the next section.
System UDFs
fn_listextendedproperty retrieves extended properties that have been
associated with various objects in a database. Extended properties are
used by Enterprise Manager to store information such as the description
of tables and columns. You can also use extended properties to document
your database or store other information that you want to associate with
database objects such as tables, views, and stored procedures. Well build
several functions that aid in documenting a database in Chapter 15, which
is devoted to fn_listextendedproperty.
fn_virtualfilestats returns raw information about file input/output
operations throughout the SQL Server instance. When troubleshooting
performance problems, it can be an important diagnostic tool. The raw
data can be sliced and diced a few different ways, and well create func-
tions to summarize it by disk drive, by database, or for the instance as a
whole. fn_virtualfilestats is covered in Chapter 16.
SQL Server traces are the foundation behind the SQL Profiler. Four
functions whose names begin with the characters fn_trace return tables of
information about the traces that are running on the SQL Server instance.
Ill refer to them as the fn_trace_* group. They can be used either with
traces that are started by the SQL Profiler or with traces that are created
by stored procedures. In Chapter 17 well use the fn_trace_* group to
build UDFs that describe running traces in terms that we humans can
understand. Then well pull them together to produce a function,
udf_Trc_RPT, that shows all the running traces and what theyre tracing.
302 Part II
Chapter 14: Introduction to System UDFs
fn_helpcollations
This function returns a list of the collations supported by SQL Server. The
syntax of the call is:
::fn_helpcollations()
It doesnt have any arguments. The resultset returned has the two col-
umns described in Table 14.2.
Table 14.2: Columns returned by fn_helpcollations
Column Name Data Type Description
name sysname The collation name. Names are coded with suf-
fixes such as _BIN for binary or _AS for accent
sensitive. This makes it possible to search for a
particular type of collation using the LIKE
operator.
description nvarchar(1000) A textual description of the collation. This column
is useful when searching for a particular type of
collation. The characteristics of the collation are
spelled out for easier searching.
Part II 303
Chapter 14: Introduction to System UDFs
name description
-------------------------------- -----------------------------------------------
Albanian_BIN Albanian, binary sort
Albanian_CI_AI Albanian, case-insensitive, accent-insensitive,
...
SQL_Latin1_General_CP1253_CI_AI Latin1-General, case-insensitive, accent-insens
SQL_Latin1_General_CP1253_CI_AS Latin1-General, case-insensitive, accent-sensit
SQL_Latin1_General_CP1253_CS_AS Latin1-General, case-sensitive, accent-sensitiv
...
SQL_Latin1_General_CP850_BIN Latin1-General, binary sort for Unicode Data, S
SQL_Latin1_General_CP850_CI_AI Latin1-General, case-insensitive, accent-insens
SQL_Latvian_CP1257_CI_AS Latvian, case-insensitive, accent-sensitive, ka
...
SQL_Ukrainian_CP1251_CS_AS Ukrainian, case-sensitive, accent-sensitive, ka
As of SQL Server 2000 Service Pack 2 there are 753 of them.
You dont have to see all the collations at once. If youre searching for a
binary collation, you can ask for just the collations that have _BIN in their
name with the following query:
System UDFs
-- All the binary collations
SELECT *
FROM ::fn_helpcollations()
WHERE [name] like '%_BIN%'
ORDER BY [name]
GO
name description
-------------------------------- -----------------------------------------------
Albanian_BIN Albanian, binary sort
Arabic_BIN Arabic, binary sort
Chinese_PRC_BIN Chinese-PRC, binary sort
...
Slovenian_BIN Slovenian, binary sort
SQL_Latin1_General_CP437_BIN Latin1-General, binary sort for Unicode Data, S
SQL_Latin1_General_CP850_BIN Latin1-General, binary sort for Unicode Data, S
Thai_BIN Thai, binary sort
...
Vietnamese_BIN Vietnamese, binary sort
Most of the time youll only use your databases default collation. But if
youre working with multiple languages, where text might or might not
have Unicode characters or accent marks, collations can be important.
304 Part II
Chapter 14: Introduction to System UDFs
fn_virtualservernodes
This function is used for fallover clustering. It returns a table with a list of
nodes on which the virtual server can run. The syntax of the call is:
::fn_virtualservernodes()
The function takes no arguments, and there is only one column in the
result set, NodeName. When youre not running on a clustered server,
fn_virtualservernodes returns an empty table. I dont have a cluster so
the results to this sample query are made up:
(Results - simulated)
NodeName
--------
Moe
Larry
Curly
fn_servershareddrives
This function returns a table with a row for each shared drive used by the
clustered server. The syntax of the call is:
::fn_servershareddrives()
This function has no arguments, and theres only one column in the result
table, DriveName, which is an nchar(1) column. If the current server is not
a clustered server, fn_servershareddrives returns an empty table. Im not
running a cluster, so the results shown in this query are made up. But if
youre running in a cluster, give it a try:
(Results - simulated)
DriveName
---------
p
q
The next UDF is a bit more interesting. It lets us take a look into the SQL
statements that are being executed by any user of the system. Its particu-
larly useful when deadlocks have occurred.
Part II 305
Chapter 14: Introduction to System UDFs
fn_get_sql
SQL Server 2000 Service Pack 3 (SP3) includes a new system user-
defined function, fn_get_sql. It was actually in an earlier hotfix, but SP3 is
the best way to get it. (See Microsoft Knowledge Base article 325607 for
details.) Throughout this section, Im going to assume that youve
installed SP3, including the updated documentation.
Based on a conversation that I had with a gentleman representing a
vendor of SQL performance tools, I suspect that fn_get_sql was added pri-
marily to make it possible for such vendors to produce more robust tools.
But the motivation for creating the function doesnt matter. Its available
to us all.
fn_get_sql retrieves the text of the SQL being executed by active
SQL processes. This is a technique commonly used when diagnosing a
deadlock or other blocking problem. Diagnostic tools that monitor activity
inside the database engine can also use it.
Prior to the availability of fn_get_sql, the only way to see the SQL
being used by a SQL process was by executing the DBCC INPUTBUFFER com-
mand. Lets take a look at that first.
System UDFs
DBCC INPUTBUFFER takes a SPID as its argument and shows the first 255
characters of the statement that the connection is executing. SPIDs are
integers that uniquely identify a database connection. A connection can
retrieve its own SPID by using the @@SPID built-in function.
When invoked, DBCC INPUTBUFFER returns a rowset consisting of the
columns listed in Table 14.3. Notice that the data type of the EventInfo
column is nvarchar(255). The size of the column has proven to be an
annoying limitation because it restricts the results to the first 255 charac-
ters of any SQL statement. While that might be enough when the
statement is executing a stored procedure, its often insufficient when a
complex SELECT or UPDATE is involved.
Table 14.3: Columns returned by DBCC INPUTBUFFER
Column Name Data Type Description
EventType nvarchar(30) Language, EventNo, or EventRPC
Parameters int 0=text 1n=parameters
EventInfo nvarchar(255) For an RPC (stored procedure), it contains the pro-
cedure name. For a language event, it contains the
text of the SQL being executed.
This query gives you an idea of how DBCC INPUTBUFFER works by showing
you its own text:
306 Part II
Chapter 14: Introduction to System UDFs
(Results)
Calling fn_get_sql
The syntax of the call to fn_get_sql is:
::fn_get_sql(@HandleVariable)
(Results)
DBCC execution completed. If DBCC printed error messages, contact your system
administrator.
Trace flags are turned off with the DBCC TRACEOFF command. The Listing 0
file has a script that uses DBCC TRACEOFF after the other scripts that use
fn_get_sql are done.
Query Analyzer can truncate the output of any column to a specific
size. Be sure you set the Maximum characters per column field on the
Results tab of the Tools Options menu command to 8192 so the output
isnt truncated. Then, with trace flag 2861 on, the following script shows
System UDFs
itself:
SELECT [text]
FROM ::fn_get_sql(@handle)
GO
(Results)
text
---------------------------------------------------------------
-- Retrieve the sql of this connection
DECLARE @handle binary(20)
SELECT [text]
FROM ::fn_get_sql(@handle)
308 Part II
Chapter 14: Introduction to System UDFs
Dont get confused by the fact that the output is identical to the query. Its
supposed to be the same. It even includes the comment line that starts
the batch.
Handles expire very quickly and must be used immediately. If you
pass in a handle that is no longer in the cache, fn_get_sql returns an
empty resultset. Remember, on a highly active, memory-constrained sys-
tem, statements might be aged out of the cache almost instantly.
One of the most common situations for using DBCC INPUTBUFFER, and
now fn_get_sql, involve situations where a process cant run because of
resources locked by another process. The most severe of these situations
is a deadlock.
(Results)
Next, run Script A Batch A-2. This batch begins a transaction and deletes
a row in the Authors table. Ive deliberately chosen an author who hasnt
written any books, so there are no referential integrity issues. Dont
Part II 309
Chapter 14: Introduction to System UDFs
worry about losing the row, well roll back the transaction in Batch A-6.
Heres Batch A-2:
-- Batch A-2
PRINT 'Batch A-2 Begin a transaction and create the blockage'
BEGIN TRAN -- the transaction will cause an exclusive lock
DELETE FROM authors WHERE au_id = '527-72-3246'
GO
-- Stop Batch A-2 here
(Results)
(1 row(s) affected)
Batch A-2 leaves open a transaction, which isnt closed until Batch A-6. In
Script A-5, well see that the open transaction causes the SPID to hold
several locks, including an exclusive lock on the row being deleted.
The next step is to open a new Query Analyzer connection using the
File Connect menu command and load file Script B.sql. The first batch
in script B is B-3, which prints the SPID of the connection for Script B.
Well use that SPID in Batch A-5. Heres Batch B-3 with the results of
running it on my system:
System UDFs
-- Batch B-3 Moves to the pubs sample database
-- And prints the SPID
PRINT 'Batch B-3 Printing the SPID and Using pubs'
PRINT 'Script B -- Has SPID ' + CAST(@@SPID as varchar)
USE pubs
GO
(Results)
You will probably get a different number for the SPID. Once again, take
note of the SPID because its needed later in Batch A-6.
Batch B-4 selects from the Authors table. Heres the batch:
-- Batch B-4
PRINT 'Batch B-4 SELECT a blocked resource.'
SELECT * from authors
GO
There are no results because the batch cant run due to the open transac-
tion left by Batch A-2. Figure 14.1 shows what my Query Analyzer
window looks like after I execute B-4.
310 Part II
Chapter 14: Introduction to System UDFs
Ive circled the red execution flag to highlight the fact that the batch is
running. If you look down in the information bar near the bottom of the
figure, youll see that it had been running for one minute and 11 seconds
by the time I took the screen shot.
Leave Batch B-4 running and switch back to the connection with
Script A. Batch A-5 uses the sp_lock system stored procedure to show the
locks being held by the system. The exclusive locks (Mode = X) held by
Script A and the shared lock (Mode = S) are shaded in the result.
(Results)
SPID 55, which is running Batch B-4, is waiting for a shared lock on Key
0801c4f7a625. But SPID 53 was granted an exclusive lock on that key. Had
we set the transaction isolation level in Batch B-4 to READ UNCOMMITTED,
Part II 311
Chapter 14: Introduction to System UDFs
Batch B-4 wouldnt have requested the shared lock and would not have to
wait.
Finally, its time to use fn_get_sql to examine the SQL that Batch B-4
is running. This is done with Batch A-6. Before you can run A-6, you must
change the line WHERE spid=55 to replace the 55 with the SPID that was
printed by Batch B-3. Heres Batch A-6 with its results on my system:
-- Batch A-6 You must change the SPID number in this batch
-- before executing this step!
PRINT 'Batch A-6 -- Get the text of the blocked connection'
DECLARE @Handle binary(20)
SELECT @handle=sql_handle
FROM master..sysprocesses
WHERE spid= 55 -- <<<<<< Change 55 to the SPID of Script B
(Results)
System UDFs
PRINT 'Batch B-4 SELECT a blocked resource.'
select * from authors
(1 row(s) affected)
The text column has carriage returns in it that show up in the output. To
make it easier to see the results, Ive shaded the output of the text col-
umn. Since there were three lines in the batch, it wraps onto a second and
third line of output.
The last line of A-6 is a ROLLBACK TRAN statement. This undoes the
effect of the DELETE done earlier. It also has the effect of releasing the
exclusive locks that are held by Script As connection. If you flip back to
Script B, youll see that it has run and sent its output to the results
window.
fn_get_sql is a new function to aid the DBA and programmer in the
diagnosis of blocking problems. Its going to be used by diagnostic and
performance-monitoring tools to monitor the SQL by continually sampling
the SQL of all processes to discover the statements that are executed
most often. Im aware of at least one tool on the market thats using it in
this way. But you dont need an expensive tool to put fn_get_sql to good
use. A simple script, like the one in Batch A-6 that gets a sql_handle and
uses it, is all you need.
312 Part II
Chapter 14: Introduction to System UDFs
Summary
This chapter introduced system UDFs. While theyre written in T-SQL,
they are different from other UDFs that weve seen previously in this
book. Theyre distinguished by:
n Their name, which must begin with fn_ and contain only lowercase
characters, digits, and underscores.
n Their location, which must be in the master database, under the own-
ership of a special-purpose owner, system_function_schema.
n Their use of reserved T-SQL syntax.
You may or may not ever use the system UDFs shown in this chapter, at
least not directly. However, theyre worth knowing.
The next three chapters are devoted to putting system UDFs to work
in useful ways:
n Chapter 15 discusses fn_listextendedproperty, which retrieves
extended properties, a new feature in SQL Server 2000.
n Chapter 16 discusses fn_virtualfilestats, which returns a table of
input/output statistics about database files. Itll be put to use to create
performance diagnostic UDFs.
n Chapter 17 discusses the fn_trace_* functions and how to use them
on SQL Server traces, both those created by SQL Profiler and those
that are created with stored procedures.
After weve discussed the documented system UDFs, the last two chap-
ters in this part of the book go further:
n Chapter 18 discusses the undocumented system UDFs and when you
might choose to use them.
n Chapter 19 shows you how to make your own system UDFs and why
you might create them.
Enterprise Manager uses extended properties to store descriptions for
tables and columns, but there are actually several ways to add extended
properties to your database. The next chapter shows you how theyre
associated with database objects and retrieved with
fn_listextendedproperty.
15
Documenting DB
Objects with
fn_listextendedproperty
Extended properties are user-defined or application-defined information
about a database object. Theyre new in SQL Server 2000. This chapter
describes and then builds on the fn_listextendedproperty system UDF to
create functions useful for managing extended properties.
Enterprise Manager uses extended properties to store its description
fields in the MS_Description extended property for tables, views, and col-
umns. Extended properties can also be added using stored procedures or
through an interface provided by Query Analyzer.
Once theyre entered, fn_listextendedproperty is used to retrieve
them. Its seven parameters tell fn_listextendedproperty which extended
properties to return and for which set of database objects to return them.
While there are many possibilities, well narrow the choices down and
create these task-oriented UDFs:
n udf_Tbl_DescriptionsTAB Returns a table of the descriptions for all
user tables in a database
n udf_Tbl_ColDescriptionsTAB Returns a table of the descriptions for
all columns in all user tables
n udf_Tbl_MissingDescrTAB Returns a table listing any table that does
not have a description. Its used to locate tables that need more atten-
tion to their documentation.
n udf_Tbl_RptW Returns a table with information about a table; some
of it comes from the extended properties
I call them task-oriented because they accomplish a task that I consider
worthwhile. Additional helper functions are created along the way.
313
314 Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
Before the functions are developed, the next section has some infor-
mation to expand your knowledge of extended properties and how to
create them. Thats followed by a description of the ins and outs of invok-
ing fn_listextendedproperty.
Note:
As with other chapters, the short queries that appear without listing
numbers are in the file Chapter 15 Listing 0 Short Queries.sql in the
chapters download directory.
(Results)
(Results)
System UDFs
value
----------------
Cpny
Now lets delete the extended property so it doesnt get retrieved in any
of the other queries:
(Results)
to add, update, and delete extended properties. Figure 15.1 shows the
Query Analyzer editing the extended properties for the TSQLUDFS
database.
Query Analyzer lets you enter any name for the extended property,
whereas Enterprise Manager uses only one name: MS_Description.
Using fn_listextendedproperty
I find the description of fn_listextendedpropertys arguments in Books
Online to be confusing, so Ill try to be clearer. The function definition is:
System UDFs
fn_listextendedproperty (
{ default | [ @name = ] 'property_name' | NULL }
, { default | [ @level0type = ] 'level0_object_type' | NULL }
, { default | [ @level0name = ] 'level0_object_name' | NULL }
, { default | [ @level1type = ] 'level1_object_type' | NULL }
, { default | [ @level1name = ] 'level1_object_name' | NULL }
, { default | [ @level2type = ] 'level2_object_type' | NULL }
, { default | [ @level2name = ] 'level2_object_name' | NULL }
)
Lets try an example. This query retrieves the MS_Description for the
Cust.CustomerID column using fn_listextendedproperty:
(Results - reformatted)
The valid object types depend on the level. In the query above, the
@level0type is USER, the @level1type is TABLE, and the @level2type is
COLUMN.
Table 15.2: Valid entries for Level 0 extended properties
Level 0 Object Valid Level 1 Objects Description
NULL NULL Used for database-wide properties
USER NULL, TABLE, VIEW, Used for objects related to a user,
PROCEDURE, FUNCTION, including dbo
DEFAULT, RULE
TYPE NULL Used for properties related to a
user-defined type
Level 0 has three possible object types, which are shown in Table 15.2.
USER can have various entries at Level 1, which are shown in Table 15.3
along with the Level 2 objects that are paired with them. NULL and TYPE
have no valid entries at the lower levels.
Table 15.3: Level 1 objects and the Level 2 objects that go with them
Level 1 Objects Valid Level 2 Objects
TABLE NULL, COLUMN, INDEX, CONSTRAINT, TRIGGER
PROCEDURE NULL, PARAMETER
System UDFs
VIEW NULL, COLUMN, TRIGGER, INDEX*
FUNCTION NULL, COLUMN, PARAMETER
DEFAULT NULL
RULE NULL
-- The BOL says this should return all the extended properties in the database
-- but it does not. What it does is return all extended properties at the
-- database level. That is, those with all null levels.
SELECT * FROM ::fn_listextendedproperty (NULL, NULL, NULL, NULL
, NULL, NULL, NULL)
GO
(Results)
(Results)
After running that batch, the following query shows that we have an
extended property defined at the database level. Default has been substi-
tuted for NULL. They work identically.
Part II 321
Chapter 15: Documenting DB Objects with fn_listextendedproperty
(Results)
Of course, the query could have requested just the one property that
were interested in:
If you want to leave your database in its original state, delete the 'Respon-
sible Developer' extended property with this script:
System UDFs
DECLARE @rc int -- return code
EXEC @rc = sp_dropextendedproperty 'Responsible Developer'
, NULL, NULL, NULL, NULL, NULL, NULL
IF @rc = 1 -- 1 means failure
PRINT 'Extended property not dropped, please check out.'
ELSE
PRINT 'Extended property Responsible Developer dropped.'
GO
(Results)
(Results)
(Result - reformatted)
As you can see, none of the properties for any of the columns are in the
results. Thats because using NULL for @level2type requests results for
extended properties that are defined for @level1@type (in this case TABLE).
Since the @level1name is NULL, results are produced for all tables. If a spe-
cific table is used for the @level1name argument, only properties for the
given table are in the result, as in this query:
(Results - reformatted)
System UDFs
-------- ----------------- ---------------- -----------------------------------
TABLE CurrencyXchange Caption Currency Exchange Rates
TABLE CurrencyXchange MS_Description Exchange rates between currencies.
-- This query doesn't return anything because it doesn't work the way that
-- I imagine it should work.
SELECT * from ::fn_listextendedproperty ('Caption', 'USER', 'dbo',
'TABLE', NULL,
'COLUMN', NULL)
GO
(Results)
If the query is modified to include a table name, the new query produces
results for just that table, as seen here:
(Results - reformatted)
RETURNS TABLE
/*
* Returns the description extended property for all user tables
* in the database.
*
* Example:
select * from udf_Tbl_DescriptionsTAB()
****************************************************************/
AS RETURN
SELECT Owner as [Owner]
, objname as [TableName]
, OBJECT_ID(objname) as [ID]
, CAST(value as nvarchar(255)) as [Description]
FROM dbo.udf_EP_AllUsersEPsTAB('MS_Description'
, 'TABLE'
, default)
-- Get all MS_Description extended properties for all TABLES in the database.
SELECT * from udf_Tbl_DescriptionsTAB() ORDER BY TableName
System UDFs
GO
Note:
This query was run in TSQLUDFS. By the time that database reaches
you, there may be a different result.
Now that weve seen the output, lets turn to how the function accom-
plishes its task. Most of the work is turned over to the function
udf_EP_AllUsersEPsTAB, which does the job of searching for an extended
property associated with all tables for all users. It does it in a general-pur-
pose way. Ive made it general purpose so that it can be the foundation for
several functions. Listing 15.2 shows its CREATE FUNCTION script.
326 Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
RETURN
System UDFs
END
Once the user name is fetched into @user_name, a SELECT from fn_list-
extendedproperty gets the requested extended properties and inserts
them into the @EP result table variable:
The rest of the function is the looping structure for the cursor.
In addition to USER, the other Level 1 objects that could have extended
properties are NULL and TYPE. They could get a similar treatment if there
328 Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
System UDFs
select * from udf_EP_AllTableLevel2EPsTAB('MS_Description'
, NULL -- All Owners/Users/Schema
, NULL -- All Table Names
, 'COLUMN' -- The Level 2 Object
, default -- All columns
)
*****************************************************************************/
AS BEGIN
RETURN
END
The rest of the function is the loop that runs the cursor and fetches each
row.
So lets get the information that we were after in the first place.
Heres a query on udf_Tbl_ColDescriptionsTAB that uses udf_EP_All-
TableLevel2EPsTAB to ask for the MS_Description property in every column
of every table where it exists:
-- Get all MS_Description extended properties for all columns in the database.
SELECT * from udf_Tbl_ColDescriptionsTAB(NULL, NULL)
ORDER BY TableName, ColumnName
GO
System UDFs
CurrencyCD Comment Descriptive comment
CurrencyCD CurrencyCD ISO 4217 Currency Code used by Currency
CurrencyCD CurrencyName Common Name for the currency.
...
) RETURNS TABLE
-- No SCHEMABINDING due to use of INFORMATION_SCHEMA
/*
* Returns the schema name and table name for all tables that do
* not have the MS_Description extended property.
*
* Example:
SELECT Owner + '.' + TABLE_NAME as
[Tables without MS_Description] FROM udf_Tbl_MissingDescrTAB()
****************************************************************/
AS RETURN
SELECT TOP 100 PERCENT WITH TIES
TABLE_SCHEMA as [Owner]
, TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES I
Left Outer Join udf_Tbl_DescriptionsTAB () F
On I.TABLE_SCHEMA=F.Owner
and I.TABLE_NAME = F.TableName
WHERE TABLE_TYPE = 'BASE TABLE'
and F.TableName is NULL -- NO description
ORDER BY I.TABLE_SCHEMA
, I.TABLE_NAME
By the time you get the TSQLUDFS database, there will be a different
group of tables without MS_Description, so youll get a different answer
than is shown by this query:
System UDFs
*
* Example:
select * from udf_Tbl_RptW(default)
************************************************************************/
AS RETURN
SELECT TOP 100 PERCENT
dbo.udf_Txt_FixLen( [Owner] + N'.' + [Name], 64, N' ')
+ N' Created: ' + convert(char(10), [Create Date], 120)
+ N' RefDT: ' + convert(char(10), [Reference Date], 120)
+ N' Rows: ' + dbo.udf_Txt_FmtInt( [Rows], 10, ' ')
+ NCHAR(10) + space(22)
+ N'Indexes: '
+ CASE WHEN ClustIndex = 1 THEN N'Clustered ' ELSE N'' END
+ CASE WHEN NonclustIndex = 1 THEN N'NonClust ' ELSE N'' END
+ CASE WHEN PrimaryKey=1 THEN N'PK ' ELSE N'' END
+ CASE WHEN UniqueCnst=1 THEN N'Unique ' ELSE N'' END
+ CASE WHEN ActiveFulltextIndex=1 THEN N'FullText ' ELSE N'' END
+ N' Triggers: '
+ CASE WHEN AfterTrig=1 THEN N'After ' ELSE N'' END
+ CASE WHEN InsertTrig =1 THEN N'Insert ' ELSE N'' END
+ CASE WHEN InsteadOfTrig =1 THEN N'Instead ' ELSE N'' END
+ CASE WHEN UpdateTrig=1 THEN N'Update ' ELSE N'' END
+ CASE WHEN DeleteTrig=1 THEN N'Delete ' ELSE N'' END
+ N' Misc: '
+ CASE WHEN AnsiNullsOn = 1 THEN N'AnsiNulls ' ELSE N'' END
+ CASE WHEN QuotedIdentOn =1 THEN N'QuotedIdent ' ELSE N'' END
+ CASE WHEN Pinned = 1 THEN N'Pinned ' ELSE N'' END
+ NCHAR(10) + space(22)
+ dbo.udf_TxtN_WrapDelimiters([Description], 129, N' ', N' ',
NCHAR(10), 22, 22)
334 Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
+ NCHAR(10) + NCHAR(10)
as rptline
FROM udf_Tbl_InfoTAB (@table_name_pattern)
ORDER BY [Name] -- table name.
, [Owner] -- owner
TableRpt
-------------------------------------------------------------------------------
dbo.Broker Created: 2002-09-11 RefDT: 2002-09-11 Rows: 0
Indexes: Triggers: Misc: AnsiNulls QuotedIdent
Example table with stock broker names and IDs.
Summary
fn_listextendedproperty is the tool that SQL Server provides for access to
extended properties. Weve seen how careful use of its seven arguments
gives us access to extended properties for all database objects. Attention
to the meaning of NULL arguments is particularly important.
Along the way, weve constructed a group of UDFs that are useful for
managing extended properties. Theyve been oriented to the documenta-
tion tasks that Ive found most important when working with databases:
n Creating reports on tables and columns that include the descriptions
maintained in Enterprise Manager
n Ensuring that every table has a description
n Reporting about the characteristics of a table
If you think that you might execute the scripts in this chapter again, it
would be a good idea to run the next script. It cleans out the extended
properties created for this chapter:
System UDFs
EXEC sp_dropextendedproperty 'Caption'
, 'USER', 'dbo', 'TABLE', 'CurrencyXchange', NULL, NULL
EXEC sp_dropextendedproperty 'Caption'
, 'USER', 'dbo', 'TABLE', 'CUST', 'COLUMN', 'CustomerID'
EXEC sp_dropextendedproperty 'Caption'
, 'USER', 'dbo', 'TABLE', 'CUST', 'COLUMN', 'CompanyName'
EXEC sp_dropextendedproperty 'Caption'
, 'USER', 'dbo', 'TABLE', 'CUST', 'COLUMN', 'City'
EXEC sp_addextendedproperty 'Pager'
, 'USER', 'LimitedUser', NULL, NULL, NULL, NULL
GO
Using
fn_virtualfilestats
to Analyze I/O
Performance
fn_virtualfilestats returns a table of input/output statistics for database
files. They can be used to diagnose performance issues and for capacity
planning. This chapter builds on the raw information that fn_virtual-
filestats returns to produce UDFs that summarize the I/O statistics in
various ways. Specifically, at these levels:
n A single database udf_Perf_FS_DBTotalsTAB
n For all databases in the SQL Server instance udf_Perf_FS_ByDbTAB
n By physical file udf_Perf_FS_ByPhysicalFileTAB
n By disk drive letter udf_Perf_FS_ByDriveTAB
The data returned by fn_virtualfilestats is pretty raw and almost impos-
sible to use without interpretation. Before we can make use of its results,
were going to have to perform several minor interpretation tasks:
n The DbID and FileID columns have to be converted to a more
human-friendly name.
n Logical file names must be translated into physical file names.
n We have to know how long the system has been running to be able to
put the numbers into perspective.
Well start with converting numeric IDs to names that you and I can
understand. Then well find that fn_virtualfilestats isnt the only way to
ask SQL Server for I/O statistics. There is a group of system statistical
functions that report similar information.
337
338 Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
Warning:
SQL Server 2000 Service Pack 1 fixed a bug in fn_virtual-
filestats. Prior to the fix, if there are more than four files in the
database, fn_virtualfilestats fails to return the last file. See
Microsoft Knowledge Base article 290916. You should install SQL
Server Service Pack 3 or above.
Calling fn_virtualfilestats
The format of the function call is:
::fn_virtualfilestats ([ @DatabaseID= ] database_id
, [ @FileID = ] file_id )
(Results - reformatted)
System UDFs
Our I/O analysis needs human-readable names for the database, logical
files, and physical files. In addition, we need to know how long the
instance has been running. This kind of supporting information is pretty
easy to find, once you know where to look. The next few sections lay out
where to get it.
(Results)
Logical file names and file IDs can be converted with the FILE_ID() and
the FILE_NAME() functions. They are similar to the DB_ID and DB_Name func-
tions. However, they only return information about files in the current
database. Run this query from inside TSQLUDFS:
(Results - reformatted)
ID 1 ID 2 ID of the Data File ID of the Log File
-------------------- ------------------ ------------------- ------------------
TSQLUDFS_Data TSQLUDFS_Log 1 2
(Results - reformatted)
Database Logical File NumberReads NumberWrites BytesRead BytesWritten IoStallMS
-------- ------------ ----------- ------------ --------- ------------ ---------
pubs pubs 67 10 1810432 81920 871
pubs pubs_log 8 18 270336 177152 471
As you can see, pubs has two logical files. The file name returned by
fn_virtualfilestats is the logical file name.
Note:
This chapter contains several queries with numeric results. If youre
using Query Analyzers Output to Text instead of Output to Grid,
theres a way to make the numbers line up to be more readable. Use
the Results tab of the Tools Options menu command and check
Right Align Numerics (*). I used it for the queries in this chapter.
Part II 341
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
The logical file name is used within SQL Server. The operating system
uses a physical file name that includes the directory path. Well want that
to better understand where the I/O is being performed.
Whats in sysfiles?
Every database has its own copy of sysfiles. Any query on sysfiles must
be run from the database in question. Heres a query that shows whats in
sysfiles. The output was broken into two groups of columns so that the
System UDFs
filename column could be shown.
-- What's in sysfiles
USE TSQLUDFS
GO
filename
------------------------------------------------------------------------------
C:\BT\Projects\Book T-SQL Functions\Data\TSQLUDFS_Data.MDF
C:\BT\Projects\Book T-SQL Functions\Data\TSQLUDFS_Log.LDF
USE pubs
GO
-- Join with the sysfiles to get a physical file name
DECLARE @DatabaseID smallint -- holds the database ID
, @FileID smallint -- holds the file name for pubs
SET @DatabaseID = DB_ID() -- no argument means use the current database
SET @FileID = File_ID('pubs') -- Get ID of the pubs logical file
The limitation when using sysfiles is that it exists in every database and
has information only for that database. That makes it difficult to use from a
UDF that isnt created in the database for which you want information.
Fortunately, theres another table that consolidates information about all
database files used by the instance.
-- What's in master..sysaltfiles?
SELECT fileid, groupid, [size], growth, [status], dbid, [name], [filename]
FROM master..sysaltfiles
GO
Part II 343
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
filename
-----------------------------------------------------------------------------
C:\Program Files\Microsoft SQL Server\MSSQL\data\master.mdf
C:\Program Files\Microsoft SQL Server\MSSQL\data\mastlog.ldf
C:\Program Files\Microsoft SQL Server\MSSQL\data\tempdb.mdf
C:\Program Files\Microsoft SQL Server\MSSQL\data\templog.ldf
...
C:\BT\Projects\Book T-SQL Functions\Data\TSQLUSDF_Data.mdf
C:\BT\Projects\Book T-SQL Functions\Data\TSQLUDFS_Log.ldf
Since master..sysaltfiles has both the dbid and the fileid, it can easily
be joined with the output of fn_virtualfilestats. This script first moves
back to TSQLUDFS and then does the join:
System UDFs
-- Join fn_virtualfilestats with master..sysaltfiles
USE TSQLUDFS
GO
The next question to answer is, How do I know how long the counts
have been accumulating? Thats important for converting the raw counts
into rates.
/* Returns the date/time that the SQL Server instance was started.
*
* Common Usage:
select dbo.udf_SQL_StartDT() as [System Started AT]
****************************************************************/
AS BEGIN
RETURN @WorkingVariable
END
Part II 345
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
(Results)
Now to figure out how long its been since SQL Server started, we need
to know the current date and time. But we cant use GETDATE in a UDF.
udf_Instance_UptimeSEC, shown in Listing 16.2, uses the view Function_
Assist_GETDATE that bypasses the prohibition on calling GETDATE. The view
and the technique that gets around the restriction were discussed in
Chapter 4.
System UDFs
*
* Example:
select dbo.udf_Instance_UptimeSEC()/3600.0
[Hours since SQL Server started]
*****************************************************************/
AS BEGIN
So, to answer our question, How long has SQL Server been up? run the
query:
(Result)
Some of the same totals are available as system statistical functions such
as @@Total_Read and @@Total_Write. That group has other functions that
measure resource use since the system started, and that can be valuable
for analyzing system performance. These are listed in Table 16.2.
Table 16.2: System statistical functions for reporting resource consumption
Function Description
@@CPU_BUSY Number of milliseconds of CPU time consumed by SQL Server since
it started.
@@IDLE Number of milliseconds that SQL Server has been idle since it was
started.
@@IO_WAIT Number of milliseconds that SQL Server has spent performing input
and output since the system started.
@@PACK_RECEIVED Number of input packets read from the network since SQL Server
was started.
@@PACK_SENT Number of output packets sent to the network since SQL Server was
started.
@@TOTAL_READ The number of disk reads since the SQL Server instance was started.
@@TOTAL_WRITE The number of disk writes since the SQL Server instance was started.
@@Total_Read and @@Total_Write are used by the next query, which com-
pares them to the results of fn_virtualfilestats:
Part II 347
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
(Results - reformatted)
NumReads @@Total_Read NumrWrites @@Total_Write BytesRead BytesWritten IoSTallMS
-------- ------------ ---------- ------------- --------- ------------ ---------
1768 1778 1347 1377 65080832 20875776 32288
System UDFs
ing number of seconds that the system has been running, we can turn to
creating UDFs that summarize the raw numbers into useful information.
(Results reformatted)
Server Files Reads Writes BytesRead BytesWritten IoStallMS Sec
------ ----- ----- ------ --------- ------------ --------- ------
NSL2 39 1171 1234 61747200 26354688 37795 142309
348 Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
That gives you the big picture, but to locate the cause of any bottleneck,
were going to have to look at more detail. Lets start at the database
level.
RETURN
END
(Results - reformatted)
NumberOfFiles NumberReads NumberWrites BytesRead BytesWritten IoStallMS Sec
------------- ----------- ------------ ---------- ------------ --------- -------
2 75 33 2080768 271360 1342 218312
On many large systems, there are many databases, and you may want to
find the most active one. The next UDF lets you do that.
System UDFs
@DB_Name_Pattern sysname = NULL -- LIKE name of the database
-- to get stats for or NULL for ALL
) RETURNS TABLE
-- No SCHEMABINDING due to use of system UDF
/*
* Returns a table of total statistics for one database or
* a group of databases where the name matches a pattern. Null for
* all. Done by grouping by database.
*
* Example:
select * from dbo.udf_Perf_FS_ByDbTAB ('pubs')
*****************************************************************/
AS RETURN
SELECT TOP 100 PERCENT WITH TIES
DB_Name(DbId) [DatabaseName]-- get the name
, DbId AS [DBID]-- ID might be useful sometimes
, Count(DbId) [NumberOfFiles]-- Number of files
, Sum(NumberReads) as [NumberReads]
, Sum(NumberWrites) as [NumberWrites]
, Sum(BytesRead) as [BytesRead]
, Sum(BytesWritten) as [BytesWritten]
, Sum(IoStallMS) as [IoStallMS]
, Avg([TimeStamp] / 1000) as SecondsInMeasurement
FROM ::fn_virtualfilestats(-1, -1) -- -1 for all db and files
WHERE (@DB_Name_Pattern IS NULL
OR db_Name(dbid) LIKE @DB_Name_Pattern)
GROUP BY DbID
ORDER BY Sum(NumberReads)
+ Sum(NumberWrites) desc -- Top I/O first
350 Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
Sorting the results makes it easy to spot the databases with the most
activity. Heres a sample query run on my rather quiet development
system:
-- Get the top 5 databases in terms of number of I/O operations since startup
SELECT Top 5 DatabaseName, NumberOfFiles as [Files]
, NumberReads as NumReads, NumberWrites as NumWrites
, SecondsInMeasurement as [Sec]
FROM dbo.udf_Perf_FS_ByDbTAB (default)
GO
(Results - reformatted)
System UDFs
-- Physical files with the most I/O
SELECT Top 5 PhysicalFile, SizeMB, NumberReads, NumberWrites
FROM dbo.udf_Perf_FS_ByPhysicalFileTAB(default)
GO
These statistics are even more useful when a database is split into differ-
ent physical files. For example, by creating indexes on a separate file
group, the amount of I/O devoted to the data vs. the amount devoted to
indexes would be apparent. That can lead to separating the two file groups
on different drives.
The physical file name contains the path to the file. Since that con-
tains the drive letter, we can use it to summarize by drive letter. The drive
letters often correspond to physical disks but not always.
352 Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
some drives today, an improvement of maybe six times. Contrast that with
the improvement in CPU speed since the mid-1980s, which is around
100,000 times, and in drive capacity, which is around 1,000 times, and you
can see that I/O operations is one performance factor that just hasnt kept
pace with other advances in computer technology.
Heres a summary of the I/O performance of my desktop system pro-
duced with udf_Perf_FS_ByDriveTAB. Obviously, its not a highly stressed
system.
(Results - reformatted)
System UDFs
whole story. Youre going to have to know the I/O structure of your sys-
tem for these numbers to be meaningful. On most servers, drive letters
dont correspond to individual physical disks. For example, when disks are
put into a RAID 1, RAID 5, or RAID 10 array, several disks appear as one
drive letter. On small systems, one physical drive might be divided into
multiple partitions, each with its own drive letter. So be careful.
What I do know from experience is that more disk spindles almost
always give better drive performance. Making the most of the spindles is a
matter of spreading the I/O as uniformly as possible. For a more thorough
description of the analysis of I/O, including a description of RAID perfor-
mance, I use Microsoft Presss SQL Server 2000 Administrators
Companion.
Summary
Ive found the four functions that use fn_virtualfilestats to be pretty
useful in giving a quick impression of the I/O on a system. Often, the
quick impression is going to point out an obvious problem. However,
theyre not a complete solution to monitoring system I/O performance.
A more complete solution that would help isolate bottlenecks in sys-
tem performance would monitor I/O along with memory use, CPU use,
and network traffic as it varied over time. Most importantly, it would have
354 Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
fn_trace_* and
How to Create and
Monitor System
Traces
Chapter 3 shows the SQL Profiler in action as it captures output from a
trace. But SQL Profiler isnt the only way to run a trace. Traces can also
be created with a T-SQL script that uses a group of system stored proce-
dures that Ill refer to collectively as sp_trace_*. Under the hood, the SQL
Profiler uses these procedures to create and manipulate traces.
This chapter discuss four system functions that Ill refer to collec-
tively as the fn_trace_* UDFs. It relates them to the sp_trace_*
procedures that create traces. The UDFs retrieve information about
traces that are running in the SQL Server instance. Before discussing how
to use them, we need some background information on tracing and the
SQL Profiler.
One of the goals of the SQL Server 2000 development team was
reaching the C2 level of security. In order to achieve the C2 security des-
ignation, SQL Server had to be able to provide a complete audit of all
successful and unsuccessful statements and attempts to access database
objects. The mechanism chosen to fulfill this requirement is the trace.
The trace facility originated in SQL Server Version 6.5 where its pur-
pose was to enable the SQL Trace program that has evolved into the SQL
Profiler. In versions of SQL Server before 2000, it was known and
accepted that in times of high system load, events might get lost. When
profiling is used during software development or performance analysis,
the loss of events is annoying but acceptable. For C2 security compliance,
the SQL Server engine has to guarantee that no events are lost. Traces
can be written to three different places:
355
356 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
These functions return tables that consist mostly of coded numeric fields
that are difficult to read. The purpose of the UDFs created in this chapter
is to turn the raw information from the fn_trace_* functions into some-
thing meaningful to DBAs and programmers.
The task-oriented functions and stored procedures built in this chap-
ter are:
n udf_Trc_InfoTAB Produces a short summary of the traces running
on the SQL Server
n udf_Trc_RPT Produces a very readable summary of every trace
thats running on the system with the details of its columns, events,
and filter definitions
n usp_Admin_TraceStop Stops a trace or all traces. Its a stored proce-
dure because it does things that UDFs cant do.
Along the way, a dozen other functions are created to build udf_Trc_RPT.
Most do mundane jobs like translating numeric codes into character
strings, but a few are more interesting than that.
Part II 357
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
udf_Trc_RPT is the most robust way to see what traces are running in a
SQL Server instance, and its the ultimate goal of this chapter. To give you
some perspective about where were headed, take a look at its results. I
was running one SQL Profiler trace, with traceid=1, when I ran this
query:
(Results)
rptline
-------------------------------------------------------------------------------
Trace: 1 RUNNING Rowset:YES Rollover:NO ShutOnErr:NO BlckBx:NO MaxSize:5
Stop At: NULL Filename:NULL
Events: RPC:Completed, SQL:BatchCompleted, Login, Logout, ExistingConnection,
SQL:StmtStarting, SQL:StmtCompleted
Columns: TextData, NTUserName, ClientProcessID, ApplicationName,
SQLSecurityLoginName, SPID, Duration, StartTime, Reads, Writes, CPU,
Success
Filter: ApplicationName NOT LIKE N'SQL Profiler' AND NOT LIKE N'sqla%
I find that pretty readable. If youre familiar with the SQL Profiler, I think
you will also.
System UDFs
As with most other chapters, the download directory has a file with
the short queries that are interspersed within the chapters text: Chapter
17 Listing 0 Short Queries.sql. You wont get exactly the same results
shown in this chapter unless you happen to be running exactly the same
set of traces. Also, if youre on a shared server, traces run by everyone
show up in these functions.
Scripting Traces
There are five system stored procedures for creating and managing traces.
Theyre listed in Table 17.2. The calling sequence and the codes for the
parameters are listed in Books Online and wont be repeated here. The
documentation on these stored procedures is important for understanding
the fn_trace_* functions. The Books Online articles are the only places
that the codes in the result set of the fn_trace_* functions are
documented.
358 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
A script that creates a trace usually follows this general order of calls:
1. The script is created with sp_trace_create.
2. sp_trace_setevent is called repeatedly to configure the events and col-
umns to be monitored.
3. sp_trace_setfilter is called repeatedly to set up the filter on the
trace.
4. sp_trace_setstatus is called with a status of 1 to start the trace.
To stop the trace, this sequence is usually used:
1. sp_trace_setstatus is called with a status of 0 to stop the trace.
2. sp_trace_setstatus is called with a status of 2 to close the trace.
The process of writing the script to perform a trace is tedious. Fortu-
nately, SQL Profiler can do the job for you. Once youve used SQL Profiler
to set up a trace, use the Profiler menu File Script Trace SQL Server
2000 option to create an almost equivalent script file. Listing 17.1 shows
most of a script created for the SQLServerStandard.trc trace profile. The
full script is in the chapter download in the file Chapter 17 Listing 1 SQL
Trace.sql.
T-SQL scripts cant accept a rowset in the way that SQL Profiler does,
so the script is created with the results sent to a file. The instructions in
the beginning of the script tell you which lines of the script that you must
modify to set the file name.
The script that SQL Profiler doesnt give you is the one that stops the
trace. You need that one also. The script in Listing 17.1 returns the
traceid, so youll know which trace to stop. You can also get a list of traces
that are defined in your system from the fn_trace_getinfo system UDF,
which is the subject of the next section. Ill defer the code that stops a
trace until near the end of that section.
Part II 359
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
-- Create a Queue
declare @rc int
declare @TraceID int
declare @maxfilesize bigint
set @maxfilesize = 5
System UDFs
exec sp_trace_setevent @TraceID, 10, 6, @on
...
exec sp_trace_setevent @TraceID, 17, 18, @on
error:
select ErrorCode=@rc
finish:
go
360 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
fn_trace_getinfo
Use this function to get information about the traces currently running on
your server. The syntax of the call is:
::fn_trace_getinfo (@traceid)
@traceid, an int, is the only argument. It identifies the trace that the
caller is requesting information about. If @traceid is NULL or default, then
information for all traces is returned.
Each row of the returned rowset consists of the three columns shown in
Table 17.3. A row only has information about a single property of the
trace. To make the output of fn_trace_getinfo more readable, the results
must be pivoted.
Table 17.3: Rowset returned by fn_trace_getinfo
Column Data Type Description
Traceid int Identifies the trace
Property int Identifies the property
Value sql_variant The value of the property. The data type depends on
the property.
(Results)
Figure 17.1: SQL Profiler Trace Properties screen starting a new trace
Two traces were started because Server processes SQL Server trace
System UDFs
data was checked on the Trace Properties screen. Trace number 1 has
the TRACE_PRODUCE_ROWSET status bit set and no output file. The rowset is
sent to the Profiler to produce its GUI display. Trace number 2 is the
server trace. It has the TRACE_FILE_ROLLOVER status bit set, and its output is
written to the file C:\SampleTrace.trc. Both traces are set to stop at 36
minutes after midnight on 2002-09-20.
Server traces are written directly by the database engine and are not
relayed to SQL Profiler or any other program. Only server traces are guar-
anteed not to lose any events. Theyre always written to disk files.
The next two tables contain the information that I used to interpret
the table of output from fn_trace_getinfo. Table 17.4 has a description of
each of the properties.
Table 17.4: Properties returned by fn_trace_getinfo
Number Name Data Type Description
1 Trace Options int This is a bit field that holds four flags
from the parameter to sp_trace_cre-
ate. Table 17.5 describes each bit in
the Trace Options property.
2 FileName nvarchar(254) Name of the file that the trace is being
written to. It will be NULL if the trace is
not being written to a file.
362 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
The Trace Options property is a bit field. Table 17.5 has a breakdown of
the meaning of each bit of the property. When multiple properties are
requested, the Trace Options property is the sum of the property values.
Table 17.5: Bits in the Trace Options field
Value Trace Flag Description
1 TRACE_PRODUCE_ROWSET Results are sent to the trace client as rowsets. SQL
Profiler and other GUI interfaces use a rowset to
get events from the tracing facility. It is not used
by a server trace.
2 TRACE_FILE_ROLLOVER Files roll over when each reaches 5 megabytes.
4 SHUTDOWN_ON_ERROR Specifies that if the file cannot be written for any
reason, the SQL Server should shut down. This is
available to ensure that the SQL Server is creating
traces when required to satisfy the C2 security
level.
8 TRACE_PRODUCT_BLACKBOX Specifies that SQL Server will keep a record of the
last 5 megabytes of trace information. This is
used to produce the blackbox trace used by
Microsoft product support. This flag must be set
alone. When it is used, a trace file named
blackbox.trc is created in the default \Data direc-
tory of your SQL Server.
The Status column is 0 or 1, as shown in Table 17.6. These are the same
values used for the @status argument to sp_trace_setstatus. Of course,
the third status, 2, meaning close the trace, never shows up in
fn_trace_getinfo. Closed traces are removed from memory, and SQL
Server no longer has any knowledge of them. The last column in Table
17.6 is the name that the function udf_Trc_InfoTAB returns for each of the
codes. That function is discussed in the next subsection of this chapter.
Part II 363
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
udf_Trc_InfoTAB
For a quick summary of a single trace or all traces, try udf_Trc_InfoTAB.
Its shown in Listing 17.2. Its sole argument is the traceid that the caller
wants summarized. Like fn_trace_getinfo, if the traceid is NULL, a sum-
mary of all running traces is returned.
System UDFs
Listing 17.2: udf_Trc_InfoTAB
CREATE FUNCTION dbo.udf_Trc_InfoTAB (
Heres a query that uses udf_Trc_InfoTAB to show the properties for the
currently running traces:
What were after when we pivot the data from fn_trace_getinfo is one
row for each trace with multiple columns instead of one row for each prop-
erty. Each column in the pivoted output is one property of the trace. In
order to produce the desired output, a GROUP BY clause must be employed.
udf_Trc_InfoTAB groups the data by the traceid column, so there is one
row of output for each traceid.
Next we want to create our columns. Heres the expression for the
FileName column:
Lets strip out the CAST since it doesnt have anything do to with the pivot
operation, and were left with:
The CASE expression is applied to every row of input, but it will return NULL
for any row that doesnt have property=2. That is, it has a non-NULL value
only when the input row is a file name. An aggregation function must be
System UDFs
applied to the CASE expression because the SELECT has a GROUP BY clause
that is not grouped by the CASE expression. If it were grouped by case when
property=2 then value else NULL end and the other case expressions,
there would be a separate row for each property, and thats the situation
thats being pivoted in the first place.
The aggregation function chosen must aggregate the five rows in the
input for every traceid. Four of the values, the ones where property!=2,
are NULL. Theres no aggregation function for: Give me the one value
thats not NULL. So Ive used the MAX function. The only non-NULL value is
MAX. If the value column is NULL in all rows, the result of the MAX function is
NULL, as was the case for traceid=1 in the query above.
The cast(... as nvarchar(254)) expression that surrounds the case
expression doesnt have anything to do with pivoting the data. Its used to
convert the value column to nvarchar(254), even if the result of the aggre-
gation is NULL. The user of udf_Trc_InfoTAB is going to want the data type
to be something other than sql_variant, which can be difficult to work
with.
366 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
converts one bit in the Status property into a YES/NO character string
thats easier to understand. The case when property=1... expression was
explained above. The result of the case is going to be 'NO' for rows that
are not status columns. The test:
tests the second bit in the Status column. Ampersand (&) is the bitwise
AND operator. Because bitwise AND cant be applied to a sql_variant,
value is first CAST to an int. The bitwise AND is applied, and the result is
an int, which is 2 if the TRACE_FILE_ROLLOVER bit is set or 0 if its not. The
result dictates the choice between YES and NO. The MAX operator is used
to choose between all the NOs and the one possible YES. The comparison
is alphabetic, and the YES wins out, as the MAX, if its present in the input.
If were scripting traces, we need the T-SQL script to stop them. Now
that we have udf_Trc_InfoTAB, the job will be pretty easy because it shows
us the traceid and other characteristics of all running traces in your SQL
Server instance.
Stopping Traces
In the section Scripting Traces, you were shown the script to create a
trace without SQL Profiler. Once a trace is started with a script, eventu-
ally it has to be stopped or it will run on and on until it fills the disk or you
shut down SQL Server. This section shows the rather simple script
required to stop a trace and then builds a useful stored procedure to stop
either a specific trace or all traces.
All there is to stopping traces is a couple of calls to sp_trace_set-
status. It has to be called twice: first to stop the trace from running and
the second time to close the trace and release it from memory. Assuming
that there is a trace 1, the script at the top of the following page stops it
and closes it.
Because you may not be the person running trace 1, the script is com-
mented out when it appears in the Chapter 17 Listing 0 Short Queries.sql
file. You dont want to do this to someone else on your server without a
good reason.
Part II 367
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
(Results)
System UDFs
Its shown in Listing 17.3. To ease the impact on any unsuspecting
SQL Profiler users, it only pauses traces that are being sent to the
SQL Profiler window. It tells which traces are going to SQL Profiler by
checking the PRODUCE_ROWSET column. If the trace is going to SQL Profiler,
its only stopped, not closed out.
OPEN TraceCursor
FETCH TraceCursor INTO @tr, @status, @fn, @Rowset
IF @RC = 0
SELECT @MSG = 'Stopped'
, @Status = 'Stopped'
ELSE
SET @MSG = 'Not Stopped (' + convert(varchar, @RC) + ') '
+ dbo.udf_Trc_SetStatusMSG(@RC)
END -- IF
IF @RC = 0
SELECT @Msg = @Msg + ' Closed'
, @Status = 'Closed'
ELSE
SET @msg = @msg + ' Not Closed ('
+ convert(varchar, @rc) + ') '
+ dbo.udf_Trc_SetStatusMSG(@rc)
END
As you can see, usp_Admin_TraceStop first stops traces if they are running.
Next, if no rowset is being created by the trace, which means SQL Profiler
isnt showing it, the trace can be closed. This query shows the result of
closing a few traces that were in different states when the SP was run:
(Results - reformatted)
Since were being such nice guys and not closing the traces started by
everyone, how about being nicer and telling them what were doing? They
really ought to be told about whats going to happen before it happens, so
Ive separated the next stored procedure from usp_Admin_TraceStop.
Unfortunately, none of the fn_trace_* functions tell us which user
started each trace. The best we can do is find out which users are running
SQL Profiler. For all practical purposes, that strategy works pretty well.
System UDFs
The information is in master..sysprocesses. Right now Im the only one on
the system, as shown by this query:
(Results - reformatted)
Ive turned the query, with a few additional columns, into an inline UDF,
udf_Trc_ProfilerUsersTAB, which youll find in the TSQLUDFS database.
udf_Trc_ProfilerUsersTAB and udf_Trc_InfoTAB show us information
about traces that are running. The next system UDF, fn_trace_gettable,
gives us access to the data in traces that are no longer running but have
been saved to a disk file.
370 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
fn_trace_gettable
fn_trace_gettable converts a trace file stored on disk into a table format.
The table can then be examined using whatever method you choose,
such as:
n View it in SQL Profiler or some other tool.
n Analyze it with the Index Wizard or your own code.
n Save it to a SQL Server table for later use.
n Put it to another use that I havent thought up.
The syntax of the function call is:
fn_trace_gettable( @filename
, @numfiles )
@filename is the path and name of the file. There is no default for this
argument.
@numfiles is the number of files to load. If default is used for this argu-
ment, all the rollover files will be loaded.
There is a sample .trc file in the download directory for this chapter under
the name ExampleTrace.trc. The next query loads ExampleTrace.trc using
a sample call to fn_trace_gettable. The script assumes that youve copied
that file to the root directory of your C drive. Please copy it before running
this query:
System UDFs
/*
* Translates a SQL Trace EventClass into its descriptive name.
* Used when viewing trace tables or when converting a trace
* file with fn_trace_gettable.
*
* Example: -- assumes existence of the c:\ExampleTrace.trc file
select TextData, dbo.ufn_SQL_TraceEventName(EventClass)
from ::fn_trace_gettable ('c:\ExampleTrace.trc', default)
***************************************************************/
AS BEGIN
A complete list of the codes with an explanation of what causes the events
to occur is in Books Online in the documentation for sp_trace_setevent.
A sample query shows what the translation looks like:
Analyzing trace data is beyond the scope of this book. There is additional
information on using SQL Profiler to monitor UDFs in Chapter 3. That
information also applies to traces that are stored in files and analyzed later
using fn_trace_gettable.
Translating the numeric code for EventClass that fn_trace_gettable
returns is only one of the translations that we need to make to create
udf_Trc_RPT. When we retrieve event information and filter definitions
using the next two system UDFs, well have to translate several more
codes.
fn_trace_geteventinfo
Use fn_trace_geteventinfo to retrieve information about what events are
being recorded by any particular trace. The syntax of the function call is:
::fn_trace_geteventinfo ( @traceid )
To see how it works, I first started a SQL Profiler trace with minimal
events and data columns. The first SELECT in this script gets a list of the
traces. If a trace is found, the second SELECT uses the first traceid as the
parameter to call fn_trace_geteventinfo.
-- Get the first running trace and request the event information
DECLARE @traceID int
System UDFs
(Results - abridged )
eventid columnid
----------- -----------
10 1
10 12
10 16
10 17
...
15 16
15 17
15 18
As you can see, the rows of this table have information for only one trace
column each, and columnid is a numeric code. The output wasnt meant for
you or me to understand.
Lets start by translating the columnid into something more under-
standable, like the column name. The complete list of column IDs is in the
Books Online documentation for sp_trace_setevent. The function
udf_Trc_ColumnName translates the columnid into a name. The code for the
function is in Listing 17.5.
374 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
There are a couple of reasonable ways to slice and dice the output from
fn_trace_geteventinfo to make it more useful. A complete pivot of the
table that listed the event name and then 44 columns, one for each possi-
ble columnid, is probably not very useful. When data is so sparse (lets say
eight columns of data out of 44), its often more useful to turn it into a sin-
gle comma-separated list. Since the data columns are the same for every
event, lets turn both the events and the columns into lists of names.
By the way, when the SQL Profiler starts the trace, it requests the
same set of data columns on every event type. Thats a lot of columns.
When traces are created with the sp_trace_setevent stored procedure, a
different set of columns can be requested for each event type, providing a
level of control that isnt available in SQL Profiler.
System UDFs
AS BEGIN
RETURN @EventList
END
This isnt a legal place for the shaded DISTINCT keyword, and SQL Server
wont create the function. I also tried putting the DISTINCT right after
SELECT where I intuitively think it belongs. Heres what I tried:
Although SQL Server accepts the function, it doesnt produce the desired
result. That function, udf_Example_Trc_EventListBadAttempt, is in the
TSQLUDFS database if you want to try it.
As you can see in Listing 17.6, the way to get the UDF to work is to
move the SELECT on fn_trace_geteventinfo with its DISTINCT clause into a
derived table. This isolates it from the looping SELECT, and we get the cor-
rect results. This query runs udf_Trc_EventList on trace number 1:
(Results)
Event List
-------------------------------------------------------------------------------
RPC:Completed, SQL:BatchCompleted, Login, Logout, ExistingConnection
fn_trace_getfilterinfo
This function retrieves information about the active filters for a trace. The
syntax of the call is:
::fn_trace_getfilterinfo( @traceid )
System UDFs
logical_operator int Code for the operator used to compare the col-
umns value to the value column of this filter
clause. The list of operators is in Table 17.9 in
the next subsection.
value sql_variant The value compared to the traces data column
using the logical operator.
(Results)
This trace has one filter. If you look at the Filters tab of the profile defini-
tion, youll see that it filters out events generated by SQL Profiler itself.
To translate this to more meaningful text, lets use the functions for
translating comparison_operator and logical_operator that were created
for the previous section:
-- Get filter info for trace 1 with translations. Assumes trace 1 is running.
SELECT dbo.udf_Trc_ColumnName(columnid) as [Column ID]
, dbo.udf_Trc_LogicalOp(logical_operator) as [Logical Op]
, dbo.udf_Trc_ComparisonOp(comparison_operator) as [Comp Op]
, value
FROM ::fn_trace_getfilterinfo(1)
GO
(Results)
System UDFs
) RETURNS nvarchar(64) -- Equivalent comparison expression
-- such as 'Database ID > 5'
WITH SCHEMABINDING
/*
* Translates a SQL Profiler filter expression into text in a
* form similar to a WHERE clause. Used when retrieving event
* information from fn_trace_getfilterinfo. The length of the
* output is limited to about 64 characters.
*
* Example: -- assumes existence of the c:\ExampleTrace.trc file
select dbo.udf_Trc_FilterClause(columnid, comparison_operator
, value) from ::fn_trace_getfilterinfo (1)
***************************************************************/
AS BEGIN
SET @Comparison_OperatorText
= dbo.udf_Trc_ComparisonOp(@Comparison_OperatorCode)
END
380 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
To make our output interesting, I created a trace with several filter condi-
tions. It started with the SQLProfilerStandard trace, which excludes
output from the SQL Profiler itself. Then a few conditions were added to
restrict the output of the trace to databases with IDs 5, 6, or 7. It also has
Exclude system IDs checked. Figure 17.2 shows the Filters tab of the
Trace Properties screen while the filter is being defined.
(Results)
Oper Expression
---- ----------------------------------------------------------------
AND DatabaseID=5
OR DatabaseID=6
OR DatabaseID=7
AND ObjectID>=100
AND ApplicationName NOT LIKE N'SQL Profiler'
AND ApplicationName NOT LIKE N'sqla%'
Part II 381
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
System UDFs
Listing 17.8: udf_Trc_FilterExpression
CREATE FUNCTION dbo.udf_Trc_FilterExpression (
SET @Comparison_OperatorText =
dbo.udf_Trc_ComparisonOp (@Comparison_OperatorCode)
IF @Comparison_OperatorText LIKE '%LIKE%'
SET @Comparison_OperatorText = @Comparison_OperatorText + N' '
END -- IFclause
ELSE -- When not compact or when there is a new column
SELECT @FilterExpression = @FilterExpression
+ @Separator
+ @Logical_OperatorText
+ N' '
+ dbo.udf_Trc_FilterClause(@ColumnID
, @Comparison_OperatorCode, @Value)
-- END IF
-- Save the previous values so they can be used to compact the string
SELECT @LastColumnID = @ColumnID
, @LastLogical_OperatorCode = @Logical_OperatorCode
Part II 383
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
, @LastComparison_OperatorCode = @Comparison_OperatorCode
RETURN @FilterExpression
END
With the compact option set to 0, a sample call shows the complete filter
expression from trace 1. Ive let the output wrap to a second line:
Filter Expression
-------------------------------------------------------------------------------
DatabaseID=5 OR DatabaseID=6 OR DatabaseID=7 AND ApplicationName NOT LIKE N'SQL
System UDFs
Profiler' AND ObjectID>=100
When the compact option is selected, the result is shorter and easier to fit
onto a single line. Its shortened by not repeating the column name in suc-
cessive comparisons to the same column. The output is a little smaller but
still wraps based on the 80-character limit of the format of this book. As
you can see in this query:
Filter Expression
--------------------------------------------------------------------------------
DatabaseID=5 OR =6 OR =7 AND ApplicationName NOT LIKE N'SQL Profiler' AND
ObjectID>=100
RETURN
END
To see how it works, start a few traces and choose various events, col-
umns, and filter expressions. Set our output to text and be sure to set the
Query Analyzer option Maximum characters per column to a big num-
ber like 4000. Youll find it on the Results pane of the Tools Options
menu command. On the same tab, turn off Print column headers (*) or
youll get long lines of dashes when you try to send the output to a file.
Then run this query:
(Results - abriged)
rptline
System UDFs
-------------------------------------------------------------------------------
Trace: 1 RUNNING Rowset:YES Rollover:NO ShutOnErr:NO BlackBox:NO MaxSize:5
Stop At: NULL Filename:NULL
Events: RPC:Completed, SQL:BatchCompleted, Login, Logout, ExistingConnection
Columns: TextData, NTUserName, ClientProcessID, ApplicationName,
SQLSecurityLoginName, SPID, Duration, StartTime, Reads, Writes, CPU
Filter: DatabaseID=5 OR =6 OR =7 AND ObjectID>=100
AND ApplicationName NOT LIKE N'SQL Profiler'
I find this format readable and easy to print so the results can be e-mailed
or shown to others when needed. The best way to get the results to print
is to send the output to a file. Theres a sample file, Output of udf_Trc_
RPT.rpt, in the download directory for this chapter.
386 Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
Now, you dont need the complete list of traces very often. But when
youre trying to diagnose a performance problem and there are 15 traces
running on the system, the traces are part of the problem. Even when
youre trying to get realistic timing information, its better not to be run-
ning any more traces than necessary and preferably only the one that is
measuring the events that youre investigating.
Speaking of timing, if you have a bunch of traces running, you might
notice that udf_Trc_RPT is surprisingly slow. How slow? I ran the two pre-
vious queries with just three traces running. The call to udf_Trc_InfoExTAB
took 993 milliseconds. The call to udf_Trc_RPT took 2323 milliseconds.
Thats more than twice the amount of time. Im pretty sure the difference
is due to the large amount of procedural code used for text processing,
particularly the calls to udf_TxtN_WrapDelimiters.
Summary
The trace is one of the most powerful tools in the SQL Server arsenal, as
it is useful for debugging, monitoring, and analyzing performance. This
chapter has produced a set of functions and stored procedures to aid in
understanding and managing traces. Most of this information comes from
a group of system UDFs whose name begins with fn_trace_.
The goal of most of the functions created in this chapter is to build a
translation of the numeric codes used to create system traces into a read-
able description. The description is summarized by udf_Trc_RPT.
In addition, there was an introduction to creating traces with T-SQL
script. While the SQL Profiler remains much easier to use, there are times
when a script is better, such as when you just cant accept the loss of any
events or when you want to have detailed control over which columns of
data are gathered for each event.
In addition to creating traces with T-SQL script, youre also going to
have to stop them. The stored procedure usp_Admin_TraceStop was created
to make that easy. Its accompanied by udf_Trc_ProfilerUsersTAB, which
can show you a list of users who are running SQL Profiler. Unfortunately,
SQL Server doesnt provide the information needed to connect the trace
to the user who is running that trace.
These last few chapters covered the documented system UDFs in
depth and built useful UDFs based on their output. If you take a look at
the list of functions in master, youll see that there are many more than
the ten documented UDFs. The next chapter explores some of the system
UDFs that Microsoft left out of Books Online.
18
Undocumented
System UDFs
The first four chapters of Part II of the book discussed the system UDFs
that are documented in Books Online. Master is full of UDFs, few of which
are documented. The undocumented UDFs in master fall into two groups:
n True system UDFs owned by system_function_schema
n Standard UDFs owned by dbo
The undocumented UDFs in master and owned by system_function_schema
have the status of being system UDFs. That status confers on them two
important characteristics:
n They can use T-SQL syntax that is reserved for system UDFs.
n They function as if they are running in the database from which they
are called, instead of from the database in which they are defined.
That last point is subtle but can be very important. It only comes into play
with a few of the undocumented system UDFs supplied by Microsoft, such
as fn_dblog. Once we define our own UDFs in the next chapter, its much
more important.
There are also UDFs in master that are owned by dbo. While theyre
not actually system UDFs, you can use them to your advantage. A few of
these are covered in this chapter. But theres nothing special about them;
theyre called like any UDF in any database and dont use the special syn-
tax of system UDFs.
This chapter discusses these two groups of undocumented UDFs in
master. It documents some of the more useful among them and shows
examples of how they can be used.
Using any undocumented routine in any software product usually
caries a risk that the vendor of the product (in this case Microsoft) will
change the behavior of the routine in a future release. In the case of the
undocumented system UDFs, this risk is mitigated by the presence of the
source code that is used during the SQL Server installation process to cre-
ate most of the system UDFs. The files that SQL Server uses when
387
388 Part II
Chapter 18: Undocumented System UDFs
creating the system UDFs is left on your disk after the installation is com-
plete. Ill give you a list of the files and their location so you can take a
look for yourself.
When you want to retrieve the text of a function, you can either exe-
cute a query using sp_helptext or use one of the GUI tools, Enterprise
Manager or Query Analyzer, to get the script. While these techniques
work with the UDFs owned by dbo in master, it doesnt work on true sys-
tem UDFs. As I just mentioned, you can get the text of many, but not all,
system UDFs from the source code files. The rest are available from
within SQL Server. Well build a UDF, udf_Func_UndocSystemUDFtext, that
can retrieve an undocumented functions source code. Ill also show why
sp_helptext doesnt work on system UDFs. Understanding why reveals
something of the status of system UDFs that sets them apart from other
functions.
Once you have the text of a system UDF, you have options to insulate
yourself from potential changes to the function in future releases. One
option is to create an identical UDF using your own function name. Thats
a sensible approach for some of the undocumented system UDFs, such as
fn_chariswhitespace, that use only standard T-SQL syntax and dont
require the status of a system UDF to be effective. However, other sys-
tem UDFs, such as fn_dblog, use syntax that is undocumented, is not part
of standard T-SQL, and only works when executed in a system UDF.
The first step in using the undocumented UDFs is to get a list of
them. This can be retrieved with a variety of tools. This chapter starts by
showing how to get the list of system UDFs.
-- get a list of all the system UDFs including the undocumented ones
USE master
GO
SELECT routine_schema
, routine_name
, data_type
FROM information_schema.Routines
WHERE ROUTINE_TYPE = 'FUNCTION'
ORDER BY routine_schema, routine_name
GO
(Results)
Part II 389
Chapter 18: Undocumented System UDFs
System UDFs
system_function_schema fn_repluniquename nvarchar
system_function_schema fn_serverid int
system_function_schema fn_servershareddrives TABLE
system_function_schema fn_skipparameterargument nvarchar
system_function_schema fn_trace_geteventinfo TABLE
system_function_schema fn_trace_getfilterinfo TABLE
system_function_schema fn_trace_getinfo TABLE
system_function_schema fn_trace_gettable TABLE
system_function_schema fn_updateparameterwithargument nvarchar
system_function_schema fn_virtualfilestats TABLE
system_function_schema fn_virtualservernodes TABLE
As you can see, the documented functions are on the list as well as many
undocumented ones. The documented system UDFs have already been
covered, so lets move on to those that Microsoft chose to leave out of
Books Online.
instdist.sql
procsyst.sql
replsyst.sql
replcomm.sql
repltran.sql
sql_dmo.sql
The original definition of some of these functions was modified in the files:
sp1_repl.sql
sp2_repl.sql
It seems that in one of the service packs for SQL Server 2000, some addi-
tional protection has been added to hide the text of the system functions.
If you try to get the text of a system UDF using sp_helptext, you just get
an error. Try it:
(Results)
Variations on the name dont seem to work. The reason is that the
OBJECT_ID metadata function returns NULL for the system UDFs, and
sp_helptext depends on OBJECT_ID. This query shows it:
(Results)
ID
-----------
NULL
Poking around reveals that most of the undocumented UDFs are still
entries in sysobjects and syscomments, but the documented system UDFs
have been removed as this query shows:
(Results)
name
---------------------------------------------
fn_updateparameterwithargument
Part II 391
Chapter 18: Undocumented System UDFs
fn_repluniquename
fn_sqlvarbasetostr
...
fn_serverid
fn_isreplmergeagent
...
fn_replquotename
fn_chariswhitespace
fn_skipparameterargument
fn_removeparameterwithargument
Using these facts, its possible to retrieve the text of the undocumented
system UDFs using the function udf_Func_UndocSystemUDFtext, shown in
Listing 18.1. Ive included the CREATE FUNCTION script in the Listing 0 file
so that you can easily create it in master. You should do this only in sys-
tems where you know its okay. The script is commented out to prevent
creating it unintentionally.
System UDFs
-- No SCHEMABINDING due to use of system tables
/*
* Returns the text of an undocumented system user-definded
* function. This function can only be used in the master
* database, where the text of the undocumented UDFs is stored.
* To work it must be created in master.
*
* Example:
select * from udf_Func_UndocSystemUDFtext('fn_serverid')
****************************************************************/
AS BEGIN
SELECT @ObjectID = id
FROM sysobjects
WHERE (type = N'FN' or type = 'IF' or type = 'TR')
and name = @FunctionName
RETURN
END
392 Part II
Chapter 18: Undocumented System UDFs
(Results)
text
--------------------------------------------------------------------------------CREATE
FUNCTION system_function_schema.fn_serverid(@servername sysname)
RETURNS int
AS
BEGIN
declare @srvid int
select @srvid = srvid from master..sysservers where UPPER(srvname)
= UPPER(@servername) collate database_default
RETURN (@srvid)
END
Since youve got the text to the function, you could pretty safely turn it
into your own UDF in your own database or even a system UDF using the
technique discussed in the next section. However, nothing guarantees that
it will continue to work forever. A new version of SQL Server, or even a
new service pack, could change the master..sysservers table that
fn_serverid relies on. Proceed at your own risk.
fn_chariswhitespace
This function is useful when trimming or word wrapping text. It accepts a
string as input and responds with a result of 1 when the character is a
whitespace character and 0 when the character is not whitespace.
The syntax of the call is:
fn_chariswhitespace (@char)
Notice that because its a system UDF, the calling sequence for fn_char-
iswhitespace doesnt include the owner. Lets try a query in master, then
move to pubs and try some more:
USE master
GO
(Results)
System UDFs
Tab A Space Unicode Space Unicode New Line Unicode A
---- ---- ----- ------------- ---------------- ---------
1 0 1 1 1 0
USE pubs
GO
SELECT fn_chariswhitespace(NCHAR(09)) as [Tab]
GO
(Results)
Tab
----
1
fn_dblog
fn_dblog returns a table of records from the transaction log. The syntax of
the call is:
::fn_dblog(@StartingLSN, @EndingLSN)
@StartingLSN and @EndingLSN are the start and end log sequence
numbers, also known as LSNs. A NULL argument for the starting LSN
requests log records from the beginning of the transaction log. A NULL
value for the ending LSN requests information to the end of the transac-
tion log.
To get an idea of what goes into the database log, I backed up my database
to clear out the log. Actually, there were a few records left in, from open
transactions I suppose. Then I ran this simple UPDATE statement that cre-
ated records in the transaction log:
USE TSQLUDFS
GO
(Results omitted)
Next, I ran a query that uses fn_dblog. Its shown here with just two
groups of the output columns. There are 85 or so columns of output.
Thats much too wide for display in this book, especially since most of the
row values are NULL and I can explain only a few of them. Heres the query
and output:
The entire output of the query is in the file Sample output of fn_dblog.txt
in the chapters download directory. It includes all columns and rows
shown above as well as a few rows that remained in my log after I did the
backup that preceded the update to ExampleAddresses.
Theres no documentation of the format of a log record in Books
Online, and I havent been able to locate it anywhere else. However, there
are a few obvious items of information in the log. LOP_BEGIN_XACT and
LOP_COMMIT_XACT mark the beginning and ending of the implicit transaction
that surrounds the statement. Each LOP_MODIFY_ROW operation on the object
dbo.ExampleAddresses is an update to a single row. Beyond that, youre
System UDFs
pretty much on your own.
Now that you know how to use fn_dblog, why would you? It could be
used to analyze the patterns of updates or the frequency. Or you could use
it to go back and check on all the updates that happened to a particular
table.
There are products that produce database audit trails that use
fn_dblog to ensure that they capture everything in the log. Microsoft has
briefed these companies about the meaning of the output columns.
Before we move on, try fn_dblog in another database. Heres a script
to try it in pubs:
(Results omitted)
The results are different than when run in TSQLUDFS. System UDFs get
their data from the database in which they are run, rather than the data-
base in which they are defined. That lets them be defined just once and
used in any script, assuming that the tables that they refer to exist in the
396 Part II
Chapter 18: Undocumented System UDFs
fn_mssharedversion
This function returns a part of the server version thats used to create a
directory in the path used to set up the current instance of SQL Server.
This is used by the SQL Server installation. Its how the directory named
80 ends up just below the Microsoft SQL Server directory in Pro-
gram Files.
If you need information about the version of SQL Server, youre
better off using the SERVERPROPERTY function, as in this query:
(Results)
ProductVersion
------------------------------
8.00.760
fn_replinttobitstring
This function converts an integer to a 32-character string of ones and
zeros that represent the bit pattern of the integer. Its the complement to
fn_replbitstringtoint, which is documented next in this chapter. The
syntax of the call is:
fn_replinttobitstring (@INT)
(Results)
26 in binary -1
-------------------------------- --------------------------------
00000000000000000000000000011010 11111111111111111111111111111111
(Results)
The numbers in the last query that start with 0x are T-SQLs binary
constants. Ive put the word binary in quotes because T-SQLs binary
constants are actually hexadecimal constants.
The TSQLUDFS database has three functions that are very similar
to fn_replinttobitstring but work in slightly different ways: udf_BitS_
System UDFs
FromInt, udf_BitS_FromSmallint, and udf_BitS_FromTinyint. In addition to
taking a numeric argument, they each take a BIT argument that requests
that leading zeros be eliminated.
udf_BitS_FromInt is shown in Listing 18.2. Its based on fn_repl-
inttobitstring but handles the elimination of leading zeros to produce a
more readable and compact result. Many bit fields use only the first few
low-order bits.
AS BEGIN
IF @TrimLeadingZerosBIT=1 BEGIN
SET @WorkingVariable =
CASE @PosOfFirst1
WHEN 1 THEN @WorkingVariable -- Negative Number
WHEN 0 THEN '0' -- return at least 1 of the 0s
ELSE SUBSTRING (@WorkingVariable
, @PosOfFirst1
, 32 - @PosOfFirst1 + 1)
END
END -- IF
RETURN @WorkingVariable
END
(Results)
(Results)
(Results)
Back in the days of $25,000 disk drives, we used to pack bits as tight as
sardines. Functions like udf_Bits_FromInt and fn_replinttobitstring
would have come in handy for examining the data. These days, I prefer to
spread my data out rather than use bit fields. If you must use them, you
might also want to take a look at the function udf_Bit_Int_NthBit, which
plucks individual bits from an int thats being used as a bit field.
Once the bit field is converted to a string, there are times when it has
to be converted back to an integer type. The next UDF takes care of that.
fn_replbitstringtoint
In the previous section weve seen how the bit fields used by many sys-
tem functions can be turned into more human-readable strings using
fn_replinttobitstring or the alternative UDFs. fn_replbitstringtoint is
System UDFs
the complement of fn_replinttobitstring, as it converts a string that
holds a 32-bit bit pattern back into the corresponding int. Its a scalar sys-
tem UDF with the syntax:
fn_replbitstringtoint (@Bitstring)
-- examples of fn_replbitstringtoint
SELECT fn_replbitstringtoint ('00000000000000000000000000000010') as [2]
, fn_replbitstringtoint ('11111111111111111111111111111111') as [-1]
, fn_replbitstringtoint ('10000000000000000000000000000000')
as [Most negative number]
, fn_replbitstringtoint ('01111111111111111111111111111111')
as [Most positive number]
GO
(Results)
(Results)
(Results)
System UDFs
, @sWorking varchar(16)
SELECT @number = 0
,@sWorking = RIGHT ('0000000000000000' + @BitString, 16)
(Results)
SELECT @number = 0
,@sWorking = RIGHT ('00000000' + @BitString, 8)
SELECT @Number =
CASE WHEN substring(@sWorking, 1,1) = '1' THEN 0x80 ELSE 0 END
| CASE WHEN substring(@sWorking, 2,1) = '1' THEN 0x40 ELSE 0 END
| CASE WHEN substring(@sWorking, 3,1) = '1' THEN 0x20 ELSE 0 END
| CASE WHEN substring(@sWorking, 4,1) = '1' THEN 0x10 ELSE 0 END
| CASE WHEN substring(@sWorking, 5,1) = '1' THEN 0x08 ELSE 0 END
| CASE WHEN substring(@sWorking, 6,1) = '1' THEN 0x04 ELSE 0 END
| CASE WHEN substring(@sWorking, 7,1) = '1' THEN 0x02 ELSE 0 END
| CASE WHEN substring(@sWorking, 8,1) = '1' THEN 0x01 ELSE 0 END
System UDFs
, dbo.udf_BitS_ToTinyint ('10000000')
as [No negative numbers]
GO
(Results)
The three UDFs created in this section all work better than fn_repl-
bitstringtoint, and I use them in preference to it. Along with the
functions that convert numbers to bit strings, these functions make it
pretty easy to use bit fields.
fn_replmakestringliteral
fn_replmakestringliteral accepts a string as input and turns its value into
a Unicode string literal that is suitable for use in a SQL statement. The
syntax of the call is:
fn_replmakestringliteral (@string)
(Results)
This might be useful when writing code that writes SQL statements, but
Listing 18.5 shows a function that Ive found to be more useful.
udf_SQL_VariantToStringConstant converts a sql_variant to a string that is
the constant value for the sql_variant in SQL script. Be warned, however:
It only works on a subset of the possible data types.
System UDFs
SET @Result = '''' + CONVERT(nvarchar(128), @InVal, 121) + ''''
END
ELSE
SET @Result = CAST (@InVal as nvarchar(3990)) + N' (' + @BaseType + N')'
RETURN @Result
END
(Results)
In the past Ive used this function to create SQL statements. For example,
this query creates INSERT statements for the ExampleDataTypes table that
was created with one record to demonstrate this function:
406 Part II
Chapter 18: Undocumented System UDFs
Insert Script
--------------------------------------------------------------------------------
INSERT INTO ExampleDataTypes VALUES(1, '1911-11-11 11:11:11.000', 'abc',
'acbdef', N'ABCDEF', 3.17, 1234.56789)
This comes in handy from time to time. Be careful though: Its not a com-
plete solution to writing INSERT scripts for a table. That requires handling
situations such as computed columns, timestamps, identity columns, text,
ntext, images, and other special columns.
fn_replquotename
This function turns its string argument into a name surrounded by brack-
ets, which is suitable for use as a name in a SQL statement. The syntax of
the call is:
fn_replquotename (@string)
(Results)
Quoted Name
--------------------------------------------------------------------------------
[My column name with embedded spaces]
System UDFs
AS BEGIN
OPEN ColumnCursor -- Open the cursor and fetch the first result
FETCH ColumnCursor INTO @view_catalog, @view_schema, @column_name
END
ELSE
SELECT @Result = @Result + ', '
+ CASE WHEN @Prefix is NOT NULL and LEN(@Prefix) > 0
THEN @Prefix + '.'
ELSE ''
END
+ fn_replquotename (@column_name)
RETURN @Result
END
-- Construct a dynamic SQL statement to get all the columns from a view
DECLARE @view sysname, @SQLStatement nvarchar(4000)-- our temporary view
SET @view = 'ExampleViewWithKeywordColumnNames'
SET @SQLStatement = 'SELECT ' + dbo.udf_View_ColumnList(@view, null, null)
+ ' FROM ' + @view
SELECT @SqlStatement as [SQL Statement]
EXECUTE (@SqlStatement)
GO
(Results)
SQL Statement
--------------------------------------------------------------------------------
SELECT [END], [VALUES], [CROSS] FROM ExampleViewWithKeywordColumnNames
There are two resultsets returned from the batch. The first one is the
SQL statement. The second one is produced by executing the SQL state-
ment dynamically. Since ExampleViewWithKeywordColumnNames is a view on
ExampleTableWithKeywordColumnNames, which contains no rows, the second
resultset is empty.
The importance of quoting the column names is easy to illustrate.
Trying to execute the SELECT created in the previous query without quot-
ing the column names gives an error. Try it:
Part II 409
Chapter 18: Undocumented System UDFs
(Results)
fn_varbintohexstr
This function can be used to convert almost any data to a hex representa-
tion. The syntax for the function is:
master.dbo.fn_varbintohexstr (@Input VarBinary(8000))
System UDFs
fn_varbintohexstr is not a real system UDF. Its owned by dbo in the
master database, not by system_function_schema. That makes it an ordi-
nary UDF that is created in master. Thus, the qualifiers master.dbo are
required when using it outside of the master database. In master,
dbo.fn_varbintohexstr is sufficient.
The length of @Input is misleading. The function returns a
nvarchar(4000) string that represents the input as hex. Each byte of the
input is going to be turned into two hex characters of Unicode output.
Effectively, the input is limited to 2,000 bytes.
A pretty simple example is:
(Results)
Hex
-------------------------------------------------------------------------------
0x41424344
Its time to drag out your ASCII charts and check to be sure its correct.
(Dont worryI checked, and its correct.) Here are some more data
types:
410 Part II
Chapter 18: Undocumented System UDFs
(Results)
Demo
-------------------------------------------------------------------------------
BIT 1 ->0x01<-
float 100 ->0x4059000000000000<-
Integer 100 ->0x00000064<-
Numeric (18,3) 100 ->0x12030001a0860100<-
ASCII Characters ABCD ->0x41424344<-
Unicode Characters ABCD ->0x4100420043004400<-
Summary
There are many useful undocumented UDFs. However, you may or may
not decide to use any of them due to their undocumented status. After all,
they could be changed by Microsoft in any service pack.
Mitigating any risk is the fact that the text for the functions is avail-
able. This chapter has shown you two places to find the CREATE FUNCTION
script for any of the undocumented UDFs:
n In master.dbo.syscomments by using the udf_Func_UndocSystemUDFtext
function
n In SQL files that are used during SQL Server installation
Part II 411
Chapter 18: Undocumented System UDFs
If you really want to use one of these functions, you might want to copy
the script, give it your own name, and make a normal UDF out of it.
Whats more, youll be able to create it using the WITH SCHEMABINDING
clause if theres any reason to use it in a schemabound object.
What you lose by turning a system UDF into a normal UDF is the
special status that system UDFs enjoy. That status can be important
enough for you to want to create your own system UDFs. For example,
when referencing scalar UDFs, such as fn_chariswhitespace, there was no
need to use the owner name prefix when referencing the UDF. Thats one
small advantage. The method for creating a system UDF is the subject of
the next and final chapter.
System UDFs
This page intentionally left blank.
19
Creating a System
UDF
The system user-defined functions can be used from any database and
invoked without referring to their database or owner. Those are powerful
advantages in simplifying the functions distribution. The chapters in Part
II have discussed the system UDFs distributed with SQL Server, both
documented and undocumented. You can make your own system UDFs
using the procedure outlined in this chapter.
Be aware that the procedure outlined here is not supported by
Microsoft and might cease working in some future release or service pack
of SQL Server. That might cause any SQL that uses the function to stop
working until all references to the UDF can be changed.
Note:
If this technique stops working, there is a quick fix: Create the function
in every database in which it is used and change all references to any
scalar functions to include the owner name.
differently than normal UDFs. The double colon syntax for system UDFs
that return tables is unique to system UDFs. Also, scalar system UDFs
arent required to include the owner name every time they are used.
This chapter highlights a third difference: Table references in system
UDFs are made to tables in the database in which the function is run, not
the database in which the function is created, which is always master. The
difference is necessary in order to make any general-purpose functions
that have table references.
Of course, any table references must be to tables that exist in the
database where the UDF is run. The most likely candidates are the set of
system tables that SQL Server puts into every database that it creates.
Before we take a look at how a system UDF behaves, lets start by
examining how an ordinary UDF that has table references works when it
is run from different databases. Its the contrast between the two types of
UDFs that really shows the differences between them.
SELECT @Result=COUNT(*)
FROM sysobjects
WHERE TYPE='U'
and (@table_name_pattern is NULL
or [name] LIKE @table_name_pattern)
Part II 415
Chapter 19: Creating a System UDF
RETURN @Result
END
Lets test this function. The first test should be performed in the
TSQLUDFS database where the function was created:
(Results)
Those look like reasonable numbers. The answer you see when you run
the query might be slightly different if tables have been added or dropped
from the database since the book was published.
Now, move into the pubs database and use the function as it is defined
System UDFs
in TSQLUDFS:
(Results)
The answer is the same! How can that be? Does pubs have 21 user tables,
four of which begin with the string Currency? I dont think so. Lets
check by running an equivalent query without using udf_Tbl_COUNT:
(Results)
Once updates are allowed in master, its possible to run a CREATE FUNCTION
script. The script to create fn_tbl_count is in the Chapter 19 Listing 0
Short Queries.sql file and shown in Listing 19.2. Its deliberately omitted
from TSQLUDFS.
System UDFs
DECLARE @Result int -- count
SELECT @Result=COUNT(*)
FROM sysobjects
WHERE TYPE='U'
and (@table_name_pattern is NULL
or [name] LIKE @table_name_pattern)
RETURN @Result
END
GO
This script includes the SET statements for connection options that should
be run before all UDFs are created and the GRANT permission is given to
PUBLIC. These were omitted from most listings in the book since Chapter
2 because the functions are already created. In this listing, the function
doesnt exist, and you must run the script to be able to execute any of the
queries in the rest of the chapter.
Leaving master in a state where updates are allowed would be inviting
trouble. After you create the UDF, you should turn off updates in master
with this script:
418 Part II
Chapter 19: Creating a System UDF
Now that fn_tbl_count has been created as a system function, lets run it
in master, TSQLUDFS, and pubs to see what answer it gives to the query
we tried with udf_Tbl_COUNT. Start in master:
(Results)
So master has a few user tables. You can use Enterprise Manager to check
the answer on your system.
Next, move to TSQLUDFS and see if we get the same answer that
was given by udf_Tbl_COUNT:
(Results)
(Results)
Thats right! fn_tbl_count is a system UDF, and as such it uses the tables
in the database in which its invoked (in this case pubs.dbo.sysobjects)
instead of the database in which it is created, master. If you dont want to
copy the function definition into every database, that difference in behav-
ior can make all the difference in the world.
Summary
This chapter and the other chapters in Part II showed the differences that
the special status accorded to system UDFs provides. The most important
difference illustrated here is the ability to reference tables in the database
where the function is running. That, plus the simpler syntax for referenc-
ing them, makes system UDFs special.
The third major difference, the ability to run reserved SQL syntax,
System UDFs
was discussed in Chapter 14. It should really be left to Microsoft. Using it
in your own UDFs is asking for trouble.
If you find yourself creating your own system UDFs, please remem-
ber that the technique shown in this chapter is an unsupported feature of
SQL Server and it might not always be around. Fortunately, there are
workarounds if something were to change, but you might find yourself
scrambling to fix your code.
This also concludes Part II, System User-Defined Functions.
The system UDFs are a useful feature introduced in SQL Server 2000. I
expect that well see more of them in future versions of SQL Server.
This chapter is also the last in this book. I hope that youve found it
interesting and useful in your work as a SQL Server developer or DBA.
The accompanying library of functions is yours to use in your own pro-
jects. I hope youll find a few functions that you can use and you have
learned the techniques that you need to create your own.
This page intentionally left blank.
Appendix A
Deterministic and
Nondeterministic
Functions
Although Books Online has information about which built-in functions are
deterministic and which are nondeterministic, the information is a bit
scattered. This appendix lists all of the built-in functions in SQL Server
2000 as of Service Pack 3. The Status column displays D for a built-in
function thats always deterministic, N for built-in functions that are never
deterministic, and C for built-in functions that are conditionally determin-
istic. The Comment column describes what factors make the difference
when using the conditionally deterministic functions.
Table A.1: Built-in functions and their status as deterministic
Status Function Name Comment
D ABS
D ACOS
N APP_NAME
D ASCII
D ASIN
D ATAN
D ATN2
D AVG
D BINARY_CHECKSUM
C CAST Deterministic unless used with datetime,
smalldatetime, or sql_variant. Use CONVERT
with datetime and smalldatetime for a
deterministic result.
D CEILING
D CHAR
N CHARINDEX
421
422
Appendix A: Deterministic and Nondeterministic Functions
N @@IO_BUSY
N IS_MEMBER
N IS_SRVROLEMEMBER
424
Appendix A: Deterministic and Nondeterministic Functions
N @@TRANCOUNT
N TYPEPROPERTY
D UNICODE
D UPPER
N USER
426
Appendix A: Deterministic and Nondeterministic Functions
Keyboard Shortcuts
for Query Analyzer
Debugging
Table B.1 shows the icons and the function key equivalents for the T-SQL
debugger windows. Books Online shows the icons, but I try to use func-
tion keys instead.
Table B.1: Debugging icons and keyboard shortcuts
Icon Keyboard Shortcut Function
F5 or Ctrl+E GO
F9 Toggle Breakpoint
Auto Rollback
F1 Help
427
This page intentionally left blank.
Appendix C
Implementation
Problems in
SQL Server 2000
Involving UDFs
During the course of writing this book, Ive encountered various problems
in the implementation of SQL Server 2000. Some of these, particularly the
first one, might be considered bugs. The status of the others is less clear.
These problems were verified as of SQL Server 2000 Service Pack 3:
n sp_rename doesnt work correctly on a UDF. Although the function
runs and the name is changed in sysobjects, the name isnt changed
in syscomments. Thus, when the UDF is scripted, its incorrect. This
problem may also occur with other object types.
n If you have a check constraint on the return TABLE variable of a
multistatement UDF, you cant alter the UDF. See udf_Exam-
ple_Runtime_Error_Multistatement in the TSQLUDFS database.
n @@ERROR is not available to T-SQL code after an error is generated
inside a UDF. See Chapter 5 for more details.
n COLUMNPROPERTY IsDeterministic doesnt work for computed columns
in functions. Its NULL. See Chapter 9.
n COLUMNPROPERTY IsPrimaryKey doesnt work for columns returned by
multistatement UDFs. See Chapter 9.
n OBJECTPROPERTY IsPrimaryKey and IsQuotedIdentOn return NULL for
UDFs. See Chapter 9.
n SQL Server wont allow the use of sp_trace_generateevent in a UDF,
even though its an extended stored procedure.
429
430
Appendix C: Implementation Problems in SQL Server 2000 Involving UDFs
431
432
Function Type Page Description
fn_tbl_count SCALAR 417 User-created system UDF to return the number of user tables in the database
in which it is run.
getcommaname SCALAR 113 Example UDF to show really bad formatting.
udf_BBTeam_AllPlayers TABLE 168 Returns a list of all players on all BBTeams.
udf_BBTeams_LeagueTAB INLINE 142 Returns the ID, name, and manager of all teams in a league.
udf_Bit_Int_NthBIT SCALAR Returns the Nth bit in a int. Bits are numbered with zero beginning with the
least-significant bit up to 31, which is the sign bit of the int. If @Nth is > 31,
it returns NULL.
User-Defined Functions
udf_BitS_FromInt SCALAR 397 Translates an int into a corresponding 32-character string of 1s and 0s. It will
optionally trim leading zeros.
udf_BitS_FromSmallint SCALAR Translates a smallint into a corresponding 16-character string of 1s and 0s. It
will optionally trim leading zeros.
udf_BitS_FromTinyint SCALAR Translates a tinyint into a corresponding 8-character string of 1s and 0s. It
will optionally trim leading zeros.
udf_BitS_ToInt SCALAR Converts a bit string of up to 32 characters into the corresponding int. The
string is padded on the left with any missing zeros.
udf_BitS_ToSmallint SCALAR 401 Converts a bit string of up to 16 one and zero characters into the correspond-
ing smallint. The string is padded on the left to fill in any missing zeros.
udf_BitS_ToTinyint SCALAR 402 Converts a bit string of up to eight one and zero characters into the corre-
sponding tinyint. The string is padded on the left to fill in any missing zeros.
The result is a value from 0 to 255. There are no negative tinyint values.
udf_Category_BigCategoryProductsTAB TABLE 159 Returns a table of product information for all products in all categories with at
least @MinProducts products.
udf_Category_ProductCountTAB INLINE 138 Categories and their count of products.
udf_Category_ProductsTAB INLINE Returns a table of product information for all products in the named category.
Function Type Page Description
udf_Currency_DateStatus SCALAR 288 Used together with udf_Currency_XchangeNearDate to understand the status
of the exchange rate.
udf_Currency_RateOnDate SCALAR Converts from one currency to another on a specific date. The date must be
in the database or the result is NULL.
udf_Currency_StatusName SCALAR Returns a meaningful name for the return code from udf_Cur-
rency_DateStatus.
udf_Currency_XlateNearDate SCALAR 286 Converts from one currency to another using a rate thats on or near the
specified date. If the date is not found in the table, an approximate result is
returned.
udf_Currency_XlateOnDate SCALAR 284 Converts from one currency to another on a specific date. The date must be
in the database or the result is NULL.
udf_DT_2Julian SCALAR Returns the Julian date the number of days since 1990-01-01.
udf_DT_Age SCALAR Returns the age of a person, in years, for a given date of birth, as of a given
date. If no @AsOfDate is supplied, then today is used.
udf_DT_CurrTime SCALAR 87 Returns the time as a CHAR(8) in the form HH:MM:SS. This function bypasses
SQL Servers usual restriction against using getdate by selecting it from a
view.
udf_DT_DaysTAB TABLE Returns one row for each day that falls in a date range. Each date is the SOD.
udf_DT_dynamicDATEPART SCALAR Does the same job as DATEPART but takes a character string as the datepart
instead of having the datepart in the script.
udf_DT_FileNameFmt SCALAR Formats a date/time so that it can be used in a file name. It changes any
colons and periods to dashes and includes only the parts the user requests.
Time and milliseconds are optional.
udf_DT_FromYMD SCALAR Returns a smalldatetime giving the Year, Month, and Day of the month.
User-Defined Functions
433
udf_DT_WeekdayNext SCALAR Returns the next weekday after @Date. Always returns a start-of-day
(00:00:00 for the time). Takes into account @@DATEFIRST and works with
any setting, but it is always based on Saturday and Sunday not being
weekdays.
udf_DT_WeekdaysBtwn SCALAR Computes the number of weekdays between two dates. Does not account for
holidays. Theyre counted if theyre M-F. It counts only one of the end days.
So if the dates are the same, the result is zero. Order does not matter. Will
swap dates if @dtEnd < @dtStart. This function is sensitive to the setting of
@@DATEFIRST. Any value other than 7 (the default) would make the results
incorrect, so a test for this condition causes the function to return NULL when
@@DATEFIRST is not 7.
udf_EP_AllTableLevel2EPsTAB TABLE 329 Returns a table of extended properties for a property (or NULL for all), for an
owner (or NULL for all), for a table (or NULL for all) in the database. The
Level 2 object name must be specified (NULL means on the table itself). The
Level 2 object name may be given to get info-specific Level 2 object or use
NULL for all Level 2 objects.
udf_EP_AllUsersEPsTAB TABLE 326 Returns the extended property value for a property (or NULL for all properties)
on all USERS in the database. The Level 1 object type must be specified. Use-
ful for finding a property for all tables, views, etc. The cursor is needed
because it does not assume that every object is owned by dbo.
Function Type Page Description
udf_Example_Inline_WithComputedColumn INLINE Inline UDF that returns a table with a computed column. It turns out that the
column isnt marked as computed even though its the result of an expres-
sion. Run sp_help to see that PRODUCT isnt considered computed by SQL
Server.
udf_Example_Multistatement_With TABLE Example UDF that returns a table with a computed column. Use sp_help to
ComputedColumn see that SQL Server considers PRODUCT a computed column.
udf_Example_OAhello SCALAR 225 Example UDF to exercise the TSQLUDFVB.cTSQLUDFVBDemo OLE automa-
tion object.
udf_Example_Palindrome SCALAR 30 A palindrome is the same from front to back as it is from back to front. This
function doesnt ignore punctuation as a human might.
udf_Example_Runtime_Error SCALAR 102 Example UDF to demonstrate what happens when an error is raised by a SQL
statement.
udf_Example_Runtime_Error_Inline INLINE 104 Example UDF to demonstrate what happens when an error is raised by a SQL
statement. This is an inline UDF.
udf_Example_Runtime_Error_MultiStatement TABLE Example UDF to demonstrate what happens when an error is raised by a SQL
statement. It returns a table of results.
udf_Example_TABLEmanipulation SCALAR 35 An example UDF to demonstrate the use of INSERT, UPDATE, DELETE, and
SELECT on table variables. Uses data from the Northwind Employees table.
udf_Example_Trc_EventListBadAttempt SCALAR This is an incorrect attempt to write the equivalent of udf_Trc_EventList. See
the text for a further explanation.
udf_Example_User_Event_Attempt SCALAR 79 Example UDF that attempts to call the extended stored procedure
sp_trace_generateevent. This demonstrates that the call fails with server mes-
sage 557 in spite of sp_trace_generateevent being an extended and not a
regular stored procedure.
udf_Exchange_MembersList INLINE 130 Returns a table with a list of all brokers who are members of the exchange
User-Defined Functions
@ExchangeCD.
435
udf_Perf_FS_ByDbTAB INLINE 349 Returns a table of total statistics for one database or a group of databases
where the name matches a pattern. NULL for all. Done by grouping by
database.
udf_Perf_FS_ByDriveTAB INLINE 352 Returns a table of statistics by drive letters for all drives with database files in
this instance. They must match @Driveletter (or NULL for all). Returns one
row for each drive. Information about physical files is taken from mas-
ter..sysaltfiles, which has the physical file name needed. Warning: Drive letters
do not always correspond to physical disk drives.
udf_Perf_FS_ByPhysicalFileTAB INLINE 350 Returns a table of statistics for all files in all databases in the server that
match the @File_Name_Pattern. NULL for all. The results are one row for
each file including both data files and log files. Information about physical
files is taken from master..sysaltfiles, which has the physical file name
needed.
udf_Perf_FS_DBTotalsTAB TABLE 348 Returns a table of total statistics for one particular database by summing the
statistics for all of its files.
udf_Perf_FS_InstanceTAB INLINE Returns a table of statistics for all databases in the instance. There is only one
row for each database, aggregating all files in all databases.
udf_Proc_WithRecompile INLINE Returns a table of names of procedures that have been created with the WITH
RECOMPILE option. These procedures are never cached and require a new
plan for every execution. This can be a performance problem.
Function Type Page Description
udf_Session_OptionsTAB TABLE 95 Returns a table that describes the current execution environment inside this
UDF. This UDF is based on the @@OPTIONS system function and a few
other @@ functions. Use DBCC USEROPTIONS to see some current options
from outside the UDF environment. See the BOL section titled User Options
Option in the section Setting Configuration Options for a description of
each option. Note that QUOTED_IDENTIFER and ANSI_NULLS are parse time
options and the code to report them has been commented out. See also MS
KB article 306230.
udf_SQL_DataTypeString SCALAR Returns a data type with full length and precision information based on fields
originally queried from INFORMATION_SCHEMA.ROUTINES or from
SQL_VARIANT_PROPERTIES. This function is intended to help when reporting
on functions and about data.
udf_SQL_DefaultsTAB INLINE Returns a table of all defaults that exist on all user tables in the database.
udf_SQL_LogMsgBIT SCALAR 203 Adds a message to the SQL Server log and the NT application event log.
Uses xp_logevent. xp_logevent can be used in place of this function. One
potential use of this UDF is to cause the logging of a message in a place
where xp_logevent cannot be executed, such as in the definition of a view.
udf_SQL_StartDT SCALAR 344 Returns the date/time that the SQL Server instance was started.
udf_SQL_UserMessagesTAB INLINE Returns a table of user messages in the SQL Server.
udf_SQL_VariantToDatatypeName SCALAR Returns the data type name of a sql_variant.
udf_SQL_VariantToStringConstant SCALAR 404 Converts a value with the type sql_variant to a string constant that is the same
as the constant would appear in a SQL statement.
udf_SQL_VariantToStringConstantLtd SCALAR Converts a value with the type sql_variant to a string constant that is the same
as the constant would appear in a SQL statement. The length of the constant
is limited for display purposes to a specified length. Ellipsis is used when trun-
cation is performed to let the caller know that it has happened. This may
User-Defined Functions
udf_Tbl_COUNT SCALAR 414 Returns the number of user tables in the database that match
@table_name_pattern.
udf_Tbl_DescriptionsTAB INLINE 325 Returns the description extended property for all user tables in the database.
udf_Tbl_InfoTAB INLINE Returns a table of information about a table. Information is from sysobjects,
sysowner, sysindexes, extended properties, and OBJECTPROPERITES.
Intended as input to a report writer.
udf_Tbl_MissingDescrTAB INLINE 332 Returns the schema name and table name for all tables that do not have the
MS_Description extended property.
udf_Tbl_NotOwnedByDBOTAB INLINE Returns a table of tables not owned by DBO.
udf_Tbl_RowCOUNT SCALAR Returns the row count for a table by examining sysindexes. This function must
be run in the same database as the table.
udf_Tbl_RptW INLINE 333 Returns a report of information about a table. Information is from sysobjects,
sysowner, extended properties, and OBJECTPROPERITES. Intended to output
from Query Analyzer. It can be sent to a file and then printed from the file
with landscape layout.
udf_Test_ConditionalDivideBy0 SCALAR 108 Creates a divide-by-zero error conditionally based on the @bCauseError
parameter.
Function Type Page Description
udf_Test_Quoted_Identifier_Off SCALAR 93 Test UDF to show that its the state of the quoted_identifier setting when the
UDF is created that matters, not the state at run time. This UDF was created
when quoted_identifier was OFF.
udf_Test_Quoted_Identifier_On SCALAR 93 Test UDF to show that its the state of the quoted_identifier setting when the
UDF is created that matters, not the state at run time. This UDF was created
when quoted_identifier was ON.
udf_Test_RenamedToNewName SCALAR Example UDF to show that sp_rename doesnt work correctly for user-defined
functions.
udf_Title_AuthorsTAB INLINE Returns a table of information about all authors of the title.
udf_Titles_AuthorList SCALAR 170 Returns a comma separated list of the last name of all authors for a title.
udf_Titles_AuthorList2 SCALAR 171 Returns a comma separated list of the last name of all authors for a title. Illus-
trates a technique for an aggregate concatenation.
udf_Trc_ColumnCount SCALAR Returns the number of columns being reported by a trace.
udf_Trc_ColumnList SCALAR Returns a comma separated list of columns being monitored by a trace. Each
column is given by its name.
udf_Trc_ColumnName SCALAR 374 Translates a SQL Trace ColumnID into its descriptive name. Used when
retrieving event information from fn_trace_geteventinfo.
udf_Trc_ComparisonOp SCALAR Translates a SQL Trace comparison operator code into its text equivalent.
Used when retrieving event information from fn_trace_getfilterinfo. The code
numbers are originally used when creating the filter with sp_trace_setfilter.
udf_Trc_EventCount SCALAR Returns the number of events being reported by a trace.
udf_Trc_EventList SCALAR 375 Returns a separated list of events being monitored by a trace. Each event is
given by its name. The separator supplied is placed between entries. This
could be N', ' or NCHAR(9) for TAB.
User-Defined Functions
441
udf_Trc_FilterExpression SCALAR 381 Returns a separated list of Filters being monitored by a trace. Each Filter is
given by its name. The separator supplied is placed between entries. This
could be ', ' or NCHAR(9) for TAB.
udf_Trc_InfoExTAB INLINE Returns a table of information about a trace. These are the original argu-
ments to sp_trace_create and to the other sp_trace_* procedures. The status
field for the trace is broken down to four individual fields. The list of events,
list of columns, and the filter expression are each string columns. There are
two arguments available to provide the separator character one for the two
lists and the other for the filter expression. When displaying the results, the
defaults work well. When passing the lists to a wrapping function, such as
udf_Text_WrapList, a tab (NCHAR(9)) is cleaner.
udf_Trc_InfoTAB INLINE 363 Returns a table of information about a trace. These are the original argu-
ments to sp_trace_create. The status field is broken down to four individual
fields.
udf_Trc_LogicalOp SCALAR Translates a SQL Trace Logical operator code into its text equivalent. Used
when retrieving event information from fn_trace_getfilterinfo. The code was
originally used when creating the filter with sp_trace_setfilter. Result is 0 for
AND or 1 for OR.
udf_Trc_ProfilerUsersTAB INLINE Table listing user information for processes running SQL Profiler.
Function Type Page Description
udf_Trc_RPT TABLE 384 Returns a report of information about a trace. These are the original argu-
ments to sp_trace_create and to the other sp_trace_* procedures. The status
field for the trace is broken down to four individual fields. The list of events,
list of columns, and the filter expression are each on their own line or
wrapped to multiple lines.
udf_Trc_SetStatusMSG SCALAR Returns the textual equivalent of a return code from sp_trace_setstatus.
udf_Trig_TANH SCALAR Returns the hyperbolic tangent of a number. Its equal to sinh(x)/cosh(x) or the
expression found below. See http://mathworld.wolfram.com/
HyperbolicTangent.html.
udf_Txt_CharIndexRev SCALAR 18 Searches for a string in another string working from the back. It reports the
position (relative to the front) of the first such expression that it finds. If the
expression is not found, it returns zero.
udf_Txt_FixLen SCALAR Returns the input string in a character string of a specified length.
udf_Txt_FixLenR SCALAR Right justifies @sSource String in a field of @nTargetLength. Padding is done
with @sPadCharacters.
udf_Txt_FmtInt SCALAR Formats an integer with no decimal and right justification to the specified
number of characters, padding with @sPadChar.
udf_Txt_FullLen SCALAR Returns the length of a character string including trailing blanks. The built-in
LEN function excludes trailing blanks when it calculates the length. This func-
tion uses a sql_variant so that both Unicode and ANSI strings can be @input.
udf_Txt_IsPalindrome SCALAR A palindrome is the same from front to back as it is from back to front. This
function removes spaces, periods, question marks, quotes, and double quotes
and then checks to see if the string is the same in both directions.
udf_Txt_RemoveChars SCALAR Removes characters from a string.
User-Defined Functions
443
line and subsequent lines to be padded for alignment with other text.
udf_Unit_CONVERT_Distance SCALAR 260 Converts a distance measurement from any unit to any other unit.
udf_Unit_EqualFpBIT SCALAR 269 Checks for equality of two floating-point numbers within a specific number of
digits. Returns 1 if equal, otherwise 0.
udf_Unit_EqualNumBIT SCALAR 271 Checks for equality of two floating-point numbers within a specific number of
digits by converting to numeric and comparing those digits.
udf_Unit_Km2Distance SCALAR 258 Returns a distance measure whose input is a kilometer measurement. The
parameter @UnitSystemOfOutput requests that the output be left as kilome-
ters M or converted to the U.S. standard system unit miles signified by a
U.
udf_Unit_Km2mi SCALAR 263 Converts kilometers to miles.
udf_Unit_KmFromDistance SCALAR Returns a kilometer measure whose input is a distance measurement in the
unit system given by the parameter @UnitSystemOfOutput.
udf_Unit_lb2kg SCALAR 267 Converts pounds (advp) to kilograms (kg).
udf_Unit_mi2Km SCALAR Converts miles to kilometers.
udf_Unit_mi2m SCALAR 256 Converts miles to meters.
Function Type Page Description
udf_Unit_Rounding4Factor SCALAR 254 Returns the number of digits of precision that a units conversion is performed
on a measurement with @nDigits2RtofDecimal of precision and a conversion
factor of @fConversionFactor. The result is intended to be used as input into
the ROUND function as the length parameter.
udf_Unit_RoundingPrecision SCALAR Returns the number of digits of precision in a result after a units conversion is
performed on a measurement with @nDigits2RtOfDecimal of precision and a
conversion factor of @fConversionFactor. The result is intended to be used as
input into the ROUND function as the length parameter.
udf_View_ColumnList SCALAR 407 Creates a quoted list of the columns in a view, suitable for creation of a
dynamic SQL statement. Depends on INFORMATION_SCHEMA.VIEW_
COLUMN_USAGE. This prevents use with views with only one column.
User-Defined Functions
445
Index
.NET, 17 CAST, 251-252, 272
@@DATEFIRST, 84-85, 234 determinism, 421
@@ERROR, 100, 103-105, 296 CGI, 146
@@ROWCOUNT, 100 char data type, 23
@@SPID, 305-306 CHARINDEX, 236, 239
@@Total_Read, 346-347 CHECK clause, 157
@@Total_Write, 346-347 CHECK constraint, 32, 44, 52-56, 100
COALESCE, 282
A COBOL, 213
Access 2002, 64 code reuse, 16
ADO, 60 code tables, replacing with UDF, 172
algorithm, 123 collations, 299-300
ALTER FUNCTION, 25-26 COLUMNPROPERTY, 58, 183, 186-187
permission, 25-27, 133, 156 IsDeterministic, 429
ALTER TABLE, 54 IsPrimaryKey, 429
AND operator short-circuit, 47-48, 207 COM, 223
ANSI_NULLS, 60, 92, 95 COM Interop, 223
ANSI_PADDING, 60, 92 COMMIT TRAN statement restrictions, 88
ANSI_WARNINGS, 60, 92 component design, 17
MAX function, 363 COMPUTE, 135, 274
ARITHABORT, 60-61, 92 COMPUTE BY, 135
ARITHIGNORE, 106 computed column, 44, 52-53, 56-58, 60
AS BEGIN, 29 CONCAT_NULL_YIELDS_NULL, 60, 92
ASP, 17, 146-148 CONSTRAINT clause of CREATE TABLE,
ASP.NET, 146-148 160-161
attribution, 120 CONSTRAINT_TABLE_USAGE
INFORMATION_SCHEMA view, 184
B CONVERT, 251, 252, 272
BASIC, 213 determinism, 422
BEGIN TRAN statement restrictions, 88 convert.exe, 249
bigint, 23 copyright, 122
binary, 23 CREATE FUNCTION, 4-9, 24
bit, 23, 269 permission
BitS, 125 inline, 134
blackbox trace, 362 multistatement, 156
Boolean, 269 scalar, 25-27
syntax, 28
C CREATE INDEX, 78
C, 197 CREATE RULE, 282
C++, 129, 197 CREATE TABLE, 32
C++ WINERORR.H file, 215 CREATE UNIQUE CLUSTERED INDEX,
C2 security, 355 43
CacheHit, 75 CREATE VIEW, 86
Carberry, Josiah, 320-321 cryptography, 227
CASE expression used in pivoting, 363-364 Crystal Reports, 250
447
448
Index
D E
DATA_TYPE column of INFORMATION_ ENCRYPTION, 29, 36, 118
SCHEMA.ROUTINES, 184 Enterprise Manager, 80-81
DATEDIFF, 296 generate SQL scripts, 80
DATEFIRST, 91, 234 see also server logs, 111, 200
@@DATEFIRST SQL-DMO, 196
DATENAME, 59, 84 use of MS_Description, 316-317
DATEPART, 59, 84, 91, 230 equivalent template, 238
datetime, 23 error handling, 99-112
db_ddladmin, 25-27 Event Viewer, 201-202
DB_ID, 339 EventRPC, 305
DB_Name, 339 example, 120
db_owner, 27 Excel, 251
DBCC, 78, 90 exclusive lock, 310
DBCC INPUTBUFFER, 305-306, 308 EXEC, 34, 36, 51-52, 297
DBCC PINTABLE, 241, 246 permission, 4, 10, 44-45
DBCC TRACEOFF(2861), 307 restrictions, 88
DBCC TRACEON(2861), 307 ExecIsQuotedIdentOn, 95
DBCC UNPINTABLE, 265 EXECUTE statement, see EXEC
DBCC USEROPTIONS, 97 extended stored procedure, 36, 197-228
DDL, 24, 31-32, 44 restrictions, 88
DEBUG_udf_DT_NthDayInMon, 65-69
debugger F
Callstack, 68 FILE_ID, 340, 350
Globals window, 68 FILE_NAME, 340, 250
Locals window, 69 filegroup, 32
debugging UDFs, 65-70 FileSystemObject, 211-212, 214
decimal, 23 fillfactor, 32
DECLARE, 24, 31 float, 23, 252
DEFAULT, 148 fn_chariswhitespace, 388-389, 392-393
DELETE, 10, 24, 50, 88, 133 fn_dblog, 389, 394-396
delimited text, 165 fn_generateparameterpattern, 389
DENY statement, 27, 45 fn_get_sql, 296, 300, 301, 307-311
deterministic functions, 83, 87, 421 fn_getpersistedservernamecasevariation,
development time, 291 389
DISTINCT, 141, 375-376 fn_helpcollations, 298-302
DML, 24, 31-32, 44, 50 fn_isreplmergeagent, 389
documentation, 113
domain, 125, 129 fn_listextendedproperty, 300, 313-335, 389
domain error, 106 NULL arguments, 320-324
double-dash comment, 116 output columns, 319-320
DriveExists, 212 syntax, 317
DROP FUNCTION, 25-26 fn_MSFullText, 389
permission fn_MSgensqescstr, 389
inline, 133 fn_MSsharedversion, 389
multistatement, 156 fn_mssharedversion, 396
scalar, 25-27 fn_removeparameterwithargument, 389
449
Index
Index
fn_replcomposepublicationsnapshotfolder, HRESULT, 215
389 Hungarian notation, 129
fn_replgenerateshorterfilenameprefix, 389
fn_replgetagentcommandlinefromjobid, 389 I
fn_replgetbinary8lodword, 389 I/O activity, 338, 344, 351
fn_replinttobitstring, 389, 396-397 IDENTITY column, 157, 176, 179
fn_replmakestringliteral, 389, 403 IF ELSE statement, 24, 31, 33
fn_replprepadbinary8, 389 image, 23
fn_replquotename, 389, 406-409 Imperial system of measures, 248
fn_replrotr, 389 Index Wizard, 370
fn_repltrimleadingzerosinhexstr, 389 indexed views, 39-43
fn_repluniquename, 389 indexes on computed columns, 52-53, 58-61
fn_serverid, 389-390, 392 INFORMATION_SCHEMA, 175, 182-183,
fn_servershareddrives, 300, 304, 389 187
fn_skipparameterargument, 389 .CONSTRAINT_TABLE_USAGE, 184
fn_sqlvarbasetostr, 389 .KEY_COLUMN_USAGE, 184
fn_tbl_count, 416-418 .PARAMETERS, 184, 185, 192
fn_trace_*, 300, 355-386 .ROUTINE_COLUMNS, 184, 185
fn_trace_geteventinfo, 300, 356, 372-383, .ROUTINES, 180, 184, 388
389 .TABLES, 329-330
fn_trace_getinfo, 300, 356, 358, 360-363, 389 .VIEW_COLUMN_USAGE, 407
fn_trace_gettable, 300, 356, 370-372, 389 inline UDF, 9-11, 83, 133-153
fn_updateparameterwithargument, 389 INSERT, 24
fn_varbintohexstr, 389, 409-410 permission, 10, 133
fn_varbintohexsubstring, 389 restrictions, 88
fn_virtualfilestats, 300, 337-354, 389 VALUES clause, 50
fn_virtualservernodes, 300, 304, 389 instance, 125, 344
FOR BROWSE, 135 instdst.sql, 390
FOR XML, 135 int, 23
foreign key constraint, 157 Intellisense, 123-124
formatting, 113 INTO, 135
FORTRAN, 17, 213 ISDATE determinism, 424
FROM clause multistatement, 155 ISO 4271, 279
function body, 29, 31 IsPrimaryKey COLUMNPROPERTY, 187
Function_Assist_GETDATE, 86, 345 ISQL, 63-64, 80
IsQuotedIdentOn OBJECTPROPERTY, 187
G
getcommaname, 113 J
GETDATE, 59, 84-85, 86 JOIN, 49-50, 135
GOTO, 31, 33, 212 Jscript, 212
GRANT, 4, 10, 27-28 JSP, 146
inline UDF, 142
multistatement, 161 K
group, 125 KEY_COLUMN_USAGE
GROUP BY clause, 141 INFORMATION_SCHEMA view, 184
H L
HAVING, 141 LAZY WRITER, 344
header, 118-123 LEFT OUTER JOIN, 50
LOG WRITER, 344
450
Index
replace template parameters, 72-73, 163, 238 SET statement, 34, 50-51, 78
replcomm.sql, 390 SET STATISTICS TIME, 242-244
Index
replsyst.sql, 390 SET XACT_ABORT, 101
repltran.sql, 390 SFF, 115-118
RETURN statement, 29 short-circuit, 47, 207
RETURNS clause multistatement, 157 SHUTDOWN_ON_ERROR, 363
reusable code, 16 side effects, 23-24
REVERSE, 236, 239 smalldatetime, 23
REVOKE statement, 27, 45 smallint, 23
ROLLBACK TRAN, 88, 311 smallmoney, 23
ROUND, 251, 253, 258 SP:CacheHit, 74, 127-128
rounding, 271 SP:CacheInsert, 74
ROUTINE_COLUMNS INFORMATION_ SP:CacheMiss, 74, 127-128
SCHEMA view, 184-186 SP:CacheRemove, 74
ROUTINE_DEFINITION of INFOR- SP:Completed, 74
MATION_SCHEMA.ROUTINES, 180 SP:ExecContextHit, 74, 127-128
ROUTINE_TYPE of INFORMATION_ SP:Recompile, 74
SCHEMA.ROUTINES, 184 SP:Starting, 74
ROUTINES INFORMATION_SCHEMA SP:StmtCompleted, 74, 127-128
view, 184 SP:StmtStarting, 74, 127-128
ROWGUID column, 176, 179, 157 sp_addextendedproperty, 314-315, 321-322
RPC:Complete, 74-75 sp_addtype, 129, 281
RPC:Output Parameter, 74 sp_configure, 416
RPC:Starting, 74-75 sp_cycle_errorlog, 200
sp_depends, 176, 182
S sp_displayoaerrorinfo, 213, 216
SAS, 107 sp_dropextendedproperty, 314-315, 321, 335
scalar UDF, 4-9, 23-62 sp_help, 176-179
SCHEMABINDING, 29, 36, 39-43, 118, 130, inline, 177
416, multistatement, 179
conflict with UDT, 280, 283 scalar, 177
in multistatement, 161 sp_helptext, 176, 180, 299, 388
in template, 136 sp_hexadecimal, 213, 216
two-part name, 135, 138 sp_monitor, 353
SCOPE_IDENTITY, 100 sp_OA*, 198, 210-228
Scripting.FileSystemObject, see sp_OACreate, 210, 213, 221, 225-226
FileSystemObject sp_OADestroy, 210, 215
SELECT, 6-9, 24, 34, sp_OAGetErrorInfo, 210
permission, 10, 133, 156 sp_OAGetProperty, 210
separator first formatting, see SFF sp_OAMethod, 210, 214, 222, 225-226
server trace, 361 sp_OASetProperty, 210, 225-226
SERVERPROPERTY, 396 sp_OAStop, 210
SESSION object, 148 sp_processmail, 199
SESSIONPROPERTY, 93, 95 sp_recompile, 78, 430
SET ANSI_NULLS, 136, 161 sp_rename, 176, 181, 429
SET ARITHIGNORE, 106 sp_revokelogin, 199
SET clause, 50 sp_trace_*, 355
SET DATEFIRST, 92, 234 sp_trace_create, 358
SET NOCOUNT ON, 90-91 sp_trace_generateevent, 79, 429
SET QUOTED_IDENTIFIER, 91, 136, 161 sp_trace_setevent, 358-359, 373-374
SET session options, 50, 90-91 sp_trace_setfilter, 358-358
452
Index
Index
udf_Currency_DateStatus, 288-291 udf_Order_Amount, 53-56, 78, 182
udf_Currency_XlateNearDate, 284-287, 289 udf_Paging_ProductsByUnits_Forward,
udf_Currency_XlateOnDate, 283-284 149-150
udf_DT_2Julian, 73 udf_Paging_ProductsByUnits_Reverse,
udf_DT_Age, 76, 185 151-153
udf_DT_CurrTime, 65, 86-87 udf_Perf_FS_ByDbTAB, 337
udf_DT_dynamicDATEPART, 230-231 udf_Perf_FS_ByDriveTAB, 337, 351-352
udf_DT_MonthsTAB, 12-16, 49, 158 udf_Perf_FS_ByPhysicalFileTAB, 337,
udf_DT_NthDayInMon, 44-49, 51, 54, 57-59, 350-351
65, 68, 422 udf_Perf_FS_DBTotalsTAB, 337, 348-349
udf_DT_SecSince, 84-85 udf_Perf_FS_Instance, 347
udf_DT_TimePart, 41-43 udf_SESSION_OptionsTAB, 75, 95-97
udf_DT_WeekdayNext, 232-234 udf_SQL_IsOK4Index, 95
udf_DT_WeekDaysBtwn, 46, 48 udf_SQL_LogMsgBIT, 110, 203-206, 220
udf_EmpTerritoriesTAB, 9-11, 20 udf_SQL_StartDT, 344-345
udf_EmpTerritoryCOUNT, 7-8, 47 udf_SQL_VariantToDataTypeName, 268
udf_EP_AllTableLevel2EPsTAB, 328-330 udf_SQL_VariantToStringConstLtd, 379,
udf_EP_AllUsersEPsTAB, 325-327 403-404
udf_Example_OAHello, 225, 227 udf_SYS_DriveExistsBIT, 221-222
udf_Example_Palindrome, 30 udf_Tax_TaxabilityCD, 173
udf_Example_Runtime_Error, 100, 102 udf_Tbl_ColDescriptionsTAB, 313, 328, 331
udf_Example_Runtime_Error_Inline, 104 udf_Tbl_ColumnsIndexableTAB, 58-59
udf_Example_Runtime_Error_ udf_Tbl_Count, 414-416
Multistatement, 104, 429 udf_Tbl_DescriptionsTAB, 313, 324, 325
udf_Example_TABLEmanipulation, 35 udf_Tbl_InfoTAB, 334
udf_Example_Trc_EventListBadAttempt, udf_Tbl_MissingDescrTAB, 313, 331-332
376 udf_Tbl_RptW, 313, 333-334
udf_Example_user_Event_Attempt, 79 udf_Test_ConditionalDivideBy0, 108-109
udf_Example_With_Encryption, 36-38 udf_Test_Quoted_Identifier_Off, 93-94
udf_Exchange_MembersList, 130 udf_Test_Quoted_Identifier_On, 93-94
udf_Func_ColumnsTAB, 178, 190-191 udf_Test_SET_DATEFIRST, 91
udf_Func_COUNT, 189 udf_Titles_AuthorList, 169-172
udf_Func_InfoTAB, 189 udf_Titles_AuthorList2, 171-172
udf_Func_ParmsTAB, 176, 192 udf_Trace_InfoTAB, 356
udf_Func_Type, 189 udf_Trc_ColumnCount, 376
udf_Func_UndocSystemUDFtext, 388, 391, udf_Trc_ColumnList, 376
410 udf_Trc_ColumnName, 373-374
udf_Instance_EditionName, 396 udf_Trc_ComparisonOp, 377-378, 382
udf_Instance_UptimeSEC, 345 udf_Trc_EventCount, 376
udf_Name_Full, 4-6 udf_Trc_EventList, 375-376, 381
udf_Name_FullWithComma, 114, 116-117, udf_Trc_EventListCursorBased, 374-375
121 udf_Trc_EventName, 371
udf_Name_SuffixCheckBIT, 125-127 udf_Trc_FilterClause, 379-380
udf_Num_FactorialTAB, 158 udf_Trc_FilterExpression, 381-383
udf_Num_IsPrime, 33 udf_Trc_InfoExTAB, 262-363, 369, 372, 376,
udf_Num_LOGN, 106 381, 384
udf_OA_ErrorInfo, 217, 220 udf_Trc_LogicalOp, 378, 382
udf_OA_LogError, 219-220, 222-223, udf_Trc_ProfileUsersTAB, 369
225-226 udf_Trc_Rpt, 356-357, 372, 384-386, 394
454
Index
udf_Trc_SetStatusMSG, 367 W
udf_Txt_CharIndexRev, 18-19, 236-239 WAITFOR, 33
udf_Txt_SplitTAB, 165-167, 169 Washington State Department of Transporta-
udf_Txt_Sprintf, 208 tion, see WSDOT
udf_TxtN_WrapDelimiters, 333, 386 WHERE, 7, 48, 50, 148-149
udf_Unit_CONVERT_Distance, 260-266 WHILE STATEMENT, 24, 31, 33-34
udf_Unit_EqualFpBIT, 269-273 Windows Scripting Host, 212-211, 230
udf_Unit_EqualNumBIT, 271-273 WINERROR.H, 215
udf_Unit_Km2Distance, 258-260, 266 WITH, 116
udf_Unit_Km2mi, 266 WITH CHECK OPTION, 135, 142-145
udf_Unit_lb2kg, 266-267 WITH ENCRYPTION, see ENCRYPTION
udf_Unit_mi2m, 255-257 WITH GRANT OPTION, 27, 44
udf_Unit_Rounding4Factor, 254-256, 268 WITH SCHEMABINDING, see
udf_Unit_RoundingPrecision, 254 SCHEMABINDING
udf_View_ColumnList, 407-408 WITH VIEW_METADATA, 135
UDT, 29, 129-130, 132, 279 WMI, 227
undocumented system UDFs, 387 WSDOT, 249
UNION, 167
UNION ALL, 172 X
UNIQUE, 32 XACT_ABORT, 101
uniqueidentifier, 23 xp_cmdshell, 198
updatable inline UDF, 133, 141-145 xp_deletemail, 198
UPDATE, 10, 133 xp_enumgroups, 198
restrictions, 88 xp_findnextmsg, 198
SET clause, 50 xp_gettable_dblib, 198
WHERE clause, 50 xp_grantlogin, 198
U.S. standard system of measures, 248 xp_hello, 199
user-defined type, see UDT xp_logevent, 88, 109, 199-207
userEvents, 79 xp_loginconfig, 199
usp_Admin_TraceStop, 367-369 xp_logininfo, 199
usp_CreateExampleNumberString, 240-241 xp_msver, 199
usp_Example_Runtime_Error, 100-101 xp_readerrorlog, 201
usp_ExampleSelectWithOwner, 127-128 xp_readmail, 199
usp_ExampleSelectWithoutOwner, 127-128 xp_revokelogin, 199
usp_SQL_MyLogRpt, 201-202, 205-206 xp_sendmail, 199
xp_snmp_getstate, 199
V xp_snmp_raisetrap, 199
VALUES clause, 50 xp_sprintf, 199, 208-210
varbinary, 23, 409 xp_sqlmant, 199
varchar, 23 xp_sscanf, 199
VB Script, 175, 212, 249 xp_startmail, 199
VB .NET, 223, 230 xp_stopmail, 199
VBA lack of metric conversion functions, xp_trace_*, 199
249
Visual Basic, 6, 17, 129, 175, 197, 223, 227,
249
Visual Sourcesafe, 227
Visual Studio, 64, 123-124
Visual Studio .NET, 64, 70
T-SQL UDF of the Week
Newsletter
If youve read this far, youre probably interested in the topic of user-
defined functions. The T-SQL User-Defined Function of the Week News-
letter is a free service of Novick Software. Each issue has a new UDF
with a short article describing how its used. Subscribing to the UDF of
the Week Newsletter is a great way to keep in touch with the wide range
of possibilities for UDFs.
Sign up for the newsletter at:
http://www.NovickSoftware.com/UDFofWeek/UDFofWeekSignup.htm
The newsletter archives with all the past issues is available at:
http://www.NovickSoftware.com/UDFofWeek/UDFofWeekArchive.htm
455