1. Trang chủ
  2. » Công Nghệ Thông Tin

SQL Server MVP Deep Dives- P4

40 350 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Error Handling In SQL Server And Applications
Trường học University of SQL Server
Chuyên ngành Database Management
Thể loại chapter
Định dạng
Số trang 40
Dung lượng 804,57 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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 1

The 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 2

Handling 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 3

This 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 4

Handling 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 5

IF @@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 6

Handling 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 7

Console.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 8

Console.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 9

try { 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 10

Summary

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 11

Rob 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 12

JOIN 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 13

SELECT 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 14

Formatting 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 15

INNER 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 16

Formatting 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 17

just 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 18

Filtering 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 19

Often 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 20

The 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)

Ngày đăng: 24/10/2013, 19:15