Académique Documents
Professionnel Documents
Culture Documents
Problem
Many times people come across the Coalesce function and think that it is just a more powerful
form of ISNULL. In actuality, I have found it to be one of the most useful functions with the
least documentation. In this tip, I will show you the basic use of Coalesce and also some features
you probably never new existed.
Solution
Let's start with the documented use of coalesce. According to MSDN, coalesce returns the first
non-null expression among its arguments.
For example,
will return the current date. It bypasses the first NULL values and returns the first non-null
value.
SELECT Name
FROM HumanResources.Department
WHERE (GroupName = 'Executive General and Administration')
If you want to pivot the data you could run the following command.
PRINT @SQL
EXEC(@SQL)
My personal favorite is being able to kill all the transactions in a database using three lines of
code. If you have ever tried to restore a database and could not obtain exclusive access, you
know how useful this can be.
DECLARE @SQL VARCHAR(8000)
PRINT @SQL --EXEC(@SQL) Replace the print statement with exec to execute
Next Steps
• Whenever I think I may need a cursor, I always try to find a solution using Coalesce first.
• I am sure I just scratched the surface on the many ways this function can be used. Go try
and see what all you can come up with. A little innovative thinking can save several lines
of code.
Lets say we have to return a non-null from more than one column, then we can use COALESCE
function.
SELECT COALESCE(hourly_wage, salary, commission) AS 'Total Salary' FROM wages
In this case,
If hourly_wage is not null and other two columns are null then hourly_wage will be returned.
If hourly_wage, commission are null and salary is not null then salary will be returned.
If commission is non-null and other two columns are null then commission will be returned.
Garth is back with another article. This one talks about building a comma-separated value string
for use in HTML SELECT tags. It's also handy anytime you need to turn multiple records into a
CSV field. It's a little longer and has some HTML but a good read.
I was reading the newsgroups a couple of days ago and came across a solution
posted by Itzik Ben-Gan I thought was really smart. In order for you to understand
why I like it so much I have to give you a little background on the type of
applications I work on. Most of my projects for the past 2.5 years have focused on
developing browser-based applications that access data stored in SQL Server. On
almost all the projects I have worked on there has been at least one Add/Edit screen
that contained a multi-select list box.
For those of you with limited experience working with HTML, I need to explain that the values
selected in a multi-select list box are concatenated in a comma-delimited string. The following
HTML creates a multi-select list box that displays retail categories.
If a user selects more than one option the value associated with RetailCategory is a comma-
delimited string. For example, if the user selects Shoes, Women's Clothes and Toys, the value
associate with RetailCategory is 1, 4, 5. When the user Submits their form I call a stored
procedure to insert the data into the appropriate tables. The comma-delimited string is processed
with a WHILE loop and the individual values are inserted into a dependent table.
Now that we have covered the Add part, let's take a look at what happens when a user wants to
Edit a row. When editing a row, you need to populate the form with the existing values--this
includes making sure all the multi-select list box values that are associated with the row are
shown as selected. To show an option as selected, you use the "selected" attribute. If we were
editing the row associated with the previous example the final HTML would look like the code
shown here.
I say final, because the actual HTML is built on-the-fly using VBScript. To determine which
options are shown as selected, you must return a comma-delimited string to IIS so you can
manipulate it with VBScript. I use the Split function and a For loop to determine which options
should be shown as selected. The following VBScript shows how this is done.
<%
EmpArray = Split(rs("EmployeeList"))
For Each i In EmpArray
If rs2("Emp_UniqueID") = CInt(i) Then
response.write "selected"
End If
Next
%>
The remainder of this article shows the inefficient way I used to build the string along with the
new, efficient way I learned from the newsgroup posting.
Let's create and populate some tables so we have some data to work with. Assume you have a
sales effort management (SEM) system that allows you to track the number of sales calls made
on a potential client. A sales call is not a phone call, but a get together such as lunch or another
type of person-to-person meeting. One of the things the VP of Sales wants to know is how many
of his sales personnel participate in a call. The following tables allow you to track this
information.
A limited number of columns are used in order to make this article easier to digest. The
SalesCallsEmployees table is a junction table (aka associative table) that relates the employees
(sales personnel) to a particular sales call. Let's populate the tables with sample data using these
INSERT statements.
The first sales call (Lunch w/ John Smith) had three employees participate. Using the old
approach, I used the code shown here (inside a stored procedure) to build the comma-delimited
string. The resultset shows the output when the "Lunch w/ John Smith" sales call is edited.
OPEN crs_Employees
FETCH NEXT FROM crs_Employees INTO @Emp_UniqueID
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT @EmployeeList = @EmployeeList+CAST(@Emp_UniqueID AS varchar(5))+ ', '
FETCH NEXT FROM crs_Employees INTO @Emp_UniqueID
END
CLOSE crs_Employees
DEALLOCATE crs_Employees
SELECT @EmployeeList
--Results--
---------
1, 2, 4
This code may look a little complicated, but all it's doing is creating a cursor that holds the
Emp_UniqueID values associated with the sales call and processing it with a WHILE to build the
string. The important thing for you to note is that this approach takes several lines of code and
uses a cursor. In general, cursors are considered evil and should only be used as a last resort.
The new and improved approach can create the same resultset with a single SELECT statement.
The following code shows how it's done.
SELECT @EmployeeList
--Results--
---------
1, 2, 4
Many consider ISNULL()'s readability and common sense naming to be an advantage. While I
will agree that it easier to spell and pronounce, I disagree that its naming is intuitive. In other
languages such as VB/VBA/VBScript, ISNULL() accepts a single input and returns a single
boolean output.
ISNULL() accepts exactly two parameters. If you want to take the first non-NULL among more
than two values, you will need to nest your ISNULL() statements. COALESCE(), on the other
hand, can take multiple inputs:
-- yields:
Server: Msg 174, Level 15, State 1, Line 1
The isnull function requires 2 arguments.
-- yields:
----
foo
In order to make this work with ISNULL(), you would have to say:
-- yields:
-----
12345
-- yields:
---------
123456789
This gets more complicated if you start mixing incompatible datatypes, e.g.:
-- yields:
-----
foo
-- yields:
Server: Msg 8111, Level 16, State 2, Line 1
Cannot define PRIMARY KEY constraint on nullable column in table
'Try'.
Server: Msg 1750, Level 16, State 1, Line 1
Could not create constraint. See previous errors.
Whereas the following works successfully:
PRIMARY KEY
)
GO
If you are using COALESCE() and or ISNULL() as a method of allowing optional parameters
into your WHERE clause, please see Article #2348 for some useful information (the most
common techniques will use a scan, but the article shows methods that will force a more efficient
seek).
Finally, COALESCE() can generate a less efficient plan in some cases, for example when it is
used against a subquery. Take the following example in Pubs and compare the execution plans:
USE PUBS
GO
SET SHOWPLAN_TEXT ON
GO
SELECT COALESCE
(
(SELECT a2.au_id
FROM pubs..authors a2
WHERE a2.au_id = a1.au_id),
''
)
FROM authors a1
SELECT ISNULL
(
(SELECT a2.au_id
FROM pubs..authors a2
WHERE a2.au_id = a1.au_id),
''
)
FROM authors a1
GO
SET SHOWPLAN_TEXT OFF
GO
Notice the extra work that COALESCE() has to do? This may not be a big deal against this tiny
table in Pubs, but in a bigger environment this can bring servers to their knees. And no, this hasn't
been made any more efficient in SQL Server 2005, you can reproduce the same kind of plan
difference in AdventureWorks:
USE AdventureWorks
GO
SET SHOWPLAN_TEXT ON
GO
SELECT COALESCE
(
(SELECT MAX(Name)
FROM Sales.Store s2
WHERE s2.name = s1.name),
''
)
FROM Sales.Store s1
SELECT ISNULL
(
(SELECT MAX(Name)
FROM Sales.Store s2
WHERE s2.name = s1.name),
''
)
FROM Sales.Store s1
GO
SET SHOWPLAN_TEXT OFF
GO
Let's say you are searching for 'foobar' in all your stored procedures. You can do this using the
INFORMATION_SCHEMA.ROUTINES view, or syscomments:
If you want to present these results in ASP, you can use the following code, which also highlights the
searched string in the body (the hW function is based on Article #2344):
<%
set conn = CreateObject("ADODB.Connection")
conn.Open = "<connection string>"
set rs = conn.execute(sql)
if not rs.eof then
do while not rs.eof
s = hW(str, server.htmlEncode(rs(1)))
s = replace(s, vbTab, " ")
s = replace(s, vbCrLf, "<br>")
response.write "<b>" & rs(0) & "</b><p>" & s & "<hr>"
rs.movenext
loop
else
response.write "No procedures found."
end if
SELECT OBJECT_NAME(id)
FROM syscomments
WHERE [text] LIKE '%foobar%'
AND OBJECTPROPERTY(id, 'IsProcedure') =
1
GROUP BY OBJECT_NAME(id)
Now, why did I use GROUP BY? Well, there is a curious distribution of the procedure text in system
tables if the procedure is greater than 8KB. So, the above makes sure that any procedure name is only
returned once, even if multiple rows in or syscomments draw a match. But, that begs the question, what
happens when the text you are looking for crosses the boundary between rows? Here is a method to create
a simple stored procedure that will do this, by placing the search term (in this case, 'foobar') at around
character 7997 in the procedure. This will force the procedure to span more than one row in
syscomments, and will break the word 'foobar' up across rows.
Run the following query in Query Analyzer, with results to text (CTRL+T):
SET NOCOUNT ON
SELECT 'SELECT '''+REPLICATE('x', 7936)+'foobar'
SELECT REPLICATE('x', 500)+''''
This will yield two results. Copy them and inject them here:
Basically, what we're going to do next is loop through a cursor, for all procedures that exceed 8KB. We
can get this list as follows:
SELECT OBJECT_NAME(id)
FROM syscomments
WHERE OBJECTPROPERTY(id, 'IsProcedure') = 1
GROUP BY OBJECT_NAME(id)
HAVING COUNT(*) > 1
We'll need to create a work table to hold the results as we loop through the procedure, and we'll need to
use UPDATETEXT to append each row with the new 8000-or-less chunk of the stored procedure code.
DECLARE c CURSOR
LOCAL FORWARD_ONLY STATIC READ_ONLY FOR
SELECT OBJECT_NAME(id), text
FROM syscomments s
INNER JOIN #temp t
ON s.id = t.Proc_id
ORDER BY id, colid
OPEN c
FETCH NEXT FROM c INTO @curName, @curtext
-- clean up
DROP TABLE #temp
CLOSE c
DEALLOCATE c
Adam Machanic had a slightly different approach to the issue of multiple rows in syscomments, though it
doesn't solve the problem where a line exceeds 4000 characters *and* your search phrase teeters on the
end of such a line. Anyway, it's an interesting function, check it out!
Luckily, SQL Server 2005 will get us out of this problem. There are new functions like
OBJECT_DEFINITION, which returns the whole text of the procedure. Also, there is a new catalog view,
sys.sql_modules, which also holds the entire text, and INFORMATION_SCHEMA.ROUTINES has been
updated so that the ROUTINE_DEFINITION column also contains the full text of the procedure. So, any
of the following queries will work to perform this search in SQL Server 2005:
SELECT Name
FROM sys.procedures
WHERE OBJECT_DEFINITION(object_id) LIKE '%foobar%'
SELECT OBJECT_NAME(object_id)
FROM sys.sql_modules
WHERE Definition LIKE '%foobar%'
AND OBJECTPROPERTY(object_id, 'IsProcedure') = 1
SELECT ROUTINE_NAME
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_DEFINITION LIKE '%foobar%'
AND ROUTINE_TYPE = 'PROCEDURE'
Note that there is no good substitute for documentation around your application. The searching above can
provide many irrelevant results if you search for a word that happens to only be included in comments in
some procedures, that is part of a larger word that you use, or that should be ignored due to frequency
(e.g. SELECT). It can also leave things out if, for example, you are searching for the table name
'Foo_Has_A_Really_Long_Name' and some wise developer has done this:
Likewise, sp_depends will leave out any procedure like the above, in addition to any procedure that was
created before the dependent objects exist. The latter scenario is allowed due to deferred name resolution
—the parser allows you to create a procedure that references invalid objects, and doesn't check that they
exist until you actually run the stored procedure.
So, long story short, you can't be 100% certain that any kind of searching or in-built query is going to be
the silver bullet that tracks down every reference to an object. But you can get pretty close.
Third-party products
Whenever there is a void, someone is going to come up with a solution, right? I was alerted recently of
this tool, which indexes all of your metadata to help you search for words...