Transact-SQL User-Defined Functions For MSSQL Server
Transact-SQL User-Defined Functions For MSSQL Server
User-Defined
Functions
Andrew Novick
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
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . xv
Part ICreating and Using User-Defined Functions . . . . . 1
1 Overview of User-Defined Functions . . . . . . . . . . . . . 3
Introduction to UDFs . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Scalar UDFs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Inline User-Defined Functions . . . . . . . . . . . . . . . . . . . . 9
Multistatement Table-Valued User-Defined Functions . . . . . . . 12
Why Use UDFs? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Reuse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Organizing Code through Modularization . . . . . . . . . . . . . . 19
Ease of Coding . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Why Not Use UDFs? . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2 Scalar UDFs . . . . . . . . . . . . . . . . . . . . . . . . . .
Creating, Dropping, and Altering Scalar UDFs. . . . . . . . . . . . .
Permissions to Use CREATE/DROP/ALTER FUNCTION . . . .
Using the CREATE FUNCTION Statement . . . . . . . . . . . .
The Function Body . . . . . . . . . . . . . . . . . . . . . . . . .
Declaring Local Variables (Including TABLEs) . . . . . . . . .
Control-of-flow Statements and Cursors . . . . . . . . . . . . .
Using SQL DML in Scalar UDFs. . . . . . . . . . . . . . . . .
Adding the WITH Clause . . . . . . . . . . . . . . . . . . . . . .
Specifying WITH ENCRYPTION . . . . . . . . . . . . . . . .
Specifying WITH SCHEMABINDING. . . . . . . . . . . . . .
Using Scalar UDFs . . . . . . . . . . . . . . . . . . . . . . . . . . .
Granting Permission to Use Scalar UDFs. . . . . . . . . . . . . .
Using Scalar UDFs in SQL DML . . . . . . . . . . . . . . . . . .
Using Scalar UDFs in the Select List . . . . . . . . . . . . . .
Using Scalar UDFs in the WHERE and ORDER BY Clauses . .
Using Scalar UDFs in the ON Clause of a JOIN . . . . . . . . .
Using Scalar UDFs in INSERT, UPDATE, and DELETE
Statements . . . . . . . . . . . . . . . . . . . . . . . . . .
Using Scalar UDFs in SET Statements . . . . . . . . . . . . .
Using Scalar UDFs in EXECUTE and PRINT Statements . . .
23
25
25
28
31
31
33
34
36
36
39
44
44
45
45
47
49
50
50
51
v
Contents
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
52
53
56
58
61
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. .
. .
. .
. .
. .
. .
. .
. .
63
64
65
70
74
79
80
81
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. .
. .
. .
. .
. .
. .
. .
. .
. .
. .
. .
. .
83
84
85
86
88
88
89
90
91
91
92
98
. .
. .
. .
. .
. .
. .
. .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
99
100
106
107
108
109
111
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. .
. .
. .
. .
. .
. .
. .
. .
. .
. .
. .
. .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . 113
. . . 115
. . . 118
. . . 120
. . . 120
. . . 120
. . . 120
. . . 120
. . . 121
. . . 122
Contents
* History. . . . . . . . . . . . . . . . . . . . .
* Copyright . . . . . . . . . . . . . . . . . . .
Whats Not in the Header . . . . . . . . . . . . .
* Parameters . . . . . . . . . . . . . . . . . .
* Algorithm and Formulas. . . . . . . . . . . .
Naming Conventions . . . . . . . . . . . . . . . . .
Naming User-Defined Functions . . . . . . . . . .
Naming Columns . . . . . . . . . . . . . . . . . .
Domain Names . . . . . . . . . . . . . . . . . . .
Naming Function Parameters and Local Variables .
Summary. . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
122
122
122
122
123
123
123
129
129
131
131
7 Inline UDFs . . . . . . . . . . . . . . . . . . . . .
Managing Permissions on Inline UDFs . . . . . . . . . . .
Permissions to Create, Alter, and Drop Inline UDFs . .
Permission to Use Inline UDFs . . . . . . . . . . . . .
Creating Inline UDFs . . . . . . . . . . . . . . . . . . . .
Template for Inline UDFs . . . . . . . . . . . . . . . .
Creating a Sample Inline UDF . . . . . . . . . . . . . .
Retrieving Data with Inline UDFs. . . . . . . . . . . . . .
Sorting Data in Inline UDFs. . . . . . . . . . . . . . . . .
Updatable Inline UDFs . . . . . . . . . . . . . . . . . . .
Using Inline UDFs for Paging Web Pages . . . . . . . . . .
Retrieving Minimal Data for Each Web Page . . . . . .
Creating the Inline UDF . . . . . . . . . . . . . . . . .
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . 133
. . . 133
. . . 134
. . . 134
. . . 134
. . . 136
. . . 137
. . . 138
. . . 140
. . . 141
. . . 146
. . . 146
. . . 147
. . . 153
.
.
.
.
.
.
.
.
.
.
.
.
. . .
. . . .
. . . .
. . . .
. . . .
. . . .
.
.
.
.
.
.
.
.
.
.
.
.
. . 175
. . . 176
. . . 176
. . . 177
. . . 177
. . . 179
vii
Contents
viii
sp_helptext . . . . . . . . . . . . . . . . . . . . . . . .
sp_rename . . . . . . . . . . . . . . . . . . . . . . . .
sp_depends . . . . . . . . . . . . . . . . . . . . . . . .
Retrieving Metadata about UDFs . . . . . . . . . . . . . .
Finding out about UDFs in INFORMATION_SCHEMA
INFORMATION_SCHEMA.ROUTINES. . . . . . .
INFORMATION_SCHEMA.ROUTINE_COLUMNS
INFORMATION_SCHEMA.PARAMETERS . . . .
Built-in Metadata Functions . . . . . . . . . . . . . . .
Information about UDFs in System Tables . . . . . . .
Metadata UDFs . . . . . . . . . . . . . . . . . . . . . . .
Function Information . . . . . . . . . . . . . . . . . . .
What Are the Columns Returned by a UDF? . . . . . .
What Are the Parameters Used When Invoking a UDF?
Metadata Functions that Work on All Objects . . . . . .
SQL-DMO . . . . . . . . . . . . . . . . . . . . . . . . . .
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . 197
. . . 198
. . . 199
. . . 208
. . . 210
. . . 210
. . . 211
. . . 212
. . . 215
. . . 219
. . . 221
. . . 223
. . . 223
. . . 228
.
.
.
.
.
.
.
.
. . 229
. . . 230
. . . 231
. . . 235
. . . 236
. . . 239
. . . 242
. . . 244
.
.
.
.
. . 247
. . . 248
. . . 250
. . . 253
. . .
. . . .
. . . .
. . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
180
181
182
183
183
184
185
185
186
187
188
189
190
192
193
196
196
Contents
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
253
255
255
258
260
262
266
268
274
275
13 Currency Conversion . . . . . . . . . . . . . . . . .
Background on Variable Exchange Rates . . . . . . . . . . .
Design Issues for Currency Conversion . . . . . . . . . . .
Creating the Schema to Support the Functions . . . . . .
Picking Data Types for Amounts and Exchange Rates . .
Returning a Meaningful Result When Data Is Missing .
Writing the Functions for Currency Conversion . . . . . . .
Using Currency Conversion Functions . . . . . . . . . . . .
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
. . 277
. . . 278
. . . 279
. . . 279
. . . 280
. . . 281
. . . 283
. . . 289
. . . 291
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. . 295
. . . 296
. . . 297
. . . 297
. . . 298
. . . 299
. . . 300
. . . 302
. . . 302
. . . 304
. . . 304
. . . 305
. . . 305
. . . 306
. . . 308
. . . 312
Contents
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
322
324
324
328
331
333
335
. . 337
. . . 338
. . . 339
. . . 339
. . . 341
. . . 341
. . . 342
. . . 344
. . . 346
. . . 347
. . . 348
. . . 349
. . . 350
. . . 352
. . . 353
. . .
. . . .
. . . .
. . . .
.
.
.
.
.
.
.
.
. . 387
. . . 388
. . . 389
. . . 392
Contents
fn_chariswhitespace . .
fn_dblog. . . . . . . . .
fn_mssharedversion . .
fn_replinttobitstring . .
fn_replbitstringtoint . .
fn_replmakestringliteral
fn_replquotename . . .
fn_varbintohexstr. . . .
Summary. . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
392
394
396
396
399
403
406
409
410
.
.
.
.
.
.
.
.
. . .
. . . .
. . . .
. . . .
.
.
.
.
.
.
.
.
. . .
. . . .
. . . .
. . . .
.
.
.
.
.
.
.
.
. . 413
. . . 414
. . . 416
. . . 419
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
User-Defined Functions . . . . . . . . . . . . . . . . . . . 431
Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
xi
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 acknowledge their support and thank them for their help.
My family, especially my wife, Ulli, has been very supportive throughout 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 turnaround 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 editing 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 knowledge of how to create practical computer software, which I hope is
reflected throughout the book. For that Im thankful to a handful of entrepreneurs 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
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 feature. In particular, well cover:
n
Creating UDFs
Using UDFs
Debugging UDFs
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 Anywhere), 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 functions. 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 documentation 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-
Scalar UDFs
Its an overview to give you the basics of creating and using UDFs
enough so that you can understand the rest of the chapter, which is
devoted to the really big question Why? as in Why would you use
UDFs?
I have a few answers to that question. The best reasons are:
n
Code reuse
Management of knowledge
Simplification of programs
There are also reasons not to use UDFs. They revolve principally around
performance, and they are covered in Chapter 1 with more detail in Chapters 3 and 11. The performance issue stems from the procedural nature of
scalar and multistatement UDFs.
Once UDFs are introduced, Chapter 2 goes into depth about scalar
UDFs. It covers how to create them, where you can use them, and how to
control the permissions to use them. Most of what is written about scalar
UDFs applies to the other types as well.
Before discussing the other types of UDFs, Chapters 3 through 6 go
into depth about several topics that affect all UDFs. Theyre important
enough to be discussed before getting into the details of the other types.
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 features 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 handling. 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 maintaining 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 maintaining 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 conversion 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 system UDFs are available and the differences between system UDFs and
the ordinary UDFs that you and I create. It covers four of the ten documented 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 master. 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 different 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 discovered in SQL Server 2000 during the course of writing this book.
Its not that you need to know everything about these topics, but I dont
introduce them. What I do is concentrate on how each of them relate to
UDFs.
xx
Introduction
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 Analyzer 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:
-- start in the TSQLUDFS database
USE TSQLUDFS
GO
-- short query to illustrate how Listing 0 files should be used.
SELECT 'Just a sample to show how the Listing 0 works'
GO
(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:
[email protected]. Thanks.
xxiii
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
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 reusable, 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 performance degradation and loss of portability, and we shouldnt ignore them.
In particular, UDFs have the potential for introducing significant performance 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
Part I
Chapter 1: Overview of User-Defined Functions
which youll find in the download directory for this chapter. I strongly suggest 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 function 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 documentation 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 determining 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 statements, 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 permissions 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
Chapter 1: Overview of User-Defined Functions
Part I
Chapter 1: Overview of User-Defined Functions
The Authors table in pubs stores separate first and last names in the columns au_fname and au_lname, respectively. Heres how you might use
udf_Name_Full in a select list to combine them:
-- Get the first five authors and book titles.
SELECT TOP 5
dbo.udf_Name_Full (au_fname, NULL, au_lname, NULL) as [Author]
, Title
FROM pubs..authors a
INNER JOIN pubs..titleauthor ta
ON a.au_id = ta.au_id
INNER JOIN pubs..titles t
ON ta.title_id = t.title_id
GO
(Results -- truncated on the right)
Author
------------------Cheryl Carson
Stearns MacFeather
Livia Karsen
Michael O'Leary
Stearns MacFeather
Title
-------------------------------------------------------But Is It User Friendly?
Computer Phobic AND Non-Phobic Individuals: Behavior Var
Computer Phobic AND Non-Phobic Individuals: Behavior Var
Cooking with Computers: Surreptitious Balance Sheets
Cooking with Computers: Surreptitious Balance Sheets
function name are required for scalar UDFs. Theyre optional for other
types of UDFs. If the UDF is in a different database, it must be identified
with the three-part name: database.owner.function_name.
In addition to columns, the parameters to the scalar function may also
be constants or expressions. In the first invocation of udf_Name_Full in the
next example, all parts of the name are constants. In the second invocation, the first and middle names are shortened in an expression to initials
with periods. Heres the example:
-- calling a function with constants and expressions
SELECT dbo.udf_Name_Full('John', 'Jacob'
, 'Jingleheimer-Schmitt', 'Esq.') as [My Name Too]
, dbo.udf_Name_Full(LEFT('Andrew', 1) + '.'
, LEFT('Stewart', 1) + '.'
, 'Novick'
, null) as [Author]
GO
(Results)
My Name Too
Author
-------------------------------------- -----------------------John Jacob Jingleheimer-Schmitt Esq. A. S. Novick
In addition to T-SQL logic and calculations, a scalar UDF can read data
using a SELECT statement.
The next UDF illustrates the ability to retrieve information using
SELECT. It uses the data from EmployeeTerritories in the Northwind sample
database that comes with SQL Server. The script moves into the
Northwind database before creating the UDF:
USE Northwind
GO
-- These options should be set this way before creating any UDF
Set Quoted_Identifier ON
Set ANSI_Warnings ON
GO
CREATE FUNCTION dbo.udf_EmpTerritoryCOUNT (
@EmployeeID int -- ID of the employee
) RETURNS INT -- Number of territories assigned to the employee
AS BEGIN
DECLARE @Territories int -- Working Count
SELECT @Territories = count(*)
FROM EmployeeTerritories
WHERE EmployeeID = @EmployeeID
RETURN @Territories
END
GO
-- EXEC permission is short for EXECUTE
GRANT EXEC ON dbo.udf_EmpTerritoryCOUNT to PUBLIC
GO
The logic of ufd_EmpTerritoryCOUNT is simple: Retrieve the number of territories for the EmployeeID given by the @EmployeeID parameter and return
it as the result. This short query illustrates how it works. Employee 1 is
the very famous Nancy Davolio.
-- Get the territory count for one employee
SELECT dbo.udf_EmpTerritoryCOUNT (1) as [Nancy's Territory Count]
GO
(Results)
Nancy's Territory Count
----------------------2
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:
Part I
Chapter 1: Overview of User-Defined Functions
Part I
Chapter 1: Overview of User-Defined Functions
FirstName Territories
---------- ----------Robert
10
Steven
7
Andrew
7
Part I
Chapter 1: Overview of User-Defined Functions
10
Part I
Chapter 1: Overview of User-Defined Functions
The columns of the table returned by udf_EmpTerritoriesTAB are determined by the select list. Theyre drawn from the EmployeeTerritories
(alias et) and Territory (alias t) tables. However, the TerritoryID column
could just as well have been taken from Territories. One of the columns,
TerritoryDescription, has a column alias so that the rowset uses the column name that I prefer. The functions parameter, @EmployeeID, is used in
this WHERE clause:
WHERE et.EmployeeID = @EmployeeID
Part I
Chapter 1: Overview of User-Defined Functions
11
TerritoryID
-------------------01730
02116
02184
02139
01833
40222
01581
Territory
-----------------Bedford
Boston
Braintree
Cambridge
Georgetow
Louisville
Westboro
(Results)
12
Part I
Chapter 1: Overview of User-Defined Functions
FUNCTION dbo.udf_DT_MonthsTAB (
13
Part I
Chapter 1: Overview of User-Defined Functions
14
Part I
Chapter 1: Overview of User-Defined Functions
M
1
2
3
4
5
6
Name
-------January
February
March
April
May
June
Mon
--Jan
Feb
Mar
Apr
May
Jun
StartDT
----------------------1998-01-01 00:00:00.000
1998-02-01 00:00:00.000
1998-03-01 00:00:00.000
1998-04-01 00:00:00.000
1998-05-01 00:00:00.000
1998-06-01 00:00:00.000
EndDT
----------------------1998-01-31 23:59:59.997
1998-02-28 23:59:59.997
1998-03-31 23:59:59.997
1998-04-30 23:59:59.997
1998-05-31 23:59:59.997
1998-06-30 23:59:59.997
NextMonStartDT
----------------------1998-02-01 00:00:00.000
1998-03-01 00:00:00.000
1998-04-01 00:00:00.000
1998-05-01 00:00:00.000
1998-06-01 00:00:00.000
1998-07-01 00:00:00.000
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 situations. 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 combine 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:
15
-- Months will be joined with this subquery in another example that follows
SELECT o.ShippedDate, od.ProductID
, od.UnitPrice * od.Quantity
* (1 - Discount) as Revenue
FROM Orders o
INNER JOIN [Order Details] od
ON o.OrderID = od.OrderID
WHERE (o.ShippedDate >= '1998-01-01'
AND o.ShippedDate < '1999-07-01')
GO
(Results - first five rows)
ShippedDate
ProductID Revenue
-------------------------- ----------- -----------------1998-01-01 00:00:00.000
29
1646.407
1998-01-01 00:00:00.000
41
183.34999
1998-01-02 00:00:00.000
71
344.0
1998-01-02 00:00:00.000
14
279.0
1998-01-02 00:00:00.000
54
35.760002
Next, combine the two queries to produce a report of revenue per month
for the first six months of 1998:
-- Report on Revenue per month for first six months of 1998
SELECT [Year], [Month], [Name]
, CAST (SUM(Revenue) as numeric (18,2)) Revenue
FROM udf_DT_MonthsTAB('1998-01-01', '1998-06-01') m
LEFT OUTER JOIN -- Shipped Line items
(SELECT o.Shippeddate, od.ProductID
, od.UnitPrice * od.Quantity
* (1 - Discount) as Revenue
FROM Orders o
INNER JOIN [Order Details] od
ON o.OrderID = od.OrderID
WHERE (o.ShippedDate >= '1998-01-01'
AND o.ShippedDate < '1999-07-01')
) ShippedItems
ON ShippedDate Between m.StartDT and m.EndDT
GROUP BY m.[Year], m.[Month], m.[Name]
ORDER BY m.[Year], m.[Month]
GO
(Results)
Year Month Name
Revenue
------ ------ --------- -------------------1998
1 January
83651.59
1998
2 February
115148.77
1998
3 March
77529.58
1998
4 April
142901.96
1998
5 May
18460.27
1998
6 June
NULL
Warning: Null value is eliminated by an aggregate or other SET operation.
Part I
Chapter 1: Overview of User-Defined Functions
16
Part I
Chapter 1: Overview of User-Defined Functions
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.
Ease of coding
17
costs and benefits associated with a database. UDFs have a part to play in
the cost/benefit equation.
Youre paying one of the costs of using UDFsthe learning curve
right now. The other costs of UDFs that Im aware of are:
n
Performance
Loss of portability
The execution cost occurs when code that includes UDFs is used instead
of faster alternatives. In this chapter and later ones, well discuss where
the execution costs of UDFs come into play.
Using T-SQL UDFs diminishes the portability of the database. Of
course, that could be said for stored procedures as well. T-SQL UDFs are
not yet compliant with any technical standard. More importantly, theyre
not part of any de facto standard. When we choose to code in T-SQL, were
giving up most database portability. There is some prospect that that will
change. There are efforts to extend SQL to include both object and procedural features. Its clear that were not there yet, and wont get there for
several years.
The prospect of reusing software between projects has been one of
the best ways to sell a new technology to management. It requires an
up-front investment in the original code but promises large returns down
the road.
Reuse
Software reuse has been the Holy Grail of programming methodologies as
long as I can remember. I recall Modular Programming, Structured Programming, Object-Oriented Programming, and, more recently, Component
Design. All these methodologies have reuse as one of their primary objectives 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 general-purpose subroutine can be used over and over again. As a consultant
developing SQL, VB, ASP, and .NET applications, I carry around a library
Part I
Chapter 1: Overview of User-Defined Functions
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_CharIndexRev searches from the back.
Listing 1.3: udf_Txt_CharIndexRev
CREATE FUNCTION dbo.udf_Txt_CharIndexRev (
@SearchFor varchar(255) -- Sequence to be found
, @SearchIn varchar(8000) -- The string to be searched
) RETURNS int -- Position from the back of the string where
-- @SearchFor is found in @SearchIn
/*
* Searches for a string in another string working from the back.
* It reports the position (relative to the front) of the first
* such expression it finds. If the expression is not found, it
* returns zero.
*
* Example:
select dbo.udf_Txt_CharIndexRev('\', 'C:\temp\abcd.txt')
* Test:
PRINT 'Test 1
' + CASE WHEN 8=
dbo.udf_Txt_CharIndexRev ('\', 'C:\temp\abcd.txt')
THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 2
' + CASE WHEN 0=
dbo.udf_Txt_CharIndexRev ('*', 'C:\tmp\d.txt')
THEN 'Worked' ELSE 'ERROR' END
****************************************************************/
AS BEGIN
DECLARE
,
,
,
@WorkingVariable int
@StringLen int
@ReverseIn varchar(8000)
@ReverseFor varchar(255)
19
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.
Part I
Chapter 1: Overview of User-Defined Functions
20
Part I
Chapter 1: Overview of User-Defined Functions
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 customer 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 * FROM udf_EmpTerritoriesTAB(@EmpID)
21
Loss of portability
Performance
I dont have much more to say about loss of portability. If you want your
application to be portable between database management systems, dont
use T-SQL; its unique to SQL Server. That means you cant use stored
procedures or UDFs.
Performance is another matter. There is a lot to say about the performance of UDFs. The next section scratches the surface, but youll find
more performance-related information in the rest of this book, particularly
in Chapter 11.
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 mentioned 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:
Part I
Chapter 1: Overview of User-Defined Functions
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
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
binary
bigint
bit
char
datetime
decimal
float
int
money
nchar
nvarchar
numeric
real
smalldatetime
smallint
smallmoney
sql_variant
sysname
tinyint
varbinary
varchar
uniqueidentifier
Thats all there is to it. The middle step can be as long a program as you
care to write or just a single expression.
What a scalar UDF cant do is change the database or any operating
system resource. This is referred to as a side effect. Microsoft has gone to
great lengths to make it nearly impossible for UDFs to produce side
effects. Some of these restrictions are discussed in the section Using the
23
24
Part I
Chapter 2: Scalar UDFs
SQL DDL contains CREATE TABLE and similar statements that define database objects that contain data such as tables, views, indexes, rules, and
defaults.
SQL DML1 is for working with data. It consists of:
n
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.
25
LimitedDBA can now create any type of function, not just scalar UDFs.
If a user without CREATE FUNCTION permission tries to create a func-
Part I
Chapter 2: Scalar UDFs
26
Part I
Chapter 2: Scalar UDFs
27
can manipulate, the DENY statement can be used to remove permissions for
particular statements.
Only users with the fixed server role sysadmin or the db_owner and
db_ddladmin database roles can grant permission on CREATE FUNCTION. However, the statement permissions cant be passed on using the WITH GRANT
OPTION clause. So if dbo executed the following GRANT statement, it would
fail:
-- Try to extend the right to GRANT CREATE FUNCTION
GRANT CREATE FUNCTION to LimitedDBA WITH GRANT OPTION
GO
(Result)
Server: Msg 1037, Level 15, State 2, Line 1
The CASCADE, WITH GRANT or AS options cannot be specified with
statement permissions.
Part I
Chapter 2: Scalar UDFs
28
Part I
Chapter 2: Scalar UDFs
The Books Online gives definitions for each of the elements of the function 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 function 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
29
function group name, such as Txt for character string processing functions, and then a descriptive name. Theres more on my naming
convention in Chapter 6.
@parameter_name Try to use as descriptive a name as possible. If
the name doesnt tell the whole story, add a descriptive comment after the
parameter declaration.
[ = default ] Providing meaningful default values is a good practice. It
tells the caller something about how the parameter can be used.
scalar_return_data_type This is the return type of the function. You
may use any of the return types listed in Table 2.1. You may also use any
user-defined type (UDT) thats defined in the database as long as it
resolves into one of the allowable types.
Note:
I usually encourage the use of UDTs. They add consistency to the definition 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.
Part I
Chapter 2: Scalar UDFs
30
Part I
Chapter 2: Scalar UDFs
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, comments, 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 parameter 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.
Listing 2.1: udf_Example_Palindrome
SET Quoted_Identifier ON
SET ANSI_Warnings ON
GO
CREATE FUNCTION dbo.udf_Example_Palindrome (
@Input varchar(8000) -- Input to test for being a palindrome
) RETURNS int -- 1 if @Input is a palindrome
/*
* A palindrome is the same from front to back as it is from back
* to front. This function doesn't ignore punctuation as a human
* might.
*
* Related Functions: udf_Txt_IsPalindrome is more robust.
*
* Example:
select dbo.udf_Example_Palindrome('Able was I ere I saw Elba')
* Test:
PRINT 'Test 1
' + CASE WHEN 1 =
dbo.udf_Example_Palindrome ('Able was I ere I saw Elba')
THEN 'Worked' ELSE 'ERROR' END
****************************************************************/
AS BEGIN
RETURN CASE WHEN REVERSE(@Input) = @Input
THEN 1
ELSE 0
END
END
GO
GRANT EXEC on dbo.udf_Example_Palindrome to [PUBLIC]
GO
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.
31
variables.
n
Most of the three parts of DML can be used in the function body of a UDF.
Unlike stored procedures, none of the SQL DDL, such as the CREATE TABLE
statement, can be used in a UDF. UDFs may contain the full syntax of the
DECLARE and the control-of-flow statements. Only a subset of SQL DML
and no SQL DDL may be used in a UDF.
Although the function body is similar to the body of a stored procedure, there are many more restrictions on the statements that can be used
in a function. DECLARE is one statement thats identical in a function to its
use in stored procedures. The introduction of TABLE variables in SQL
Server 2000 is an important innovation that affects many other statements, so well cover DECLARE first and then move on to the other
statements.
Part I
Chapter 2: Scalar UDFs
32
Part I
Chapter 2: Scalar UDFs
CREATE TABLE. TABLE variables are visible only within the function body of
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 filegroup 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.
33
Part I
Chapter 2: Scalar UDFs
34
Part I
Chapter 2: Scalar UDFs
SELECT statements that set the values of local variables either from
expressions or by retrieving data from a database.
SET statements that set the value of a single local variable from an
expression.
Notably absent from this list are the INSERT, UPDATE, and DELETE statements
for tables in the database. They may not be used inside a UDF.
SELECT may be used to read data from the current database or any
other database and to change the value of local variables within the function. In Chapter 1 we saw the udf_EmpTerritoryCOUNT function, which
counts the number of territories assigned to an employee. That number
was assigned to the local variable @Territories with the statement:
SELECT @Territories = count(*)
FROM EmployeeTerritories
WHERE EmployeeID = @EmployeeID
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 overhead of a single SELECT is slightly less than the overhead for the individual
SET statements and there are fewer statements to work with when debugging the function or tracing it with the SQL Profiler.
35
TABLE variables are very similar to database tables, and the INSERT,
UPDATE, and DELETE statements are used to modify them. SELECT is often
used to provide the values for an INSERT statement. This can be seen in
Listing 2.3, which shows the udf_Example_TABLEmanipulation function. The
function is contrived and could be done a zillion other ways, but it does
show all four DML statements at work on a TABLE variable.
Listing 2.3: udf_Example_TABLEmanipulation
SET Quoted_Identifier ON
SET ANSI_Warnings ON
GO
CREATE FUNCTION dbo.udf_Example_TABLEmanipulation (
) RETURNS int -- Min age at hiring of employee with non-prime IDs.
/*
* An example UDF to demonstrate the use of INSERT, UPDATE, DELETE,
* and SELECT on TABLE variables.
* Uses data from the Northwind Employees table.
****************************************************************/
AS BEGIN
DECLARE @Emp TABLE (
,
,
,
,
,
,
Part I
Chapter 2: Scalar UDFs
36
Part I
Chapter 2: Scalar UDFs
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.
37
/*
* Example UDF to demonstrate the use of the WITH ENCRYPTION
* option on the CREATE FUNCTION statement.
****************************************************************/
AS BEGIN
-- This is the body of the function. All this one does is
-- return the original parameter.
RETURN @Parm1
END
GO
(Results)
The command(s) completed successfully.
GO
(Results)
A string
A numeric
A float A Date
------------ ------------ --------- ----------------------My string
37.123
1.23E+36 1956-07-10 22:44:00.000
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 definition of the function.
Part I
Chapter 2: Scalar UDFs
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:
-- Try to retrieve the CREATE FUNCTION script with sp_helptext
DECLARE @rc int -- return code
EXEC @rc = sp_helptext udf_Example_WithEncryption
PRINT 'sp_helptext return code = ' + CONVERT(varchar(10), @rc)
GO
(Results)
The object comments have been encrypted.
sp_helptext return code = 0
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 planning 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 protection to any object that references your function.
39
The latter reason is usually the most important, and I believe it was the
motivation for creating the SCHEMABINDING option in the first place. Thats
why this section discusses WITH SCHEMABINDING in the context of an
indexed view.
Indexed views are a feature of SQL Server 2000. In order to create an
index on a view, the view must be schemabound. And in order to be
schemabound, all the views and UDFs that it references must be
schemabound. In the process of creating the index, SQL Server must
extract the fields that are in the index from the base tables and create
index rows with dataeven if the columns it extracts are computed.
In essence, the view is stored, or materialized, in the database. Its
sort of like a table, but you dont manipulate data in the view; rather, you
manipulate data in the base tables and SQL Server takes care of updating
the index of the view. Oracle calls its analogous feature materialized
views.
Think about that for a second. Every time you change a row in a base
table that is referenced by an indexed view, SQL Server has to update the
base table and the indexes in the base table that reference any modified
columns. If there are any indexed views that reference modified columns,
it also has to update any rows of the indexed view that might be affected.
Its a big job, but hey, SQL Servers a great product, and its up to the task.
The reason behind the creation of indexed views is that range scans of
the indexed view are going to be lightning fast because SQL Server is
storing a copy of the data in the order that its needed. The only reason
that youd create an index on a view in the first place is that you need a
particular query or group of queries to take advantage of the speed
improvement provided by the index.
Part I
Chapter 2: Scalar UDFs
40
Part I
Chapter 2: Scalar UDFs
41
Now try to create a view that uses the UDF. It must be created using the
WITH SCHEMABINDING option in order to be indexed:
-- Try to create the view CalendarByLocation
CREATE VIEW CalendarByLocation
WITH SCHEMABINDING
AS SELECT CL.LocationCD
, CL.LocationName
, COUNT_BIG (*) AS [EventCount]
, CE.EventDATE
, LEFT(dbo.udf_DT_TimePart(StartDT), 5) StartsAt
FROM dbo.CalendarEvent CE
INNER JOIN dbo.CalendarLocationCD CL
ON CE.LocationCD = CL.LocationCD
GROUP BY CL.LocationCD, CL.LocationName
, CE.EventDATE
, CE.StartDT
GO
(Results)
Server: Msg 4513, Level 16, State 1, Procedure CalendarByLocation, Line 4
Cannot schema bind view 'CalendarByLocation'. 'dbo.udf_DT_TimePart' is not schema
bound.
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:
Part I
Chapter 2: Scalar UDFs
42
Part I
Chapter 2: Scalar UDFs
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:
43
Scalar UDFs that perform computations but do not have data access
are created using WITH SCHEMABINDING.
Scalar UDFs that access data and all inline and multistatement UDFs
are a judgment call.
The judgment call for UDFs that access data is mostly a matter of style
and convenience. The advantage of using additional WITH SCHEMABINDING
options is that UDFs are protected from changes that might modify or
invalidate their definition. The disadvantage of WITH SCHEMABINDING is that
it can become difficult to change your UDFs, even when the change wont
affect the objects that are bound to it.
Now that youve seen how to create UDFs, lets turn our attention to
using them. The next section goes over every way to invoke a scalar UDF.
As with function creation, the discussion of using UDFs must start with
the permissions that govern who can use them.
Part I
Chapter 2: Scalar UDFs
44
Part I
Chapter 2: Scalar UDFs
45
When permission to execute a UDF is granted to a user, the user can also
be given the right to pass that permission on to others using the WITH
GRANT OPTION clause. Heres an example that grants the right to use
udf_DT_NthDayInMon to LimitedDBA and includes the right to pass on the
permission:
-- Let LimitedDBA pass on permission to use udf_DT_NthDayInMon
GRANT EXEC, REFERENCES on dbo.udf_DT_NthDayInMon
TO LimitedDBA WITH GRANT OPTION
GO
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 different 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.
Part I
Chapter 2: Scalar UDFs
46
Part I
Chapter 2: Scalar UDFs
ShippedDate
----------------------1998-01-01 00:00:00.000
1998-02-02 00:00:00.000
1998-03-12 00:00:00.000
1998-04-17 00:00:00.000
Billing Date
Grace Period
------------------- -----------1998-02-10 00:00:00
28
1998-03-10 00:00:00
26
1998-04-14 00:00:00
23
1998-05-12 00:00:00
17
47
udf_DT_NthDayInMon is used twice in this query. The first time, its used to
compute the [Billing Date] column. The second time, the number of
weekdays between ShippedDate and the billing date is calculated by
udf_DT_WeekdaysBtwn. In business thats referred to as a grace period, thus
the column name. In that expression the result of udf_DT_NthDayInMon is
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.)
Part I
Chapter 2: Scalar UDFs
48
Part I
Chapter 2: Scalar UDFs
ShippedDate
Grace Period
------------------------ -----------1998-01-01 00:00:00.000
28
1998-02-02 00:00:00.000
26
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 number 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.
49
There are other places that scalar UDFs can be used. The next section shows how to use them in the ON clause of a JOIN.
The query now has a non-NULL result for June 1998 because there are billings 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
Part I
Chapter 2: Scalar UDFs
50
Part I
Chapter 2: Scalar UDFs
the original query from Chapter 1 and the refined query joined on the billing 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.
51
The assignment type of SET changes the value of a local variable. It has the
form:
SET @local_variable = expression
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 discusses 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.
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.
Part I
Chapter 2: Scalar UDFs
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 executed dynamically. Heres an example of dynamic SQL that uses a UDF in
a PRINT statement:
-- Invoking a UDF in dynamic SQL
DECLARE @SQL varchar(8000)
SET @SQL = 'PRINT dbo.udf_DT_NthDayInMon (YEAR(getdate()) '
+ ', MONTH (getdate()) '
+ ', 2, ''TUESDAY'')'
PRINT 'The statement is ->' + @SQL + '<-'
EXEC (@SQL)
PRINT 'End of Script'
GO
(Results - truncated on the right)
The statement is ->PRINT dbo.udf_DT_NthDayInMon (YEAR(getdate()) ...
Feb 11 2003 12:00AM
End of Script
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 permission 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.
CHECK constraints
Computed columns
53
Part I
Chapter 2: Scalar UDFs
54
Part I
Chapter 2: Scalar UDFs
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:
-- Remove the constraint created above.
ALTER TABLE NWOrders
DROP CONSTRAINT ShippedDate_GracePeriod_LessThan_27
GO
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
55
bypass the usual restriction that CHECK constraints only work on data in the
same base table.
The next constraint uses udf_Order_Amount in the TSQLUDFS database to demonstrate this feature. Heres the script to create the
constraint:
-- 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:
-- Find Orders that can test the Ship_Only_Orders_Over_100 constraint
SELECT OrderID, ShippedDate
, dbo.udf_Order_Amount(OrderID)
FROM NWOrders
WHERE ShippedDate IS NULL
AND dbo.udf_Order_Amount(OrderID) <= 100
GO
(Result)
OrderID
----------11019
11051
ShippedDate
Amount
-------------- --------------------NULL
76.0000
NULL
45.0000
The first order wont satisfy the constraint, so this next script uses it in an
attempted update:
-- Try an update that violates the new constraint.
BEGIN TRAN
UPDATE NWOrders
SET ShippedDate = GETDATE()
WHERE OrderID = 11019
GO
ROLLBACK TRAN
GO
(Results)
Server: Msg 547, Level 16, State 1, Line 1
UPDATE statement conflicted with TABLE CHECK constraint
'Ship_Only_Orders_Over_100'. The conflict occurred in database 'TSQLUDFS',
table 'NWOrders'.
The statement has been terminated.
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
Part I
Chapter 2: Scalar UDFs
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 computed column. This script attempts to alter udf_Order_Amount:
-- Attempt to alter udf_Order_Amount
ALTER FUNCTION dbo.udf_Order_Amount (
@OrderID int -- The order to determine the amount
) RETURNS money -- Total amount of the order
/*
* Test modification that always returns 17.
****************************************************************/
AS BEGIN
RETURN 17
END
GO
(Results)
Server: Msg 3729, Level 16, State 3, Procedure udf_Order_Amount, Line 10
Cannot ALTER 'dbo.udf_Order_Amount' because it is being referenced by object
'Ship_Only_Orders_Over_100'.
57
Price] column synchronized becomes quite a job for the user of the database and often leads to errors in the database.
Having the [Extended Price] column around would be a great convenience. Views are one solution to getting it without denormalizing the
data. A view such as [Order Details Extended] could be defined on [Order
Details], and it could be used instead of the base table.
Theres another way. SQL Server allows the creation of computed columns. Theyre columns that are defined as a scalar expression. The
expression may include other columns in the same row. Also, the expression can invoke a scalar UDF so long as the arguments to the UDF are
constants, other columns in the table, or expressions involving other constants in the table. Having a column like [Extended Price] in the table
would be very convenient, particularly for reporting purposes. Computed
columns are added to a base table, but they are computed when they are
referenced and not stored with the row.
A UDF that is used in a computed column is not restricted to using
data in the table in which it is defined. It can do any computations that are
allowed in a UDF, and it may select data from other tables in order to produce its result. This ability extends what could previously be done with a
computed column.
To create an example of adding a computed column that is based on a
UDF to a table and because you may not want to modify tables in your
copy of Northwind, Ive copied the Orders table from Northwind into the
TSQLUDFS database with its data and called it NWOrders. The following
example creates a computed column on TSQLUDFS.NWOrders using
udf_DT_NthDayInMon:
-- Create the BillingDate computed column on NWOrders
ALTER TABLE NWOrders
ADD BillingDate AS
dbo.udf_DT_NthDayInMon (YEAR(DATEADD(month, 1, ShippedDate))
, MONTH(DATEADD(Month, 1, ShippedDate))
, 2, 'TUESDAY')
GO
-- Show a few rows of the new column
SELECT TOP 4 OrderID, ShippedDate, BillingDate
FROM NWOrders
GO
(Results)
OrderID
----------10248
10249
10250
10251
ShippedDate
----------------------1996-07-16 00:00:00.000
1996-07-10 00:00:00.000
1996-07-12 00:00:00.000
1996-07-15 00:00:00.000
BillingDate
------------------1996-08-13 00:00:00
1996-08-13 00:00:00
1996-08-13 00:00:00
1996-08-13 00:00:00
Part I
Chapter 2: Scalar UDFs
58
Part I
Chapter 2: Scalar UDFs
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:
Server: Msg 3729, Level 16, State 3, Procedure udf_DT_NthDayInMon, Line 89
Cannot ALTER 'dbo.udf_DT_NthDayInMon' because it is being referenced by object
'NWOrders'.
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 interesting aspect of computed columns is that they can be indexed. That
includes computed columns based on UDFs. This is where determinism
really becomes important.
All built-in and user-defined functions used by the function must also
be deterministic.
The body of the function may not reference any database tables or
views. (It may reference TABLE variables that are created in the
function.)
It isnt always obvious that a computed column meets all the requirements, so SQL Server provides the IsIndexable argument to the built-in
COLUMNPROPERTY function to let you know if a column is indexable. The
function udf_Tbl_ColumnIndexableTAB, which youll find in the TSQLUDFS
database, uses COLUMNPROPERTY to report on the indexability of any table
column and on some related properties. Heres a call for the NWOrders
table:
59
DATA_TYPE
------------int
nchar(5)
int
datetime
datetime
datetime
Comp
---NO
NO
NO
NO
NO
NO
Indexable
--------YES
YES
YES
YES
YES
YES
datetime
YES YES
Deterministic
------------NO
NO
NO
NO
NO
NO
Precise
------NO
NO
NO
NO
NO
NO
YES
YES
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
Part I
Chapter 2: Scalar UDFs
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 values. 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
ANSI_PADDING
ON
ANSI_WARNINGS
ON
ARITHABORT
ON
CONCAT_NULL_YIELDS_NULL
ON
QUOTED_IDENTIFIER
ON
NUMERIC_ROUNDABORT
OFF
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. Creating and maintaining it become a waste of effort.
For UDFs to be used in computed columns that are indexed, particular 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
61
Summary
Scalar UDFs are only one of the three types of functions, but they represent 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 governed 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 scalar 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 righthand 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 computed columns that are indexed. Using a UDF, a CHECK constraint or
computed column allows calculations to be made on data thats not in the
Part I
Chapter 2: Scalar UDFs
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 requirement 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 maintained 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.
Query Analyzer
SQL Profiler
Enterprise Manager
I assume you already know how to use all three of these tools. Ill be concentrating the discussion on features that are specific to UDFs. In
particular:
n
Debugging UDFs
The command-line tools OSQL and ISQL can still be used with UDFs. I
find them much less useful than the GUI tools. ISQL is obsolete, and
OSQL should really be the only DOS tool that you use with SQL Server
2000. About the only time that I use it is when I have a SQL script that I
want to run from a Windows BAT or CMD file. The section Enterprise
Manager and Other Tools covers them briefly.
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.
65
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 multistatement 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 procedure 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.
Listing 3.1: Debugging with DEBUG_udf_DT_NthDayInMon
CREATE PROCEDURE DEBUG_udf_DT_NthDayInMon AS
DECLARE @dt datetime -- answer from udf_DT_NthDayInMon
-- First case taken from the function test
SELECT @dt = dbo.udf_DT_NthDayInMon(2003, 2, 3, 'Mon')
SELECT CAST('2003-02-17' as datetime) as [Correct Answer]
, @dt as [udf_DT_NthDayInMon answer]
, Case when '2003-02-17'=@dt THEN 'Worked' ELSE 'ERROR' END
-- Second case is harder one. Should be 3/30/03
SELECT @dt = dbo.udf_DT_NthDayInMon (2003, 3, 5, 'SUNDAY')
SELECT CAST('2003-03-30' as datetime) as [Correct Answer]
, @dt as [udf_DT_NthDayInMon answer]
, Case when '2003-03-30'=@dt THEN 'Worked' ELSE 'ERROR' END
GO
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
66
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
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 highlight 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.
67
Its a good idea to leave the Auto roll back check box checked. Leaving it
checked causes the debugger to surround the execution of your procedure
with BEGIN TRAN and ROLLBACK TRAN statements. This prevents any undesired changes to the database from taking place. Of course, your UDF
cant make any such changes, but your stored procedure might.
Once you press the Execute button, Query Analyzer brings up the
debugging window, as shown in Figure 3.4. In the figure Ive closed the
Object Browser to give more room to view debugging information.
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
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
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 conversion 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_NthDayInMon. 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.
69
Part I
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-resolution, 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 intermediate 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 completed 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.
2.
71
They let you start each function with standard parts of the function,
such as the function header, the comment block, the function body,
and the GRANT statement.
The substitution feature places the same text into every place where
it is used in the function.
Theres nothing magical about starting out with standard text for creating
UDFs. You could do that with a plain text file. The advantage comes when
you use the substitution feature to replace text strings, such as the function name, everywhere that they should appear in the text. This makes
creation of a UDF faster, more consistent, and less error prone.
Take a look at Listing 3.2 on the following page for an example of how
substitution works. Its the template I use for creating scalar UDFs.
Strings in angle brackets (< >) are replaced during the substitution process. The substitution <scalar_function_name> is used in four places in
the script, and when the substitution is performed the function name is
entered identically in each place.
SQL Server ships with templates for creating each of the three types
of functions. The download directory Templates has my version of all of
the templates including inline and multistatement UDFs. If you like them,
copy them to the directory where SQL Server keeps templates for creating UDFs: %Install Directory%\80\tools\Templates\SQL Query\Analyzer\
Create Function, so theyll show up when you select a template.
The details of the comment block are discussed at length in Chapter 6
so I wont repeat them here. Lets step through a quick example of creating a function with a template.
The first step is to load the template into a query window. There are
three ways to do this:
n
Use the Templates tab on the Object Browser, select a template, and
use its context menus Open item.
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
72
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
Figure 3.8 shows the Query Analyzer as the TSQL UDFS Create Scalar 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.
73
Part I
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 procedures. 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
RPC:Complete
RPC:Starting
SP:CacheHit
One event each time the UDF is found in the procedure cache
SP:CacheInsert
SP:CacheMiss
SP:CacheRemove
SP:Completed
When a UDF completes. TextData shows the statement that invoked the UDF.
SP:ExecContextHit
SP:Recompile
SP:Starting
SP:StmtCompleted
SP:StmtStarting
75
If you turn on all these events, youll see either a CacheHit or a CacheMiss and then a CacheHit every time a UDF is invoked. The next event
is an ExecContextHit followed by a StmtStarting event. Rather than my
talking about what happens, the best way to become familiar with profiling
UDFs is to try them out using the following series of steps.
Start by running this short script to get the object ID of two UDFs in
the TSQLUDFS database:
-- Get the object ID of two UDFs for tracing
SELECT OBJECT_ID ('dbo.udf_SESSION_OptionsTAB')
as [udf_SESSION_OptionsTAB]
, OBJECT_ID ('dbo.udf_DT_age') as [udf_DT_age]
GO
(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.
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
76
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
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.
77
To filter on a UDF, you should use ObjectID and not ObjectName. The
tracing mechanism doesnt capture the name of a UDF, and so
ObjectName isnt useful for filtering.
Now, flip back to Query Analyzer and run a few queries. Start with:
-- How old is the author
SELECT dbo.udf_DT_age('1956-07-10', null) as [Andy's Age]
GO
(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.
The only event you should see is the SQL:BatchCompleted event for the
SELECT statement. That is assuming that you didnt remove SQL:BatchCompleted from the Events tab of the Trace Properties dialog box. Filters
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
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 column, 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:
-- Try to force a recomple event.
SELECT dbo.udf_Order_Amount(10929) as [Amount of order 10929]
exec sp_recompile udf_Order_Amount
exec sp_recompile 'NWOrderDetails'
SELECT dbo.udf_Order_Amount(10929) as [Amount of order 10929]
GO
(Results)
Amount of order 10929
--------------------1174.7500
Object 'udf_Order_Amount' was successfully marked for recompilation.
Object 'NWOrderDetails' was successfully marked for recompilation.
Amount of order 10929
--------------------1174.7500
79
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
80
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
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.
81
Summary
For writing and debugging UDFs, stored procedures, and scripts, Query
Analyzer and SQL Profiler are big improvements over their predecessors
and pretty good tools. Their debugging capabilities dont rise to the level
of Visual Studio .NET or some Java debuggers, but they get the job done.
Enterprise Manager is there for overall management of servers, databases, and database objects, including UDFs. It treats UDFs the same as
other database objects, so all of Enterprise Managers usual tools can be
used on them.
The topic of what can and what cannot go into a UDF has come up
several times. The next chapter is about what you cant do in a UDF. In
some cases Ill show you a way around the limitation.
Part I
Chapter 3: Working with UDFs in the SQL Server Tools
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 deterministic 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 nondeterministic 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:
-- Try to create a UDF that returns seconds since a time
CREATE FUNCTION dbo.udf_DT_SecSince (
@FromDate datetime
) RETURNS INT -- Seconds since a time
AS BEGIN
RETURN DATEDIFF (s, @FromDate, getdate())
END
GO
(Results)
Server: Msg 443, Level 16, State 1, Procedure udf_DT_SecSince, Line 7
Invalid use of 'getdate' within a function.
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
DATENAME
85
and then might get different results when the UDF is invoked with the
default value of @@DATEFIRST. The potential for changing the results makes
them unsuitable for use in a computed column that is indexed. You can
still use them in UDFs that are never referred by an index.
There are two ways around the restriction on using nondeterministic
functions:
n
Supplying the second argument isnt convenient, and it makes the caller
do the work. UDFs should make the callers job easier, not harder. The
Part I
Chapter 4: You Cant Do That in a UDF
86
Part I
Chapter 4: You Cant Do That in a UDF
/*
* Returns the time as a CHAR(8) in the form HH:MM:SS
* This function bypasses SQL Server's usual restriction
* against using getdate by selecting it from a view.
*
* Related Objects: Function_Assist_Getdate
*
* Example:
select dbo.udf_DT_CurrTime()
***************************************************************/
AS BEGIN
DECLARE @CurrDate datetime
SELECT @CurrDate = [GetDate]
FROM Function_Assist_Getdate
RETURN CONVERT (char(8), @currDate, 108)
-- 108 is HH:MM:SS 24-hour clock
END
GO
GRANT EXECUTE ON [dbo].[udf_DT_CurrTime] TO [public]
GO
Obviously, its time for lunch. Ill go eat before discussing restrictions on
access to data.
87
Part I
Chapter 4: You Cant Do That in a UDF
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.
89
That pretty much covers what can be done with temporary tables. Theres
just no access to them from a UDF. Even if a stored procedure creates one
and then creates the UDF dynamically, the temporary table is inaccessible.
If I forgot to mention something that could be done with a temporary
table, dont worryyou cant do it in a UDF.
In place of temporary tables, SQL Server 2000 provides the TABLE
variable, which can be used in UDFs as well as in stored procedures,
triggers, and SQL scripts. TABLE variables fill the need for short-term
multi-row storage that sometimes comes up in the various types of procedures. Theyre very much like temporary tables.
TABLE variables were discussed at length in Chapter 2, so I wont
repeat everything that was said there. One item that bears repeating is
that TABLE variables are not just a memory object. They are created in
tempdb in a special way. Although they use storage in tempdb, there are
no entries for TABLE variables in the tempdb system tables. Small TABLE
variables may be stored in memory, but larger ones eventually are written
to and read back from disk.
Many stored procedures use temporary tables to communicate either
among stored procedures or between triggers and stored procedures. That
kind of communication isnt available to UDFs. Another mode of communication that isnt available from a UDF is messages.
Part I
Chapter 4: You Cant Do That in a UDF
90
Part I
Chapter 4: You Cant Do That in a UDF
91
Changing the value of DATEFIRST doesnt make any code more nondeterministic than it already is, since using functions that are sensitive to
DATEFIRST, such as DATEPART, already make a UDF nondeterministic. In any
Part I
Chapter 4: You Cant Do That in a UDF
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 consistently. The next subsection demonstrates this feature, which turns out to
be important for maintaining determinism along with the exceptions to the
rule.
Required Value
Description
ANSI_NULLS
ON
ANSI_PADDING
ON
ANSI_WARNINGS
ON
ARITHABORT
ON
CONCAT_NULL_YIELDS_NULL
ON
QUOTED_IDENTIFIER
ON
NUMERIC_ROUNDABORT
OFF
93
Listing 4.3 has the creation script of two UDFs that can be used to demonstrate how this works for the setting QUOTED_IDENTIFIER. The UDFs have
already been created in the TSQLUDFS database under the correct setting of QUOTED_IDENTIFIER.
Listing 4.3: udf_Test_Quoted_Identifier_Off and _On
SET QUOTED_IDENTIFIER OFF
GO
SET ANSI_NULLS ON
GO
CREATE
FUNCTION dbo.udf_Test_Quoted_Identifier_Off (
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
Part I
Chapter 4: You Cant Do That in a UDF
94
Part I
Chapter 4: You Cant Do That in a UDF
Now that the function has been created, the following script shows
how the setting that is in effect when a UDF is run has nothing to do with
the current setting of QUOTED_IDENTIFIER. The setting depends only on the
QUOTED_IDENTIFIER setting in effect when the UDF was created. Heres the
script, which youll find in this chapters Listing 0 Short Queries.sql file.
-- To verify that QUOTED_IDENTIFIER is a parse time option
-- Begin the test here and ....
SET QUOTED_IDENTIFIER ON
GO
-PRINT 'With QUOTED_IDENTIFIER ON'
SELECT dbo.udf_test_quoted_identifier_off() as [Off]
, dbo.udf_test_quoted_identifier_on() as [On]
GO
SET QUOTED_IDENTIFIER OFF
GO
PRINT 'With QUOTED_IDENTIFIER OFF'
SELECT dbo.udf_test_quoted_identifier_off() as [Off]
, dbo.udf_test_quoted_identifier_on() as [On]
GO
PRINT 'From OBJECTPROPERTY With QUOTED_IDENTIFIER OFF'
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
Off
---0
QUOTED_IDENTIFIER ON
On
---1
With
Off
---0
QUOTED_IDENTIFIER OFF
On
---1
95
change the behavior inside a UDF that was created with QUOTED_IDENTIFIER ON. Finally, the last SELECT shows the state of the ExecIsQuotedIdentOn
property for each UDF using the OBJECTPROPERTY function.
Its the object property ExecIsQuotedIdentOn that Query Analyzer uses
when it creates the SET QUOTED_IDENTIFIER statement that it inserts at the
top of scripts that it generates. In fact, its because the QUOTED_IDENTIFIER
and ANSI_NULLS settings are only in effect when the UDF is created that
Query Analyzer puts them at the top of the CREATE FUNCTION or
ALTER FUNCTION script each time theyre generated.
If you recall the discussion of indexes on computed columns from
Chapter 2, the successful indexing of computed columns, including those
that invoke UDFs, requires that seven database settings always be set to
specific states. The function udf_SQL_IsOK4Index, not listed, checks the
session properties at run time to verify that the properties meet the
indexing requirements. Listing 4.4 shows part of udf_Session_OptionsTAB,
which reports on those options and several others.
These options can be queried using the SESSIONPROPERTY built-in function or by picking bits out of the @@Options built-in function. The latter
approach is used by udf_Session_OptionsTAB. An abridged version of the
function is shown in Listing 4.4. Youll find the full version in the database.
Listing 4.4: udf_Session_OptionsTAB abridged
SET QUOTED_IDENTIFIER OFF -- Deliberately set off for this function
SET ANSI_NULLS OFF
-- Deliberately set off for this function
GO
CREATE FUNCTION dbo.udf_Session_OptionsTAB (
) RETURNS @Options TABLE
([Set Option] varchar (32)
, [Value]
varchar (17) -- @@LANGUAGE
-- could be 17 chars.
)
/*
* Returns a table that describe 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 of the UDF
* environment. See 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 MS KB article 306230
*
* Example:
select * from udf_Session_OptionsTAB ()
Part I
Chapter 4: You Cant Do That in a UDF
96
Part I
Chapter 4: You Cant Do That in a UDF
****************************************************************/
AS BEGIN
DECLARE @CurrOptn as int -- holds @@Options
SET @CurrOptn = @@OPTIONS -- get it once
INSERT INTO @Options ([Set Option], [Value])
VALUES ('DISABLE_DEF_CNST_CHK'
, CASE WHEN @CurrOptn & 1 = 1
THEN 'ON' ELSE 'OFF' END
)
. . .
INSERT INTO @Options ([Set Option], [Value])
VALUES ('ANSI_NULL_DFLT_ON'
, CASE WHEN @CurrOptn & 1024 = 1024
THEN 'ON' ELSE 'OFF' END
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('ANSI_NULL_DFLT_OFF'
, CASE WHEN @CurrOptn & 2048 = 2048
THEN 'ON' ELSE 'OFF' END
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('CONCAT_NULL_YIELDS_NULL'
, CASE WHEN @CurrOptn & 4096 = 4096
THEN 'ON' ELSE 'OFF' END
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('NUMERIC_ROUNDABORT'
, CASE WHEN @CurrOptn & 8192 = 8192
THEN 'ON' ELSE 'OFF' END
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('XACT_ABORT'
, CASE WHEN @CurrOptn & 16384 = 16384
THEN 'ON' ELSE 'OFF' END
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('@@DATEFIRST', CONVERT(varchar(17), @@DATEFIRST)
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('@@LOCK_TIMEOUT'
, CONVERT(varchar(17), @@LOCK_TIMEOUT)
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('@@TEXTSIZE'
, CONVERT(varchar(17), @@LOCK_TIMEOUT)
)
INSERT INTO @Options ([Set Option], [Value])
VALUES ('@@LANGUAGE'
, CONVERT(varchar(17), @@LANGUAGE)
)
RETURN
END
Value
-------------------200000000
us_english
mdy
4
SET
SET
SET
SET
SET
SET
SET
97
Part I
Chapter 4: You Cant Do That in a UDF
98
Part I
Chapter 4: You Cant Do That in a UDF
ANSI_NULL_DFLT_ON
ANSI_NULL_DFLT_OFF
CONCAT_NULL_YIELDS_NULL
NUMERIC_ROUNDABORT
XACT_ABORT
@@DATEFIRST
@@LOCK_TIMEOUT
@@TEXTSIZE
@@LANGUAGE
OFF
OFF
ON
ON
ON
4
-1
-1
us_english
Its a good idea to close the Query Analyzer session right now. The database 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 situations. 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
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.
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 solution. 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.
101
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:
-- use usp_Example_Runtime_Error to demonstrate normal error handling
DECLARE @RC int, @ErrorAfterProc int
EXEC @RC = usp_Example_Runtime_Error
SELECT @ErrorAfterProc = @@ERROR
PRINT '@@Error After Proc = ' + CONVERT(varchar(10), @ErrorAfterProc)
PRINT 'Return Code=' + CONVERT(varchar(10), @RC)
GO
Part I
Chapter 5: Handling Run-time Errors in UDFs
102
Part I
Chapter 5: Handling Run-time Errors in UDFs
(Results)
Server: Msg 547, Level 16, State 1, Procedure usp_Example_Runtime_Error, Line 29
INSERT statement conflicted with COLUMN CHECK constraint
'CK__@SampleTa__EvenN__1B89C169'. The conflict occurred in database 'tempdb', table
'#1A959D30', column 'EvenNumber'.
The statement has been terminated.
No Update after second insert due to error 547
ID
EvenNumber Error
RowCount
----------- ----------- ----------- ----------1
2
0
1
@@Error After Proc = 0
Return Code=0
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 statement 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 procedure depends on this setting. If XACT_ABORT is ON, a stored procedure
terminates as soon as any error is encountered, and the current transaction 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.
Listing 5.2: udf_Example_Runtime_Error, to demonstrate error handling
CREATE
FUNCTION dbo.udf_Example_Runtime_Error (
103
Part I
Chapter 5: Handling Run-time Errors in UDFs
104
Part I
Chapter 5: Handling Run-time Errors in UDFs
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 intelligent 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_Multistatement that you can use to demonstrate how it works. The procedure
DEBUG_udf_Example_Runtime_Error_Multistatement has a reasonable demonstration 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 created 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 expression 100/Num is evaluated for the third row.
Listing 5.3: udf_Example_Runtime_Error_Inline
CREATE
FUNCTION dbo.udf_Example_Runtime_Error_Inline (
) 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
105
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 statement 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.
Part I
Chapter 5: Handling Run-time Errors in UDFs
106
Part I
Chapter 5: Handling Run-time Errors in UDFs
107
Meaning
Unknown error
Not applicable
Input illegal
Division by zero
10
Aggregation error
11
12
13
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.
Part I
Chapter 5: Handling Run-time Errors in UDFs
108
Part I
Chapter 5: Handling Run-time Errors in UDFs
The following queries illustrate what happens when you execute a SELECT
that calls this function using three variations:
-- Query with UDF that doesn't divide by zero
SELECT top 2 au_id, au_fname, au_lname
, dbo.udf_Test_ConditionalDivideBy0(0) [Don't Divide by Zero]
FROM pubs..authors
GO
(Results)
au_id
----------409-56-7008
648-92-1872
au_fname
--------Abraham
Reginald
au_lname
Don't Divide by Zero
-------------- -------------------Bennet
0
Blotchet-Halls
0
109
-- 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)
au_id
----------409-56-7008
648-92-1872
au_fname
--------Abraham
Reginald
au_lname
Request Divide by Zero
-------------- ---------------------Bennet
0
Blotchet-Halls
0
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 middle 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.
Part I
Chapter 5: Handling Run-time Errors in UDFs
110
Part I
Chapter 5: Handling Run-time Errors in UDFs
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 mentions 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.
Listing 5.6: udf_SQL_LogMsgBIT
CREATE FUNCTION dbo.udf_SQL_LogMsgBIT (
@nMessageNumber int = 50001 -- User-defined message >= 50000
, @sMessage varchar(8000) -- The message to be logged
, @sSeverity varchar(16) = NULL -- The severity of the message
-- may be 'INFORMATIONAL', 'WARNING', OR 'ERROR'
) RETURNS BIT -- 1 for success or 0 for failure
/*
* Adds a message to the SQL Server log and the NT application
* event log. Uses xp_logevent. xp_logevent can used whenever
* 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.
*
* Example:
select dbo.udf_SQL_LogMsgBIT(default,
'Now that''s what I call a message!', NULL)
****************************************************************/
AS BEGIN
DECLARE @WorkingVariable BIT
IF @sSeverity is NULL
EXEC @WorkingVariable = master..xp_logevent
,
ELSE
EXEC @WorkingVariable = master..xp_logevent
,
,
END
-- xp_logevent has it backwards
RETURN CASE WHEN @WorkingVariable=1 THEN 0 ELSE
END
@nMessageNumber
@sMessage
@nMessageNumber
@sMessage
@sSeverity
1 END
111
You can view the SQL Server message log with Enterprise Manager. Figure 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
Chapter 10 has an entire section on using xp_logevent, so I wont elaborate any more here. Its a technique that comes in handy in a pinch. Be
careful not to overuse it, or youll fill your event log in a hurry.
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 handling issue in your UDF code. These techniques boil down to:
n
Let the error happen and make the caller responsible for handling it.
Find errors by careful checking of parameters and intermediate values, and return NULL for invalid values.
Return a special value for the function that tells the caller what type
of error occurred.
Part I
Chapter 5: Handling Run-time Errors in UDFs
112
Part I
Chapter 5: Handling Run-time Errors in UDFs
Ive also shown two techniques that should be reserved for desperate
times:
n
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 potential 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 separators 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 pattern. 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.
Documentation,
Formatting, and
Naming
Conventions
What does this next function do?
Create function getcommaname (@f varchar(20),@m varchar(20),@l
varchar(40),@s Varchar(20))returns Varchar(128) as begin
declare @t varchar(64) set @t = ltrim(rtrim(@l))
if len(@t) > 0 set @t = @t + ',' if len(ltrim(rtrim(@f))) > 0
set @t = @t + ' ' + ltrim(rtrim(@f))
if len(ltrim(rtrim(@m))) > 0 set @t = @t + ' ' + ltrim(rtrim(@m))
if len(ltrim(rtrim(@s))) > 0
set @t = @t + ' ' + ltrim(rtrim(@s)) return @t end
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 TSQLUDFS 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 reformatted 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
Any of these people might be the original author of the function. A couple
of weeks after writing any code, Im hard pressed to recite what it does,
much less the exact calling sequence of a function.
One problem with writing a chapter on programming conventions
such as variable naming, comment formats, and SQL statement formats is
that theres no right answer. Ive been working with SQL for decades and
with SQL Server UDFs for a few years, and I think I have something to
say about how to write them. I can explain why I think a convention is the
best choice, but there are always going to be good arguments for some
other way. Please take this chapter the way its intended: the well-intentioned conventions from an experienced programmer whos spent some
time thinking about how to produce good code.
With that said, for purposes of illustration, Listing 6.1 shows the function udf_Name_FullWithComma, which concatenates the parts of a name to
form a last name first, first name last string. Its used as an example
throughout this chapter. Lets start with an explanation of how I format
SQL statements.
Listing 6.1: udf_Name_FullWithComma
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE FUNCTION dbo.udf_Name_FullWithComma (
@sFirstName nvarchar(20) = N'' -- First Name
, @sMiddleName nvarchar(20) = N'' -- Middle or Initial
, @sLastName nvarchar(40) = N'' -- Last Name
, @sSuffixName nvarchar(20) = N'' -- Suffix name, like Jr or MD
) RETURNS nvarchar(128)
WITH SCHEMABINDING -- Or comment why not
115
/*
* Creates a concatented name in the form
* @sLastname, @sFirstName @sMiddleName @SuffixName
* in the process it is careful not to add double spaces.
* Prefix names such as Mr. are not common in this type of
* name and are not supported in this function.
*
* Example:
select dbo.udf_NameFullWithComma (au_Fname, null, au_lname, null)
from pubs..authors
* Test:
print 'Test 1 JJJS Jr ' + case when N'Jingleheimer-Schmitt, John J. Jr.'
= dbo.udf_NameFullWithComma ('John', 'Jacob',
'Jingleheimer-Schmitt', 'Jr.')
THEN 'Worked' ELSE 'ERROR' END
***********************************************************************/
AS BEGIN
DECLARE @sTemp nvarchar(128) -- Working copy of the name
SET @sTemp = ltrim(rtrim(@sLastName))
IF LEN(@sTemp) > 0
SET @sTemp = @sTemp + N','
IF LEN(ltrim(rtrim(@sFirstName))) > 0
SET @sTemp = @sTemp + N' ' + ltrim(rtrim(@sFirstName))
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
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
116
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
When formatting with separators first, almost all separators start their
own line. That leads to a lot of lines, but most importantly, when combined
with T-SQLs double-dash comment, it makes it easier to put an explanatory comment on each, and sometimes every, line. Heres an example:
-- SELECT with Separator First Formatting
SELECT a.au_id
, [dbo].[udf_Name_FullWithComma](au_fname, null
, au_lname, null) as Name -- so it's sortable
, au_fname, au_lname -- sent so client grid can sort
, t.title
, ta.royaltyper
FROM pubs..authors a with (nolock)
RIGHT OUTER JOIN pubs..titleauthor ta WITH (NOLOCK)
on a.au_id = ta.au_id
INNER JOIN pubs..titles t WITH (NOLOCK)
on ta.title_id = t.title_id
WHERE t.type = 'trad_cook'
and ta.royaltyper > 50 -- 50 cents. authors aren't paid well.
ORDER BY Name
, t.title -- multiple titles per author.
GO
For starters, each subclause (FROM, WHERE, ORDER BY) of the SELECT statement 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:
, au_fname, au_lname -- sent so client grid can sort on these columns.
117
My overriding decision criteria for layout is to make the code more readable. Its just too bad that I dont get paid by the line of code.
I try to put each table in the FROM clause on its own line with any WITH
clause. WITH clauses are hints instructing SQL Server how to perform the
query. This line pulls in information from the pubs.titleauthor table:
RIGHT OUTER JOIN pubs..titleauthor ta WITH (NOLOCK)
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 expression. 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 eliminate 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:
WHERE t.type = 'trad_cook'
-- and ta.royaltyper > 50 -- 50 cents. authors aren't paid well.
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:
CREATE FUNCTION dbo.udf_Name_FullWithComma (
@sFirstName nvarchar(20) = N'' -- First Name
, @sMiddleName nvarchar(20) = N'' -- Middle or Initial
, @sLastName nvarchar(40) = N'' -- Last Name
, @sSuffixName nvarchar(20) = N'' -- Suffix name, like Jr or MD
) RETURNS nvarchar(128)
WITH SCHEMABINDING -- Or comment why not
/*
* The function comment block goes here
***************************************************************/
AS BEGIN
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
118
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
Put the opening parenthesis on the line with the function name.
Skip a line.
Put the right parenthesis that closes the parameter list on a line with
the RETURNS clause.
Add the WITH SCHEMABINDING line. It has the WITH SCHEMABINDING line or
a comment that says why the function is not schemabound. If you add
WITH ENCRYPTION, it also goes here.
Not only do these practices make formatting a little easier, they document
the parameters. One of the reasons to get every parameter on its own line
is to leave room for the comment describing how the parameter is used.
Unless the name says it all, each parameter deserves a comment
describing how to use it.
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 templates was discussed in Chapter 3 in the Query Analyzer section.
119
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
120
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
* 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:
121
* Example:
SELECT dbo.udf_Name_FullWithComma ('John', 'Jacob'
, 'Jingleheimer-Schmitt', default)
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.
PRINT 'Test 1 JJJS Jr ' + case when 'Jingleheimer-Schmitt, John J. MD'
= dbo.udf_NameFullWithComma ('John', 'Jacob',
'Jingleheimer-Schmitt', 'Jr.')
THEN 'Worked' ELSE 'ERROR' END
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 additional 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 1 JJJS
Worked
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
122
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
* 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 comprehensive 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 functions 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 information 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
123
Naming Conventions
There are many naming conventions. Throughout this book, youll see a
pretty consistent convention for naming functions, parameters, local variables, 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.
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
124
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
function. Thats why there is so much documentation in the function headers in this book. There isnt going to be any external documentation
unless I can figure out how to write a program that creates it.
The name is the part of the function that lets it be found. Some of my
ideas are influenced by long-term exposure to Intellisense in Visual Studio. If youre not familiar with it, its the technology that Microsoft has
built into its Visual Studio integrated development environment (IDE) that
helps complete the names of variables by displaying a list of the known
variables that are in scope and that start with the characters you just
typed. Sadly, Intellisense isnt part of SQL Server 2000. I strongly suspect
that when SQL Server is integrated with .NET, well see it.
Figure 6.1 illustrates how Intellisense shows the list of functions
available while editing a small Visual Basic 6 program. While editing, I
typed three characters, Tex, and then pressed Ctrl+Space. That brings
up a list of all the names in scope that begin with Tex. These include all
of my library functions in the Text group and the TextBox group. I can
then scroll up or down to find the function that Im interested in.
In the English language, naming functions with the group name followed
by the action sounds somewhat unnatural. Its more natural to put the
verb first and a name function GetSoundex than Text2Soundex, as shown in
Figure 6.1. I have to ask, How is starting 500 function names with Get
ever going to help anyone find the function or even understand what it
does no matter how natural it sounds? Ive never heard a satisfactory
answer to that question, and so Ive reversed the order of most names to
maximize the ease of finding the function that the programmer is looking
for.
Here is how I break down function names:
udf_<group><object><action><domain>
Obviously, the udf_ prefix tells anyone who sees such a reference that
theyre looking at a user-defined function. Thats only one of the possible
prefixes that could be used for UDFs. For system UDFs, Microsoft uses
fn_. Chapter 19 shows you how to create your own system UDFs. Unless
thats what youre doing, dont use the fn_ prefix.
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
125
Description
Currency
DT
Example
BitS
For working with strings that represent the bits of a longer data
type
Func
UDF-related functions
Instance
Lst
Name
Num
Numeric functions
Object
Proc
Session
SQL
SYS
Tbl
Test
Trc
Trig
Trigonometric functions
Txt
Text manipulation
TxtN
Unit
126
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
udf_
Standard prefix
Name
Suffix
Check
BIT
The domain BIT says that it returns the SQL Server data type BIT,
which is used for a true/false meaning. BOOL is an alternative
name thats less SQL Server specific.
127
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
128
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
To see whats really happening, start the SQL Profiler and add these
events: SP:CacheHit, SP:CacheMiss, SP:StmtStarting, SP:StmtCompleted, 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
129
Naming Columns
Column names should tell the programmers as much as possible about
what is stored in the column. In many programming environments, naming convention dictates that the data type of a variable become part of the
name. For example, the Hungarian notation commonly used in C++ and
often in Visual Basic starts each variable with a one- to three-character
prefix that denotes the type of the variable. For example, s or str indicate
string and f indicates float.
However, the data type isnt something that should go into a column
name. Microsoft recommends against using Hungarian notation when
naming columns, and I agree (see http://www.microsoft.com/technet/
treeview/default.asp?url=/technet/prodtechnol/sql/plan/inside6.asp).
When the data type of a column changes, the name of the column might
also have to change. That has a ripple effect on stored procedures, functions, and views. The pain involved in changing all the code in stored
procedures and views when a data type changes is not worth the benefits
in adding the additional description in the column name. Whats more, client programs such as Windows applications and reports might also have to
change.
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:
EXEC sp_addtype N'DomainExchangeCD', N'CHAR(3)', N'not null'
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
130
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
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:
-- Try to create a UDF with a UDT and SCHEMABINDING
CREATE Function dbo.udf_Exchange_TestCodeBIT (
@ExchangeCD DomainExchangeCD -- Which exchange, NYSE, NASD, etc.
RETURNS BIT
WITH SCHEMABINDING
AS BEGIN
RETURN CASE WHEN @ExchangeCD in ('NYSE', 'DAX', 'AMEX')
THEN 1 ELSE 0 END
END
GO
)
(Results)
Server: Msg 2012, Level 16, State 1, Procedure udf_Exchange_TestCodeBIT, Line 0
User-defined variables cannot be declared within a schema-bound object.
/*
* Returns a TABLE with a list of all brokers who are members of
* the exchange @ExchangeCD.
*
* Example:
SELECT * from dbo.udf_Exchange_MembersList('NYSE')
****************************************************************/
AS RETURN
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
131
Using the domain name tells the programmer something about what the
column stores. It doesnt tell about the type. DomainExchangeCD could have
been stored as a tinyint without changing any of the scripts above, except
the one that created the type in the first place.
The reason given earlier to avoid Hungarian notation when naming
columns is no longer a problem when a domain name is used. It would
make no difference to the code in any stored procedure or view when the
definition of a domain name was changed from, lets say, integer to Numeric
(18,2). Thats not to say that changing a UDT is easy; its not.
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 readability and easier manipulation of the text.
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
132
Part I
Chapter 6: Documentation, Formatting, and Naming Conventions
rable categories. A noun-verb pair that tells the prospective caller what
object is manipulated and what type of manipulation occurs follows the
group.
The objective is always to communicate fully to the users of the function and to the person responsible for maintaining the function in the
future. What else is a name for?
Now that youve read about various topics regarding creating UDFs,
its time to take a more detailed look at inline and multistatement types.
They are subject to most of the same rules as scalars, but each type has
its own peculiarities. Chapter 7 covers inline UDFs, and Chapter 8 covers
multistatement UDFs.
Inline UDFs
Inline UDFs are a form of SQL view that accepts parameters. The parameters 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 available 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 parameters, 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 statement. 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
135
adding the extra characters either makes a name too long or when the
name already tells the programmer that this UDF returns a table.
Weve already seen several inline UDFs in previous chapters. Most
of them use their parameters in the WHERE clause to restrict the rows
returned by the function. The parameters can also be used in:
n
Inline UDFs can have inline SELECT statements, GROUP BY, HAVING, and any
other clause that could go into a SELECT with the exception of COMPUTE,
COMPUTE BY, FOR XML, FOR BROWSE, OPTION, and INTO. The SELECT statement is
also subject to the restrictions discussed in Chapter 4. For example, a
UDFs SELECT cant reference temporary tables or TABLE variables.
As with all UDFs, there are two options that may be used in a WITH
clause when the UDF is created: SCHEMABINDING and ENCRYPTION.
SCHEMABINDING ties the UDF to the objects that it references, preventing
changes to those objects unless the binding is removed. ENCRYPTION
secures the CREATE FUNCTION script from being viewed by casual users.
However, its not very secure. As mentioned elsewhere in this book, the
formula to decrypt SQL objects, including UDFs, has been published on
the web.
In the FROM clause of the SELECT, all references to tables, views, or
other UDFs should use a two-part name that includes the name of the
object owner. Its usually dbo. There are two reasons for the owner
qualification:
n
Its slightly more efficient because SQL Server doesnt have to check
to see if theres an object of that name owned by the current user.
Qualifying the name is always a good idea because it ensures that you
avoid any errors caused by having multiple versions of a UDF in the database. That might result in invoking the wrong UDF at run time.
A few SQL Server features that are available for views are not available for UDFs:
n
Part I
Chapter 7: Inline UDFs
136
Part I
Chapter 7: Inline UDFs
This mixture of feature availability makes the inline UDF a possible substitute 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 preferable, 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.
dbo always owns the UDF. I recommend that only dbo own database
objects.
No DROP statement for the UDF. If the UDF already exists, change
CREATE to ALTER.
Inclusion of a comment block with a place for a description, an example, test cases, and the modification history.
There are two GRANT statements at the bottom of the file. The first is the
one thats almost always used to give SELECT permission to PUBLIC. The
second GRANT statement is in the file for use with an updatable UDF. Its
commented out because its only used on rare occasions.
The procedure for turning the template into an inline UDF is the
same procedure that was shown in Chapter 3 for turning the template for
a scalar UDF into a working function. Theres no need to repeat it here, so
we can move on to creating inline UDFs.
137
Part I
Chapter 7: Inline UDFs
138
Part I
Chapter 7: Inline UDFs
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.
139
As with all other objects, I suggest that dbo be the only user to own
inline UDFs.
The previous section created the function udf_Category_ProductCountTAB in Listing 7.2. We can use it to retrieve categories that have nine
or more products, as shown in this query:
-- Get products by category
SELECT *
FROM dbo.udf_Category_ProductCountTAB(9)
ORDER BY NumProducts asc
GO
(Results)
CategoryID
----------4
8
1
2
3
CategoryName
NumProducts
--------------- ----------Dairy Products
10
Seafood
12
Beverages
12
Condiments
12
Confections
13
Description
NumProducts
------------------------------------------ ----------Soft drinks, coffees, teas, beers, and ...
12
Sweet and savory sauces, relishes, spre...
12
Desserts, candies, and sweet breads
13
Cheeses
10
Breads, crackers, pasta, and cereal
7
Prepared meats
6
Dried fruit and bean curd
5
Seaweed and fish
12
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_Category_ProductCountTAB with the Categories table is not the optimal way of
Part I
Chapter 7: Inline UDFs
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.
141
Once again, were faced with a three-way trade-off between performance, functionality, and ease of coding. Like the other times that this
trade-off was mentioned, youre going to have to decide. Do you want a
UDF that makes the programming job easier, or are you more concerned
with performance?
There are no hard and fast rules about when to choose performance,
but the factors that I take into account are the number of rows that are
likely to be returned by the UDF and the frequency of use. If the number
of likely rows goes over one thousand or the frequency of invocation goes
over once per minute, I think about it seriously and may remove the ORDER
BY clause from the UDF. When in doubt, leave it out. The caller can always
sort the functions resultset, and the SQL Server optimizer can do a better
job of creating an optimal plan.
Part I
Chapter 7: Inline UDFs
142
Part I
Chapter 7: Inline UDFs
League (NL) and American League (AL). Figure 7.1 shows the schema of
the BBTeams table in a SQL Server diagram.
/*
* Returns the ID, Name, and Manager of all teams in a league
*
* Example:
select * FROM dbo.udf_BBTeams_LeagueTAB('NL')
****************************************************************/
AS RETURN
SELECT [ID], [Name], League, Manager
FROM dbo.BBTeams
WHERE League = @LeagueCD
GO
GRANT SELECT, INSERT, UPDATE, DELETE
ON dbo.udf_BBTeams_LeagueTAB
GO
TO [PUBLIC]
143
The following series of batches demonstrate that unlike the limits placed
on INSERT statements into views that use WITH CHECK OPTION, the parameters to the inline function have no bearing on what can be inserted into the
UDF. Start by listing the teams in the American League:
-- Get the list of teams in the American League
SET NOCOUNT OFF
SELECT * FROM dbo.udf_BBTeams_LeagueTAB ('AL')
GO
(Results)
ID
----------2
7
Name
-----------Yankees
Red Sox
League
-----AL
AL
Manager
-----------Yogi
Zimmer
(2 row(s) affected)
Part I
Chapter 7: Inline UDFs
144
Part I
Chapter 7: Inline UDFs
The message indicates that one row is affected, confirming the success of
the statement. The next queries verify the contents of the BBTeams table:
-- Verify that the insert worked
PRINT 'American League'
SELECT * FROM dbo.udf_BBTeams_LeagueTAB ('AL')
PRINT 'National League'
SELECT * FROM dbo.udf_BBTeams_LeagueTAB ('NL')
GO
(Results)
American League
ID
Name
----------- -----------2 Yankees
7 Red Sox
League
-----AL
AL
Manager
-----------Yogi
Zimmer
League
-----NL
NL
NL
Manager
-----------Walt
Joe
Casey
(2 row(s) affected)
National League
ID
Name
----------- -----------1 Dodgers
3 Cubs
11 Mets
(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 executed. For example, these two statements dont modify the database as
intended:
145
(0 row(s) affected)
Both of the statements modify zero rows because udf_BBTeams_LeagueTAB('AL') doesnt return any rows that satisfy the statements WHERE
clause. The UDF is updatable when supplied with arguments that return
the rows were trying to update. Here we go:
-- Updates only occur if the row is in the result of the UDF
UPDATE dbo.udf_BBTeams_LeagueTAB ('NL')
SET Manager = 'Hodges'
WHERE name = 'Mets'
GO
(Results)
(1 row(s) affected)
This last batch gets rid of the Mets so that you can run the experiment
again some other time:
-- Clean up the data for the next experiment
DELETE FROM BBTeams WHERE [Name] = 'Mets'
GO
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 preferable to updatable UDFs. At least with views, the limitations are explicit
and consistent.
Part I
Chapter 7: Inline UDFs
146
Part I
Chapter 7: Inline UDFs
Database access is the most expensive component of the web application to scale up, and it should be optimized.
147
screen. Ive come to the conclusion that its best to keep pages short, at
about the amount that can be shown on a single screen.
If the aim is to retrieve rows in screen size groups and not cache extra
rows at the web server, an inline UDF works very well. The next section
shows how to create UDFs to support the page forward and page back
operations.
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 creation 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 essential. 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 columns 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 UnitsInStock column doesnt provide uniqueness, and we must add an additional
column or columns. While there might be some benefit to using [Total
Part I
Chapter 7: Inline UDFs
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:
ORDER BY UnitsInStock desc
, P.ProductName asc
, P.ProductID asc
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 arguments, 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 numbers 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 comparisons as:
149
But thats wrong! The problem is that it only returns rows with ProductName 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:
AND (P.UnitsInStock <= @LastUnitsInStock
OR (P.UnitsInStock = @LastUnitsInStock
AND P.ProductName >= @LastProductName)
OR (P.UnitsInStock = @LastUnitsInStock
AND P.ProductName = @LastProductName
AND P.ProductID >= @LastProductID)
)
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_ProductByUnits_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.
Listing 7.5: udf_Paging_ProductByUnits_Forward
CREATE FUNCTION dbo.udf_Paging_ProductByUnits_Forward (
-- 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
Part I
Chapter 7: Inline UDFs
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:
-- Get the first page of data
SELECT ProductID, ProductName, UnitsInStock as Units
, [Total Sold], CategoryName as Cat
FROM udf_Paging_ProductByUnits_Forward (default, default, default)
GO
(Results - abridged with some fields truncated)
ID
----75
40
6
...
59
65
ProductName
Units Total Sold Category
------------------------------ ------ ----------- -------------Rhnbru Klosterbier
125
1155 Beverages
Boston Crab Meat
123
1103 Seafood
Grandma's Boysenberry Spread
120
301 Condiments
Raclette Courdavault
Louisiana Fiery Hot Pepper Sau
79
76
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:
-- Get the second page
SELECT ProductID as [ID], ProductName, UnitsInStock as Units
, [Total Sold], CategoryName as Category
FROM udf_Paging_ProductByUnits_Forward
(76, 'Louisiana Fiery Hot Pepper Sauce', 65)
GO
Part I
Chapter 7: Inline UDFs
151
ID
ProductName
Units Total Sold Category
----- ------------------------------ ------ ----------- -------------65 Louisiana Fiery Hot Pepper Sau
76
745 Condiments
25 NuNuCa Nu-Nougat-Creme
76
318 Confections
39 Chartreuse verte
69
793 Beverages
...
52 Filo Mix
38
500 Grains/Cereals
The query plan is cached, which saves the time it takes to create the
execution plan after the first time its used.
As youve already heard, its the latter reason that I think is most important. Separating the SQL logic from other page creation logic is a
simplifying step that pays many times over in a reduction in complexity
and thus in maintenance effort. For this reason, I almost always move all
my SQL into stored procedures or UDFs and out of the web creation
script.
How about paging backward? I know of two approaches to paging back
based on the inline UDF technique:
n
Save a stack with the key values for the start of each page as the user
navigates forward.
The first method requires that you save the key values for the top of each
page and use them as the arguments to the forward paging UDF. Once
youre saving one set of values, you might as well save an array, used as a
stack. This approach involves more coding on the web page creation side
and has the additional disadvantage that it could miss one or more rows if
insertion of rows was going on at the same time as paging.
Writing the inline UDF to page in reverse is very similar to writing
the forward paging UDF, with the addition of an extra sort operation.
Listing 7.6 shows the CREATE FUNCTION script for udf_Paging_ProductByUnits_Reverse.
152
Part I
Chapter 7: Inline UDFs
153
By the way, to use the reverse paging function, the web page creation
code must save the three key values from the first row on the page. These
can be saved in the SESSION object in the same way that the key values
from the last row on the page are saved for paging forward.
Inline UDF parameters combined with the TOP clause can be put to
use retrieving just the right number of rows to show on each web page.
This has proven to be an effective strategy in data-driven web sites.
Summary
This chapter has shown how inline UDFs are similar to views. The addition of parameters makes them more powerful. By using the parameters
in the SELECT statement, choices that the user of a similar view would normally 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.
Part I
Chapter 7: Inline UDFs
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
Multistatement
Table-valued
TABLE
Table function
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
156
Part I
Chapter 8: Multistatement UDFs
Permissions for managing and using multistatement UDFs are very similar to permissions on the other types of UDFs. The mix is slightly
different due to the limits on what can be done with them.
157
It returns a table.
The table returned by the UDF is a TABLE variable with a scope of the
entire UDF. Its declared in the RETURNS clause in the function header.
Some of the useful features of the returned table are that it can have:
n
An identity column
A ROWGUID column
A primary key
CHECK clauses
UNIQUE constraints
Defaults
Triggers
A storage clause
The table returned by a UDF is treated like a TABLE variable. As such, its
an object in tempdb that is not stored in tempdbs system tables. Like
TABLE variables, the pages of the table are written to disk in tempdb.
Depending on the amount of data in the table and the available RAM, data
pages from the table may or may not ever be removed from SQL Servers
data cache and stored on disk. You only have to worry about this if the
amount of data in the TABLE grows large relative to the available RAM or if
theres a possibility of running out of space in tempdb.
Once the function header with the table definition is declared, you
may write the function body. All the limitations on which T-SQL statements are allowed in a UDF that are discussed in Chapter 5 apply to
multistatement UDFs. While this limits what you can do, it ensures that
the UDF has no side effects.
The lack of side effects is one of the features of multistatement UDFs
that distinguish them from stored procedures. In many ways, theyre similar to a stored procedure that returns a single resultset. If you can live
with the restrictions, including the restriction on calling stored
Part I
Chapter 8: Multistatement UDFs
158
Part I
Chapter 8: Multistatement UDFs
159
Part I
Chapter 8: Multistatement UDFs
160
Part I
Chapter 8: Multistatement UDFs
If you recall from Chapter 7, udf_Category_ProductCountTAB was constructed to return the list of categories with at least @MinProducts
products. The TSQLUDFS database has the UDF udf_Category_ProductsTAB, which is not listed. It returns a list of the products in any particular
category. The data that were seeking can be obtained by a combination of
information from the two UDFs. What we need is the union of the results
of running udf_Category_ProductsTAB once for each category that is
returned by udf_Category_ProductsCountTAB. But theres no way to combine the two in a join because a column name cant be used as a parameter
to a UDF. It doesnt work.
The answer is to use a cursor. The cursor returns one category ID
and category name at a time. Then a call is made to udf_Category_ProductsTAB supplying @CategoryName from the cursor, and the results are
stored in the @Products TABLE variable that is ultimately returned as the
UDFs resultset. When weve looped through all the categories that satisfy our criteria, @Products has the union of the rows from those
categories. Heres a sample query:
-- User our cursor based multistatement UDF
SELECT CategoryName as Category, ProductName, QuantityPerUnit
, UnitsInStock as Stock, Discontinued as Disc
FROM dbo.udf_Category_BigCategoryProductsTAB(9)
GO
(Results - abridged)
Category
----------Beverages
Beverages
...
Condiments
Condiments
Confections
Confections
Confections
...
Seafood
Seafood
ProductName
-------------------Chai
Chang
QuantityPerUnit
Stock Disc
-------------------- ------ ---10 boxes x 20 bags
39
0
24 - 12 oz bottles
17
0
Sirop d'rable
Vegie-spread
Chocolade
Gumbr Gummibrchen
Maxilaku
24 - 500 ml bottles
15 - 625 g jars
10 pkgs.
100 - 250 g bags
24 - 50 g pkgs.
Rogede sild
Spegesild
1k pkg.
4 - 450 g glasses
113
24
15
15
10
0
0
0
0
0
5
95
0
0
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
161
syntax thats available when using CREATE TABLE. The key created is a
unique clustered index on the table.
The rows of the previous query are returned in the order in which
theyre stored in the UDFs temporary table. Although Ive never seen the
results returned in any other order, SQL Server is a relational database
and it doesnt guarantee the order in which rows are returned. If you want
a specific order, be sure to put an ORDER BY clause on the SELECT statement
that calls the UDF.
Another way to access data across databases is represented by
udf_Category_ProductCountTAB. Its a UDF in TSQLUDFS that reads data
from Northwind with explicit database references to its tables. The only
limitation on a UDF that references data in another database is that it cant
be created using the WITH SCHEMABINDING option.
Cursors can be used in either scalar or multistatement UDFs. Theyre
included in this chapter because its more common to find them in a
multistatement function. The code for a cursor follows a very predictable
pattern and creating them is easier with a template. Before we get to that,
lets take a look at a template for creating multistatement UDFs.
dbo always owns the UDF. I recommend that dbo own all database
objects.
No DROP statement for the UDF. If the UDF already exists, change
CREATE FUNCTION to ALTER FUNCTION.
Inclusion of a comment block with a place for a description, an example, test cases, and modification history.
The file has a GRANT statement for giving the SELECT permission to
PUBLIC.
Part I
Chapter 8: Multistatement UDFs
162
Part I
Chapter 8: Multistatement UDFs
The template reflects choices that I usually make about what to include or
exclude from a multistatement UDF. Ive never granted REFERENCES permission 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.
163
Part I
Chapter 8: Multistatement UDFs
164
Part I
Chapter 8: Multistatement UDFs
@<v1_name,
@<v2_name,
@<v3_name,
@<v4_name,
sysname,
sysname,
sysname,
sysname,
v1>
v2>
v3>
v4>
<v1_data_type,
<v2_data_type,
<v3_data_type,
<v4_data_type,
,int>
,int>
,int>
,int>
-----
<v1_description,,>
<v2_description,,>
<v3_description,,>
<v4_description,,>
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 performed 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. Sometimes 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
Chapter 8: Multistatement UDFs
165
@Item Varchar(8000)
@Pos int -- current starting position
@NextPos int -- position of next delimiter
@LenInput int -- length of input
@LenNext int -- length of next item
@DelimLen int -- length of the delimiter
@Pos = 1
@DelimLen = LEN(@Delimiter) -- usually 1
@LenInput = LEN(@sInputList)
@NextPos = CharIndex(@Delimiter, @sInputList, 1)
166
Part I
Chapter 8: Multistatement UDFs
This UDF is pretty easy to use. Just supply the delimited text and the
delimiter. This query demonstrates:
-- trial use of udf_Txt_SplitTAB
SELECT '->' + Item + '<-' as [->Item<-]
FROM udf_Txt_SplitTAB (
'Kaleigh, Phil IV, Ben, Kara, Eric, Tommy , Christine', default)
GO
(Results)
->Item<---------------------------->Kaleigh<->Phil IV<->Ben<->Kara<->Eric<->Tommy<->Christine<-
167
Notice that the embedded space in the name Phil IV is preserved, but
the extra space after Tommy is removed. Both choices are by design
and could have been coded differently. There isnt much more to using the
UDF than that.
An awkward situation occurs when you have a table that has a column
of delimited text and you want to combine the items from all the rows. As
you may recall from Chapter 1, you cant use a column name as the parameter to a multistatement UDF.
To illustrate, lets use the BBTeams table that served as an example in
Chapter 7. The table has a column, Players, that is a comma-separated list
of names. The objective is to get a resultset with the list of all players in
the league. The table is already in the TSQLUDFS database populated
with a few teams. Heres a quick look at whats in it:
-- The teams...
SELECT Top 2 * from BBTeams
GO
(Results truncated on the right)
ID
----------1
2
Name
-----------Dodgers
Yankees
Players
----------------------------------------Eric, Nick, Patrick, David, Billy, Alex, Matt, Gaven,...
Ulli, Tommy, Christine, Rika, Violet, Ken, Pat, Kenny,...
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 columns 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
Part I
Chapter 8: Multistatement UDFs
168
Part I
Chapter 8: Multistatement UDFs
/*
* Returns a list of all players on all BBTeams
*
* Example:
SELECT Player FROM dbo.udf_BBTeam_AllPlayers() ORDER BY Player
****************************************************************/
AS BEGIN
DECLARE @Players varchar(255) -- Holds one team's Player list
DECLARE TeamCURSOR CURSOR FAST_FORWARD FOR
SELECT Players
FROM BBTeams
ORDER BY [Name]
OPEN TeamCURSOR
FETCH TeamCURSOR INTO @Players
WHILE @@Fetch_status = 0 BEGIN
INSERT INTO @PlayerList (Player)
SELECT Item
FROM udf_Txt_SplitTAB (@Players, default)
FETCH TeamCURSOR INTO @Players -- next team
END -- WHILE
-- Clean up the cursor
CLOSE TeamCURSOR
DEALLOCATE TeamCURSOR
RETURN
END
Part I
Chapter 8: Multistatement UDFs
169
Player
---------------Violet
Vicky
Ulli
Tommy
Thea
Stephen
Shea
Rika
...
(Results - abridged)
170
Part I
Chapter 8: Multistatement UDFs
list. Thats the basic approach. Stay tuned for an alternative solution that
will follow shortly.
Listing 8.7: udf_Titles_AuthorList
CREATE FUNCTION udf_Titles_AuthorList (
/*
* Returns a comma-separated list of the last name of all
* authors for a title.
*
* Example:
Select Title, dbo.udf_Titles_AuthorList(title_id) as [Authors]
FROM pubs..titles ORDER by Title
****************************************************************/
AS BEGIN
DECLARE @lname varchar(40) -- one last name.
, @sList varchar(255) -- working list
SET @sList = ''
DECLARE BookAuthors CURSOR FAST_FORWARD FOR
SELECT au_lname
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
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
171
Using it is a cinch:
-- Titles and Authors
SELECT Title, dbo.udf_Titles_AuthorList(title_id) as [Authors]
FROM pubs..titles ORDER by Title
GO
(Results with selected rows reformatted)
Title
----------------------------------But Is It User Friendly?
Computer Phobic AND Non-Phobic I...
Cooking with Computers: Surrepti...
Secrets of Silicon Valley
Sushi, Anyone?
Authors
----------------------------Carson
Karsen, MacFeather
MacFeather, O'Leary
Dull, Hunter
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.
Listing 8.8: udf_Titles_AuthorList2, an alternate to udf_Titles_AuthorList
CREATE FUNCTION dbo.udf_Titles_AuthorList2 (
@Title_id char(6) -- title ID from pubs database
) RETURNS varchar(255) -- List of authors
/*
* Returns a comma-separated list of the last name of all
* authors for a title. Illustrates a technique for an aggregate
* concatenation.
*
* Example:
Select Title, dbo.udf_Titles_AuthorList2(title_id) as [Authors]
FROM pubs..titles ORDER by Title
****************************************************************/
AS BEGIN
DECLARE @lname varchar(40) -- one last name.
, @sList varchar(255) -- working list
SET @sList = ''
SELECT @sList = CASE WHEN LEN(@sList) > 0
THEN @sList + ', ' + au_lname
ELSE au_lname END
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
Part I
Chapter 8: Multistatement UDFs
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 technique of shipping UDFs instead of code tables. This can make the software
update process somewhat simpler.
173
/*
* Code table for taxabity of the entity.
*
* Example:
SELECT * FROM dbo.udf_Tax_TaxabilityCD()
****************************************************************/
AS BEGIN
INSERT INTO @TaxabilityCD (TaxabilityCD, [Description], Exempt)
SELECT 'GMT', 'Government', 1
UNION ALL SELECT 'CRP', 'Corporate' , 0
UNION ALL SELECT 'IND', 'Individual', 1
UNION ALL SELECT 'CHR', 'Charity' , 0
UNION ALL SELECT 'UNN', 'Union'
, 0
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 difference 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. Cursors 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 procedures and techniques to extend the range of possibilities for what UDFs
can do for your application.
Part I
Chapter 8: Multistatement UDFs
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 procedures, 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
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 provide 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 usually 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 information 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 system 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
System stored procedures are only one of the ways to get metadata about
UDFs, but theyre the ones that are available for use right out of the box.
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 Listing 9.3. It lists the parameters.
177
Data Type
Name
nvarchar(128)
Description
The functions name
Owner
nvarchar(128)
Function owner
Type
nvarchar(31)
Created_datetime
Datetime
Datetime when the function was created. Altering the UDF does not
change this column.
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.
Data Type
Description
Column_name
nvarchar(128)
Column name.
Type
nvarchar(128)
Part I
Chapter 9: Metadata about UDFs
178
Part I
Chapter 9: Metadata about UDFs
Column
Data Type
Description
Computed
varchar(35)
Are the values in the column computed: yes or no. For inline UDFs,
this is always no, even if the column
is based on an expression. It can be
yes for multistatement UDFs.
Length
int
Prec
char(5)
Scale
char(5)
Nullable
varchar(35)
Are NULL values allowed in the column: yes or no. Not applicable to
inline UDFs. Multistatement UDF columns can be nullable.
TrimTrailingBlanks
varchar(35)
FixedLenNullInSource
varchar(35)
Collation
sysname
Type
-------int
nvarchar
smallint
int
nvarchar
Nullable
--------no
no
yes
yes
no
TrimTraili...
-----------(n/a)
(n/a)
(n/a)
(n/a)
(n/a)
179
Type
---int
int
int
Nullable
-------no
yes
yes
TrimTraili...
----------(n/a)
(n/a)
(n/a)
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 information 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.
Part I
Chapter 9: Metadata about UDFs
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'
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.
181
sp_rename
For most object types, sp_rename is used to change the name of SQL
objects. sp_rename does not work for user-defined functions! Theres a bug
in the implementation, and you should not use it to change the name of a
UDF. Instead, you must drop the old UDF and create a new one.
This can be demonstrated with a pretty simple script that creates a
UDF, renames it, and then retrieves the text:
-- sp_rename doesn't work
IF EXISTS (select * from dbo.sysobjects
WHERE id = object_id(N'[dbo].[udf_Test_RenamedToNewName]')
AND xtype in (N'FN', N'IF', N'TF')) BEGIN
DROP FUNCTION [dbo].[udf_Test_RenamedToNewName]
END
IF EXISTS (select *
FROM dbo.sysobjects
WHERE id = object_id(N'[dbo].[udf_Test_RenameMe]')
AND xtype in (N'FN', N'IF', N'TF')) BEGIN
DROP FUNCTION [dbo].[udf_Test_RenameMe]
END
GO
CREATE FUNCTION dbo.udf_Test_RenameMe () returns int as begin return 1 end
GO
PRINT 'New udf_Test_RenameMe CREATEd.'
GO
EXEC sp_rename 'udf_Test_RenameMe', 'udf_Test_RenamedToNewName'
GO
EXEC sp_helptext 'udf_Test_RenamedToNewName'
GO
(Results from the last batch only)
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. Unfortunately, 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 Enterprise 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 referenced by it. That information is available from the system stored
procedure sp_depends.
Part I
Chapter 9: Metadata about UDFs
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:
-- What depends on udf_Order_Amount
EXEC sp_depends udf_Order_Amount
GO
(Results)
In the current database, the specified object
Name
Type
Updated
-------------------- ---------------- ------dbo.NWOrderDetails user table
no
dbo.NWOrderDetails user table
no
dbo.NWOrderDetails user table
no
In the current database, the
Name
---------------------------dbo.DEBUG_udf_Order_Amount
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 columns 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
183
Part I
Chapter 9: Metadata about UDFs
184
Part I
Chapter 9: Metadata about UDFs
ROUTINES
ROUTINE_COLUMNS
PARAMETERS
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:
-- The basics from INFORMATION_SCHEMA.ROUTINES
SELECT TOP 7
ROUTINE_NAME, DATA_TYPE, IS_DETERMINISTIC, SQL_DATA_ACCESS
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_TYPE = 'FUNCTION'
ORDER BY ROUTINE_NAME
GO
(Results)
ROUTINE_NAME
----------------------udf_DT_2Julian
udf_DT_Age
udf_DT_CurrTime
udf_DT_dynamicDATEPART
udf_DT_FromYMD
udf_DT_MonthsTAB
udf_DT_NthDayInMon
DATA_TYPE
-------------int
int
char
int
smalldatetime
TABLE
smalldatetime
IS_DETERMINISTIC
---------------YES
NO
NO
NO
YES
NO
NO
SQL_DATA_ACCESS
--------------READS
READS
READS
READS
READS
READS
READS
Functions may have been added to TSQLUDFS by the time you read this,
so you may see different results.
185
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:
-- Column information for udf_DT_MonthsTAB
SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, DATA_TYPE
FROM INFORMATION_SCHEMA.ROUTINE_COLUMNS
WHERE TABLE_NAME= 'udf_DT_MonthsTAB'
GO
(Results)
TABLE_NAME
----------------udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
udf_DT_MonthsTAB
COLUMN_NAME
ORDINAL_POSITION DATA_TYPE
---------------- ---------------- -----------Year
1 smallint
Month
2 smallint
Name
3 varchar
Mon
4 char
StartDT
5 datetime
EndDT
6 datetime
End_SOD_DT
7 datetime
StartJulian
8 int
EndJulian
9 int
NextMonStartDT
10 datetime
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:
-- parameters, including the result for udf_DT_Age
SELECT SPECIFIC_NAME, ORDINAL_POSITION, PARAMETER_NAME
, DATA_TYPE, PARAMETER_MODE, IS_RESULT
FROM INFORMATION_SCHEMA.PARAMETERS
WHERE SPECIFIC_NAME = 'udf_DT_Age'
GO
Part I
Chapter 9: Metadata about UDFs
186
Part I
Chapter 9: Metadata about UDFs
(Results)
Name
Position Parm Name
DATA_TYPE
---------- -------- ------------- --------udf_DT_Age
0
int
udf_DT_Age
1 @DateOfBirth datetime
udf_DT_Age
2 @AsOfDate
datetime
Mode
---------OUT
IN
IN
IS_RESULT
---------YES
NO
NO
Thats the last of the INFORMATION_SCHEMA views that has important information about UDFs. The next information sources are two built-in
functions that give information about many object types, UDFs included.
OBJECT_ID
OBJECTPROPERTY
COLUMNPROPERTY
PERMISSIONS
properties; many of them are relevant to UDFs. These two queries give
you a taste of whats available from these functions:
-- 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
Chapter 9: Metadata about UDFs
187
Books Online has the complete list of properties that the built-in metadata
functions can return. But the previous queries illustrate one of the limitations 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 Appendix 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:
-- Check permission to execute udf_Order_Amount
SELECT CASE WHEN 0x20 = PERMISSIONS (OBJECT_ID('udf_Order_Amount')) & 0x20
THEN 'Can execute' ELSE 'Can''t execute' END
+ ' udf_Order_Amount'
GO
(Results)
Can execute udf_Order_Amount
The built-in metadata functions should remain the same as new versions
of SQL Server are released. That makes using them preferable to interrogating the system tables. However, sometimes the system tables are the
only place to get the answer you want.
INFORMATION_SCHEMA views
(Results)
188
Part I
Chapter 9: Metadata about UDFs
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
sysobjects
syscolumns
sysdepends
sysconstraints
syscomments
syspermissions
sysprotects
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 information 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.
189
Function Information
Listing 9.1 shows udf_Func_InfoTAB, which returns a table of information
about all functions in a database. You might also want to take a look at two
related functions in the TSQLUDFS database that are not listed here:
udf_Func_Type and udf_Func_COUNT.
Listing 9.1: udf_Func_InfoTAB
CREATE
FUNCTION dbo.udf_Func_InfoTAB (
Part I
Chapter 9: Metadata about UDFs
190
Part I
Chapter 9: Metadata about UDFs
all functions. The caller is expected to put in any wildcard matching characters for the LIKE expression. That way, the caller has full control to
request information about a single UDF or multiple UDFs. A naming convention 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:
-- Functions in the Func group
SELECT FunctionName, DataType, Type, IsDeterministic
FROM udf_Func_InfoTAB ('udf_Func_%')
GO
(Results You may see any some additional functions in your results)
FunctionName
--------------------------udf_Func_BadUserOptionsTAB
udf_Func_ColumnsTAB
udf_Func_COUNT
udf_Func_InfoTAB
udf_Func_Type
DataType
----------TABLE
TABLE
int
TABLE
varchar(9)
Type
------INLINE
INLINE
SCALAR
INLINE
SCALAR
IsDeterministic
--------------NO
NO
NO
NO
NO
The query shows a couple of other interesting UDFs that are investigated
next.
or UDFs requested by the function name pattern. Listing 9.2 shows the
CREATE FUNCTION script for udf_Func_ColumnsTAB:
Listing 9.2: udf_Func_ColumnsTAB, a function to list a UDFs columns
CREATE FUNCTION dbo.udf_Func_ColumnsTAB (
@Function_Name_pattern as nvarchar(128) = NULL -- NULL for All
-- or LIKE pattern on the name of the function.
RETURNS TABLE
)
/*
* Returns a TABLE of information about the columns returned by a
* function. Works on both inline and multiline UDFs.
*
* Example:
SELECT * FROM udf_Func_ColumnsTAB (default) -- All functions
SELECT * FROM udf_Func_ColumnsTAB ('udf_Func_ColumnsTAB') -- me
****************************************************************/
AS RETURN
SELECT TOP 100 PERCENT WITH TIES
TABLE_NAME
, COLUMN_NAME
, ORDINAL_POSITION
, COLUMNPROPERTY(OBJECT_ID(c.TABLE_NAME)
as [Function]
as Column_Name
as [Position]
191
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:
-- Columns returned by udf_Func_ColumnsTAB
SELECT Column_Name, Position, DataType, Nullable
FROM udf_Func_ColumnsTAB ('udf_Func_ColumnsTAB')
GO
(Results)
Column_Name
Position DataType
Nullable
-------------------------- -------- -------------- ----------Function
1 nvarchar(128)
0
Column_Name
2 nvarchar(128)
0
Position
3 smallint
0
IsComputed
4 int
1
DataType
5 nvarchar(128)
1
BaseType
6 nvarchar(128)
1
Character_Maximum_Length
7 int
1
Numeric_Precision
8 tinyint
0
Numeric_Scale
9 int
1
Nullable
10 int
1
IsRowGUIDCol
11 int
1
A very similar set of information is retrievable for parameters to the function. The next section has a UDF that does just that.
Part I
Chapter 9: Metadata about UDFs
192
Part I
Chapter 9: Metadata about UDFs
)
/*
* Returns a TABLE of information about the parameters used to
* call any type of UDF. This includes the return type which
* is in Position=0.
*
* Example:
SELECT * FROM udf_Func_ParmsTAB (default) -- All functions
SELECT * FROM udf_Func_ParmsTAB ('udf_Func_ParmsTAB') -- me
****************************************************************/
AS RETURN
INFORMATION_SCHEMA.PARAMETERS has a row for the return type, and Ive left
193
Parameter
Position Mode DataType
---------------------- -------- ---- ------------@Function_Name_pattern
1 IN nvarchar(128)
RETURNS
0 OUT int
@function_name_pattern
1 IN nvarchar(128)
@function_name_pattern
1 IN nvarchar(128)
@Function_Name_pattern
1 IN nvarchar(128)
RETURNS
0 OUT varchar(9)
@FunctionName
1 IN nvarchar(128)
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.
Type Name
CHECK constraint
DEFAULT constraint
FN
Scalar function
IF
Inline function
Stored procedure
Rules
TF
TR
Trigger
View
Part I
Chapter 9: Metadata about UDFs
194
Part I
Chapter 9: Metadata about UDFs
udf_Object_Size returns the number of bytes taken up by the textual definition of the object by summing the DATALENGTH of the text field for all rows
used for the object. Its not listed here, but you can get the definition from
the TSQLUDFS database.
Listing 9.4 shows udf_Object_SearchTAB, which is used to search the
text of syscomments for a character string. It can be used for all objects in
the database or for only objects of a particular type. The @Just4Type parameter is either one of the entries in Table 9.5, NULL for all types, or 'F' for
any type of UDF.
Listing 9.4: udf_Object_SearchTAB
CREATE
FUNCTION udf_Object_SearchTAB (
195
Heres a query that searches for all functions that have a reference to
objects in the master database:
-- Find all functions that might reference the master database
SELECT * from udf_Object_SearchTAB('master.', 'F')
GO
(Result)
Object Type
-----------------Function/Inline
Function/Inline
Function/Scalar
Function/Scalar
Function/Scalar
Name
------------------------------udf_SQL_InstanceSummaryTAB
udf_SQL_UserMessagesTAB
udf_Example_User_Event_Attempt
udf_SQL_LogMsgBIT
udf_SQL_StartDT
Of course, this is a text search, and any function or other object that contained 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 database 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.
Part I
Chapter 9: Metadata about 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 Enterprise 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
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 information 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 procedures 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 Procedures, 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 somewhat 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 documented 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
Procedure
Can It Be Used
in a UDF?
sp_OA*
YES
xp_cmdshell
NO
Returns a resultset.
xp_deletemail
YES
xp_enumgroups
NO
Returns a resultset.
xp_findnextmsg
YES
xp_gettable_dblib
NO
xp_grantlogin
NO
199
Extended
Procedure
Can It Be Used
in a UDF?
xp_hello
YES
xp_logevent
YES
xp_loginconfig
NO
Returns a resultset.
xp_logininfo
NO
Returns a resultset.
xp_msver
NO
Returns a resultset.
xp_readmail
YES
xp_revokelogin
NO
xp_sendmail
YES
xp_snmp_getstate
NO
xp_snmp_raisetrap
NO
xp_sprintf
YES
xp_sqlmant
YES
xp_sscanf
YES
xp_startmail
YES
xp_stopmail
YES
xp_trace_*
NO
These SQL Server 7 extended stored procedures are not in SQL Server. They have been
replaced with the fn_trace_* system UDFs and
the sp_trace_* stored procedures. The
fn_trace_* functions are discussed in Chapter
17.
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 question is the information log maintained by SQL Server, not its transaction
log. The syntax to the call is:
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
200
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
@MessageNumber
, @MessageText
[,@Severity]
Note:
Before demonstrating xp_logevent, I used the sp_cycle_errorlog
stored procedure to create a new SQL Server log. It makes the displays 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.
201
The error log can also be queried using the undocumented extended
stored procedure xp_readerrorlog. Its shown in this query:
-- Read the current error log
EXEC master..xp_readerrorlog
GO
(Results truncated on the right to fit the page)
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:
-- Show just my messages
EXEC usp_SQL_MyLogRpt
GO
(Results)
When
---------------------2002-10-09 11:09:20.03
2002-10-09 11:09:20.03
Message
-------------------------------------------------------The quick brown fox jumped over the lazy dog..
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.
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
202
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
Figure 10.3: The Event Properties window with the details of a message
Listing 10.1: usp_SQL_MyLogRpt
CREATE PROCEDURE usp_SQL_MyLogRpt
/*
* Returns a report of the most recent events added to the system log
* by this process within the last 60 minutes with the most recent
* shown first.
*
* Example:
exec usp_SQL_myLogRpt
***********************************************************************/
AS BEGIN
DECLARE @RC INT -- return code
, @SPID varchar(9) -- text representation of @@SPID for searching
CREATE TABLE #ErrorLog (
ERRORLOG varchar(1000) -, ContinuationRow int -- Is this a continuation row
, SequenceNumber int identity (1,1)
)
INSERT INTO #ErrorLog (ErrorLog, ContinuationRow)
EXEC master..xp_readerrorlog
FUNCTION dbo.udf_SQL_LogMsgBIT (
@nMessageNumber
@sMessage
@nMessageNumber
@sMessage
@sSeverity
203
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
204
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
Message
-------------------------------------------------------Now that's what I call a message!.
Error: 50001, Severity: 10, State:
The quick brown fox jumped over the lazy dog..
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:
CREATE view ExampleViewThatLogsAMessage
AS
SELECT *
, CASE WHEN 1=dbo.udf_SQL_LogMsgBIT(default
, 'Executing for Customer ' + CompanyName
, NULL)
THEN 'Message Logged'
ELSE 'logevent failed'
END as [Event Logged]
FROM Cust
GO
205
The view does a SELECT on the sample Cust table in the TSQLUDFS database. If we SELECT from the view, one event is added for every row in the
table.
-- Select that adds log messages for each row.
SELECT * FROM ExampleViewThatLogsAMessage
GO
(Results)
CustomerID
----------1
2
CompanyName
-------------------Novick Software
Wordware
City
-------------------Sudbury
Dallas
Event Logged
--------------Message Logged
Message Logged
Message
-------------------------------------------------------Executing for Customer Wordware.
Error: 50001, Severity: 10, State:
Executing for Customer Novick Software.
Error: 50001, Severity: 10, State:
Now that's what I call a message..
Error: 50001, Severity: 10, State:
The quick brown fox jumped over the lazy dog..
Error: 60000, Severity: 10, State:
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
206
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
Message
--------------------------------------------------------Message In WHERE Clause for Customer Wordware.
Error: 65000, Severity: 10, State:
Executing for Customer Wordware.
Error: 50001, Severity: 10, State:
Executing for Customer Novick Software.
Error: 50001, Severity: 10, State:
Now that's what I call a message!.
Error: 50001, Severity: 10, State:
The quick brown fox jumped over the lazy dog..
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:
WHERE Left (CompanyName, 1) > 'N'
OR
0=dbo.udf_SQL_LogMsgBIT(65000
, 'Message In WHERE Clause for Customer '
+ CompanyName
, NULL) -- Always false
However, I found that switching both the order of the clauses and using an
OR operator between them does the trick. Heres the query:
-- Create a log message from the middle of a WHERE clause using OR
SELECT *
FROM Cust
WHERE
0=dbo.udf_SQL_LogMsgBIT(65000
, 'Message In WHERE Clause for Customer '
+ CompanyName
, NULL) -- Always false
OR Left (CompanyName, 1) > 'N'
GO
207
(Results)
CustomerID CompanyName
City
----------- -------------------- -------------------2
Wordware
Dallas
Message
-------------------------------------------------------Message In WHERE Clause for Customer Wordware.
Error: 65000, Severity: 10, State:
Message In WHERE Clause for Customer Novick Software.
Error: 65000, Severity: 10, State:
Message In WHERE Clause for Customer Wordware.
Error: 65000, Severity: 10, State:
Executing for Customer Wordware.
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 message 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.
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
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]]
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.
Listing 10.3: udf_Txt_Sprintf
CREATE FUNCTION dbo.udf_Txt_Sprintf (
@Format varchar(254) -- The format with embedded %s insertion chars
, @var1 sql_variant = NULL -- The first substitution string
, @var2 sql_variant = NULL -- Second substitution string
, @var3 sql_variant = NULL -- Third substitution string
) RETURNS varchar(8000) -- Format string with replacements performed.
-- No SCHEMABIND due to use of EXEC
/*
* Uses xp_sprintf to format a string with up to three insertion
* arguments.
*
* Example:
select dbo.udf_Txt_Sprintf('Insertion 1>%s 2>%s, 3>%s', 1, 2, null)
*
* Test:
209
PRINT 'Test 1
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)
Author and Contract
---------------------------------------------------------------------Johnson White Contract Value = 1
Marjorie Green Contract Value = 1
Cheryl Carson Contract Value = 1
Michael O'Leary Contract Value = 1
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
210
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
Description
sp_OACreate
sp_OADestroy
sp_OAGetErrorInfo
sp_OAGetProperty
sp_OAMethod
sp_AOSetProperty
sp_OAStop
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.
211
GRANT EXECUTE permission on the sp_OA* procedures to all users who will
use UDFs that invoke them.
Granting permission to the sp_OA* extended stored procedures may
meet your needs, but doing so means that users with permission to execute them can write their own SQL script to execute other COM objects.
This presents an opportunity for serious accidents. It also presents an
opportunity for hacking. Anyone with access to the sp_OA* procedures can
wreak havoc on your server. For example, the Windows Scripting Runtime
DLL includes the FileSystemObject, which allows disk access to the entire
server on which SQL Server runs. This access would be in the security
context of the account thats running SQL Server, possibly LocalSystem
or a local administrator account, not the user that originated the query.
This type of vulnerability is called a privilege elevation. Its a security hole
big enough to drive a truck through.
Hopefully, youre now reluctant to hand out privileges to use the
sp_OA* xp_s. My opinion is that the risks presented by giving out EXECUTE
permission on these objects is pretty high. Having said that, I do think
there is a place for using these procedures by users with the sysadmin role.
In particular, it can be a big aid in automating administrative procedures on
your servers.
Occasional tasks that recur but only when specific conditions warrant.
For example, an analysis that occurs when performance falls below
the desired service level.
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
212
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
SQLAgent Jobs can have steps that execute ActiveX scripts or execute Windows programs.
Neither of these solutions are executed from within the SQL engine and
dont entail either putting it at risk from failed objects or the potential for
excessive locking caused by long running OLE methods. Either one of
these alternatives should be considered for any OLE Automation task that
runs for anything longer than a second.
Now that youre all warned, lets go do it.
213
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
214
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
GOTO BypassErrorHandling
Automation_Error:
EXEC sp_displayoaerrorinfo @hFso, @hResult1
ByPassErrorHandling:
-- Destroy the object if we created it
IF @hFSO is not NULL
EXEC @hResult1 = sp_OADestroy @hFSO
PRINT 'done'
GO
(Results)
Drive Exists = 1
Done
to:
SET @DriveLetter = 'R' -- Check for Drive
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
215
216
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define
FACILITY_RPC
FACILITY_WIN32
FACILITY_CONTROL
FACILITY_NULL
FACILITY_MSMQ
FACILITY_MEDIASERVER
FACILITY_INTERNET
FACILITY_ITF
FACILITY_DISPATCH
FACILITY_CERT
1
7
10
0
14
13
12
4
2
11
The three parts of the code that are most useful are:
n
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
217
Severity = Warning
(Results)
218
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
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
SELECT @FacilityName = CASE @FacilityCode
WHEN 0 THEN 'NULL'
WHEN 1 THEN 'RPC'
WHEN 2 THEN 'DISPATCH'
WHEN 3 THEN 'STORAGE'
WHEN 4 THEN 'ITF'
WHEN 7 THEN 'WIN32'
WHEN 8 THEN 'WINDOWS'
WHEN 9 THEN 'SSPI'
WHEN 10 THEN 'CONTROL'
WHEN 11 THEN 'CERT'
WHEN 12 THEN 'INTERNET'
WHEN 13 THEN 'MEDIASERVER'
WHEN 14 THEN 'MSMQ'
WHEN 15 THEN 'SETUPAPI'
ELSE 'UNKNOWN ' + CONVERT(varchar, @FacilityCode)
END
, @CommonError = CASE @HRESULT
WHEN 0x80020008 THEN 'Bad variable type or NULL'
WHEN 0x80020006 THEN 'Property or Method Unknown'
WHEN 0x800401F3 THEN 'PROGID or CLSID not registered'
WHEN 0x80080005 THEN 'Registered EXE not found'
WHEN 0x8007007E THEN 'Registered DLL not found'
WHEN 0x80020005 THEN 'Type mismatch in return value.'
WHEN 0x8004275B THEN 'Context parm invalid. Must be 1, 4, or 5'
ELSE ''
END
-- Combine what we
SELECT @ErrorMsg =
+
+
+
RETURN @ErrorMsg
END
219
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.
The sp_OA* procedures are best used for administrative functions, not
high-volume queries.
Getting the error message from one of these routines is very important. You dont want to allow unhandled errors in these routines to
accumulate without being addressed. The side effects could slow or
bring down the server.
If the messages are going to go into the SQL log and NT event log, theres
another gap in the process that needs to be closed. There is only one SQL
log in use for writing at any time. Messages from all code that uses
udf_SQL_LogMsgBIT is intermixed in the log. Wed better identify precisely
what code produced the message, the call that is being made, and which
call it was. Otherwise, it will become very difficult to identify which code
created the message.
udf_OA_LogError is a UDF that creates an error message about a problem with OA automation and sends it to the log. Having this routine
around makes it easier to handle errors encountered when writing functions that use the sp_OA* procedures. Listing 10.5 shows the code for
udf_OA_LogError.
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
220
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
,
,
,
,
)
/*
* Creates an error message about an OA error and logs it to the
* SQL log and NT event log.
*
* Example:
DECLARE @HRESULT int, @hObject int
EXEC @HRESULT = sp_OAALTER 'BADPROGID', @hObject OUTPUT, 5
SELECT dbo.udf_OA_LogError(@hObject, @HRESULT, 'Common Usage'
, 'sp_OACreate', 'BADPRODID') as [Error Message]
EXEC @HRESULT = sp_OADestroy @hObject
*******************************************************************************/
AS BEGIN
DECLARE @FullMsg varchar(255) -- The full message being sent
, @ErrMsg varchar(255)
, @MessageLogged BIT -- temporary bit
SELECT @ErrMsg=dbo.udf_OA_ErrorInfo (@hObject, @HRESULT)
SELECT @FullMsg = 'OA error in ''' + @CallerName
+ ''' Invoking:' + @spName
+ ' (' + @ContextOfCall + ')' + CHAR(10)
+ @ErrMsg
-- Write the message to the log, should always succeed
IF NOT 1=dbo.udf_SQL_LogMsgBIT (50002, @FullMsg, default)
SELECT @FullMsg = 'Logging Process Failed for message:' + @FullMsg
RETURN @FullMsg
END
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
221
(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 example of what can be done.
222
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
IF @HRESULT != 0 BEGIN
SELECT @msg = dbo.udf_OA_LogError (
'udf_SYS_DriveExistsBIT'
, 'sp_OACreate'
, 'FileSystemObject'
, @HRESULT, @hFSO)
GOTO Function_Cleanup
END
EXEC @HRESULT = sp_OAMethod
,
,
,
@hFSO
'DriveExists'
@DriveExists Output
@DriveSpec = @DriveLetter
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:
-- Destroy the object if we created it
IF @hFSO is not NULL
EXEC @HRESULT = sp_OADestroy @hFSO
RETURN @DriveExists
END
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:
223
IF @HRESULT != 0 BEGIN
SELECT @msg = dbo.udf_OA_LogError (
'udf_SYS_DriveExistsBIT'
, 'sp_OACreate'
, 'FileSystemObject'
, @HRESULT, @hFSO)
GOTO Function_Cleanup
END
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.
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
224
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
The object has one property, myName, and one method, sayHello. The example script below does the following:
n
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.
You should see a dialog box like the one shown in Figure 10.4.
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
225
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
226
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
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:
227
Start your Visual Basic project in the integrated development environment and use the Run Start menu command to start up your
project.
Execute the T-SQL that uses the object. I usually do this in Query
Analyzer.
Part I
Chapter 10: Using Extended Stored Procedures in UDFs
228
Part I
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 programmers 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
Cryptography libraries
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 undocumented. 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
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 procedures. 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 evaluating 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.
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
231
run the tests. The results print out in the results window, and you can
quickly evaluate if they worked.
One of the important aspects of the tests is that they check their own
answers. An embedded test should print out either Worked or
ERROR. It shouldnt print out the result of executing the function.
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 trivial 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.
Part I
Chapter 11: Testing UDFs for Correctness and Performance
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 programming 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.
Listing 11.1: TEST_udf_DT_WeekdayNext
CREATE PROC dbo.TEST_udf_DT_WeekdayNext
@AllWorked BIT OUTPUT -- 1 when all tests worked.
, @PrintSuccessMsgs BIT = 0
AS
/*
* Test driver for udf_DT_WeekdayNext. A year's worth of dates
* are tested with every possible value of @@DATEFIRST.
* It always assumes that Sat and Sun are not weekdays.
*
* Test:
DECLARE @AllWorked BIT, @RC int
EXEC @RC = TEST_udf_DT_WeekdayNext @AllWorked OUTPUT, 1
PRINT 'Test udf_DT_WeekdayNext @RC = ' + CONVERT(char(10), @RC)
+ ' @AllWorked = ' + CONVERT(VARCHAR(1), @ALLWORKED)
****************************************************************/
DECLARE
,
,
,
,
,
,
,
,
233
Part I
Chapter 11: Testing UDFs for Correctness and Performance
234
Part I
Chapter 11: Testing UDFs for Correctness and Performance
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)
Hard coded tests worked.
Test 2
DATEFIRST=1 TestDate=2003-01-01 00:00:00
Answer=2003-01-02 00:00:00
Function=2003-01-02 00:00:00
Test 3
DATEFIRST=2 TestDate=2003-01-01 00:00:00
Answer=2003-01-02 00:00:00
Function=2003-01-02 00:00:00
...
Test 2991
DATEFIRST=6 TestDate=2004-01-09 00:00:00
Answer=2004-01-12 00:00:00
Function=2004-01-12 00:00:00
Test 2992
DATEFIRST=7 TestDate=2004-01-09 00:00:00
Answer=2004-01-12 00:00:00
Function=2004-01-12 00:00:00
TEST_udf_DT_WeekdayNext Done. All tests worked.
Test udf_DT_WeekdayNext @RC = 0
Wednesday
Thursday
Worked
Wednesday
Thursday
Worked
Friday
Monday
Worked
Friday
Monday
Worked
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_WeekdayNext 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 procedure 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
235
test, I want to see all the output. Once UDF is no longer in active development, I dont want to see the messages unless theres a problem.
Just as UDFs contain examples and tests in their comment block, I
put a short script that runs the procedure in the procedures comments.
This makes it easy to run the test. The easier it is to run, the easier it is
to avoid the temptation of assuming that the procedure is fine and that you
dont have to run the test one last time.
There are many variations on how to perform testing. The most
important thing is that the tests get written in the first place. Coding the
unit tests for UDFs in stored procedures keeps them nearby at all times.
Once testing for correctness is complete, it may be important to test
for performance. Whether its necessary to conduct performance testing
on a UDF depends on how the UDF is used. The next section shows that
under the right circumstances, a UDF can have very negative consequences for performance.
Part I
Chapter 11: Testing UDFs for Correctness and Performance
236
Part I
Chapter 11: Testing UDFs for Correctness and Performance
@Result int
@StringLen int
@ReverseIn varchar(8000)
@ReverseFor varchar(255)
237
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
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 coding 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 performance 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.
Part I
Chapter 11: Testing UDFs for Correctness and Performance
238
Part I
Chapter 11: Testing UDFs for Correctness and Performance
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.
239
After pressing the Replace All button, the result looks like this:
-- Translated query
SELECT CASE
WHEN CHARINDEX('fdf'
, 'f123 asdasfdfdfddfjas ') > 0
THEN LEN('f123 asdasfdfdfddfjas ')
- CHARINDEX(REVERSE('fad')
, REVERSE ('f123 asdasfdfdfddfjas ')) + 1
ELSE 0 END
as [Middle]
GO
(Results)
Middle
----------13
As you can see, replacing the call to the UDF with the equivalent expression 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
Part I
Chapter 11: Testing UDFs for Correctness and Performance
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.
Listing 11.3: usp_CreateExampleNumberString
CREATE PROC usp_CreateExampleNumberString
@Loops int = 20 -- creates POWER (2, @Loops) Rows
-- 20 Creates 1,000,000 rows
AS
DECLARE @LC int -- Loop counter
-- delete an existing ExampleNumberString table
if exists (select * from dbo.sysobjects
where id = object_id(N'dbo.ExampleNumberString')
and OBJECTPROPERTY(id, N'IsUserTable') = 1)
DROP TABLE dbo.ExampleNumberString
CREATE TABLE ExampleNumberString (
ID int identity (1,1)
, BigNum Numeric (38, 0)
, NumberString varchar(128) NULL
)
INSERT INTO ExampleNumberString (BigNum, NumberString)
VALUES(CONVERT (numeric(38,0), rand() * 9999999999999)
, '
') -- preallocate
SELECT @LC = 0
WHILE @LC < @Loops BEGIN
INSERT INTO ExampleNumberString (BigNum, NumberString)
SELECT BigNum * RAND(@LC + 1) * 2
, '
'
FROM ExampleNumberString
SELECT @LC = @LC + 1
END -- WHILE
UPDATE ExampleNumberString
SET NumberString = convert(varchar(128)
, convert(numeric(38,0), 9834311) * bignum)
241
usp_CreateExampleNumberString does a reasonable job of producing a different large NumberString for each row. There is usually about one percent
duplication of numbers, which seems acceptable for most testing scenarios. With a parameter of 20, the procedure takes about 55 seconds to run
on my desktop development system.
Since were going to run multiple queries against the NumberString
column to test the time it takes to use a UDF on a large number of rows,
we should try to make the circumstances for all queries as similar as possible. One way to do that is to force all the rows that are going to be
queried into memory, SQL Servers page cache, and keep them there. If
we dont have all the desired rows in memory, then we might be comparing one query that reads a million rows from disk with another query that
reads the same million rows from SQL Servers page cache. It wouldnt be
a fair comparison.
SQL Server provides the DBCC PINTABLE statement to force all pages
from a table to remain in the cache once theyve been read the first time.
Use it with caution! It can fill SQL Servers cache and cause the query
engine to lock up to the point where you have to shut it down and restart
it. Only do this if you have adequate RAM available, not just virtual memory. Theres no point in using virtual memory as a substitute for RAM in
this situation. That just substitutes one form of disk I/O (paging) for the
one were trying to eliminate (reading pages from disk).
This next script pins ExampleNumberString in memory. On my desktop
development system with a million-row table, pinning the table forced
SQL Server to consume 82 additional megabytes of RAM. The system has
512 megabytes of RAM, and I used Task Manager to see that memory
isnt full. If your system has less available RAM, reduce the number of
rows in the test to eliminate paging.
-- 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)
Warning: Pinning tables should be carefully considered. If a pinned table is larger,
or grows larger, than the available data cache, the server may need to be restarted
and the table unpinned.
DBCC execution completed. If DBCC printed error messages, contact your system
administrator.
Part I
Chapter 11: Testing UDFs for Correctness and Performance
242
Part I
Chapter 11: Testing UDFs for Correctness and Performance
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 displaying the results. To eliminate most of that work, using an aggregate
function, such as MAX, forces the UDF or expression to be evaluated without 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 represents 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 system. The three queries with their results from my desktop development
system are:
-- Query #0: The minimum time for an operation that scans the whole table.
SET STATISTICS TIME ON
SELECT MAX(LEN(NumberString)) as [Max Len]
FROM ExampleNumberString
GO
(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.
(End of Results for Query #0)
-- Query #1: The UDF. Note execution time is incorrect.
SET STATISTICS TIME ON
SELECT MAX (dbo.udf_Txt_CharIndexRev ('83', NumberString))
as [Right Most Position]
FROM ExampleNumberString
GO
(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.
(End of Results for Query #1 - It took 4 minutes 38 seconds)
(
- CPU was working at 100% during that time)
-- Query #2: The equivalent expression
SET STATISTICS TIME ON
SELECT MAX (CASE WHEN CHARINDEX('83', NumberString) > 0
THEN LEN(NumberString) - CHARINDEX(REVERSE('83')
, REVERSE (NumberString)) + 1
ELSE 0 END
) [Right Most Position]
FROM ExampleNumberString
GO
(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.
(End of Results for Query #2)
243
Part I
Chapter 11: Testing UDFs for Correctness and Performance
244
Part I
Chapter 11: Testing UDFs for Correctness and Performance
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 system, 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 column 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 isolates just the effect of running the UDF or the replacement expression.
Table 11.1: Query timings for the experiment
Query
Description
Time (milliseconds)
#0
Simple expression
991
Net
N/A
#1
UDF
278000
277009
#2
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 dramatically 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 solution 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.
245
Part I
Chapter 11: Testing UDFs for Correctness and Performance
12
Converting
between Unit
Systems
The Introduction described how I originally encountered the lack of functions 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, complexity, 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 systems 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
249
interstate highways. I still see one lonely sign on I-95 between Boston and
Providence touting Providence 28km. Whether metric ever gains wide
adoption in the United States or not, its the system used by the rest of
the world, in science, and in some industries. Until the United States
switches, American programmers often have to work in both systems.
The MDOT PMS was partially paid for with funds from the United
States Department of Transportation (USDOT). In alignment with a general policy to migrate to the metric system, USDOT mandated that the
database be stored in metric measurements. However, almost all of the
instruments, such as tape measures, owned by MDOT and its engineers
in the field are calibrated in U.S. Standard units. Although their engineers
are trained in both systems, most are more comfortable with the U.S.
Standard system. MDOT management requested that, although the database was to be in metric units, the systems user interface work in either
system based on user preference.
Converting from one system of measures to another is easy, isnt it?
Yes, once youve decided on the conversion factor and how to handle
issues of precision. Many books on math and science contain the most
important conversion factors, but theres no law that dictates them. A reliable, up-to-date source must be found because there have been small
adjustments to the conversion factors over time, based on new science.
For comprehensive coverage of the metric conversion, I use the
Washington State Department of Transportation, or WSDOT. WSDOT
maintains a very useful web site at http://www.wsdot.wa.gov/Metrics/factors.htm that is relied on by the transportation industry. It also makes
available for download a Windows program, convert.exe, by Josh Madison,
that implements the conversion factors. Youll find convert.exe in the
download directory for this chapter. The WSDOT conversion factors have
been used for the conversion functions that accompany this book.
In addition to converting between systems of units, it is sometimes
necessary to convert between measures within the same system. For
example, one foot is 12 inches. Since conversions within the same system
are defined by the system, there is no issue about which conversion factor
to use; its a matter of getting the definitions correct. Of course, in the
metric system, this is almost trivial because all intra-system conversions
are based on a factor of 10.
Conversions dont have to be performed in the database engine. It
would be perfectly legitimate for an application designer to decide that
conversions take place in the client application or some other application
layer. Although some programming tools have unit conversion functions in
their libraries, Access, VBA, VB Script, Visual Basic, and T-SQL do not
have such functions and cant be used as sites for conversion without
writing the functions in their language.
Part I
Chapter 12: Converting between Unit Systems
250
Part I
Chapter 12: Converting between Unit Systems
Performing conversion in the database has the advantage of centralization. If done properly, there is only one place to get conversion factors,
and the DBA can be responsible for managing unit system conversions in
both directions. Considering that a database may be accessed by several
client applications, such as report writers, web pages, and Windows applications, centralization in the database has substantial advantages.
Stand-alone report writers, such as Crystal Reports, make it particularly important that conversion functions be centralized. While report
writers can perform the conversions, its often necessary to code the
conversion into every field. This becomes tedious and error prone. Its difficult to make sure that every field is converted correctly, using the same
conversion factor and rules for managing precision. Keeping the conversion in the database engine promotes consistency.
If youre not sure that care in making consistent conversions is really
so important, recall that NASAs Mars Climate Orbiter was lost due to a
conversion error between one program that worked in U.S. Standard units
and another that worked in the metric system. Noel Hemmers, the
spokesman for Lockheed Martin, the primary contractor, was quoted as
saying, It was, frankly, just overlooked. See http://abcnews.go.com/sections/science/DailyNews/climateorbiter991110.html for more information
on this conversion error.
Now that we have a way to get good conversion factors, the other
design issues that affect the construction of unit conversion functions
should be discussed. Most important among them is the preservation of
measurement precision.
Data storage What data type should be used to store the dimensional data?
Data type What data type should be returned as the result of the
unit conversion functions?
251
Programmer productivity How easily can the functions be incorporated into a program?
SQL Server returned eight digits to the right of the decimal. Multiplication 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 calculation. The expression requests two digits to the right of the decimal:
Part I
Chapter 12: Converting between Unit Systems
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
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 difference between the two is that CAST doesnt accept the date conversion
parameter that CONVERT can use to format dates as strings.
253
Part I
Chapter 12: Converting between Unit Systems
254
Part I
Chapter 12: Converting between Unit Systems
-- 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 alternative of claiming hundreds of meters of precision is further from the
truth. The TSQLUDFS database has the function udf_Unit_RoundingPrecision 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.
Listing 12.1: udf_Unit_Rounding4Factor
CREATE FUNCTION dbo.udf_Unit_Rounding4Factor (
@fConversionFactor float -- Conversion factor (must be >= 0)
) RETURNS int -- Use to adjust length parm of ROUND function
/*
* 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.
*
* Example:
select dbo.udf_Unit_Rounding4Factor (1609.344) -- Miles to meters
*
* Test:
print 'Test 1 Mi to Meters ' +
case when -3 = dbo.udf_Unit_Rounding4Factor (1609.344)
then 'Worked' else 'ERROR' end
print 'Test 2 Zero Parm
' +
case when dbo.udf_Unit_Rounding4Factor (0.0) is NULL
then 'Worked' else 'ERROR' end
print 'Test 3 Negative Parm ' +
case when dbo.udf_Unit_Rounding4Factor (-1.2321) is NULL
then 'Worked' else 'ERROR' end
***********************************************************************/
AS BEGIN
-- LOG10 won't take a zero or negative parameter.
-- the result is undefined, return null instead.
IF @fConversionFactor <= 0 BEGIN
RETURN NULL
END
255
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 conversion UDFs in a variety of ways.
Create a function that converts from any unit to any other unit in the
same dimension.
These three choices are hardly the only choices available. They illustrate
some of the possible trade-offs that seem to make sense to me, particularly when they are applied to pavement management. Your application or
development environment may dictate other variations that work better
for you.
Part I
Chapter 12: Converting between Unit Systems
256
Part I
Chapter 12: Converting between Unit Systems
The function has been designed to execute quickly, but a few extra statements have been left in for ease of editing. In the middle of the function at
line 32 is the line:
-- SELECT dbo.udf_Unit_Rounding4Factor (@fConversionFactor) As Adjustment
2.
Select the code from the DECLARE at line 27 down to and including line
32.
257
3.
Press F5 to execute this code fragment, and youll get the adjustment
factor.
4.
5.
Figure 12.1 shows Query Analyzer at the end of step 3 where we get the
result. The commented line is left in the function to be used when the
function is altered. Since the factor used to adjust precision never
changes, it doesnt have to be recalculated each time the function is
executed. Its just hard coded into the function.
Figure 12.1: Getting the adjustment factor for a units conversion function
Functions that perform a single unit-to-unit conversion have the advantage that they execute quickly. All theyre really doing is one multiplication and a rounding operation. When necessary, they could even be
consolidated into a single expression to avoid the extra overhead of
splitting the code into a few statements.
Part I
Chapter 12: Converting between Unit Systems
258
Part I
Chapter 12: Converting between Unit Systems
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.
Listing 12.3: udf_Unit_Km2Distance
CREATE FUNCTION dbo.udf_Unit_Km2Distance (
@UnitSystemOfOutput char(1) = 'M' -- M for metric, U for US Std.
, @fInputKM
float -- Kilometers to convert to miles
, @nDigits2RtOfDecimal int = 0 -- Precision to right of decimal
-- negative for left of decimal, like length parm of ROUND
) RETURNS float -- use for the length parameter of the ROUND function
WITH SCHEMABINDING
/*
* Returns a distance measure whose input is a kilometer measurement.
* The parameter @UnitSystemOfOutput requests that the output be left
* as Kilometers 'M' or converted to the US Standard system unit miles
* signified by a 'U'.
*
259
* Example:
select dbo.udf_Unit_Km2Distance ('U', 2.04, 2) -- Km to Mi
*
* Test:
print 'Test 1 2.04 Km to Mi ' +
case when 1.27 = dbo.udf_Unit_Km2Distance ('U', 2.04, 2)
then 'Worked' else 'ERROR' end
print 'Test 2 1.27 Km to Km ' +
case when 1.27 = dbo.udf_Unit_Km2Distance ('M', 1.27, 2)
then 'Worked' else 'ERROR' end
print 'Test 3 Negative Parm ' +
case when 123450 = dbo.udf_Unit_Km2Distance ('U', 198680.321, -1)
then 'Worked' else 'ERROR' end
***********************************************************************/
AS BEGIN
DECLARE @nRound2Digits int -- Digits to round
, @fConversionFactor float -- factor used for conversion
-- Check to see if any conversion is needed.
IF @UnitSystemOfOutput = 'M' COLLATE Latin1_General_CI_AI
RETURN ROUND(@fInputKM, @nDigits2RtOfDecimal)
SET @fConversionFactor = 0.6213711922
-- select dbo.udf_UnitRounding4Factor (@fConversionFactor) As Adjustment
SET @nRound2Digits = @nDigits2RtOfDecimal + 0 -- <-- put adjustment here
RETURN ROUND(@fInputKM * @fConversionFactor, @nRound2Digits)
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
SELECT TOP 3 route_name
, dbo.udf_Unit_Km2Distance (@UnitSystem, begin_km, 3) as begin_mi
, dbo.udf_unit_Km2Distance (@UnitSystem, end_km, 3) as end_mi
FROM pms_analysis_section
GO
(Results)
route_name
------------------SR1
SR1
SR1
begin_mi
-------------------4.4320000000000004
4.6079999999999997
8.0630000000000006
end_mi
--------------------4.6079999999999997
8.0630000000000006
9.9179999999999993
Part I
Chapter 12: Converting between Unit Systems
260
Part I
Chapter 12: Converting between Unit Systems
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 shortened 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-anyunit conversion function. Its part of Excels Analysis ToolPak add-in. Load
that add-in before using the function or trying the spreadsheet Unit Conversions.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 statement that has every possible combination of conversion. I considered it
but decided on a different approach.
Listing 12.4: udf_Unit_CONVERT_Distance
CREATE
FUNCTION dbo.udf_Unit_CONVERT_Distance (
261
Part I
Chapter 12: Converting between Unit Systems
262
Part I
Chapter 12: Converting between Unit Systems
Instead, a base unit for each system of units is chosen. For the metric system, 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 conversion 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 conversions. 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 converting 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?
263
that define the section. In addition, there are columns for a variety of
measurements that are used in economic analysis of pavement.
Each of the three conversion methods created previously used a
slightly different calling convention, but they can each be counted on to
produce a correct result. The first method is represented by udf_Unit_
Km2mi, which is shown in Listing 12.5. The second conversion method is
based on udf_Unit_Km2Distance, and the third is based on
udf_Unit_CONVERT_Distance.
Listing 12.5: udf_Unit_Km2mi
CREATE
FUNCTION dbo.udf_Unit_Km2mi (
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:
Part I
Chapter 12: Converting between Unit Systems
264
Part I
Chapter 12: Converting between Unit Systems
Begin Expression
----------------4.4316193427704
4.6080887613552
8.0629125899872
9.9183269698964
17.8787133131706
Begin Km2mi
------------------4.4320000000000004
4.6079999999999997
8.0630000000000006
9.9179999999999993
17.879000000000001
Begin Km2Dist
Begin CONVERT
-------------------------------4.4320000000000004 4.432000000
4.6079999999999997 4.608000000
8.0630000000000006 8.063000000
9.9179999999999993 9.918000000
17.879000000000001 17.879000000
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 prevent 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:
-- Experiment into the performance of differing unit conversion functions
-- 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 'Pinning the pms_analysis_section table'
DECLARE @db_id int, @tbl_id int
SELECT @db_id = DB_ID(), @tbl_id = OBJECT_ID('pms_analysis_section')
DBCC PINTABLE(@db_id, @tbl_id)
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
PRINT 'SUM of the conversion performed in an expression'
SELECT @Start_Expr = getdate()
SELECT sum(begin_km * 0.6213711922) [Begin Expression]
, sum(end_km * 0.6213711922) [End Expression]
FROM pms_analysis_section
SET @ms_Expr = DATEDIFF(ms, @Start_Expr, getdate())
PRINT 'SUM of the conversion performed by udf_Unit_Km2mi'
265
Part I
Chapter 12: Converting between Unit Systems
266
Part I
Chapter 12: Converting between Unit Systems
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 important 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 example you see what can happen when SQL Server stores floating-point data:
-- Example from udf_Unit_lb2kg
SELECT dbo.udf_Unit_lb2kg (185.4, 1) as [Weight lb to kg]
GO
(Results)
Weight lb to kg
----------------------------------------------------84.099999999999994
267
FUNCTION udf_Unit_lb2kg (
Part I
Chapter 12: Converting between Unit Systems
268
Part I
Chapter 12: Converting between Unit Systems
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.
The result is equal because SQL Server uses numeric data types to perform the addition and comparison. Now CAST the numbers to float, and we
get a different answer:
-- Simple comparison of equality using floating-point numbers
SELECT CASE when CAST(84.01 as FLOAT) + CAST(0.01 as FLOAT)
= CAST(84.02 as FLOAT)
then 'Equal' else 'Not Equal' end as [Are they equal?]
GO
269
(Results)
Are they equal?
--------------Not Equal
Part I
Chapter 12: Converting between Unit Systems
270
Part I
Chapter 12: Converting between Unit Systems
1.1230000000,
1.1230000000,
1.1230000000,
1.1230000000,
1.1230000000,
1)
2)
3)
4)
5)
as
as
as
as
as
[To
[To
[To
[To
[To
1
2
3
4
5
Digit]
Digits]
Digits]
Digits]
Digits]
Everything looks okay there. What if the numbers are really close?
-- Comparison of very close numbers to different number of digits
SELECT dbo.udf_Unit_EqualFpBIT (1.12500000000001, 1.12499999999999, 1)
as [To 1 Digit]
, dbo.udf_Unit_EqualFpBIT (1.12500000000001, 1.12499999999999, 2)
as [To 2 Digits]
, dbo.udf_Unit_EqualFpBIT (1.12500000000001, 1.12499999999999, 3)
as [To 3 Digits]
, dbo.udf_Unit_EqualFpBIT (1.12500000000001, 1.12499999999999, 4)
as [To 4 Digits]
, dbo.udf_Unit_EqualFpBIT (1.12500000000001, 1.12499999999999, 13)
as [To 13 Digits]
, dbo.udf_Unit_EqualFpBIT (1.12500000000001, 1.12499999999999, 14)
as [To 14 Digits]
GO
To 1 Digit To 2 Digits To 3 Digits To 4 Digits To 13 Digits To 14 Digits
---------- ----------- ----------- ----------- ------------ -----------1
1
1
1
1
0
Part I
Chapter 12: Converting between Unit Systems
271
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 interpretation 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 discrepancy as a bug. Both groups can be educated and might accept a reasonable
explanation when appropriate. But the importance of the one-cent difference is going to depend on the application in which it appears and on the
people involved. If hundreds, thousands, or millions of occurrences multiply 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.
Listing 12.8: udf_Unit_EqualNumBIT
CREATE
FUNCTION dbo.udf_Unit_EqualNumBIT (
272
Part I
Chapter 12: Converting between Unit Systems
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
273
udf_Unit_EqualNumBIT tells us that the numbers are not equal, while the
floating-point comparison method in udf_Unit_EqualFpBIT says that the
numbers are equal.
The strong point about the udf_Unit_EqualNumBIT function is that numbers are considered equal only when they will be displayed the same way.
That strength is also a weakness because asking for more digits to the
right of the decimal can change the result from not equal back to equal.
The next query shows the problem case:
-- Demonstrate the problem with numeric rounding as a comparison method
SELECT dbo.udf_Unit_EqualFpBIT (1.12500000000001, 1.1249999999999, 2) [EqualFP]
, dbo.udf_Unit_EqualNumBIT (1.12500000000001, 1.1249999999999, 1) [To 1 Digit]
, dbo.udf_Unit_EqualNumBIT (1.12500000000001, 1.1249999999999, 2) [To 2 Digits]
, dbo.udf_Unit_EqualNumBIT (1.12500000000001, 1.1249999999999, 3) [To 3 Digits]
, dbo.udf_Unit_EqualNumBIT (1.12500000000001, 1.1249999999999, 4) [To 4 Digits]
GO
(Results)
EqualFP To 1 Digit To 2 Digits To 3 Digits To 4 Digits
------- ----------- ------------ ------------ ----------1
1
0
1
1
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, floating 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 implemented by udf_Unit_EqualNumBIT.
Part I
Chapter 12: Converting between Unit Systems
274
Part I
Chapter 12: Converting between Unit Systems
Part I
Chapter 12: Converting between Unit Systems
275
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 allocated 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 distinct advantages, of which the most important is consistency. If the
conversion process is moved to a higher level, such as the client application, 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 frequently that the conversion rates must be stored in a table.
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.
Its the conversion factor, or exchange rate, that creates the complexity when changing money. The exchange rate changes over time,
almost continuously.
277
278
Part I
Chapter 13: Currency Conversion
279
Part I
Chapter 13: Currency Conversion
280
Part I
Chapter 13: Currency Conversion
CurrencyName
-------------------------------Lek
Euro
Krona
Yen
Dollar
Gold
IssuingGovernment
--------------------------Albania
European Union
Iceland
Japan
United States of America
None
281
well known. Theyre usually traded by the ounce, but the required precision could be anything. Four digits to the right of the decimal provide
enough precision for economic analysis, and thats whats used in the
TSQLUDFS database. With four digits, a column can represent one-tenthousandth of an ounce. Even for platinum selling at $550 U.S. per ounce,
the value of the smallest quantity that can be represented is worth $0.055.
That might be too large for you, and youll have to increase the number of
digits of precision.
While the money type works well for amounts, its too imprecise for
rates. When exchange rates are in the same order of magnitude, such as
the U.S. dollar and the euro or the United Kingdoms pound, four digits of
precision will suffice. However, there are many currencies where the
exchange rate can be 100 or more to one, and the exchange rate must be
stored at least six digits to the right of the decimal. For example, as I write
this, one U.S. dollar is trading for 120.159 Japanese yen (JPY). That gives
a yen to dollar change rate of 0.008323 dollars for each yen. The examples
in this book use decimal(18,8) for storing exchange rates as given in this
type definition:
EXEC sp_addtype N'CurrencyExchangeRate', N'decimal(18,8)', N'null'
Part I
Chapter 13: Currency Conversion
282
Part I
Chapter 13: Currency Conversion
Part I
Chapter 13: Currency Conversion
283
,
,
,
,
)
@RateTypeCD
CHAR(3) = 'IBR' -- rate type
@mAmount numeric(18,4) -- amount to convert
@FromCurrencyCD char(3) = 'USD'
@ToCurrencyCD char(3) = 'USD'
@AsOfDATE SMALLDATETIME -- What Date? Will use SOD.
RETURNS numeric(18,4) -- Resulting amount in the To currency.
WITH SCHEMABINDING
/*
* Converts from one currency to another on a specific date.
* The date must be in the database or the result is NULL.
*
* Example:
SELECT dbo.udf_Currency_XlateOnDate (DEFAULT, 1000.00, 'USD', 'EUR'
, '2002-06-01' ) -- Convert $1000 to Euros on June 6, 2002
*
* Test: (These depend on the sample data in CurrencyXchange)
PRINT 'Test 1 $1000.00 To Euro ' + CASE when 1070.9000
= dbo.udf_Currency_XlateOnDate (DEFAULT, 1000, DEFAULT,
'EUR', '2002-06-01') then 'Worked' else 'ERROR' end
PRINT 'Test 2 Missing date
' + CASE when
dbo.udf_Currency_XlateOnDate (DEFAULT, 1000, DEFAULT,
'EUR', '1998-01-01') is NULL then 'Worked' else 'ERROR' end
******************************************************************/
AS BEGIN
DECLARE @Rate numeric(18,8)
, @nMyError int -- local for error code
, @nMyRowCount int -- local for row count
, @mResult money
SELECT TOP 1 @Rate = ExchangeRATE
FROM dbo.CurrencyXchange WITH(NOLOCK)
WHERE RateTypeCD = @RateTypeCD
AND FromCurrencyCd = @FromCurrencyCD
AND ToCurrencyCD = @ToCurrencyCD
and AsOfDate = dbo.udf_DT_SOD (@AsOfDate)
SET @mResult = @Rate * @mAmount
RETURN @mResult
END
284
Part I
Chapter 13: Currency Conversion
$1000 To EUR
-----------NULL
NULL
1121.8000
1124.7000
1107.1000
1111.9000
1117.9000
1117.7000
1116.2000
1119.4000
1120.0000
1122.0000
1122.4000
NULL
NULL
1119.8000
,
,
,
,
)
/*
* Converts from one currency to another using a rate that's on or
* near the specified date. If the date is not found in the table
* an approximate result is returned. If AsOfDate is:
* - Matched, the rate from that date is used.
* - Less than the first available date but within 30 days of it,
*
the rate from the first date is used.
* - Greater than the last available date but within 30 days of it,
*
the rate from the last date is used.
* - between two available dates but within 30 days of both,
*
straight line interpolation is performed between the nearest dates.
* Example:
select dbo.udf_Currency_XlateNearDate ('IBR', 1000.00, 'USD',
'EUR', '2002-06-01' ) -- Convert $1000 to Euros, June 1, 02
* Maintenance Note: must be maintained in sync with
*
udf_Currency_XlateOnDate they use same logic for dates.
* Test: (Depend on the sample currency data in CurrencyXchange)
print 'Test 1 $1000.00 To Euro ' +
CASE when 1070.9000 = dbo.udf_Currency_XlateNearDate ('IBR',
1000, 'USD', 'EUR','2002-06-01') then 'Worked' else 'ERROR' end
print 'Test 2 Missing date
' +
CASE when dbo.udf_Currency_XlateNearDate ('IBR', 1000, 'USD',
'EUR', '1998-01-01') is NULL then 'Worked' else 'ERROR' end
print 'Test 3 Interpolation
' +
CASE when 1120.6667 = dbo.udf_Currency_XlateNearDate ('IBR',
1000, 'USD', 'EUR', '2002-01-12') then 'Worked' else 'ERROR' end
print 'Test 4 Before 1st date ' + CASE when
dbo.udf_Currency_XlateNearDate ('IBR', 1000, 'USD', 'EUR',
'1956-07-10') is null then 'Worked' else 'Error' end
******************************************************************/
AS BEGIN
DECLARE @Rate numeric (18,8)
, @EarlierExchangeRate numeric (18,8)
, @EarlierDATE SMALLDATETIME
, @EarlierDaysDiff int -- # days Earlier
, @EarlierFactor float -- Weight for Earlier Rate
, @LaterExchangeRate numeric (18,8)
, @LaterDATE SMALLDATETIME
, @LaterDaysDiff int -- # days for next later rate
, @LaterFactor float -- Weight for LaterRate
-- Truncate the time from @AsOfDate
SET @AsOfDate = dbo.udf_DT_SOD (@AsOfDate) -- Its call by Value
-- First try for an exact hit
SELECT Top 1 @Rate = ExchangeRATE
FROM dbo.CurrencyXchange WITH(NOLOCK)
WHERE RateTypeCD = @RateTypeCD AND AsOfDate = @AsOfDate
285
Part I
Chapter 13: Currency Conversion
286
Part I
Chapter 13: Currency Conversion
287
Lets rerun the query on a dollar to euro exchange, adding a column that
calls udf_Currency_XlateOnDate for possible interpolation:
-- Sample rates with possible interpolation
SELECT convert(char(10), D.date, 120) as [Date]
, dbo.udf_Currency_XlateOnDate ('IBR' -- TYPE OF RATE
, 1000.000 -- amount
, 'USD', 'EUR' -- $ to euro
, D.date) as [$1000 To EUR]
, dbo.udf_Currency_XlateNearDate ('IBR' -- TYPE OF RATE
, 1000.000 -- amount
, 'USD', 'EUR' -- $ to euros
, D.date) as [$1000 To EUR with interpolation]
FROM udf_DT_DaysTAB ('2001-12-30', '2002-01-14') D
ORDER BY d.Date
GO
(Results)
Date
-----------Dec 30 2001
Dec 31 2001
Jan 1 2002
Jan 2 2002
Jan 3 2002
Jan 4 2002
Jan 5 2002
Jan 6 2002
Jan 7 2002
Jan 8 2002
Jan 9 2002
Jan 10 2002
Jan 11 2002
Jan 12 2002
Jan 13 2002
Jan 14 2002
$1000 To EUR
-------------------NULL
NULL
1121.8000
1124.7000
1107.1000
1111.9000
1117.9000
1117.7000
1116.2000
1119.4000
1120.0000
1122.0000
1122.4000
NULL
NULL
1119.8000
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 similar 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
Part I
Chapter 13: Currency Conversion
288
Part I
Chapter 13: Currency Conversion
footnote the results of a currency conversion. The 30-day window is arbitrary, and you might want to adjust it for your application.
Listing 13.3: udf_Currency_DateStatus
CREATE
FUNCTION dbo.udf_Currency_DateStatus (
@RateTypeCD
CHAR(3) = 'IBR' -- Which rate
, @FromCurrencyCD CHAR(3) = 'USD'
, @ToCurrencyCD CHAR(3) = 'USD'
, @AsOfDATE SMALLDATETIME -- What date? Will use SOD.
) RETURNS INT -- Status code, see description above.
WITH SCHEMABINDING
/*
* Used together with udf_Currency_XchangeNearDate to understand the
* status of the exchange rate. The returned status codes are:
*
* 1 Exact date match found
* -1 Date Missing. Interpolation performed.
* -2 Date Missing. Earliest data point used.
* -3 Date Missing. Last data point used
* -4 Date Missing. Null returned
*
* Example:
SELECT dbo.udf_Currency_DateStatus (DEFAULT, 'USD', 'EUR',
'2002-06-01') -- Status of converting dollars to euros on 6/1/02
* Maintenance Note: maintain in sync with
udf_CurrencyXlateNearDate, same logic for handling dates.
* Test: (depend on the sample currency data in CurrencyXchange)
PRINT 'Test 1 exact hit
' +
CASE when 1 = dbo.udf_Currency_DateStatus (DEFAULT, 'USD',
'EUR', '2002-06-01') then 'Worked' else 'ERROR' end
PRINT 'Test 2 Missing data
' +
CASE when -4 = dbo.udf_Currency_DateStatus ('IBR','USD',
'ABR', '1998-01-01') then 'Worked' else 'ERROR' end
PRINT 'Test 3 Interpolation ' + CASE when -1 =
dbo.udf_Currency_DateStatus ('IBR','USD','EUR',
'2002-01-12') then 'Worked' else 'ERROR' end
**********************************************************************/
AS BEGIN
DECLARE @Rate as decimal (18, 8)
, @EarlierExchangeRate decimal (18,8)
, @EarlierDATE SMALLDATETIME
, @EarlierDaysDiff int -- # days Earlier
, @LaterExchangeRate decimal (18,8)
, @LaterDATE SMALLDATETIME
, @LaterDaysDiff int -- # days for next later rate
-- Truncate the time from @AsOfDate
SET @AsOfDate = dbo.udf_DT_SOD (@AsOfDate) -- Its call by value
-- First try for an exact hit
SELECT Top 1 @Rate = ExchangeRATE
FROM dbo.CurrencyXchange WITH(NOLOCK)
WHERE RateTypeCD = @RateTypeCD AND AsOfDate = @AsOfDate
AND FromCurrencyCd = @FromCurrencyCD AND ToCurrencyCD = @ToCurrencyCD
IF @Rate IS NOT NULL RETURN 1 -- Return for a direct hit
Part I
Chapter 13: Currency Conversion
289
udf_Currency_DateStatus is used in the next section. It constructs a scenario where reporting the status of the exchange rate conversion is
important.
290
Part I
Chapter 13: Currency Conversion
Euro Sales
-----------50.30
120.36
341.68
748.61
1085.40
464.96
188.44
322.88
215.09
886.44
252.81
317.13
34.51
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
Currency OK?
------------Interpolation
Available
Available
Available
Available
Available
Available
Available
Available
Available
Available
Available
Extension
Unavailable
Unavailable
Unavailable
Unavailable
Unavailable
Unavailable
Unavailable
Unavailable
291
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 straightforward than with units of measure but still important.
Part I
Chapter 13: Currency Conversion
Part II
System
User-Defined
Functions
293
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 documented 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.
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
They are UDFs and not built into the SQL Server engine.
Although system UDFs may have been written by the SQL Server development team, shipped with SQL Server, and updated in service packs,
theyre not a part of the core functionality of SQL Server the way that
built-in functions such as DATEDIFF, SUBSTRING, and @@ERROR are implemented inside the SQL engine. System UDFs are written in T-SQL and
use the T-SQL execution engine just like other UDFs. Theyre very much
like system stored procedures, which are also shipped as part of SQL
Server.
Part II
Chapter 14: Introduction to System UDFs
297
Normal UDFs can have any name that follows the T-SQL rules for
identifiers including upper and lowercase characters, digits, and the special characters underscore, ampersand (@), and pound sign (#). System
UDFs must follow narrower rules, which are detailed in the next
subsection.
The name must be all lowercase characters, and can include digits and
underscores.
The system UDFs shipped with SQL Server already follow these rules.
When we discuss creating your own system UDFs in Chapter 19, the
naming rules are an important requirement. SQL Server wont create a
system UDF with a name that doesnt follow these conventions.
In addition to the name, the database and owner where the system
UDFs are defined is key to giving them their status as system UDFs. As
the adage goes, What are the three most important factors in being a
system UDF?
System UDFs
For starters, the name of a system UDF must satisfy the normal rules for
a T-SQL object name. In addition, system UDFs must follow these naming
rules:
298
Part II
Chapter 14: Introduction to System UDFs
description
----------------------------------------------------Albanian, binary sort
Albanian, case-insensitive, accent-insensitive, kanat
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:
-- Attempt database.owner.functionname reference to system UDF
SELECT * from master.system_function_schema.fn_helpcollations()
GO
(Results)
Server: Msg 208, Level 16, State 1, Line 2
Invalid object name 'master.system_function_schema.fn_helpcollations'.
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 curtain (that is, until Toto pulls the curtain aside).
Part II
Chapter 14: Introduction to System UDFs
299
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 procedures and UDFs.
Be sure you set the Maximum characters per column field on the
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
System UDFs
300
Part II
Chapter 14: Introduction to System UDFs
This special syntax is reserved for system UDFs and system stored procedures. 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 requirements 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.
Group
Description
fn_get_sql
Special Purpose
fn_helpcollations
Special Purpose
fn_listextendedproperty
fn_listextendedproperty
fn_servershareddrives
Special Purpose
301
Function Name
Group
Description
fn_trace_geteventinfo
fn_trace_*
fn_trace_getfilterinfo
fn_trace_*
fn_trace_getinfo
fn_trace_*
fn_trace_gettable
fn_trace_*
fn_virtualfilestats
fn_virtualfilestats
fn_virtualservernodes
Special Purpose
System UDFs
Part II
Chapter 14: Introduction to System UDFs
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 columns 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 suffixes 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)
Part II
Chapter 14: Introduction to System UDFs
303
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:
-- All the binary collations
SELECT *
FROM ::fn_helpcollations()
WHERE [name] like '%_BIN%'
ORDER BY [name]
GO
(Results - abridged and truncated on the right)
name
-------------------------------Albanian_BIN
Arabic_BIN
Chinese_PRC_BIN
...
Slovenian_BIN
SQL_Latin1_General_CP437_BIN
SQL_Latin1_General_CP850_BIN
Thai_BIN
...
Vietnamese_BIN
description
----------------------------------------------Albanian, binary sort
Arabic, binary sort
Chinese-PRC, binary sort
Slovenian, binary sort
Latin1-General, binary sort for Unicode Data, S
Latin1-General, binary sort for Unicode Data, S
Thai, binary sort
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.
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.
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:
-- get the list of nodes that the server can run on
SELECT NodeName FROM ::fn_virtualservernodes()
GO
(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:
-- get the list of shared drives in the cluster
SELECT DriveName from ::fn_servershareddrives()
GO
(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 particularly useful when deadlocks have occurred.
Part II
Chapter 14: Introduction to System UDFs
305
fn_get_sql
Data Type
Description
EventType
nvarchar(30)
Parameters
int
0=text 1n=parameters
EventInfo
nvarchar(255)
For an RPC (stored procedure), it contains the procedure 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:
System UDFs
SQL Server 2000 Service Pack 3 (SP3) includes a new system userdefined 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 primarily 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 command. Lets take a look at that first.
306
Part II
Chapter 14: Introduction to System UDFs
Calling fn_get_sql
The syntax of the call to fn_get_sql is:
::fn_get_sql(@HandleVariable)
Data Type
Description
dbid
smallint
Database ID
objectid
int
number
smallint
encrypted
bit
text
text
Part II
Chapter 14: Introduction to System UDFs
307
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
itself:
-- Retrieve the sql of this connection
DECLARE @handle binary(20)
SELECT @handle = sql_handle
FROM master..sysprocesses
WHERE spid = @@SPID
SELECT [text]
FROM ::fn_get_sql(@handle)
GO
(Results)
text
---------------------------------------------------------------- Retrieve the sql of this connection
DECLARE @handle binary(20)
SELECT @handle = sql_handle
FROM master..sysprocesses
WHERE spid = @@SPID
SELECT [text]
FROM ::fn_get_sql(@handle)
System UDFs
DBCC execution completed. If DBCC printed error messages, contact your system
administrator.
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 system, 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.
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
Chapter 14: Introduction to System UDFs
309
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)
Batch A-2 Begin a transaction and create the blockage
(1 row(s) affected)
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 transaction left by Batch A-2. Figure 14.1 shows what my Query Analyzer
window looks like after I execute B-4.
System UDFs
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:
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.
-- Batch A-5 sp_lock shows who's waiting and who's locking
PRINT 'Batch A-5 -- Output of sp_lock'
EXEC sp_lock
GO
(Results)
Batch A-5 -- Output of sp_lock
spid dbid ObjId
IndId
------ ------ ----------- -----53
5 1977058079
0
53
5 1977058079
1
53
5 1977058079
1
53
5
0
0
53
5 1977058079
1
53
5 1977058079
2
53
1
85575343
0
54
14
0
0
55
5 1977058079
1
55
5
0
0
55
5 1977058079
0
55
5 1977058079
1
Type
---TAB
PAG
KEY
DB
PAG
KEY
TAB
DB
PAG
DB
TAB
KEY
Resource
Mode
---------------- -------IX
1:127
IX
(0801c4f7a625) X
S
1:239
IX
(1f048d178a34) X
IS
S
1:127
IS
S
IS
(0801c4f7a625) S
Status
-----GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
WAIT
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
Chapter 14: Introduction to System UDFs
311
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
SELECT * FROM ::fn_get_sql(@handle)
ROLLBACK TRAN -- Releases the lock on authors
GO
(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 column. 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.
System UDFs
(Results)
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.
Their location, which must be in the master database, under the ownership of a special-purpose owner, system_function_schema.
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
After weve discussed the documented system UDFs, the last two chapters in this part of the book go further:
n
Chapter 19 shows you how to make your own system UDFs and why
you might create them.
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 columns. 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
not have a description. Its used to locate tables that need more attention to their documentation.
n
313
314
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
Before the functions are developed, the next section has some information to expand your knowledge of extended properties and how to
create them. Thats followed by a description of the ins and outs of invoking 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.
Extended properties are added, updated, and deleted by using three system stored procedures:
n
property
n
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
315
a quick look at how it can be used to retrieve the extended property just
added:
-- Retrieve the ShortCaption caption just added
SELECT value
FROM ::fn_listextendedproperty('ShortCaption'
, 'USER', 'dbo', 'TABLE', 'Cust', 'COLUMN', 'CompanyName')
GO
value
---------------Cpny
Now lets delete the extended property so it doesnt get retrieved in any
of the other queries:
-- Remove an extended property
EXEC sp_dropextendedproperty 'ShortCaption' -, 'USER'
-- level
, 'dbo'
-- level
, 'TABLE' -- level
, 'Cust'
-- level
, 'COLUMN' -- level
, 'CompanyName' -- L
GO
(Results)
The command(s) completed successfully.
It isnt necessary to use these system stored procedures directly. Enterprise Manager uses them when you edit a description field, and Query
Analyzer has a graphical user interface thats convenient for entering
extended properties on functions. It can also be used for maintaining
extended properties on most other types of database objects. When you
use it for this purpose, Query Analyzer uses the system stored procedures
System UDFs
(Results)
316
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
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.
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
317
|
|
|
|
|
|
NULL
NULL
NULL
NULL
NULL
NULL
}
}
}
}
}
}
System UDFs
Using fn_listextendedproperty
318
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
Data Type
Description
@name
sysname
The name of the property given when it was created. There is no fixed set of names. The SQL
Server tools create properties with names that begin
with the letters MS. Names may contain embedded
spaces.
@level0type
varchar(128)
@level0name
sysname
@level1type
varchar(128)
@level1name
sysname
@level2type
varchar(128)
@level2name
sysname
Lets try an example. This query retrieves the MS_Description for the
Cust.CustomerID column using fn_listextendedproperty:
-- Query to get the MS_Description property from Cust.CustomerID
SELECT *
FROM ::fn_listextendedproperty ('MS_Description'
, 'USER', 'dbo'
, 'TABLE', 'Cust'
, 'COLUMN' , 'CustomerID')
GO
(Results - reformatted)
objtype objname
name
value
------- ---------- --------------- -------------------------------------------COLUMN CustomerID MS_Description ID assigned by customer management system.
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
319
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
Description
NULL
NULL
USER
TYPE
NULL
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.
Level 1 Objects
TABLE
PROCEDURE
NULL, PARAMETER
VIEW
FUNCTION
DEFAULT
NULL
RULE
NULL
Data Type
Description
object_type
varchar(128)
object_name
sysname
name
sysname
value
sql_variant
System UDFs
Table 15.3: Level 1 objects and the Level 2 objects that go with them
320
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
After running that batch, the following query shows that we have an
extended property defined at the database level. Default has been substituted for NULL. They work identically.
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
321
Of course, the query could have requested just the one property that
were interested in:
-- Get the Responsible Developer property
SELECT *
FROM ::fn_listextendedproperty ('Responsible Developer', default, default, default
, default, default, default)
GO
If you want to leave your database in its original state, delete the 'Respon-
System UDFs
322
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
all extended properties for the objects selected. The other six arguments
determine which objects are selected.
The meaning of NULL is different when its used for a @levelNtype argument than when its used for a @levelNname argument. When NULL is used
as a @levelNtype argument, its applied to the higher level object as a
whole. So when NULL is used for @level0type argument, it means, To the
database as a whole. When its used as the @level1type argument, it
implies, Get extended properties for the object that is specified for Level
0. When its used as the @Level2type, it means, Get extended properties
for the object at Level 1. However, when NULL is used for a @levelNname
argument, it means, All the objects at this level.
This next query gets all the properties that are defined at the TABLE
level (@level1type='TABLE') for all tables (@level1name=NULL). At the time I
wrote this chapter, I only had the few properties shown here, but I may
have added a few more as the database evolved. You may see slightly different results.
-- Get all the extended properties on all the tables.
SELECT * from ::fn_listextendedproperty (NULL,
'USER', 'dbo',
'TABLE', NULL,
NULL, NULL)
ORDER BY objname, name
GO
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
323
(Result - reformatted)
objtype
------TABLE
TABLE
TABLE
TABLE
TABLE
objname
----------------CurrencySourceCD
CurrencyXchange
CurrencyXchange
Cust
ExampleNames
name
---------------MS_Description
Caption
MS_Description
Caption
MS_Description
value
-----------------------------------Defines the known currency sourc...
Currency Exchange Rates
Exchange rates between currencies.
Customer List
Sample Names
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 specific table is used for the @level1name argument, only properties for the
given table are in the result, as in this query:
-- Get all the extended properties on the CurrencyXchange table
SELECT * from ::fn_listextendedproperty (NULL, 'USER', 'dbo',
'TABLE', 'CurrencyXchange',
NULL, NULL)
GO
objtype
-------TABLE
TABLE
objname
----------------CurrencyXchange
CurrencyXchange
name
---------------Caption
MS_Description
value
----------------------------------Currency Exchange Rates
Exchange rates between currencies.
If the query is modified to include a table name, the new query produces
results for just that table, as seen here:
-- Specifying the table name gets results.
SELECT * from ::fn_listextendedproperty ('Caption', 'USER', 'dbo',
'TABLE', 'CUST',
'COLUMN', NULL)
GO
System UDFs
(Results - reformatted)
324
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
(Results - reformatted)
objtype
------COLUMN
COLUMN
COLUMN
objname
---------------City
CompanyName
CustomerID
name
----------------Caption
Caption
Caption
value
-----------------------------------City
Name
Customer ID
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
325
-- Get all MS_Description extended properties for all TABLES in the database.
SELECT * from udf_Tbl_DescriptionsTAB() ORDER BY TableName
GO
(Results - reformatted and truncated on the right)
User
----dbo
dbo
dbo
dbo
dbo
dbo
TableName
---------------------------------------A Table to Show the Length of MS_Descrip
Broker
CurrencySourceCD
CurrencyXchange
ExampleNames
ExampleTableWithKeywordColumnNames
Description
---------------------------123456789 123456789 12345678
Has all the information for
Defines the known currency s
Exchange rates between curre
Sample Names
This table has column names
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 accomplishes 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-purpose way. Ive made it general purpose so that it can be the foundation for
several functions. Listing 15.2 shows its CREATE FUNCTION script.
System UDFs
326
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
Warning: The table '@EP' has been created, but its maximum row size (9,201) exceeds the maximum number of
bytes per row (8,060). INSERT or UPDATE of a row in
this table will fail if the resulting row length exceeds 8,060
bytes.
The warning is issued by SQL Server because of the value column. Its a
sql_variant that makes it possible to insert 8,000 bytes of data into the
value column and might push the total column length beyond the maximum allowed, 8,060. MS_Description columns are limited to 255 Unicode
characters, so theyll never extend the row anywhere near the limit. However, you might store your own extended property that is 8,000 bytes
long. If you do, be aware that it could cause an error in this function and
other udf_EP_* functions created in this chapter.
Listing 15.2: udf_EP_AllUsersEPsTAB
CREATE
FUNCTION dbo.udf_EP_AllUsersEPsTAB (
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
327
RETURN
END
Once the user name is fetched into @user_name, a SELECT from fn_listextendedproperty gets the requested extended properties and inserts
them into the @EP result table variable:
INSERT INTO @EP
SELECT @user_name, objtype, objname, [name], value
FROM ::fn_listextendedproperty (@epname
, 'USER', @user_name
, @level1_object_type, @level1_object_name
, NULL, NULL)
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
System UDFs
328
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
329
FUNCTION dbo.udf_EP_AllTableLevel2EPsTAB (
System UDFs
330
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
The clause TABLE_SCHEMA = @InputTableOwner on line 06 restricts the cursor to just one owner. Table owners are sometimes referred to as User
and sometimes as Schema. In this context, all three terms mean the same
thing; I use Owner because its not a SQL Server keyword. If the owner
name isnt supplied, the clause @InputTableOwner is NULL on line 05 takes
over and the cursor finds the tables owned by all users that have tables in
the database.
Similarly, the clause TABLE_NAME = @InputTableName on line 08 restricts
the cursor to just one table, if the caller supplies one. If the table name
isnt supplied, the @InputTableName is NULL clause on line 07 takes over and
the cursor finds all the tables.
As each TABLE_NAME is read into the @table_name variable, its extended
properties are queried and inserted into the results table, @EP, with this
statement:
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
331
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_AllTableLevel2EPsTAB 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
TableName
-----------------CurrencyCD
CurrencyCD
CurrencyCD
...
ColumnName
-------------------Comment
CurrencyCD
CurrencyName
Description
--------------------------------------Descriptive comment
ISO 4217 Currency Code used by Currency
Common Name for the currency.
System UDFs
332
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
FUNCTION dbo.udf_Tbl_MissingDescrTAB (
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:
-- Find tables that are missing their description.
SELECT Owner + '.' + TABLE_NAME AS [Tables missing MS_Description]
FROM udf_Tbl_MissingDescrTAB()
GO
(Results - abridged. Your results will differ)
Tables missing MS_Description
-------------------------------------------------------------------------------dbo.BBTeams
dbo.CurrencyCD
dbo.CurrencyRateTypeCD
dbo.Cust
dbo.ExampleSales
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
333
FUNCTION udf_Tbl_RptW (
)
/*
* Returns a report of information about a table. Information is from
* sysobjects, sysowner, extended properites, and OBJECTPROPERTIES.
* Intended to output from Query Analyzer. It can be sent to a file
* and then printed from the file with landscape layout.
*
* 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)
System UDFs
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
dbo.CurrencyCD
Part II
Chapter 15: Documenting DB Objects with fn_listextendedproperty
335
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 documentation tasks that Ive found most important when working with databases:
n
System UDFs
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:
16
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_virtualfilestats returns to produce UDFs that summarize the I/O statistics in
various ways. Specifically, at these levels:
n
The data returned by fn_virtualfilestats is pretty raw and almost impossible to use without interpretation. Before we can make use of its results,
were going to have to perform several minor interpretation tasks:
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_virtualfilestats. 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 )
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
339
Data Type
Description
DbId
smallint
Database ID
FileId
smallint
File ID
TimeStamp
int
NumberReads
bigint
NumberWrites
bigint
BytesRead
bigint
BytesWritten
bigint
IoStallMS
bigint
Time, in milliseconds, that users waited for I/O operations on the file to complete
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.
System UDFs
340
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
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 functions. However, they only return information about files in the current
database. Run this query from inside TSQLUDFS:
-- Run this query from inside TSQLFunctions
USE TSQLUDFS
GO
SELECT File_Name(1) as [ID 1], File_Name(2) as [ID 2]
, File_ID('TSQLUDFS_Data') as [ID of the Data File]
, File_ID('TSQLUDFS_Log') as [ID of the Log File]
GO
(Results - reformatted)
ID 1
ID 2
ID of the Data File ID of the Log File
-------------------- ------------------ ------------------- -----------------TSQLUDFS_Data
TSQLUDFS_Log
1
2
The output of fn_virtualfilestats can be made to be more understandable by using the metadata functions to label the files. This script moves
to the pubs database to get information about its files:
-- Convert IDs to Names
USE pubs
GO
DECLARE @DatabaseID smallint -- holds the database ID
SET @DatabaseID = DB_ID() -- no argument means use the current database
SELECT DB_NAME(DbId) as [Database]
, FILE_NAME(FileID) as [File Name]
, NumberReads, NumberWrites, BytesRead, BytesWritten, IoStallMS
FROM ::fn_virtualfilestats(@DatabaseID, -1)
GO
(Results - reformatted)
Database
-------pubs
pubs
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
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
341
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.
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
filename column could be shown.
-- What's in sysfiles
USE TSQLUDFS
GO
SELECT * from sysfiles
GO
(Results - first group of columns)
fileid groupid size
maxsize growth
status
perf
name
------ ------- -------- -------- -------- --------- --------- ----------------1
1
1304
-1
10 1081346
0 TSQLUDFS_Data
2
0
3688
-1
10 1081410
0 TSQLUDFS_Log
(Results - second group of columns)
filename
-----------------------------------------------------------------------------C:\BT\Projects\Book T-SQL Functions\Data\TSQLUDFS_Data.MDF
C:\BT\Projects\Book T-SQL Functions\Data\TSQLUDFS_Log.LDF
System UDFs
Whats in sysfiles?
342
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
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
SELECT DB_NAME(DbId) as [Database]
, FILE_NAME(vfs.FileID) as [Logical File]
, RTRIM(s.filename) as [Physical File]
, NumberReads, NumberWrites, BytesRead, BytesWritten, IoStallMS
FROM ::fn_virtualfilestats(@DatabaseID, 1) vfs
Inner Join sysfiles s
on vfs.FileID = s.FileID
GO
(Results - first group of columns)
Database Logical File Physical File
-------- ------------ ---------------------------------------------------------pubs
pubs
C:\Program Files\Microsoft SQL Server\MSSQL\data\pubs.mdf
(Results - second group of columns)
NumberReads NumberWrites BytesRead BytesWritten IoStallMS
----------- ------------ --------- ------------ --------67
10
1810432 81920
871
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.
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
343
name
------------------master
mastlog
tempdev
templog
filename
-----------------------------------------C:\Program File...SSQL\data\master.mdf
C:\Program File...SSQL\data\mastlog.ldf
C:\Program File...SSQL\data\tempdb.mdf
C:\Program File...SSQL\data\templog.ldf
TSQLUDFS_Data
TSQLUDFS_Log
C:\BT\Projects\...a\TSQLFunctions5.mdf
C:\BT\Projects\...a\TSQLFunctions5_log.ldf
The advantage of using master..sysaltfiles is that information on all databases is available. This lets us run functions based on fn_virtualfilestats
from any database.
System UDFs
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:
344
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
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.
started. When was that? The SQL Server development team thought of
that issue; the third column in the result table, timestamp, is the number of
milliseconds since the system started. Its returned for every row in the
resultset.
The timestamp column is the preferred way to get a denominator for
computing rates of I/O operations. The UDFs in the second half of this
chapter that aggregate I/O statistics all use timestamp to convey the number of seconds that theyre reporting. However, its not the only way to
get that information.
Its possible to get the number of seconds since the instance started
from system tables. The function udf_SQL_StartDT, shown in Listing 16.1,
begins the process by getting the time that SQL Server started. It turns
out that system processes like LAZY WRITER, LOG WRITER, and several others
have entries in sysprocesses. As youd expect, they log in when the system starts, and they stay logged in until the system stops. udf_SQL_
StartDT queries the login_time of the process thats executing the LAZY
WRITER command, and thus the system start time is known.
Listing 16.1: udf_SQL_StartDT
CREATE FUNCTION dbo.udf_SQL_StartDT (
)
/* Returns the date/time that the SQL Server instance was started.
*
* Common Usage:
select dbo.udf_SQL_StartDT() as [System Started AT]
****************************************************************/
AS BEGIN
DECLARE @WorkingVariable datetime
SELECT @WorkingVariable = login_time
FROM master..sysprocesses
WHERE cmd='LAZY WRITER'
RETURN @WorkingVariable
END
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
345
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.
Listing 16.2: udf_Instance_UptimeSEC
So, to answer our question, How long has SQL Server been up? run the
query:
-- How Long Has SQL Server Been Up?
SELECT dbo.udf_Instance_UptimeSEC()
GO
(Result)
Seconds since SQL Server Started
-------------------------------207694
System UDFs
346
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
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
@@IDLE
Number of milliseconds that SQL Server has been idle since it was
started.
@@IO_WAIT
@@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 compares them to the results of fn_virtualfilestats:
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
347
System UDFs
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.
/*
* Returns a table of total statistics for one particular database
* by summing the statistics for all of its files.
*
* Example:
select * from dbo.udf_Perf_FS_DBTotalsTAB ('pubs')
*****************************************************************/
AS BEGIN
DECLARE @DatabaseID int -- Database ID from master..sysdatabases
SET @DatabaseID = DB_ID(@DBName)
INSERT INTO @FileStats
SELECT Count(*) -- Number of files
, Sum(NumberReads)
, Sum(NumberWrites)
, Sum(BytesRead)
, Sum(BytesWritten)
, Sum(IoStallMS)
, ROUND(max(timestamp) / 1000.0, 0) as Sec
FROM ::fn_virtualfilestats(@DatabaseID, -1) -- -1 means all files
RETURN
END
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
349
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.
instance. This is the quick picture of whats going on in the SQL Server.
There is always at least a little additional I/O activity for the operating system, but on many dedicated systems, SQL Server does the bulk of the I/O.
System UDFs
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)
DatabaseName
Files NumReads
NumWrites
Sec
---------------- ------ -------------------- -------------------- ----------VersqDev1
2
7367
29
216915
TESTDB37
3
2577
33
216915
Versq2
2
1676
640
216915
TSQLWorking
2
154
720
216915
msdb
2
378
255
216915
FUNCTION dbo.udf_Perf_FS_ByPhysicalFileTAB (
/*
* 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
* flies and log files. Information about physical files is
* taken from master..sysaltfiles which has the physical file
* name needed.
*
* Example:
select * from dbo.udf_Perf_FS_ByPhysicalFileTAB (default) -- All
****************************************************************/
AS RETURN
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
351
These statistics are even more useful when a database is split into different 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 contains the drive letter, we can use it to summarize by drive letter. The drive
letters often correspond to physical disks but not always.
System UDFs
352
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
FUNCTION dbo.udf_Perf_FS_ByDriveTAB (
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
353
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 produced with udf_Perf_FS_ByDriveTAB. Obviously, its not a highly stressed
system.
-- I/O summary for the drives on a system
SELECT DriveLetter, NumFiles, SizeMb, NumberReads, NumberWrites
, IoSTallMS, SecondsInMeasurement as Sec
FROM dbo.udf_Perf_FS_ByDriveTAB (default)
GO
(Results - reformatted)
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 system performance would monitor I/O along with memory use, CPU use,
and network traffic as it varied over time. Most importantly, it would have
System UDFs
DriveLetter
NumFiles
SizeMb NumberReads NumberWrites IoSTallMS Sec
----------- ----------- ------------ ----------- ------------ --------- -------C:
47
456.563
2477
3087
33430
70789
D:
2
5000.000
105
14
702
70789
354
Part II
Chapter 16: Using fn_virtualfilestats to Analyze I/O Performance
17
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 procedures 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 collectively 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 designation, 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 purpose 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
To a SQL table
To a rowset as the trace events are generated This is the way that
SQL Profiler gets trace information.
In SQL Server 2000, the trace facility is more robust than in earlier versions, and it can make the guarantee that no events are lost, at least when
writing to a disk file. It might still lose events when sending trace events
to the SQL Profiler or saving them in a table. Thats a trade-off well have
to accept for performance reasons.
The first section of this chapter starts with an explanation of creating
traces with the sp_trace_* system stored procedures. That section also
shows you how to get SQL Profiler to write the script for you.
Once there are traces running on your system, the SQL fn_trace_*
functions become relevant. The functions are listed in Table 17.1 in the
order in which theyre presented in the rest of the chapter.
Table 17.1: System trace UDFs
Function Name
Description
fn_trace_getinfo
fn_trace_gettable
fn_trace_geteventinfo
fn_trace_getfilterinfo
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 something meaningful to DBAs and programmers.
The task-oriented functions and stored procedures built in this chapter are:
n
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
357
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:
-- Show a report of all running traces
SELECT * from udf_Trc_Rpt(default)
GO
(Results)
I find that pretty readable. If youre familiar with the SQL Profiler, I think
you will also.
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.
System UDFs
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%
358
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
Description
sp_trace_create
sp_trace_setevent
Adds or removes events and columns from the trace. Its documentation in Books Online is where youll find the event codes
and column codes.
sp_trace_setfilter
sp_trace_setstatus
Starts, stops, and closes traces. SQL Profiler calls the stop status
Paused and the closed status Stopped.
A script that creates a trace usually follows this general order of calls:
1.
2.
umns to be monitored.
3.
trace.
4.
2.
The process of writing the script to perform a trace is tedious. Fortunately, 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
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
359
System UDFs
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
Property
int
Value
sql_variant
property
----------1
2
3
4
5
1
2
3
4
5
value
----------------------------------------------------1
NULL
5
2002-09-20 00:36:02.123
1
2
C:\SampleTrace
5
2002-09-20 00:36:02.123
1
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
361
Two traces were started because Server processes SQL Server trace
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 guaranteed 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
Trace Options
int
FileName
nvarchar(254)
System UDFs
Figure 17.1: SQL Profiler Trace Properties screen starting a new trace
362
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
Number
Name
Data Type
Description
MaxSize
bigint
StopTime
datetime
Status
int
The current status of the trace. Is it running or stopped? Status codes are
shown in Table 17.6.
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
TRACE_PRODUCE_ROWSET
TRACE_FILE_ROLLOVER
SHUTDOWN_ON_ERROR
TRACE_PRODUCT_BLACKBOX
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
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
363
Description
Stopped
Running
Interpreting the output of the fn_trace_getinfo function is a little painstaking, and two techniques make understanding the properties for each
trace easier:
n
udf_Trc_InfoTAB
FUNCTION dbo.udf_Trc_InfoTAB (
/*
* Returns a table of information about a trace; these are the
* original arguments to sp_trace_create. The status field is
* broken down to four individual fields.
*
* Example:
SELECT * from udf_Trc_InfoTAB(default)
****************************************************************/
AS RETURN
SELECT TOP 100 PERCENT WITH TIES
traceid
, MAX(CAST(CASE WHEN property=5
THEN CASE CAST(value as INT)
WHEN 1 THEN 'RUNNING'
ELSE 'STOPPED' END
ELSE NULL END
AS varchar(7))) AS [Status]
, MAX(CAST(CASE WHEN property=1 and (CAST(value as int) & 1) = 1
THEN 'YES' ELSE 'NO' END
AS varchar(3))) AS PRODUCE_ROWSET
, MAX(CAST(CASE WHEN property=1 and (CAST(value as int) & 2) = 2
THEN 'YES' ELSE 'NO' END
AS varchar(3))) AS FILE_ROLLOVER
System UDFs
364
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
Heres a query that uses udf_Trc_InfoTAB to show the properties for the
currently running traces:
-- Show the properties as a readable table
SELECT TraceID as ID, Status, PRODUCE_ROWSET AS [RS], FILE_ROLLOVER [ROLL]
, SHUTDOWN_ON_ERROR [SHUT], TRACE_PRODUCE_BLACKBOX AS [BB]
, MaxSize [MS], StopTime [ST], FileName [FN]
FROM dbo.udf_Trc_InfoTab(default)
GO
(Results - reformatted and truncated on the right)
ID
-1
2
3
Status
------RUNNING
RUNNING
STOPPED
RS
---YES
NO
YES
ROLL
---NO
YES
NO
SHUT
---NO
NO
NO
BB MS ST
FN
---- --- ----------------------- -------------------NO
5 2002-09-20 00:36:02.123 NULL
NO
5 2002-09-20 00:36:02.123 C:\Documents and Set
NO
5 NULL
NULL
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
365
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 property. 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:
MAX(CAST(CASE WHEN property=2
THEN value ELSE NULL END
AS nvarchar(254))) AS [FileName]
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
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 aggregation 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.
System UDFs
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:
CAST(value AS int) & 2) = 2
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, eventually 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_setstatus. 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 commented 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
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
367
System UDFs
368
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
369
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:
-- Stop all traces
EXEC usp_Admin_TraceStop NULL -- all traces
GO
(Results - reformatted)
Status
------Stopped
Stopped
Closed
FileName
--------------------NULL
NULL
C:\temp\trace1.trc
ActionDT
---------2002-10-31
2002-10-31
2002-10-31
ActionDescription
------------------Stopped
Stopped
Stopped Closed
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.
The information is in master..sysprocesses. Right now Im the only one on
the system, as shown by this query:
-- Who's running SQL Profiler
SELECT spid, hostname, hostprocess, nt_domain, nt_username
FROM master..sysprocesses
WHERE program_name = 'SQL Profiler'
ORDER BY nt_username
GO
(Results - reformatted)
spid hostname
hostprocess nt_domain
------ ------------ ----------- ----------------60 ASN-H1200
2148
ASN-H1200
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.
System UDFs
TraceID
----------1
2
3
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
@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 argument, 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:
-- sample call to fn_trace_gettable
SELECT CAST(replace(replace(Left(convert(varchar(30),TextData)
, 30), char(10), ' '), char(13), ' ') as char(30)) as TextData
, StartTime, EventClass
, CPU, Reads, Writes
FROM ::fn_trace_gettable('c:\ExampleTrace.trc', 1)
GO
(Results - reformatted and truncated)
TextData
StartTime
------------------------------ ------------------NULL
2002-09-20 10:45:40
-- network protocol: LPC set q NULL
-- network protocol: LPC set q NULL
-- network protocol: LPC set q NULL
-- network protocol: LPC set q NULL
-- network protocol: LPC set q NULL
-- network protocol: LPC set q NULL
-- network protocol: LPC set q NULL
-- network protocol: LPC set q NULL
select * from dbo.udf_SQL_Trac 2002-09-20 10:46:06
SELECT *
FROM ::fn_helpco 2002-09-20 10:46:27
SELECT *
FROM ::fn_liste 2002-09-20 10:46:36
EventClass
---------0
17
17
17
17
17
17
17
17
12
12
12
CPU
----NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
10
31
91
Reads
----NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
125
1155
983
Writes
-----NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
NULL
0
16
0
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
select *
use pubs
select *
select *
select *
select *
select *
NULL
from cust
from
from
from
from
from
authors
authors
authors
authors
authors
cross
cross
cross
cross
cross
jo
jo
jo
jo
jo
2002-09-20
2002-09-20
2002-09-20
2002-09-20
2002-09-20
2002-09-20
2002-09-20
NULL
10:47:26
10:48:04
10:48:32
10:48:41
10:48:48
10:48:58
10:49:12
12
12
12
12
12
12
12
5
0
0
0
0
20
70
2664
NULL
11
14
6
6
123
954
9620
NULL
371
0
0
0
0
0
0
1
NULL
System UDFs
372
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
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:
-- Query with translated event names
SELECT CAST(replace(replace(Left(convert(varchar(30),TextData)
, 30), char(10), ' '), char(13), ' ') as char(30)) as TextData
, dbo.udf_Trc_EventName(EventClass) as [Event Class]
, CPU, Reads, Writes
FROM ::fn_trace_gettable('c:\ExampleTrace.trc', 1)
GO
(Results - reformatted and abridged, columns truncated)
TextData
-----------------------------NULL
-- network protocol: LPC set q
-- network protocol: LPC set q
...
-- network protocol: LPC set q
select * from dbo.udf_SQL_Trac
...
select * from authors cross jo
NULL
Event Class
-------------------Reserved
ExistingConnection
ExistingConnection
CPU
----NULL
NULL
NULL
Reads
------NULL
NULL
NULL
Writes
------NULL
NULL
NULL
ExistingConnection
SQL:BatchCompleted
NULL NULL
10
125
NULL
0
SQL:BatchCompleted
Reserved
2664 9620
NULL NULL
1
NULL
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 )
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
373
Data Type
Description
EventID
int
ColumnID
int
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
SELECT TOP 1 @traceID = traceid
FROM dbo.udf_Trc_InfoTAB(default)
ORDER BY traceid
(Results - abridged )
eventid
----------10
10
10
10
...
15
15
15
columnid
----------1
12
16
17
16
17
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 understandable, 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.
System UDFs
374
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
/*
* Translates a SQL Trace ColumnID into its descriptive name.
* Used when viewing or retrieving event information from
* fn_trace_geteventinfo.
*
* Example: -- assumes existence of the c:\ExampleTrace.trc file
select EventID, dbo.udf_Trc_ColumnName(columnid)
from ::fn_trace_geteventinfo (1)
***************************************************************/
AS BEGIN
RETURN CASE
WHEN
WHEN
WHEN
@ColumnID
1 THEN 'TextData'
2 THEN 'BinaryData'
3 THEN 'DatabaseID'
...
WHEN 43 THEN 'TargetLoginSID'
WHEN 44 THEN 'ColumnPermissionsSet'
ELSE 'Unknown:' + CAST(@columnid as varchar(20))
END
END
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 possible 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 single 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.
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
375
Since the cursor in udf_Trc_EventListCursorBased is used to concatenate strings, the string concatenation technique discussed in Chapter 8
and first shown with the function udf_Titles_AuthorList2 can be used to
shorten and speed up the creation of the event list. I wouldnt show this
technique again except that the circumstances of this function create a few
interesting wrinkles. Listing 17.6 shows function udf_Trc_EventList that
uses the cursor alternative technique. At first, I thought it wouldnt work.
Listing 17.6: udf_Trc_EventList
System UDFs
376
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
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:
SELECT DISTINCT @EventList = @EventList
+ @Separator
+ dbo.udf_Trc_EventName(EventID)
FROM ::fn_trace_geteventinfo(@trace_id)
ORDER BY EventID
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 correct results. This query runs udf_Trc_EventList on trace number 1:
-- Run udf_SQLTraceEventList, assumes trace 1 is running
SELECT dbo.udf_Trc_EventList (1, N', ') as [Event List]
GO
(Results)
Event List
------------------------------------------------------------------------------RPC:Completed, SQL:BatchCompleted, Login, Logout, ExistingConnection
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
377
fn_trace_getfilterinfo
This function retrieves information about the active filters for a trace. The
syntax of the call is:
::fn_trace_getfilterinfo( @traceid )
Data Type
Description
columnid
int
comparison_operator
int
logical_operator
int
Code for the operator used to compare the columns 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 process of making a human-readable filter expression starts by converting the comparison_operator and the logical_operator into something
that you and I can read. The next sections create functions to take care of
those tasks.
Operator
Operator Name
Equal
<> or !=
Not equal
>
Greater than
<
Less than
System UDFs
378
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
Code Value
Operator
Operator Name
>=
<=
LIKE
Like
NOT LIKE
Not like
This trace has one filter. If you look at the Filters tab of the profile definition, 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)
Column ID
Logical Op Comp Op
value
-------------------- ---------- ---------- -----------------------------------ApplicationName
AND
NOT LIKE SQL Profiler
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
379
Were getting closer to text thats understandable for us SQL programmers. It would be easier for us to read if the filter expression used a
syntax similar to a WHERE clause.
System UDFs
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 conditions. 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.
Heres a script that uses udf_Trc_FilterClause on the trace shown in Figure 17.2:
-- use udf_Trc_FilterClause on the trace 1 -- assumes it exists
SELECT dbo.udf_Trc_LogicalOp (logical_operator) as [Oper]
, dbo.udf_Trc_FilterClause
(columnid, comparison_operator, value) as [Expression]
FROM ::fn_trace_getfilterinfo(1)
GO
(Results)
Oper
---AND
OR
OR
AND
AND
AND
Expression
---------------------------------------------------------------DatabaseID=5
DatabaseID=6
DatabaseID=7
ObjectID>=100
ApplicationName NOT LIKE N'SQL Profiler'
ApplicationName NOT LIKE N'sqla%'
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
381
clause but not too far from the way that I write them. To construct the filter expression column of udf_Trc_InfoExTAB thats built later in this
chapter, I want the filter expression as a single string or as a tab-separated
list within a string.
udf_Trc_FilterExpression does what I really want: produce a single
string that expresses the entire filter. Youll find it in Listing 17.8. Its similar to the udf_Trc_EventList function shown in Listing 17.6. This one uses
a cursor to examine the results of fn_trace_getfilterinfo, translates each
clause into text, and concatenates them together, returning the result. The
parameters are the traceid, the separator character, and one bit compact
option.
Its possible to convert this function from using a cursor to the alternate string concatenation technique used by udf_Trc_EventList. However,
I looked at the complexity of the logic inside the loop and decided it
wouldnt be that easy to convert. You have to weigh the effort that could
be expended in improving the function against the frequency with which
this function is run and the difficulty in maintaining it once its changed. In
this case, my decision is to leave the cursor in the function.
Listing 17.8: udf_Trc_FilterExpression
CREATE FUNCTION dbo.udf_Trc_FilterExpression (
@trace_id int -- which trace. No valid default
, @Separator nvarchar(128) = N', ' -- Separates entries
-- usually this is something like ', ' or NCHAR(9)
, @CompactBIT BIT = 0 -- Should the result be compacted.
) RETURNS nvarchar(4000) -- separated list of filter expressions
-- suitable for display or ready for word wrap.
/*
* 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.
*
* Example:
select dbo.udf_Trc_FilterExpression(1, ' ', 1) -- assumes 1 is valid
***********************************************************************/
AS BEGIN
DECLARE
DECLARE
,
,
,
,
,
,
,
System UDFs
Thats much better than the jumble of numbers returned by fn_trace_getfilterinfo. However, the logical_operator isnt just right. Theres an
extra AND in the first row, and the output is a rowset, not exactly a WHERE
382
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
383
, @LastComparison_OperatorCode = @Comparison_OperatorCode
FETCH FilterCursor INTO @ColumnID, @Logical_OperatorCode,
@Comparison_OperatorCode, @Value -- retrieve next filter
END -- of the WHILE LOOP
-- Clean up the cursor
CLOSE FilterCursor
DEALLOCATE FilterCursor
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:
-- use udf_Trc_FilterExpression on trace 1 without Compact option
SELECT dbo.udf_Trc_FilterExpression (1, ' ', 0) as [Filter Expression]
GO
Filter Expression
------------------------------------------------------------------------------DatabaseID=5 OR DatabaseID=6 OR DatabaseID=7 AND ApplicationName NOT LIKE N'SQL
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 successive 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:
-- use udf_Trc_FilterExpression on trace 1 with Compact option
SELECT dbo.udf_Trc_FilterExpression (1, ' ', 1) as [Filter Expression]
GO
(Results - wrapped to a second line)
Filter Expression
-------------------------------------------------------------------------------DatabaseID=5 OR =6 OR =7 AND ApplicationName NOT LIKE N'SQL Profiler' AND
ObjectID>=100
System UDFs
384
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
, @Tab nvarchar(1)
Part II
Chapter 17: fn_trace_* and How to Create and Monitor System Traces
385
To see how it works, start a few traces and choose various events, columns, and filter expressions. Set our output to text and be sure to set the
Query Analyzer option Maximum characters per column to a big number 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:
-- Run udf_Trc_Rpt to see information about all running traces
-- be sure to start a few before testing it.
SELECT rptline from udf_Trc_RPT(default)
GO
rptline
------------------------------------------------------------------------------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'
Trace: 2 RUNNING Rowset:YES Rollover:NO ShutOnErr:NO BlackBox:NO MaxSize:5
Stop At: 2002-09-25 11:14:20 Filename:NULL
Events: RPC:Completed, RPC:Starting, SQL:BatchCompleted, SQL:BatchStarting,
DTCTransaction, DOP Event, SP:CacheMiss, SP:CacheInsert,
SP:CacheRemove, SP:Recompile, SP:CacheHit, SP:ExecContextHit,
Exec Prepared SQL, Unprepare SQL, CursorExecute, CursorRecompile,
CursorImplicitConversion, CursorUnprepare, CursorClose,
Show Plan Text, Show Plan ALL, Show Plan Statistics
Columns: TextData, BinaryData, DatabaseID, TransactionID, NTUserName,
ClientHostName, ClientProcessID, ApplicationName,
DatabaseUserName, TargetLoginName, TargetLoginSID,
ColumnPermissionsSet
Filter: DatabaseID=5 OR =6 OR =7 AND ObjectID>=100
AND ApplicationName NOT LIKE N'SQL Profiler' AND NOT LIKE N'SQLAgent%'
...
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.
System UDFs
(Results - abriged)
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 running 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 previous 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 readable 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
They can use T-SQL syntax that is reserved for system UDFs.
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 syntax 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 create 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 complete. 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 execute 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 system 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 system 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.
389
routine_schema
routine_name
data_type
-----------------------------------------------------------------------------dbo
fn_isreplmergeagent
bit
dbo
fn_MSFullText
TABLE
dbo
fn_MSgensqescstr
nvarchar
dbo
fn_MSsharedversion
nvarchar
dbo
fn_sqlvarbasetostr
nvarchar
dbo
fn_varbintohexstr
nvarchar
dbo
fn_varbintohexsubstring
nvarchar
system_function_schema fn_chariswhitespace
bit
system_function_schema fn_dblog
TABLE
system_function_schema fn_generateparameterpattern
nvarchar
system_function_schema fn_getpersistedservernamecasevariation nvarchar
system_function_schema fn_helpcollations
TABLE
system_function_schema fn_listextendedproperty
TABLE
system_function_schema fn_removeparameterwithargument
nvarchar
system_function_schema fn_repladjustcolumnmap
varbinary
system_function_schema fn_replbitstringtoint
int
system_function_schema fn_replcomposepublicationsnapshotfolder nvarchar
system_function_schema fn_replgenerateshorterfilenameprefix
nvarchar
system_function_schema fn_replgetagentcommandlinefromjobid
nvarchar
system_function_schema fn_replgetbinary8lodword
int
system_function_schema fn_replinttobitstring
char
system_function_schema fn_replmakestringliteral
nvarchar
system_function_schema fn_replprepadbinary8
binary
system_function_schema fn_replquotename
nvarchar
system_function_schema fn_replrotr
int
system_function_schema fn_repltrimleadingzerosinhexstr
nvarchar
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.
System UDFs
Part II
Chapter 18: Undocumented System UDFs
390
Part II
Chapter 18: Undocumented System UDFs
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 additional 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:
-- Try to get the text of an undocumented system UDF
EXEC sp_helptext 'fn_serverid'
GO
(Results)
Server: Msg 15009, Level 16, State 1, Procedure sp_helptext, Line 53
The object 'fn_serverid' does not exist in database 'master'.
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:
-- Get the Object_ID for fn_serverid
SELECT OBJECT_ID('fn_serverid') as [ID]
GO
(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:
-- List the functions in sysobjects
SELECT * from sysobjects where (type = 'FN' or type = 'IF' or type = 'TR')
GO
(Results)
name
--------------------------------------------fn_updateparameterwithargument
Part II
Chapter 18: Undocumented System UDFs
391
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 systems where you know its okay. The script is commented out to prevent
creating it unintentionally.
Listing 18.1: udf_Func_UndocSystemUDFtext
System UDFs
392
Part II
Chapter 18: Undocumented System UDFs
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)
Part II
Chapter 18: Undocumented System UDFs
393
Decimal Value
Hex Value
Tab
0x9
New Line
10
0xA
Carriage Return
13
0xD
Space
32
0x26
Notice that because its a system UDF, the calling sequence for fn_chariswhitespace doesnt include the owner. Lets try a query in master, then
move to pubs and try some more:
USE master
GO
-- check if various characters are whitespace characters
SELECT fn_chariswhitespace(CHAR(9)) as [Tab]
, fn_chariswhitespace('A') as [A]
, fn_chariswhitespace(' ') as [Space]
, fn_chariswhitespace(N' ') as [Unicode Space]
, fn_chariswhitespace(NCHAR(10)) [Unicode New Line]
, fn_chariswhitespace(N'A') [Unicode A]
GO
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
System UDFs
(Results)
394
Part II
Chapter 18: Undocumented System UDFs
fn_dblog
fn_dblog returns a table of records from the transaction log. The syntax of
@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 transaction 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 created records in the transaction log:
USE TSQLUDFS
GO
-- make a minor change to the database
UPDATE ExampleAddresses SET StreetNumber = StreetNumber + 1
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:
-- Get the entire database log from fn_dblog
SELECT * from ::fn_dblog(null, null)
GO
(Results - first group of columns)
Current LSN
---------------------00000056:000000a0:0001
00000056:000000a0:0002
00000056:000000a0:0003
00000056:000000a0:0004
00000056:000000a0:0005
00000056:000000a0:0006
00000056:000000a0:0007
00000056:000000a0:0008
00000056:000000a0:0009
00000056:000000a0:000a
Operation
----------------LOP_BEGIN_XACT
LOP_SET_BITS
LOP_MODIFY_ROW
LOP_MODIFY_ROW
LOP_MODIFY_ROW
LOP_MODIFY_ROW
LOP_MODIFY_ROW
LOP_SET_BITS
LOP_DELTA_SYSIND
LOP_COMMIT_XACT
Context
-------------LCX_NULL
LCX_DIFF_MAP
LCX_CLUSTERED
LCX_CLUSTERED
LCX_CLUSTERED
LCX_CLUSTERED
LCX_CLUSTERED
LCX_DIFF_MAP
LCX_CLUSTERED
LCX_NULL
Transaction ID
-------------0000:00002adf
0000:00000000
0000:00002adf
0000:00002adf
0000:00002adf
0000:00002adf
0000:00002adf
0000:00000000
0000:00002adf
0000:00002adf
Tag Bits
-------0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
Part II
Chapter 18: Undocumented System UDFs
395
Flag Bits
--------0x0200
0x0000
0x0200
0x0200
0x0200
0x0200
0x0200
0x0000
0x0200
0x0200
Object Name
--------------------------NULL
dbo.ALLOCATION (99)
dbo.ExampleAddresses (98019
dbo.ExampleAddresses (98019
dbo.ExampleAddresses (98019
dbo.ExampleAddresses (98019
dbo.ExampleAddresses (98019
dbo.ALLOCATION (99)
dbo.sysindexes (2)
NULL
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
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:
-- Take a look at fn_dblog from pubs
USE pubs
GO
SELECT * from ::fn_dblog(NULL, NULL)
GO
(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 database 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
System UDFs
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 Program Files.
If you need information about the version of SQL Server, youre
better off using the SERVERPROPERTY function, as in this query:
-- Get the product version
SELECT SERVERPROPERTY ('ProductVersion') as ProductVersion
GO
(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)
Part II
Chapter 18: Undocumented System UDFs
397
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_
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_replinttobitstring 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.
Listing 18.2: udf_BitS_FromInt
CREATE FUNCTION dbo.udf_BitS_FromInt (
@INT int -- the input value
, @TrimLeadingZerosBIT bit = 0 -- 1 to trim leading 0s.
) RETURNS varchar(32) -- String of 1s and 0s representing @INT
-- No schemabinding due to use of system UDF.
/*
* Translates an int into a corresponding 32-character string
* of 1s and 0s. It will optionally trim leading zeros.
*
* Related Functions: fn_replinttobitstring used in this UDF.
* Common Usage:
select dbo.udf_BitS_FromInt(26, 0) as [With leading 0s]
, dbo.udf_BitS_FromInt(26, 1) as [Sans leading 0s]
* Test:
PRINT 'Test 1
' + CASE WHEN '11010' =
dbo.udf_BitS_FromInt (26, 1) THEN 'Worked' ELSE 'ERROR' END
****************************************************************/
System UDFs
398
Part II
Chapter 18: Undocumented System UDFs
AS BEGIN
DECLARE @WorkingVariable varchar(32)
, @PosOfFirst1 int
SELECT @WorkingVariable = fn_replinttobitstring (@INT)
IF @TrimLeadingZerosBIT=1 BEGIN
SET @PosOfFirst1 = CHARINDEX( '1', @WorkingVariable, 1)
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
Part II
Chapter 18: Undocumented System UDFs
399
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.
In the previous section weve seen how the bit fields used by many system functions can be turned into more human-readable strings using
fn_replinttobitstring or the alternative UDFs. fn_replbitstringtoint is
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 system UDF with the syntax:
fn_replbitstringtoint (@Bitstring)
System UDFs
fn_replbitstringtoint
400
Part II
Chapter 18: Undocumented System UDFs
The functions udf_BitS_ToINT, udf_BitS_ToSmallint, and udf_BitS_ToTinyint work better than fn_replbitstringtoint. They each assume
missing leading zeros and return their respective data types. Since this
type of bit string is usually used as a way to represent bit fields as opposed
to numbers, leading ones are not propagated to the left to form negative
numbers. Each of these three functions has a slightly different
implementation.
udf_BitS_ToINT, which is not listed in this book, pads the input on the
left with zeros and then passes the result to fn_replbitstringtoint to
complete the conversion. This query shows how it handles leading zeros
in a way different from fn_replbitstringtoint:
-- try
SELECT
,
,
out udf_BitS_ToINT
dbo.udf_BitS_ToINT
dbo.udf_BitS_ToINT
dbo.udf_BitS_ToINT
GO
(Results)
26
-1
Most positive number 26 without leading 0s fn_replbitstringtoint
------- ------- ------------------- --------------------- --------------------26
-1
2147483647
26
-805306368
Part II
Chapter 18: Undocumented System UDFs
401
SELECT @number = 0
,@sWorking = RIGHT ('0000000000000000' + @BitString, 16)
IF
IF
IF
IF
IF
IF
IF
IF
IF
IF
IF
IF
IF
IF
IF
IF
(SUBSTRING(@sWorking, 1,1)
(SUBSTRING(@sWorking, 2,1)
(SUBSTRING(@sWorking, 3,1)
(SUBSTRING(@sWorking, 4,1)
(SUBSTRING(@sWorking, 5,1)
(SUBSTRING(@sWorking, 6,1)
(SUBSTRING(@sWorking, 7,1)
(SUBSTRING(@sWorking, 8,1)
(SUBSTRING(@sWorking, 9,1)
(SUBSTRING(@sWorking,10,1)
(SUBSTRING(@sWorking,11,1)
(SUBSTRING(@sWorking,12,1)
(SUBSTRING(@sWorking,13,1)
(SUBSTRING(@sWorking,14,1)
(SUBSTRING(@sWorking,15,1)
(SUBSTRING(@sWorking,16,1)
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
'1')
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
SELECT
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
@number
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
0xFFFF8000
0x00004000
0x00002000
0x00001000
0x00000800
0x00000400
0x00000200
0x00000100
0x00000080
0x00000040
0x00000020
0x00000010
0x00000008
0x00000004
0x00000002
0x00000001
System UDFs
402
Part II
Chapter 18: Undocumented System UDFs
out udf_BitS_ToSmallint
dbo.udf_BitS_ToSmallint
dbo.udf_BitS_ToSmallint
dbo.udf_BitS_ToSmallint
dbo.udf_BitS_ToSmallint
GO
(Results)
26
26 sans Leading 0s -1
Most positive number Most negative number
------ ------------------ ------ ------------------- -------------------26
26
-1
32767
-32768
/*
* Converts a bit string of up to 8 one and zero characters into
* the corresponding 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.
*
* Related Functions: the undocumented built-in function
* fn_replbitstringtoint is similar but it works on an int and
* does not handle missing leading zeros the same way.
*
* Example:
select dbo.udf_Bits_ToTinyint('0101') -- should return 5
*
* Test:
PRINT 'Test 1 5
' + CASE WHEN 5=
dbo.udf_Bits_ToTinyint ('0101') THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 2 bigest TINYINT ' + CASE WHEN 255=
dbo.udf_BitS_ToTinyint ('11111111')
THEN 'Worked' ELSE 'ERROR' END
****************************************************************/
AS BEGIN
Part II
Chapter 18: Undocumented System UDFs
403
SELECT @number = 0
,@sWorking = RIGHT ('00000000' + @BitString, 8)
SELECT @Number
CASE
| CASE
| CASE
| CASE
| CASE
| CASE
| CASE
| CASE
=
WHEN
WHEN
WHEN
WHEN
WHEN
WHEN
WHEN
WHEN
substring(@sWorking,
substring(@sWorking,
substring(@sWorking,
substring(@sWorking,
substring(@sWorking,
substring(@sWorking,
substring(@sWorking,
substring(@sWorking,
1,1)
2,1)
3,1)
4,1)
5,1)
6,1)
7,1)
8,1)
=
=
=
=
=
=
=
=
'1'
'1'
'1'
'1'
'1'
'1'
'1'
'1'
THEN
THEN
THEN
THEN
THEN
THEN
THEN
THEN
0x80
0x40
0x20
0x10
0x08
0x04
0x02
0x01
ELSE
ELSE
ELSE
ELSE
ELSE
ELSE
ELSE
ELSE
0
0
0
0
0
0
0
0
END
END
END
END
END
END
END
END
-- try
SELECT
,
,
,
,
out udf_BitS_ToTinyint
dbo.udf_BitS_ToTinyint
dbo.udf_BitS_ToTinyint
dbo.udf_BitS_ToTinyint
dbo.udf_BitS_ToTinyint
dbo.udf_BitS_ToTinyint
GO
(Results)
26 26 sans leading 0s 255 127 No negative numbers
---- ------------------ ---- ---- ------------------26 26
255 127 128
The three UDFs created in this section all work better than fn_replbitstringtoint, 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)
System UDFs
404
Part II
Chapter 18: Undocumented System UDFs
The return value is also an nvarchar(4000), which contains @String surrounded by quotes and the leading N that indicates a Unicode string
literal.
This query illustrates how fn_replmakestringliteral works:
-- Turn some characters into string literals
SELECT fn_replmakestringliteral (43) as [43]
, fn_replmakestringliteral ('abcd') as [abcd]
, fn_replmakestringliteral (N'ABCD') as [Unicode ABCD]
GO
(Results)
43
abcd
Unicode ABCD
---------- ----------- --------------N'43'
N'abcd'
N'ABCD'
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.
Listing 18.5: udf_SQL_VariantToStringConstant
CREATE FUNCTION udf_SQL_VariantToStringConstant (
@InVal sql_variant -- input variant
) RETURNS nvarchar(4000) -- String constant
/*
* 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.
*
* Example:
select dbo.udf_SQL_VariantToStringConstant
(CONVERT(datetime, '1918-11-11 11:11:11', 120))
* Test:
PRINT 'Test 1 date
' + case when '''1918-11-11 11:11:00.000'''
= dbo.udf_SQL_VariantToStringConstant
(CONVERT(datetime, '1918-11-11 11:11', 120))
THEN 'Worked' else 'Error' end
PRINT 'Test 2 Nstring ' + CASE WHEN 'N''ABCD''' =
dbo.udf_SQL_VariantToStringConstant (N'ABCD')
THEN 'Worked' else 'Error' end
****************************************************************/
AS BEGIN
DECLARE
,
,
,
@Result as nvarchar(4000)
@BaseType sysname
@Precision int
-- # digits of the numeric base type
@Scale int -- # of digits to the right of the decimal of
-- numeric base btypes
, @TotalBytes int -- Storage consumed
, @Collation sysname -- the collation name of the variant
Part II
Chapter 18: Undocumented System UDFs
405
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:
System UDFs
406
Part II
Chapter 18: Undocumented System UDFs
This comes in handy from time to time. Be careful though: Its not a complete 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 brackets, which is suitable for use as a name in a SQL statement. The syntax of
the call is:
fn_replquotename (@string)
Although its impossible to execute dynamic SQL in a user-defined function, you can use UDFs to construct all or part of a dynamic SQL
statement that will be used elsewhere. fn_replquotename can help.
Part II
Chapter 18: Undocumented System UDFs
407
That help is used in the function udf_View_ColumnList, shown in Listing 18.6. It produces the select list for all the columns in a view. Each
column name is bracketed in case its a keyword, starts with a number,
contains embedded spaces, or contains special characters.
Listing 18.6: udf_View_ColumnList
System UDFs
408
Part II
Chapter 18: Undocumented System UDFs
END
ELSE
SELECT @Result = @Result + ', '
+ CASE WHEN @Prefix is NOT NULL and LEN(@Prefix) > 0
THEN @Prefix + '.'
ELSE ''
END
+ fn_replquotename (@column_name)
FETCH ColumnCursor INTO @view_catalog
, @view_schema, @column_name
END -- of the WHILE LOOP
-- Clean up the cursor
CLOSE ColumnCursor
DEALLOCATE ColumnCursor
RETURN @Result
END
Heres an example that uses the output from udf_View_ColumnList to create a SELECT statement on the view and then dynamically execute the
statement:
-- 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
END
VALUES
CROSS
---------- ---------- ----------
There are two resultsets returned from the batch. The first one is the
SQL statement. The second one is produced by executing the SQL statement 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 quoting the column names gives an error. Try it:
Part II
Chapter 18: Undocumented System UDFs
409
UDFs in master that are owned by dbo. The next one fits that bill.
fn_varbintohexstr
This function can be used to convert almost any data to a hex representation. The syntax for the function is:
master.dbo.fn_varbintohexstr (@Input VarBinary(8000))
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:
System UDFs
410
Part II
Chapter 18: Undocumented System UDFs
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 available. This chapter has shown you two places to find the CREATE FUNCTION
script for any of the undocumented UDFs:
n
Part II
Chapter 18: Undocumented System UDFs
411
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.
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.
414
Part II
Chapter 19: Creating a System UDF
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.
FUNCTION dbo.udf_Tbl_COUNT (
)
/*
* Returns the number of user tables in the database that match
* @table_name_pattern
*
* Example:
select dbo.udf_Tbl_COUNT(NULL) -- count all user tables
select dbo.udf_Tbl_COUNT('Currency%') -- tbls in currency group.
****************************************************************/
AS BEGIN
DECLARE @Result int -- count
SELECT @Result=COUNT(*)
FROM sysobjects
WHERE TYPE='U'
and (@table_name_pattern is NULL
or [name] LIKE @table_name_pattern)
Part II
Chapter 19: Creating a System UDF
415
RETURN @Result
END
Lets test this function. The first test should be performed in the
TSQLUDFS database where the function was created:
-- Move to TSQLUDFS and try udf_Tbl_COUNT
USE TSQLUDFS
GO
SELECT dbo.udf_Tbl_COUNT(default) as [All Tables]
, dbo.udf_Tbl_COUNT('Currency%') [Currency Tables]
GO
(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
in TSQLUDFS:
-- Move to pubs and try udf_Tbl_COUNT
USE pubs
GO
SELECT TSQLUDFS.dbo.udf_Tbl_COUNT(default) as [All Tables]
, TSQLUDFS.dbo.udf_Tbl_COUNT('Currency%') [Currency Tables]
GO
(Results)
All Tables Currency Tables
----------- --------------21
4
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:
-- Get the tables in pubs without using udf_Tbl_COUNT
SELECT COUNT(*) as [All pubs Tables]
, SUM (CASE WHEN name like 'Currency%' THEN 1 ELSE 0 END)
as [Currency Tables in pubs]
FROM sysobjects
WHERE TYPE = 'U'
GO
System UDFs
416
Part II
Chapter 19: Creating a System UDF
(Results)
All pubs Tables Currency Tables in pubs
--------------- ----------------------11
0
Part II
Chapter 19: Creating a System UDF
417
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.
Listing 19.2: fn_tbl_count
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_NULLS ON
GO
CREATE FUNCTION system_function_schema.fn_tbl_count (
@table_name_pattern sysname = NULL -- NULL for all
-- or a pattern that works with the LIKE operator
RETURNS int -- number of user tables fitting the pattern
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:
System UDFs
)
/*
* Returns the number of user tables in the database that match
* @table_name_pattern
*
* Example:
select dbo.fn_tbl_count(NULL) -- count all user tables
select dbo.fn_tbl_count('Currency%') -- tbls in currency group.
****************************************************************/
AS BEGIN
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:
-- Use the new system UDF
SELECT fn_tbl_count (null) [All Tables]
, fn_tbl_count ('Currency%') [Currency Tables]
GO
(Results)
All Tables Currency Tables
----------- --------------10
0
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:
-- Try fn_tbl_count in TSQLUDFS
USE TSQLUDFS
GO
SELECT fn_tbl_count (null) [All Tables]
, fn_tbl_count ('Currency%') [Currency Tables]
GO
(Results)
All Tables Currency Tables
----------- --------------21
4
Part II
Chapter 19: Creating a System UDF
419
(Results)
All Tables Currency Tables
----------- --------------11
0
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 behavior can make all the difference in the world.
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 referencing them, makes system UDFs special.
The third major difference, the ability to run reserved SQL syntax,
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 remember 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 projects. I hope youll find a few functions that you can use and you have
learned the techniques that you need to create your own.
System UDFs
Summary
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 deterministic. 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
ABS
ACOS
APP_NAME
ASCII
ASIN
ATAN
ATN2
AVG
BINARY_CHECKSUM
CAST
CEILING
CHAR
CHARINDEX
Comment
421
422
Appendix A: Deterministic and Nondeterministic Functions
Status
Function Name
Comment
CHECKSUM
CHECKSUM_AGG
COALESCE
COL_LENGTH
COL_NAME
COLUMNPROPERTY
@@CONNECTIONS
CONVERT
COS
COT
COUNT
COUNT_BIG
@@CPU_BUSY
CURRENT_TIMESTAMP
CURRENT_USER
@@CURSOR_ROWS
CURSOR_STATUS
DATABASEPROPERTY
DATABASEPROPERTYEX
DATALENGTH
DATEADD
DATEDIFF
@@DATEFIRST
DATENAME
DAY
DB_ID
DB_NAME
@@DBTS
DEGREES
DIFFERENCE
@@ERROR
EXP
@@FETCH_STATUS
423
Appendix A: Deterministic and Nondeterministic Functions
Function Name
FILE_ID
FILE_NAME
FILEGROUP_ID
FILEGROUP_NAME
FILEGROUPPROPERTY
FILEPROPERTY
FLOOR
fn_getsql
fn_helpcollations
fn_listextendedproperty
fn_servershareddrives
fn_trace_geteventinfo
fn_trace_getfilterinfo
fn_trace_getinfo
fn_trace_gettable
fn_virtualfilestats
fn_virtualservernodes
FORMATMESSAGE
FULLTEXTCATALOGPROPERTY
FULLTEXTSERVICEPROPERTY
GETANSINULL
GETDATE
GETUTCDATE
GROUPING
HAS_DBACCESS
HOST_ID
HOST_NAME
IDENT_INCR TEXTPTR
IDENT_SEED
IDENTITY
@@IDENTITY
@@IDLE
INDEX_COL
INDEXKEY_PROPERTY
INDEXPROPERTY
@@IO_BUSY
IS_MEMBER
IS_SRVROLEMEMBER
Comment
Appendixes
Status
424
Appendix A: Deterministic and Nondeterministic Functions
Status
Function Name
Comment
ISDATE
ISNULL
ISNUMERIC
@@LANGID
@@LANGUAGE
LEFT
LEN
@@LOCK_TIMEOUT
LOG
LOG10
LOWER
LTRIM
MAX
@@MAX_CONNECTIONS
@@MAX_PRECISION
MIN
MONTH
NCHAR
@@NESTLEVEL
NEWID
NULLIF
OBJECT_ID
OBJECT_NAME
OBJECTPROPERTY
@@OPTIONS
@@PACK_RECEIVED
@@PACK_SENT
@@PACKET_ERRORS
PARSENAME
PATINDEX
PERMISSIONS
POWER
@@PROCID
QUOTENAME
RADIANS
RAND
@@REMSERVER
425
Appendix A: Deterministic and Nondeterministic Functions
Function Name
REPLACE
REPLICATE
REVERSE
RIGHT
ROUND
@@ROWCOUNT
RTRIM
@@SERVERNAME
@@SERVICENAME
SESSION_USER
SIGN
SIN
SOUNDEX
SPACE
@@SPID
SQL_VARIANT_PROPERTY
SQRT
SQUARE
STATS_DATE
STDEV
STDEVP
STR
STUFF
SUBSTRING
SUM
SUSER_SID
SUSER_SNAME
SYSTEM_USER
TAN
@@TEXTSIZE
TEXTVALID
@@TIMETICKS
@@TOTAL_ERRORS
@@TOTAL_READ
@@TOTAL_WRITE
@@TRANCOUNT
TYPEPROPERTY
UNICODE
UPPER
USER
Comment
Appendixes
Status
426
Appendix A: Deterministic and Nondeterministic Functions
Status
Function Name
USER_ID
USER_NAME
VAR
VARP
@@VERSION
YEAR
Comment
Appendix B
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 function keys instead.
Table B.1: Debugging icons and keyboard shortcuts
Icon
Keyboard Shortcut
Function
F5 or Ctrl+E
GO
F9
Toggle Breakpoint
Ctrl+Shift+F9
F11
Step Into
F10
Step Over
Shift+F11
Step Out
Shift+F10
Run to Cursor
Ctrl+Shift+F5
Restart Debugging
Shift+F5
Stop Debugging
Auto Rollback
F1
Help
427
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
429
430
Appendix C: Implementation Problems in SQL Server 2000 Involving UDFs
Running sp_recompile on a UDF doesnt force the recompile to happen. I realize that there are no statistics on UDFs, but there might be
a reason to force a recompile. It doesnt look like the UDF is forced
out of the cache.
User-Defined
Functions
431
168
142
159
138
TABLE
INLINE
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
TABLE
INLINE
INLINE
udf_BBTeam_AllPlayers
udf_BBTeams_LeagueTAB
udf_Bit_Int_NthBIT
udf_BitS_FromInt
udf_BitS_FromSmallint
udf_BitS_FromTinyint
udf_BitS_ToInt
udf_BitS_ToSmallint
udf_BitS_ToTinyint
udf_Category_BigCategoryProductsTAB
udf_Category_ProductCountTAB
udf_Category_ProductsTAB
402
401
397
113
SCALAR
getcommaname
Description
417
SCALAR
fn_tbl_count
Returns a table of product information for all products in the named category.
Returns a table of product information for all products in all categories with at
least @MinProducts products.
Converts a bit string of up to eight one and zero characters into the corresponding 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.
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-created system UDF to return the number of user tables in the database
in which it is run.
Page
Type
Function
432
User-Defined Functions
Does the same job as DATEPART but takes a character string as the datepart
instead of having the datepart in the script.
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.
Returns a smalldatetime giving the Year, Month, and Day of the month.
286
284
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
TABLE
SCALAR
SCALAR
SCALAR
udf_Currency_StatusName
udf_Currency_XlateNearDate
udf_Currency_XlateOnDate
udf_DT_2Julian
udf_DT_Age
udf_DT_CurrTime
udf_DT_DaysTAB
udf_DT_dynamicDATEPART
udf_DT_FileNameFmt
udf_DT_FromYMD
87
Converts from one currency to another on a specific date. The date must be
in the database or the result is NULL.
SCALAR
udf_Currency_RateOnDate
Returns one row for each day that falls in a date range. Each date is the SOD.
Converts from one currency to another on a specific date. The date must be
in the database or the result is NULL.
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.
288
SCALAR
udf_Currency_DateStatus
Description
Page
Type
Function
User-Defined Functions
433
Returns a datetime for the Nth occurrence of a day in the month, such as the
third Tuesday. Returns NULL if the date doesnt exist, such as the fifth Monday
in June, 2002. Sensitive to setting of DATEFIRST. Assumes default value of 7.
Seconds since a time.
Returns a smalldatetime with just the date portion of a datetime.
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.
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.
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.
329
326
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
TABLE
TABLE
udf_DT_NthDayInMon
udf_DT_SecSince
udf_DT_SOD
udf_DT_WeekdayNext
udf_DT_WeekdaysBtwn
udf_EP_AllTableLevel2EPsTAB
udf_EP_AllUsersEPsTAB
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. Useful 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.
Returns a table of months that are between two dates including both end
points. Each row has several ways to represent the month. The result table is
intended to be used in reports that are reporting on activity in all months in a
range.
12
TABLE
udf_DT_MonthsTAB
Description
Page
Type
Function
434
User-Defined Functions
30
102
104
SCALAR
SCALAR
SCALAR
INLINE
TABLE
SCALAR
SCALAR
SCALAR
INLINE
udf_Example_OAhello
udf_Example_Palindrome
udf_Example_Runtime_Error
udf_Example_Runtime_Error_Inline
udf_Example_Runtime_Error_MultiStatement
udf_Example_TABLEmanipulation
udf_Example_Trc_EventListBadAttempt
udf_Example_User_Event_Attempt
udf_Exchange_MembersList
130
79
35
225
TABLE
udf_Example_Multistatement_With
ComputedColumn
Description
Returns a table with a list of all brokers who are members of the exchange
@ExchangeCD.
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.
Example UDF that returns a table with a computed column. Use sp_help to
see that SQL Server considers PRODUCT a computed column.
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 expression. Run sp_help to see that PRODUCT isnt considered computed by SQL
Server.
udf_Example_Inline_WithComputedColumn
Page
Type
INLINE
Function
User-Defined Functions
435
189
192
SCALAR
INLINE
INLINE
INLINE
SCALAR
TABLE
SCALAR
INLINE
SCALAR
TABLE
udf_Func_COUNT
udf_Func_InfoTAB
udf_Func_ParmsTAB
udf_Func_TableCols
udf_Func_Type
udf_Func_UndocSystemUDFtext
udf_Instance_EditionName
udf_Instance_InfoTAB
udf_Instance_UptimeSEC
udf_Lst_Stooges
345
391
190
INLINE
udf_Func_ColumnsTAB
Returns a table of the Stooges. This function illustrates what happens when
you try to write a book. After a while, youve got to do anything but really
work on the book. Serious research into the Stooges is an alternative.
Returns a single column table. Each row is the declaration of a column in the
output of a UDF. It works for both inline and multistatement UDFs but is most
useful as an aid to converting an inline UDF to a multistatement UDF.
Returns a table of information about the parameters used to call any type of
UDF. This includes the return type that is in Position=0.
Returns a table of functions that have been created without the proper
QUOTED_IDENTIFIER and ANSI_NULLS settings. Both of these should be
ON. These UDFs may cause problems in the future, particularly with INDEXES
on computed columns.
INLINE
Description
udf_Func_BadUserOptionsTAB
Page
Type
Function
436
User-Defined Functions
5
114
SCALAR
SCALAR
SCALAR
udf_Name_Full
udf_Name_FullWithComma
udf_Name_SuffixCheckBIT
106
217
220
194
SCALAR
SCALAR
SCALAR
SCALAR
INLINE
udf_Num_LOGN
udf_Num_RandInt
udf_OA_ErrorInfo
udf_OA_LogError
udf_Object_SearchTAB
Searches the text of SQL objects for the string @SearchFor. Returns the object
type and name as a table.
Creates an error message about an OA error and logs it to the SQL log and
NT event log.
The logarithm to the base @Base of @n. Returns NULL for any invalid input
instead of raising an error.
33
SCALAR
udf_Num_IsPrime
158
TABLE
SCALAR
udf_Num_FpEqual
Returns 1 when the name is one of the common suffix names such as Jr., Sr.,
II, III, IV, MD, etc. Trailing periods are ignored.
Concatenates the parts of a person's name with the proper amount of spaces.
Description
udf_Num_FactorialTAB
126
Page
Type
Function
User-Defined Functions
437
349
352
350
348
SCALAR
INLINE
SCALAR
INLINE
INLINE
INLINE
INLINE
TABLE
INLINE
INLINE
udf_Order_Amount
udf_Order_RandomRow
udf_Paging_ProductByUnits_Forward
udf_Paging_ProductByUnits_REVERSE
udf_Perf_FS_ByDbTAB
udf_Perf_FS_ByDriveTAB
udf_Perf_FS_ByPhysicalFileTAB
udf_Perf_FS_DBTotalsTAB
udf_Perf_FS_InstanceTAB
udf_Proc_WithRecompile
152
149
53
SCALAR
udf_Object_Size
Page
Type
Function
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.
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.
Returns a table of total statistics for one particular database by summing the
statistics for all of its files.
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.
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 master..sysaltfiles, which has the physical file name needed. Warning: Drive letters
do not always correspond to physical disk drives.
Returns the bytes taken by the object definition in syscomments. The definition
may be split over multiple rows. These objects types have definitions in
syscomments: CHECK constraints, DEFAULT constraints, user-defined functions, stored procedures, triggers, and views.
Description
438
User-Defined Functions
344
INLINE
SCALAR
SCALAR
INLINE
SCALAR
SCALAR
SCALAR
udf_SQL_DefaultsTAB
udf_SQL_LogMsgBIT
udf_SQL_StartDT
udf_SQL_UserMessagesTAB
udf_SQL_VariantToDatatypeName
udf_SQL_VariantToStringConstant
udf_SQL_VariantToStringConstantLtd
404
203
SCALAR
udf_SQL_DataTypeString
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 truncation is performed to let the caller know that it has happened. This may
make the constant unsuitable for use as SQL script text.
Returns the date/time that the SQL Server instance was started.
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.
Returns a table of all defaults that exist on all user tables in the database.
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.
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.
95
TABLE
udf_Session_OptionsTAB
Description
Page
Type
Function
User-Defined Functions
439
328
414
325
INLINE
INLINE
SCALAR
INLINE
INLINE
INLINE
udf_Tbl_ColDescriptionsTAB
udf_Tbl_ColumnIndexableTAB
udf_Tbl_COUNT
udf_Tbl_DescriptionsTAB
udf_Tbl_InfoTAB
udf_Tbl_MissingDescrTAB
108
SCALAR
udf_Test_ConditionalDivideBy0
333
INLINE
udf_Tbl_RptW
INLINE
SCALAR
udf_Tbl_RowCOUNT
Returns the description extended property for all user tables in the database.
Returns the description extended property for all columns of user tables in the
database. A specific owner or table can be named. If @TableOwner is NULL,
extended properties for all owners is returned. If @TableName is NULL, columns for all tables are returned.
udf_Tbl_NotOwnedByDBOTAB
332
Returns a table of the columns in tables whose name matches the patterns in
@tbl_name_pattern and @col_name_pattern. Includes the status of the columns as indexable, deterministic, and precise.
173
TABLE
udf_Tax_TaxabilityCD
221
SCALAR
udf_SYS_DriveExistsBIT
Description
Page
Type
Function
440
User-Defined Functions
170
171
SCALAR
INLINE
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
udf_Test_RenamedToNewName
udf_Title_AuthorsTAB
udf_Titles_AuthorList
udf_Titles_AuthorList2
udf_Trc_ColumnCount
udf_Trc_ColumnList
udf_Trc_ColumnName
udf_Trc_ComparisonOp
udf_Trc_EventCount
udf_Trc_EventList
375
374
Example UDF to show that sp_rename doesnt work correctly for user-defined
functions.
93
SCALAR
udf_Test_Quoted_Identifier_On
Returns a comma separated list of the last name of all authors for a title. Illustrates a technique for an aggregate concatenation.
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.
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.
93
udf_Test_Quoted_Identifier_Off
Description
Page
Type
SCALAR
Function
User-Defined Functions
441
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.
Table listing user information for processes running SQL Profiler.
379
381
SCALAR
SCALAR
INLINE
INLINE
SCALAR
INLINE
udf_Trc_FilterClause
udf_Trc_FilterExpression
udf_Trc_InfoExTAB
udf_Trc_InfoTAB
udf_Trc_LogicalOp
udf_Trc_ProfilerUsersTAB
363
Returns a table of information about a trace. These are the original arguments to sp_trace_create. The status field is broken down to four individual
fields.
Returns a table of information about a trace. These are the original arguments 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.
Translates a SQL Trace EventClass into its descriptive name. Used when viewing trace tables or when converting a trace file with fn_trace_gettable.
SCALAR
udf_Trc_EventName
371
SCALAR
Description
udf_Trc_EventListCursorBased
Page
Type
Function
442
User-Defined Functions
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.
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
udf_Trig_TANH
udf_Txt_CharIndexRev
udf_Txt_FixLen
udf_Txt_FixLenR
udf_Txt_FmtInt
udf_Txt_FullLen
udf_Txt_IsPalindrome
udf_Txt_RemoveChars
18
SCALAR
udf_Trc_SetStatusMSG
Description
384
TABLE
udf_Trc_RPT
Returns a report of information about a trace. These are the original arguments 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.
Page
Type
Function
User-Defined Functions
443
Spreads the input string out by placing characters between each character of
the input.
Wraps text that is separated by a set of delimiters. The wrap allows the first
line and subsequent lines to be padded for alignment with other text.
Converts a distance measurement from any unit to any other unit.
260
269
271
258
263
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
SCALAR
udf_TxtN_SpreadChars
udf_TxtN_WrapDelimiters
udf_Unit_CONVERT_Distance
udf_Unit_EqualFpBIT
udf_Unit_EqualNumBIT
udf_Unit_Km2Distance
udf_Unit_Km2mi
udf_Unit_KmFromDistance
udf_Unit_lb2kg
udf_Unit_mi2Km
udf_Unit_mi2m
256
267
208
SCALAR
udf_Txt_Sprintf
Returns a table of strings that have been split by a delimiter. Similar to the
Visual Basic (or VBA) SPLIT function. The strings are trimmed before being
returned. NULL items are not returned, so if there are multiple separators
between items, only the non-NULL items are returned. Space is not a valid
delimiter.
165
TABLE
udf_Txt_SplitTAB
Description
Page
Type
Function
444
User-Defined Functions
SCALAR
udf_View_ColumnList
407
SCALAR
udf_Unit_RoundingPrecision
254
SCALAR
udf_Unit_Rounding4Factor
Description
Page
Type
Function
User-Defined Functions
445
.NET, 17
@@DATEFIRST, 84-85, 234
@@ERROR, 100, 103-105, 296
@@ROWCOUNT, 100
@@SPID, 305-306
@@Total_Read, 346-347
@@Total_Write, 346-347
A
Access 2002, 64
ADO, 60
algorithm, 123
ALTER FUNCTION, 25-26
permission, 25-27, 133, 156
ALTER TABLE, 54
AND operator short-circuit, 47-48, 207
ANSI_NULLS, 60, 92, 95
ANSI_PADDING, 60, 92
ANSI_WARNINGS, 60, 92
MAX function, 363
ARITHABORT, 60-61, 92
ARITHIGNORE, 106
AS BEGIN, 29
ASP, 17, 146-148
ASP.NET, 146-148
attribution, 120
B
BASIC, 213
BEGIN TRAN statement restrictions, 88
bigint, 23
binary, 23
bit, 23, 269
BitS, 125
blackbox trace, 362
Boolean, 269
C
C, 197
C++, 129, 197
C++ WINERORR.H file, 215
C2 security, 355
CacheHit, 75
Carberry, Josiah, 320-321
CASE expression used in pivoting, 363-364
Index
Index
CAST, 251-252, 272
determinism, 421
CGI, 146
char data type, 23
CHARINDEX, 236, 239
CHECK clause, 157
CHECK constraint, 32, 44, 52-56, 100
COALESCE, 282
COBOL, 213
code reuse, 16
code tables, replacing with UDF, 172
collations, 299-300
COLUMNPROPERTY, 58, 183, 186-187
IsDeterministic, 429
IsPrimaryKey, 429
COM, 223
COM Interop, 223
COMMIT TRAN statement restrictions, 88
component design, 17
COMPUTE, 135, 274
COMPUTE BY, 135
computed column, 44, 52-53, 56-58, 60
CONCAT_NULL_YIELDS_NULL, 60, 92
CONSTRAINT clause of CREATE TABLE,
160-161
CONSTRAINT_TABLE_USAGE
INFORMATION_SCHEMA view, 184
CONVERT, 251, 252, 272
determinism, 422
convert.exe, 249
copyright, 122
CREATE FUNCTION, 4-9, 24
permission
inline, 134
multistatement, 156
scalar, 25-27
syntax, 28
CREATE INDEX, 78
CREATE RULE, 282
CREATE TABLE, 32
CREATE UNIQUE CLUSTERED INDEX,
43
CREATE VIEW, 86
cryptography, 227
Crystal Reports, 250
447
448
Index
CurrencyRateTypeCD, 278
CurrencyXchange, 278-279
cursor, 162-168
D
DATA_TYPE column of INFORMATION_
SCHEMA.ROUTINES, 184
DATEDIFF, 296
DATEFIRST, 91, 234 see also
@@DATEFIRST
DATENAME, 59, 84
DATEPART, 59, 84, 91, 230
datetime, 23
db_ddladmin, 25-27
DB_ID, 339
DB_Name, 339
db_owner, 27
DBCC, 78, 90
DBCC INPUTBUFFER, 305-306, 308
DBCC PINTABLE, 241, 246
DBCC TRACEOFF(2861), 307
DBCC TRACEON(2861), 307
DBCC UNPINTABLE, 265
DBCC USEROPTIONS, 97
DDL, 24, 31-32, 44
DEBUG_udf_DT_NthDayInMon, 65-69
debugger
Callstack, 68
Globals window, 68
Locals window, 69
debugging UDFs, 65-70
decimal, 23
DECLARE, 24, 31
DEFAULT, 148
DELETE, 10, 24, 50, 88, 133
delimited text, 165
DENY statement, 27, 45
deterministic functions, 83, 87, 421
development time, 291
DISTINCT, 141, 375-376
DML, 24, 31-32, 44, 50
documentation, 113
domain, 125, 129
domain error, 106
double-dash comment, 116
DriveExists, 212
DROP FUNCTION, 25-26
permission
inline, 133
multistatement, 156
scalar, 25-27
E
ENCRYPTION, 29, 36, 118
Enterprise Manager, 80-81
generate SQL scripts, 80
server logs, 111, 200
SQL-DMO, 196
use of MS_Description, 316-317
equivalent template, 238
error handling, 99-112
Event Viewer, 201-202
EventRPC, 305
example, 120
Excel, 251
exclusive lock, 310
EXEC, 34, 36, 51-52, 297
permission, 4, 10, 44-45
restrictions, 88
ExecIsQuotedIdentOn, 95
EXECUTE statement, see EXEC
extended stored procedure, 36, 197-228
restrictions, 88
F
FILE_ID, 340, 350
FILE_NAME, 340, 250
filegroup, 32
FileSystemObject, 211-212, 214
fillfactor, 32
float, 23, 252
fn_chariswhitespace, 388-389, 392-393
fn_dblog, 389, 394-396
fn_generateparameterpattern, 389
fn_get_sql, 296, 300, 301, 307-311
fn_getpersistedservernamecasevariation,
389
fn_helpcollations, 298-302
fn_isreplmergeagent, 389
fn_listextendedproperty, 300, 313-335, 389
NULL arguments, 320-324
output columns, 319-320
syntax, 317
fn_MSFullText, 389
fn_MSgensqescstr, 389
fn_MSsharedversion, 389
fn_mssharedversion, 396
fn_removeparameterwithargument, 389
449
fn_repladjustcolumnmap, 389
fn_replbitstringtoint, 389-400
fn_replcomposepublicationsnapshotfolder,
389
fn_replgenerateshorterfilenameprefix, 389
fn_replgetagentcommandlinefromjobid, 389
fn_replgetbinary8lodword, 389
fn_replinttobitstring, 389, 396-397
fn_replmakestringliteral, 389, 403
fn_replprepadbinary8, 389
fn_replquotename, 389, 406-409
fn_replrotr, 389
fn_repltrimleadingzerosinhexstr, 389
fn_repluniquename, 389
fn_serverid, 389-390, 392
fn_servershareddrives, 300, 304, 389
fn_skipparameterargument, 389
fn_sqlvarbasetostr, 389
fn_tbl_count, 416-418
fn_trace_*, 300, 355-386
fn_trace_geteventinfo, 300, 356, 372-383,
389
fn_trace_getinfo, 300, 356, 358, 360-363, 389
fn_trace_gettable, 300, 356, 370-372, 389
fn_updateparameterwithargument, 389
fn_varbintohexstr, 389, 409-410
fn_varbintohexsubstring, 389
fn_virtualfilestats, 300, 337-354, 389
fn_virtualservernodes, 300, 304, 389
FOR BROWSE, 135
FOR XML, 135
foreign key constraint, 157
formatting, 113
FORTRAN, 17, 213
FROM clause multistatement, 155
function body, 29, 31
Function_Assist_GETDATE, 86, 345
G
getcommaname, 113
GETDATE, 59, 84-85, 86
GOTO, 31, 33, 212
GRANT, 4, 10, 27-28
inline UDF, 142
multistatement, 161
group, 125
GROUP BY clause, 141
H
HAVING, 141
header, 118-123
hints, 116
history, 122
HRESULT, 215
Hungarian notation, 129
I
I/O activity, 338, 344, 351
IDENTITY column, 157, 176, 179
IF ELSE statement, 24, 31, 33
image, 23
Imperial system of measures, 248
Index Wizard, 370
indexed views, 39-43
indexes on computed columns, 52-53, 58-61
INFORMATION_SCHEMA, 175, 182-183,
187
.CONSTRAINT_TABLE_USAGE, 184
.KEY_COLUMN_USAGE, 184
.PARAMETERS, 184, 185, 192
.ROUTINE_COLUMNS, 184, 185
.ROUTINES, 180, 184, 388
.TABLES, 329-330
.VIEW_COLUMN_USAGE, 407
inline UDF, 9-11, 83, 133-153
INSERT, 24
permission, 10, 133
restrictions, 88
VALUES clause, 50
instance, 125, 344
instdst.sql, 390
int, 23
Intellisense, 123-124
INTO, 135
ISDATE determinism, 424
ISO 4271, 279
IsPrimaryKey COLUMNPROPERTY, 187
ISQL, 63-64, 80
IsQuotedIdentOn OBJECTPROPERTY, 187
J
JOIN, 49-50, 135
Jscript, 212
JSP, 146
K
KEY_COLUMN_USAGE
INFORMATION_SCHEMA view, 184
L
LAZY WRITER, 344
LEFT OUTER JOIN, 50
LOG WRITER, 344
Index
Index
450
Index
Oracle, 183
ORDER BY, 7, 133, 135
column numbers, 48
inline UDF, 140-141
paging, 148
ORDINAL_POSITION column, 185-186
OSQL, 63-64, 80
owner name, 28, 125, 127, 138
P
paging web pages, 146
parameters, 122
PARAMETERS INFORMATION_SCHEMA
view, 184, 185-186
performance, 17, 21, 39, 250
PERMISSIONS function, 186-187
PHP, 146
physical file name, 341
pivoting data, 363-364
PL/1, 213
pms_analysis_section, 262
portability, 17, 21
precision, 250
prefix udf_, 124, 126
PRIMARY KEY constraint, 32
PRINT, 51-52, 90, 99-100
procsyst.dql, 390
PRODUCE_ROWSET, 367
programmer productivity, 291
Q
Query Analyzer, 37-38, 308-309
debugger, 67-70, 427
extended properties, 316
Object Browser, 67
Options menu, 180
templates, 70-74, 238
testing, 230
QUOTED_IDENTIFIER, 60, 92, 94
R
RAID, 352
RAISEERROR, 90, 99-100, 281
RAND determinism, 424
range scan, 39
READ UNCOMMITTED, 310
real, 23, 252
RECONFIGURE WITH OVERRIDE, 416,
418
REFERENCES permission, 44-45, 133, 156,
162
451
Index
Index
452
Index
T
TABLE function, 155
TABLE variable, 31-32, 34-35, 89, 157
table-valued, 155
tempdb, 157
template
cursor, 163-164
equivalent, 238
header, 118-123
inline, 136-137
multistatement, 161-162
scalar, 70-74
temporary tables, restrictions on, 89
test script, 121
TEST_udf_DT_WeekdayNext, 232
testing UDFs, 229
text, 23
TF type code from sysobjects, 155
third normal form, 56
three-part name, 6, 298
tinyint, 23
TOP clause, 133, 140-141, 147
TQL, 118-119
trace columns, 76
trace events, adding, 75
trace filters, 76
TRACE_FILE_ROLLOVER, 361-362, 366
TRACE_PRODUCE_BLACKBOX, 362
TRACE_PRODUCE_ROWSET, 361-362
transitive dependency, 56
triggers, 157
T-SQL Debugger, see debugger
T-SQL UDF of the Week Newsletter, 410,
455
TSQLUDFVB project, 224, 227
two-part name, 6, 135
U
udf_BBTeam_AllPlayers, 168-169
udf_BBTeams_LeagueTAB, 142-145
udf_Bit_Int_NthBIT, 399
udf_BitS_FromInt, 216, 397-399
udf_BitS_FromSmallInt, 397
udf_BitS_FromTinyInt, 397
udf_BitS_ToINT, 400
udf_BitS_ToSmallint, 400-401
udf_BitS_ToTinyInt 400, 402-403
udf_Category_BigCategoryProductsTAB,
159-160
udf_Category_ProductCountTAB, 137,
139-140, 160-161
453
udf_Category_ProductsTAB, 160
udf_CONVERT_Distance, 260-262
udf_Currency_DateStatus, 288-291
udf_Currency_XlateNearDate, 284-287, 289
udf_Currency_XlateOnDate, 283-284
udf_DT_2Julian, 73
udf_DT_Age, 76, 185
udf_DT_CurrTime, 65, 86-87
udf_DT_dynamicDATEPART, 230-231
udf_DT_MonthsTAB, 12-16, 49, 158
udf_DT_NthDayInMon, 44-49, 51, 54, 57-59,
65, 68, 422
udf_DT_SecSince, 84-85
udf_DT_TimePart, 41-43
udf_DT_WeekdayNext, 232-234
udf_DT_WeekDaysBtwn, 46, 48
udf_EmpTerritoriesTAB, 9-11, 20
udf_EmpTerritoryCOUNT, 7-8, 47
udf_EP_AllTableLevel2EPsTAB, 328-330
udf_EP_AllUsersEPsTAB, 325-327
udf_Example_OAHello, 225, 227
udf_Example_Palindrome, 30
udf_Example_Runtime_Error, 100, 102
udf_Example_Runtime_Error_Inline, 104
udf_Example_Runtime_Error_
Multistatement, 104, 429
udf_Example_TABLEmanipulation, 35
udf_Example_Trc_EventListBadAttempt,
376
udf_Example_user_Event_Attempt, 79
udf_Example_With_Encryption, 36-38
udf_Exchange_MembersList, 130
udf_Func_ColumnsTAB, 178, 190-191
udf_Func_COUNT, 189
udf_Func_InfoTAB, 189
udf_Func_ParmsTAB, 176, 192
udf_Func_Type, 189
udf_Func_UndocSystemUDFtext, 388, 391,
410
udf_Instance_EditionName, 396
udf_Instance_UptimeSEC, 345
udf_Name_Full, 4-6
udf_Name_FullWithComma, 114, 116-117,
121
udf_Name_SuffixCheckBIT, 125-127
udf_Num_FactorialTAB, 158
udf_Num_IsPrime, 33
udf_Num_LOGN, 106
udf_OA_ErrorInfo, 217, 220
udf_OA_LogError, 219-220, 222-223,
225-226
udf_Object_SearchTAB, 193-195
udf_Object_Size, 193-194
udf_Order_Amount, 53-56, 78, 182
udf_Paging_ProductsByUnits_Forward,
149-150
udf_Paging_ProductsByUnits_Reverse,
151-153
udf_Perf_FS_ByDbTAB, 337
udf_Perf_FS_ByDriveTAB, 337, 351-352
udf_Perf_FS_ByPhysicalFileTAB, 337,
350-351
udf_Perf_FS_DBTotalsTAB, 337, 348-349
udf_Perf_FS_Instance, 347
udf_SESSION_OptionsTAB, 75, 95-97
udf_SQL_IsOK4Index, 95
udf_SQL_LogMsgBIT, 110, 203-206, 220
udf_SQL_StartDT, 344-345
udf_SQL_VariantToDataTypeName, 268
udf_SQL_VariantToStringConstLtd, 379,
403-404
udf_SYS_DriveExistsBIT, 221-222
udf_Tax_TaxabilityCD, 173
udf_Tbl_ColDescriptionsTAB, 313, 328, 331
udf_Tbl_ColumnsIndexableTAB, 58-59
udf_Tbl_Count, 414-416
udf_Tbl_DescriptionsTAB, 313, 324, 325
udf_Tbl_InfoTAB, 334
udf_Tbl_MissingDescrTAB, 313, 331-332
udf_Tbl_RptW, 313, 333-334
udf_Test_ConditionalDivideBy0, 108-109
udf_Test_Quoted_Identifier_Off, 93-94
udf_Test_Quoted_Identifier_On, 93-94
udf_Test_SET_DATEFIRST, 91
udf_Titles_AuthorList, 169-172
udf_Titles_AuthorList2, 171-172
udf_Trace_InfoTAB, 356
udf_Trc_ColumnCount, 376
udf_Trc_ColumnList, 376
udf_Trc_ColumnName, 373-374
udf_Trc_ComparisonOp, 377-378, 382
udf_Trc_EventCount, 376
udf_Trc_EventList, 375-376, 381
udf_Trc_EventListCursorBased, 374-375
udf_Trc_EventName, 371
udf_Trc_FilterClause, 379-380
udf_Trc_FilterExpression, 381-383
udf_Trc_InfoExTAB, 262-363, 369, 372, 376,
381, 384
udf_Trc_LogicalOp, 378, 382
udf_Trc_ProfileUsersTAB, 369
udf_Trc_Rpt, 356-357, 372, 384-386, 394
Index
Index
454
Index
udf_Trc_SetStatusMSG, 367
udf_Txt_CharIndexRev, 18-19, 236-239
udf_Txt_SplitTAB, 165-167, 169
udf_Txt_Sprintf, 208
udf_TxtN_WrapDelimiters, 333, 386
udf_Unit_CONVERT_Distance, 260-266
udf_Unit_EqualFpBIT, 269-273
udf_Unit_EqualNumBIT, 271-273
udf_Unit_Km2Distance, 258-260, 266
udf_Unit_Km2mi, 266
udf_Unit_lb2kg, 266-267
udf_Unit_mi2m, 255-257
udf_Unit_Rounding4Factor, 254-256, 268
udf_Unit_RoundingPrecision, 254
udf_View_ColumnList, 407-408
UDT, 29, 129-130, 132, 279
undocumented system UDFs, 387
UNION, 167
UNION ALL, 172
UNIQUE, 32
uniqueidentifier, 23
updatable inline UDF, 133, 141-145
UPDATE, 10, 133
restrictions, 88
SET clause, 50
WHERE clause, 50
U.S. standard system of measures, 248
user-defined type, see UDT
userEvents, 79
usp_Admin_TraceStop, 367-369
usp_CreateExampleNumberString, 240-241
usp_Example_Runtime_Error, 100-101
usp_ExampleSelectWithOwner, 127-128
usp_ExampleSelectWithoutOwner, 127-128
usp_SQL_MyLogRpt, 201-202, 205-206
V
VALUES clause, 50
varbinary, 23, 409
varchar, 23
VB Script, 175, 212, 249
VB .NET, 223, 230
VBA lack of metric conversion functions,
249
Visual Basic, 6, 17, 129, 175, 197, 223, 227,
249
Visual Sourcesafe, 227
Visual Studio, 64, 123-124
Visual Studio .NET, 64, 70
W
WAITFOR, 33
Washington State Department of Transportation, see WSDOT
WHERE, 7, 48, 50, 148-149
WHILE STATEMENT, 24, 31, 33-34
Windows Scripting Host, 212-211, 230
WINERROR.H, 215
WITH, 116
WITH CHECK OPTION, 135, 142-145
WITH ENCRYPTION, see ENCRYPTION
WITH GRANT OPTION, 27, 44
WITH SCHEMABINDING, see
SCHEMABINDING
WITH VIEW_METADATA, 135
WMI, 227
WSDOT, 249
X
XACT_ABORT, 101
xp_cmdshell, 198
xp_deletemail, 198
xp_enumgroups, 198
xp_findnextmsg, 198
xp_gettable_dblib, 198
xp_grantlogin, 198
xp_hello, 199
xp_logevent, 88, 109, 199-207
xp_loginconfig, 199
xp_logininfo, 199
xp_msver, 199
xp_readerrorlog, 201
xp_readmail, 199
xp_revokelogin, 199
xp_sendmail, 199
xp_snmp_getstate, 199
xp_snmp_raisetrap, 199
xp_sprintf, 199, 208-210
xp_sqlmant, 199
xp_sscanf, 199
xp_startmail, 199
xp_stopmail, 199
xp_trace_*, 199
455