BEGIN TRY PRINT 'One' BEGIN TRY PRINT 1/0 END TRY BEGIN CATCH PRINT 'Caught by the inner catch' END CATCH PRINT 'Two' END TRY BEGIN CATCH PRINT 'Caught by the outer catch' END CATCH Li
Trang 1The ERROR_STATE() function can be used to determine the error state Some tem error messages can be raised at different points in the SQL Server engine SQLServer uses the error state to differentiate when these errors are raised.
The last two properties of an error are the line number and the name of the storedprocedure where the error occurred These can be returned using the ERROR_LINE()function and the ERROR_PROCEDURE() function, respectively The ERROR_PROCEDURE()function will return NULL if the error occurs outside a stored procedure Listing 4 is anexample of these last two functions inside a stored procedure
CREATE PROCEDURE ChildError AS
BEGIN RAISERROR('My Error', 11, 1) END
GO CREATE PROCEDURE ParentError AS
BEGIN EXEC ChildError END
GO
BEGIN TRY EXEC ParentError END TRY
BEGIN CATCH SELECT Error_Line = ERROR_LINE(), Error_Proc = ERROR_PROCEDURE() END CATCH
This returns the following result:
Error_Line Error_Proc - -
4 ChildError
Let’s look now at how we can generate our own custom error messages
Generate your own errors using RAISERROR
The RAISERROR function can be used to generate SQL Server errors and initiate anyerror processing The basic use of RAISERROR for a dynamic error looks like this:
RAISERROR('Invalid Customer', 11, 1)
This returns the following when run in SQL Server Management Studio:
Msg 50000, Level 11, State 1, Line 1 Invalid Customer
The first parameter is the custom error message The second is the severity (or level).Remember that 11 is the minimum severity that will cause a CATCH block to fire Thelast parameter is the error state
Listing 4 ERROR_LINE and ERROR_PROCEDURE functions in a stored procedure
Trang 2Handling errors inside SQL Server
RAISERROR can also be used to return user-created error messages The code in ing 5 illustrates this
list-EXEC sp_addmessage @msgnum = 50001, @severity = 11, @msgtext = 'My custom error', @replace = 'replace';
GO RAISERROR(50001, 11, 1);
GO
This returns the following result:
Msg 50001, Level 11, State 1, Line 1
My custom error
The @REPLACE parameter of sp_addmessage says to replace the error if it alreadyexists When RAISERROR is called with a message description rather than an error num-ber, it returns an error number 50000
Ordinary users can specify RAISERROR with severity levels up to 18 To specify ity levels greater than 18, you must be in the sysadmin fixed server role or have beengranted ALTER TRACE permissions You must also use the WITH LOG option Thisoption logs the messages to the SQL Server log and the Windows Application EventLog WITH LOG may be used with any severity level Using a severity level of 20 orhigher in a RAISERROR statement will cause the connection to close
sever-Nesting TRY CATCH blocks
TRY CATCH blocks can be nested inside either TRY or CATCH blocks Nesting inside aTRY block looks like listing 6
BEGIN TRY PRINT 'One'
BEGIN TRY PRINT 1/0 END TRY BEGIN CATCH PRINT 'Caught by the inner catch' END CATCH
PRINT 'Two' END TRY
BEGIN CATCH PRINT 'Caught by the outer catch' END CATCH
Listing 5 Returning user-created error messages with RAISERROR
Listing 6 Nesting TRY CATCH blocks
Trang 3This batch returns the following result:
One Caught by the inner catch Two
This allows specific statements inside a larger TRY CATCH to have their own errorhandling A construct like this can be used to selectively handle certain errors andpass any other errors further up the chain Here’s an example in listing 7
BEGIN TRY PRINT 'One'
BEGIN TRY PRINT CAST('Hello' AS DATETIME) END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 PRINT 'Divide by zero Again.' ELSE
BEGIN DECLARE @ErrorNumber INT;
DECLARE @ErrorMessage NVARCHAR(4000) DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
SELECT @ErrorNumber = ERROR_NUMBER(), @ErrorMessage = ERROR_MESSAGE() + ' (%d)', @ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE();
RAISERROR( @ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber ) END
END CATCH
PRINT 'Two' END TRY
BEGIN CATCH PRINT 'Error: ' + ERROR_MESSAGE() END CATCH
This returns the following result:
One Error: Conversion failed when converting datetime from character string (241)
In the inner CATCH block I’m checking whether we generated error number 8134(divide by zero) and if so, I print a message For every other error message, I “reraise”
or “rethrow” the error to the outer catch block Note the text string that’s added toListing 7 Error handling with nested TRY CATCH statements
Trang 4Handling errors inside SQL Server
the error message variable The %d is a placeholder that’s replaced by the first tional parameter passed to RAISERROR, which is @ErrorNumber in my example BooksOnline has more information on the different types of replace variables that can beused in RAISERROR
The error functions can be used in any stored procedure called from inside theCATCH block This allows you to create standardized error-handling modules such asthe one in listing 8
CREATE PROCEDURE ErrorHandler AS
BEGIN PRINT 'I should log this error:' PRINT ERROR_MESSAGE()
END GO
BEGIN TRY SELECT 1/0 END TRY
BEGIN CATCH EXEC ErrorHandler END CATCH
This block of code will return the following results:
I should log this error:
Divide by zero error encountered.
This is typically used to handle any errors in the code that logs the error information
to a custom error table
TRY CATCH and transactions
A common use for a TRY CATCH block is to handle transaction processing A mon pattern for this is shown in listing 9
com-BEGIN TRY BEGIN TRANSACTION
INSERT INTO dbo.invoice_header (invoice_number, client_number) VALUES (2367, 19)
INSERT INTO dbo.invoice_detail (invoice_number, line_number, part_number) VALUES (2367, 1, 84367)
COMMIT TRANSACTION END TRY
BEGIN CATCH
Listing 8 An error-handling module
Listing 9 Transaction processing in a TRY CATCH block
Trang 5IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION And rethrow the error
END CATCH
Remember that the CATCH block completely consumes the error; therefore it is tant to return some type of error or message back to the calling program
impor-Handling SQL Server errors on the client
The examples in this section use C# as the client application Any NET client tion that supports try catch constructs will behave in a similar fashion The keypoints to learn here are which NET classes are involved in error handling and whatmethods and properties they expose
When a NET application executes a SQL statement that causes an error, it throws aSqlException This can be caught using a try catch block like we saw previously.The SQL Server exception could also be caught by catching a plain Exception, but theSqlException class provides additional SQL Server–specific properties A simpleexample of this in C# is shown in listing 10
using System.Data;
using System.Data.SqlClient;
class Program {
SqlConnection conn = new SqlConnection(" ");
SqlCommand cmd = new SqlCommand("RAISERROR('My Error', 11, 1)", conn);
try { cmd.Connection.Open();
cmd.ExecuteNonQuery();
Console.WriteLine("No error returned");
} catch (SqlException sqlex) {
Console.WriteLine("Error Message: " + sqlex.Message);
Console.WriteLine("Error Severity: {0}", sqlex.Class.ToString()); Console.WriteLine("Line Number: {0}", sqlex.LineNumber.ToString()); }
}
This returns the following result:
Error Message: My Error Error Severity: 11 Line Number: 1
Exceptions with a severity of 10 or less don’t trigger the catch block on the client Theconnection is closed if the severity level is 20 or higher; it normally remains open ifthe severity level is 19 or less You can use RAISERROR to generate severities of 20 orhigher and it’ll close the connection and fire the try catch block on the client.Listing 10 Outputting SQL Server–specific error properties with SqlException
Trang 6Handling SQL Server errors on the client
The error returned typically indicates that the connection was closed rather than theerror text you specified
The SqlException class inherits from the System.SystemException and includesmany properties that are specific to NET Some key SQL Server–specific properties ofthe SqlException class are shown in table 1
Another interesting property of the SqlException class is the Errors property This is
a collection of SqlError objects The SqlError class includes only the SQL Server–specific properties from the SqlException object that are listed in table 1 Because abatch of SQL can generate multiple SQL Server errors, an application needs to checkwhether multiple errors have occurred The first error in the Errors property willalways match the error in the SqlExcpetion’s properties Listing 11 is an example
using System.Data;
using System.Data.SqlClient;
class Program {
static void Main(string[] args) {
SqlConnection conn = new SqlConnection(@"Server=L60\YUKON;
➥Integrated Security=SSPI");
SqlCommand cmd = new SqlCommand(
@"RAISERROR('My Error', 11, 17) SELECT 1/0
SELECT * FROM dbo.BadTable", conn);
try { cmd.Connection.Open();
cmd.ExecuteReader();
Table 1 SQLException class properties
Class Error severity level
LineNumber Line number in the batch or stored procedure where the error occurred
Message Description of the error
Number SQL Server error number
Procedure Stored procedure name where the error occurred
Server SQL Server instance that generated the error
Source Provider that generated the error (for example, Net SqlClient Data Provider)
State SQL Server error state (the third parameter of RAISERROR)
Listing 11 Handling multiple errors with the Errors property
Trang 7Console.WriteLine("No error returned");
} catch (SqlException sqlex) {
for (int i = 0; i < sqlex.Errors.Count; i++) {
Console.WriteLine("Error #{0}: {1}", i.ToString(), sqlex.Errors[i].Message);
} }
} }
This returns the following result:
Error #0: My Error Error #1: Divide by zero error encountered.
Error #2: Invalid object name 'dbo.BadTable'.
In closing, let’s look at how we can handle SQL Server messages inside our applicationcode
Handling SQL Server messages on the client
When a message is sent from SQL Server via a PRINT statement or a RAISERROR with aseverity level of 10 or less, it generates an event on the NET side You can capture thisevent by writing a handler for the SqlConnection class’s InfoMessage event The han-dler for the InfoMessage event takes two parameters: the sender and an instance ofSqlInfoMessageEventArgs This class contains three properties The first is the Mes-sage that was printed or generated by the RAISERROR statement The second is theSource, which is usually the Net SqlClient Data Provider The third is the Errorsproperty, which is a collection of SqlError objects and behaves just like it did when
we saw it earlier Listing 12 is an example
using System.Data;
using System.Data.SqlClient;
class Program {
static void Main(string[] args) {
SqlConnection conn = new SqlConnection(@"Server=L60\YUKON;
➥Integrated Security=SSPI");
SqlCommand cmd = new SqlCommand("PRINT 'Hello'", conn);
conn.InfoMessage += new ➥SqlInfoMessageEventHandler(conn_InfoMessage);
try { cmd.Connection.Open();
Listing 12 Outputting SQL Server messages
Trang 8Console.WriteLine("First Error Message: " + sqlex.Message); Console.WriteLine("Error Count: {0}",
Console.WriteLine("SQL Server Message: {0}", e.Message);
Console.WriteLine("Message Source: {0}", e.Source);
Console.WriteLine("Message Count: {0}", e.Errors.Count.ToString()); }
}
This returns the following result:
SQL Server Message: Hello Message Source: Net SqlClient Data Provider Message Count: 1
SQL Server Message: An error as message Message Source: Net SqlClient Data Provider Message Count: 1
No error returned
Another interesting characteristic of this approach is that you can capture tional RAISERROR statements as they’re executed rather than when a batch ends List-ing 13 shows an example
informa-using System.Data;
using System.Data.SqlClient;
class Program {
static void Main(string[] args) {
SqlConnection conn = new SqlConnection(@"Server=L60\YUKON;
➥Integrated Security=SSPI");
SqlCommand cmd = new SqlCommand(
@"PRINT 'Printed at buffer flush' RAISERROR('Starting', 0, 1) WITH NOWAIT;
Listing 13 Capturing RAISERROR statements
Trang 9try { cmd.Connection.Open();
cmd.ExecuteReader();
Console.WriteLine("No error returned");
} catch (SqlException sqlex) {
Console.WriteLine("First Error Message: " + sqlex.Message); Console.WriteLine("Error Count: {0}",
➥sqlex.Errors.Count.ToString());
} } static void conn_ShortMessage(object sender, SqlInfoMessageEventArgs e) {
Console.WriteLine("[{0}] SQL Server Message: {1}", System.DateTime.Now.ToLongTimeString(), e.Message);
} }
This returns the following result:
[3:39:26 PM] SQL Server Message: Printed at buffer flush [3:39:26 PM] SQL Server Message: Starting
[3:39:29 PM] SQL Server Message: Status [3:39:32 PM] SQL Server Message: Done
No error returned
Normally, when you do a series of PRINT statements inside a SQL Server batch orstored procedure, the results are all returned at the end A RAISERROR WITH NOWAIT issent immediately to the client, as is any previous PRINT statement If you remove theWITH NOWAIT from the first RAISERROR, the first three lines are all printed at the sametime when the RAISERROR WITH NOWAIT pushes them all to the client This approachcan provide a convenient way to return status information for long-running tasks thatcontain multiple SQL statements
Summary
SQL Server error handling doesn’t need to be an afterthought SQL Server 2005 vides powerful tools that allow developers to selectively handle, capture, and consumeerrors inside SQL Server Errors that can’t be handled on the server can be passedback to the application .NET has specialized classes that allow applications to capturedetailed information about SQL Server exceptions
Trang 10Summary
About the author
Bill Graziano has been a SQL Server consultant for 10 years,doing production support, performance tuning, and applica-tion development He serves on the board of directors for theProfessional Association for SQL Server (PASS), where heserves as the vice president of marketing and sits on the exec-utive committee He’s a regular speaker at conferences anduser groups across the country Bill runs the popular web sitehttp://SQLTeam.com and is currently a SQL Server MVP
Trang 11Rob Farley
When can you ever have a serious query that doesn’t involve more than one table?Normalization is a wonderful thing and helps ensure that our databases haveimportant characteristics such as integrity But it also means using JOINs, becauseit’s unlikely that all the data we need to solve our problem occurs in a single table.Almost always, our FROM clause contains several tables
In this chapter, I’ll explain some of the deeper, less understood aspects of theFROM clause I’ll start with some of the basics, to make sure we’re all on the samepage You’re welcome to skip ahead if you’re familiar with the finer points of INNER,OUTER, and CROSS I’m often amazed by the fact that developers the world overunderstand how to write a multi-table query, and yet few understand what they’reasking with such a query
The INNER JOIN
The most common style of JOIN is the INNER JOIN I’m writing that in capitalsbecause it’s the keyword expression used to indicate the type of JOIN being used,but the word INNER is completely optional It’s so much the most common style ofJOIN that when we write only JOIN, we’re referring to an INNER JOIN
An INNER JOIN looks at the two tables involved in the JOIN and identifies ing rows according to the criteria in the ON clause Every INNER JOIN requires an ONclause—it’s not optional Aliases are optional, and can make the ON clause much
Trang 12JOIN basics
shorter and simpler to read (and the rest of the query too) I’ve made my life easier byspecifying p and s after the table names in the query shown below (to indicate Prod-uct and ProductSubcategory respectively) In this particular example, the match con-dition is that the value in one column in the first table must be the same as thecolumn in the second table It so happens that the columns have the same name;therefore, to distinguish between them, I've specified one to be from the table p, andthe other from the table s It wouldn’t have mattered if I specified these two columns
in the other order—equality operations are generally commutative, and it doesn’tmatter whether I write a=b or b=a
The query in listing 1 returns a set of rows from the two tables, containing all therows that have matching values in their ProductSubcategoryID columns This query,
as with most of the queries in this chapter, will run on the AdventureWorks database,which is available as a sample database for SQL Server 2005 or SQL Server 2008
SELECT p.Name, s.Name FROM Production.Product p JOIN
Production.ProductSubcategory s
ON p.ProductSubcategoryID = s.ProductSubcategoryID;
This query could return more rows than there are in the Product table, fewer rows, orthe same number of rows We’ll look at this phenomenon later in the chapter, as well
as what can affect this number
The OUTER JOIN
An OUTER JOIN is like an INNER JOIN except that rows that do not have matching values
in the other table are not excluded from the result set Instead, the rows appear withNULL entries in place of the columns from the other table Remembering that a JOIN isalways performed between two tables, these are the variations of OUTER JOIN:
LEFT—Keeps all rows from the first table (inserting NULLs for the second table’scolumns)
RIGHT—Keeps all rows from the second table (inserting NULLs for the firsttable’s columns)
FULL—Keeps all rows from both tables (inserting NULLs on the left or the right,
as appropriate) Because we must specify whether the OUTER JOIN is LEFT, RIGHT, or FULL, note that thekeyword OUTER is optional—inferred by the fact that we have specified its variation As
in listing 2, I generally omit it, but you’re welcome to use it if you like
Listing 1 Query to return rows with matching product subcategories
Trang 13SELECT p.Name, s.Name FROM Production.Product p LEFT JOIN
Production.ProductSubcategory s
ON p.ProductSubcategoryID = s.ProductSubcategoryID;
The query in listing 2 produces results similar to the results of the last query, exceptthat products that are not assigned to a subcategory are still included They have NULLlisted for the second column The smallest number of rows that could be returned bythis query is the number of rows in the Product table, as none can be eliminated Had we used a RIGHT JOIN, subcategories that contained no products would havebeen included Take care when dealing with OUTER JOINs, because counting the rowsfor each subcategory would return 1 for an empty subcategory, rather than 0 If youintend to count the products in each subcategory, it would be better to count theoccurrences of a non-NULL ProductID or Name instead of using COUNT(*) The que-ries in listing 3 demonstrate this potential issue
/* First ensure there is a subcategory with no corresponding products */ INSERT Production.ProductSubcategory (Name) VALUES ('Empty Subcategory');
SELECT s.Name, COUNT(*) AS NumRows FROM Production.ProductSubcategory s LEFT JOIN
Production.Product p
ON s.ProductSubcategoryID = p.ProductSubcategoryID GROUP BY s.Name;
SELECT s.Name, COUNT(p.ProductID) AS NumProducts FROM Production.ProductSubcategory s
LEFT JOIN Production.Product p
ON s.ProductSubcategoryID = p.ProductSubcategoryID GROUP BY s.Name;
Although LEFT and RIGHT JOINs can be made equivalent by listing the tables in theopposite order, FULL is slightly different, and will return at least as many rows as thelargest table (as no rows can be eliminated from either side)
The CROSS JOIN
A CROSS JOIN returns every possible combination of rows from the two tables It’s like
an INNER JOIN with an ON clause that evaluates to true for every possible combination
of rows The CROSS JOIN doesn’t use an ON clause at all This type of JOIN is relativelyrare, but it can effectively solve some problems, such as for a report that must includeevery combination of SalesPerson and SalesTerritory (showing zero or NULL whereappropriate) The query in listing 4 demonstrates this by first performing a CROSSJOIN, and then a LEFT JOIN to find sales that match the criteria
Listing 2 A LEFT OUTER JOIN
Listing 3 Beware of COUNT(*) with OUTER JOINs
Trang 14Formatting your FROM clause
SELECT p.SalesPersonID, t.TerritoryID, SUM(s.TotalDue) AS TotalSales FROM Sales.SalesPerson p
CROSS JOIN Sales.SalesTerritory t LEFT JOIN
Sales.SalesOrderHeader s
ON p.SalesPersonID = s.SalesPersonID AND t.TerritoryID = s.TerritoryID GROUP BY p.SalesPersonID, t.TerritoryID;
Formatting your FROM clause
I recently heard someone say that formatting shouldn’t be a measure of good or badpractice when coding He was talking about writing C# code and was largely takingexception to being told where to place braces I’m inclined to agree with him to a cer-tain extent I do think that formatting guidelines should form a part of companycoding standards so that people aren’t put off by the layout of code written by a col-league, but I think the most important thing is consistency, and Best Practices guidesthat you find around the internet should generally stay away from formatting When itcomes to the FROM clause, though, I think formatting can be important
A sample query
The Microsoft Project Server Report Pack is a valuable resource for people who useMicrosoft Project Server One of the reports in the pack is a Timesheet Audit Report,which you can find at http://msdn.microsoft.com/en-us/library/bb428822.aspx Isometimes use this when teaching T-SQL
The main query for this report has a FROM clause which is hard to understand I’veincluded it in listing 5, keeping the formatting exactly as it is on the website In the fol-lowing sections, we’ll demystify it by using a method for reading FROM clauses, andconsider ways that this query could have retained the same functionality without being
so confusing
FROM MSP_EpmResource LEFT OUTER JOIN MSP_TimesheetResource
INNER JOIN MSP_TimesheetActual
ON MSP_TimesheetResource.ResourceNameUID = MSP_TimesheetActual.LastChangedResourceNameUID
ON MSP_EpmResource.ResourceUID = MSP_TimesheetResource.ResourceUID
LEFT OUTER JOIN MSP_TimesheetPeriod
INNER JOIN MSP_Timesheet
ON MSP_TimesheetPeriod.PeriodUID = MSP_Timesheet.PeriodUID
Listing 4 Using a CROSS JOIN to cover all combinations
Listing 5 A FROM clause from the Timesheet Audit Report
Trang 15INNER JOIN MSP_TimesheetPeriodStatus
ON MSP_TimesheetPeriod.PeriodStatusID = MSP_TimesheetPeriodStatus.PeriodStatusID
INNER JOIN MSP_TimesheetStatus
ON MSP_Timesheet.TimesheetStatusID = MSP_TimesheetStatus.TimesheetStatusID
ON MSP_TimesheetResource.ResourceNameUID = MSP_Timesheet.OwnerResourceNameUID
The appearance of most queries
In the western world, our languages tend to read from left to right Because of this,people tend to approach their FROM clauses from left to right as well
For example, they start with one table:
FROM MSP_EpmResource
Then they JOIN to another table:
FROM MSP_EpmResource LEFT OUTER JOIN MSP_TimesheetResource
ON MSP_EpmResource.ResourceUID = MSP_TimesheetResource.ResourceUID
And they keep repeating the pattern:
FROM MSP_EpmResource LEFT OUTER JOIN MSP_TimesheetResource
ON MSP_EpmResource.ResourceUID = MSP_TimesheetResource.ResourceUID INNER JOIN MSP_TimesheetActual
ON MSP_TimesheetResource.ResourceNameUID = MSP_TimesheetActual.LastChangedResourceNameUID
They continue by adding on the construct JOIN table_X ON table_X.col = table_Y.col
Effectively, everything is done from the perspective of the first table in the FROMclause Some of the JOINs may be OUTER JOINs, some may be INNER, but the principleremains the same—that each table is brought into the mix by itself
When the pattern doesn’t apply
In our sample query, this pattern doesn’t apply We start with two JOINs, followed bytwo ONs That doesn’t fit the way we like to think of our FROM clauses If we try to rear-range the FROM clause to fit, we find that we can’t But that doesn’t stop me from try-ing to get my students to try; it’s good for them to appreciate that not all queries can
be written as they prefer I get them to start with the first section (before the secondLEFT JOIN):
FROM MSP_EpmResource LEFT OUTER JOIN MSP_TimesheetResource
INNER JOIN MSP_TimesheetActual
Trang 16Formatting your FROM clause
ON MSP_TimesheetResource.ResourceNameUID = MSP_TimesheetActual.LastChangedResourceNameUID
ON MSP_EpmResource.ResourceUID = MSP_TimesheetResource.ResourceUID
Many of my students look at this part and immediately pull the second ON clause(between MSP_EpmResource and MSP_TimesheetResource) and move it to beforethe INNER JOIN But because we have an INNER JOIN applying to MSP_Timesheet-Resource, this would remove any NULLs that are the result of the OUTER JOIN Clearlythe logic has changed Some try to fix this by making the INNER JOIN into a LEFT JOIN,but this changes the logic as well Some move the tables around, listingMSP_EpmResource last, but the problem always comes down to the fact that peopledon’t understand this query This part of the FROM clause can be fixed by using a RIGHTJOIN, but even this has problems, as you may find if you try to continue the patternwith the other four tables in the FROM clause
How to read a FROM clause
A FROM clause is easy to read, but you have to understand the method When I ask mystudents what the first JOIN is, they almost all say “the LEFT JOIN,” but they’re wrong.The first JOIN is the INNER JOIN, and this is easy to find because it’s the JOIN thatmatches the first ON To find the first JOIN, you have to find the first ON Having found
it, you work backwards to find its JOIN (which is the JOIN that immediately precedesthe ON, skipping past any JOINs that have already been allocated to an ON clause) Theright side of the JOIN is anything that comes between an ON and its correspondingJOIN To find the left side, you keep going backwards until you find an unmatchedJOIN or the FROM keyword You read a FROM clause that you can’t immediately under-stand this way:
1 Find the first (or next) ON keyword
2 Work backwards from the ON, looking for an unmatched JOIN
3 Everything between the ON and the JOIN is the right side
4 Keep going backwards from the JOIN to find the left side
5 Repeat until all the ONs have been found
Using this method, we can clearly see that the first JOIN in our sample query is theINNER JOIN, between MSP_TimesheetResource and MSP_TimesheetActual This formsthe right side of the LEFT OUTER JOIN, with MSP_EpmResource being the left side
When the pattern can’t apply
Unfortunately for our pattern, the next JOIN is the INNER JOIN between MSP_TimesheetPeriod and MSP_Timesheet It doesn’t involve any of the tables we’vealready brought into our query When we continue reading our FROM clause, we even-tually see that the query involves two LEFT JOINs, for which the right sides are a series
of nested INNER JOINs This provides logic to make sure that the NULLs are introducedonly to the combination of MSP_TimesheetResource and MSP_TimesheetActual, not
Trang 17just one of them individually, and similarly for the combination of sheetPeriod through MSP_TimesheetStatus
And this is where the formatting becomes important The layout of the query in itsinitial form (the form in which it appears on the website) doesn’t suggest that any-thing is nested I would much rather have seen the query laid out as in listing 6 Theonly thing I have changed about the query is the whitespace and aliases, and yet youcan see that it’s far easier to read
FROM MSP_EpmResource r LEFT OUTER JOIN MSP_TimesheetResource tr INNER JOIN
MSP_TimesheetActual ta
ON tr.ResourceNameUID = ta.LastChangedResourceNameUID
ON r.ResourceUID = tr.ResourceUID LEFT OUTER JOIN
MSP_TimesheetPeriod tp INNER JOIN
MSP_Timesheet t
ON tp.PeriodUID = t.PeriodUID INNER JOIN
MSP_TimesheetPeriodStatus tps
ON tp.PeriodStatusID = tps.PeriodStatusID INNER JOIN
MSP_TimesheetStatus ts
ON t.TimesheetStatusID = ts.TimesheetStatusID
ON tr.ResourceNameUID = t.OwnerResourceNameUID
Laying out the query like this doesn’t change the importance of knowing how to read
a query when you follow the steps I described earlier, but it helps the less experiencedpeople who need to read the queries you write Bracketing the nested sections mayhelp make the query even clearer, but I find that bracketing can sometimes make peo-ple confuse these nested JOINs with derived tables
Writing the FROM clause clearly the first time
In explaining the Timesheet Audit Report query, I’m not suggesting that it’s wise tonest JOINs as I just described However, being able to read and understand a complexFROM clause is a useful skill that all query writers should have They should also writequeries that less skilled readers can easily understand
Filtering with the ON clause
When dealing with INNER JOINs, people rarely have a problem thinking of the ONclause as a filter Perhaps this comes from earlier days of databases, when we listedtables using comma notation and then put the ON clause predicates in the WHEREclause From this link with the WHERE clause, we can draw parallels between the ON andListing 6 A reformatted version of the FROM clause in listing 5
Trang 18Filtering with the ON clause
WHERE clauses, but the ON clause is a different kind of filter, as I’ll demonstrate in thefollowing sections
The different filters of the SELECT statement
The most obvious filter in a SELECT statement is the WHERE clause It filters out therows that the FROM clause has produced The database engine controls entirely theorder in which the various aspects of a SELECT statement are applied, but logically theWHERE clause is applied after the FROM clause has been completed
People also understand that the HAVING clause is a filter, but they believe enly that the HAVING clause is used when the filter must involve an aggregate function
mistak-In actuality (but still only logically), the HAVING clause is applied to the groups thathave been formed through the introduction of aggregates or a GROUP BY clause Itcould therefore be suggested that the ON clause isn’t a filter, but rather a mechanism
to describe the JOIN context But it is a filter
Filtering out the matches
Whereas the WHERE clause filters out rows, and the HAVING clause filters out groups, the
ON clause filters out matches
In an INNER JOIN, only the matches persist into the final results Given all possiblecombinations of rows, only those that have successful matches are kept Filtering out
a match is the same as filtering out a row For OUTER JOINs, we keep all the matchesand then introduce NULLs to avoid eliminating rows that would have otherwise beenfiltered out Now the concept of filtering out a match differs a little from filtering out
Now the success of the match has a different kind of dependency on one side than
on the other Suppose a LEFT JOIN has an ON clause like the one in listing 7
SELECT p.SalesPersonID, o.SalesOrderID, o.OrderDate FROM
Sales.SalesPerson p LEFT JOIN
Sales.SalesOrderHeader o
ON o.SalesPersonID = p.SalesPersonID AND o.OrderDate < '20020101';
For an INNER JOIN, this would be simple, and we’d probably have put the OrderDatepredicate in the WHERE clause But for an OUTER JOIN, this is different
Listing 7 Placing a predicate in the ON clause of an outer join
Trang 19Often when we write an OUTER JOIN, we start with an INNER JOIN and then changethe keyword to use LEFT instead of INNER (in fact, we probably leave out the wordINNER) Making an OUTER JOIN from an INNER JOIN is done so that, for example, thesalespeople who haven’t made sales don’t get filtered out If that OrderDate predicatewas in the WHERE clause, merely changing INNER JOIN to LEFT JOIN wouldn’t havedone the job.
Consider the case of AdventureWorks’ SalesPerson 290, whose first sale was in
2003 A LEFT JOIN without the OrderDate predicate would include this salesperson,but then all the rows for that salesperson would be filtered out by the WHERE clause.This wouldn’t be correct if we were intending to keep all the salespeople
Consider also the case of a salesperson with no sales That person would beincluded in the results of the FROM clause (with NULL for the Sales.SalesOrderHeadercolumns), but then would also be filtered out by the WHERE clause
The answer is to use ON clause predicates to define what constitutes a valid match,
as we have in our code segment above This means that the filtering is done before theNULLs are inserted for salespeople without matches, and our result is correct I under-stand that it may seem strange to have a predicate in an ON clause that involves onlyone side of the JOIN, but when you understand what’s being filtered (rows with WHERE,groups with HAVING, and matches with ON), it should feel comfortable
JOIN uses and simplification
To understand the FROM clause, it’s worth appreciating the power of the query mizer In this final part of the chapter, we'll examine four uses of JOINs We’ll then seehow your query can be impacted if all these uses are made redundant
opti-The four uses of JOINs
Suppose a query involves a single table For our example, we'll use uct from the AdventureWorks sample database Let’s imagine that this table is joined
Production.Prod-to another table, say Production.ProductSubcategory A foreign key relationship isdefined between the two tables, and our ON clause refers to the ProductSubcategoryIDcolumn in each table Listing 8 shows a view that reflects this query
CREATE VIEW dbo.ProductsPlus AS SELECT p.*, s.Name as SubcatName FROM
Production.Product p JOIN
Production.ProductSubcategory s
ON p.ProductSubcategoryID = s.ProductSubcategoryID;
This simple view provides us with the columns of the Product table, with the name ofthe ProductSubcategory to which each product belongs The query used is a standardlookup query, one that query writers often create
Listing 8 View to return products and their subcategories
Trang 20The four uses of JOINs
Comparing the contents of this view to the Product table alone (I say contents
loosely, as a view doesn’t store data unless it is an indexed view; it is simply a storedsubquery), there are two obvious differences
First, we see that we have an extra column We could have made our view contain
as many of the columns from the second table as we like, but for now I’m using onlyone This is the first of the four uses It seems a little trivial, but nevertheless, we haveuse #1: additional columns
Secondly, we have fewer rows in the view than in the Product table A little gation shows that the ProductSubcategoryID column in Production.Product allowsNULL values, with no matching row in the Production.ProductSubcategory table Aswe’re using an INNER JOIN, these rows without matches are eliminated from ourresults This could have been our intention, and therefore we have use #2: elimi-nated rows
We can counteract this side effect quite easily To avoid rows from Production.Product being eliminated, we need to convert our INNER JOIN to an OUTER JOIN I havemade this change and encapsulated it in a second view in listing 9, nameddbo.ProductsPlus2 to avoid confusion
CREATE VIEW dbo.ProductsPlus2 AS SELECT p.*, s.Name AS SubcatName FROM
Production.Product p LEFT JOIN
Production.ProductSubcategory s
ON p.ProductSubcategoryID = s.ProductSubcategoryID;
Now when we query our view, we see that it gives us the same number of rows as in theProduction.Product table
JOINs have two other uses that we don’t see in our view
As this is a foreign-key relationship, the column in Production.ProductSubcategory
is the primary key, and therefore unique There must be at most one matching row foreach row in Production.Product—thereby not duplicating any of the rows from Pro-duction.Product If Production.ProductSubcategory.ProductSubcategoryID weren’tunique, though, we could find ourselves using a JOIN for use #3: duplicated rows Please understand I am considering this only from the perspective of the firsttable, and the lack of duplications here is completely expected and desired If we con-sider the query from the perspective of the second table, we are indeed seeing theduplication of rows I am focusing on one table in order to demonstrate when theJOIN serves no direct purpose
The fourth use is more obscure When an OUTER JOIN is performed, rows thatdon’t have matches are persisted using NULL values for columns from the other table
In our second view above, we are using a LEFT JOIN, and NULL appears instead of theName column from the Production.ProductSubcategory table This has no effect onListing 9 View to return all products and their subcategories (if they exist)