Tải bản đầy đủ (.pdf) (76 trang)

SQL Server 2000 Stored Procedure Programming phần 7 pps

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (646.88 KB, 76 trang )

438
SQL Server 2000 Stored Procedure Programming
Close curItems
Deallocate curItems
Return 0
Go
The second stored procedure is generic and converts information
from cursors into a single variable:
Create Procedure prProcess_Cursor_Nested
Process information from cursor initiated in calling sp.
Convert records into a single varchar.
(
@chvResult varchar(8000) OUTPUT,
@debug int = 0
)
As
Declare @intCountProperties int,
@intCounter int,
@chvItem varchar(255),
@insLenItem smallint,
@insLenResult smallint
Fetch Next From curItems
Into @chvItem
While (@@FETCH_STATUS = 0)
Begin
If @debug <> 0
Select @chvItem Item
check will new string fit
Select @insLenItem = DATALENGTH(@chvItem),
@insLenResult = DATALENGTH(@chvResult)
If @insLenResult + @insLenItem > 8000


Begin
Select 'List is too long (over 8000 characters)!'
Return 1
End

440
SQL Server 2000 Stored Procedure Programming
HOW TO PROCESS THE RESULTSET OF A
STORED PROCEDURE
From time to time, you will encounter stored procedures that return
resultsets you need to process. This is not as simple as it sounds.
One option is to receive the resultset in a client application or
middleware component and to process it further from there.
Sometimes this option is not acceptable for a variety of reasons. For
example, the resultset might be too big and network traffic could be
considerably increased in this way. Since the resultset needs to be
transferred to the middleware server before it is processed, the
performance of the system could be degraded. There might be
security implications—for example, if a user should have access
only to a segment of a resultset and not to the complete resultset.
Another option is to copy the source code of the stored procedure
into your stored procedure. This could be illegal. It will also reduce
the maintainability of your code since you have two copies to
maintain. If the other stored procedure is a system stored procedure,
Microsoft can change its internals with the release of each new version
of SQL Server. Your stored procedure will then need to be changed.
It is possible to collect the resultset of a stored procedure in
Transact-SQL code. You need to create a (temporary) table, the
structure of which matches the structure of the resultset exactly, and
then redirect (insert) the resultset into it. Then you can do whatever

you want with it.
The following stored procedure uses the sp_dboption system
stored procedure to obtain a list of all database options and to obtain
a list of database options that are set on the Asset database. Records
that have a structure identical to that of the resultset as returned by
the stored procedure are collected in temporary tables. The Insert
statement can then store the resultset in the temporary table. The
contents of the temporary tables are later compared and a list of
database options not currently set is returned to the caller.
Create Procedure prNonSelectedDBOption
return list of non-selected database options
@chvDBName sysname
As
Set Nocount On
Create Table #setable
(
name nvarchar(35)
)
Create Table #current
(
name nvarchar(35)
)
collect all options
Insert Into #setable
Exec sp_dboption
collect current options
Insert Into #current
Exec sp_dboption @dbname = @chvDBName
return non-selected
Select name non_selected

From #setable
Where name not in ( Select name
From #current
)
Drop Table #setable
Drop Table #current
Return 0
The only trouble with this method is that you need to know the
structure of the resultset of the stored procedure in advance in order
to create a table with the same structure. This is not a problem for
user-defined stored procedures. It used to be a problem for system
Chapter 10: Advanced Stored Procedure Programming
441
442
SQL Server 2000 Stored Procedure Programming
stored procedures, but SQL Server Books Online now provides that
information.
NOTE:
Unfortunately, it is not possible to collect information if a
stored procedure returns more than one resultset, as is the case with
sp_spaceused.
This technique also works with the Exec statement. For example,
if you try to collect a resultset from the DBCC command this way,
SQL Server will return an error. But you can encapsulate the DBCC
statement in a string and execute it from Exec.
The following stored procedure returns the percentage of log
space used in a specified database:
Create Procedure prLogSpacePercentUsed
return percent of space used in transaction log for
specified database

(
@chvDbName sysname,
@fltPercentUsed float OUTPUT
)
As
Set Nocount On
Declare @intErrorCode int
Set @intErrorCode = @@Error
If @intErrorCode = 0
Begin
Create Table #DBLogSpace
( dbname sysname,
LogSizeInMB float,
LogPercentUsed float,
Status int
)
Set @intErrorCode = @@Error
End
Chapter 10: Advanced Stored Procedure Programming
443
get log space info. for all databases
If @intErrorCode = 0
Begin
Insert Into #DBLogSpace
Exec ('DBCC SQLPERF (LogSpace)')
set @intErrorCode = @@Error
end
get percent for specified database
if @intErrorCode = 0
begin

select @fltPercentUsed = LogPercentUsed
from #DBLogSpace
where dbname = @chvDbName
set @intErrorCode = @@Error
end
drop table #DBLogSpace
return @intErrorCode
You can test this stored procedure from Query Analyzer, as
shown on Figure 10-7.
These techniques were extremely important before SQL Server
2000. It is now possible to use the table datatype as a return value
for user-defined functions. We showed how can you use table-valued
user-defined functions in Chapter 9. Unfortunately, it is (still) not
possible to use a table variable as the output parameter of a stored
procedure.
You have another option when you want to pass a resultset (or
multiple resultsets) to a calling stored procedure. You can use the
cursor datatype as the output parameter of a stored procedure. In
the following example, prGetInventoryProperties_CursorGet creates
and opens a cursor. It is then returned as a cursor output parameter
to the calling procedure:
Create Procedure prGetInventoryProperties_CursorGet
Return Cursor that contains properties
that are describing selected asset.
(
@intInventoryId int,
@curProperties Cursor Varying Output
)
As
Set @curProperties = Cursor Forward_Only Static For

Select Property, Value, Unit
444
SQL Server 2000 Stored Procedure Programming
Figure 10-7. Percentage of log space used in a specified database
Chapter 10: Advanced Stored Procedure Programming
445
From InventoryProperty inner join Property
On InventoryProperty.PropertyId = Property.PropertyId
Where InventoryProperty.InventoryId = @intInventoryId
Open @curProperties
Return 0
The nested stored procedure will be called from following stored
procedure:
Create Procedure prGetInventoryProperties_UseNestedCursor
return comma-delimited list of properties
that are describing asset.
i.e.: Property = Value unit;Property = Value unit;
Property = Value unit;Property = Value unit;
(
@intInventoryId int,
@chvProperties varchar(8000) OUTPUT,
@debug int = 0
)
As
Declare @intCountProperties int,
@intCounter int,
@chvProperty varchar(50),
@chvValue varchar(50),
@chvUnit varchar(50),
@insLenProperty smallint,

@insLenValue smallint,
@insLenUnit smallint,
@insLenProperties smallint
Set @chvProperties = ''
Declare @CrsrVar Cursor
446
SQL Server 2000 Stored Procedure Programming
Exec prGetInventoryProperties_CursorGet @intInventoryId,
@CrsrVar Output
Fetch Next From @CrsrVar
Into @chvProperty, @chvValue, @chvUnit
While (@@FETCH_STATUS = 0)
Begin
Set @chvUnit = Coalesce(@chvUnit, '')
If @debug <> 0
Select @chvProperty Property,
@chvValue [Value],
@chvUnit [Unit]
check will new string fit
Select @insLenProperty = DATALENGTH(@chvProperty),
@insLenValue = DATALENGTH(@chvValue),
@insLenUnit = DATALENGTH(@chvUnit),
@insLenProperties = DATALENGTH(@chvProperties)
If @insLenProperties + 2
+ @insLenProperty + 1
+ @insLenValue + 1 + @insLenUnit > 8000
Begin
Select 'List of properties is too long (over 8000
chrs)!'
Return 1

End
assemble list
Set @chvProperties = @chvProperties
+ @chvProperty + '='
+ @chvValue + ' '
+ @chvUnit + '; '
If @debug <> 0
Select @chvProperties chvProperties
Fetch Next From @CrsrVar
Into @chvProperty, @chvValue, @chvUnit
End
Close @CrsrVar
Deallocate @CrsrVar
Return 0
It is the responsibility of the caller to properly close and deallocate
the cursor at the end.
TIP:
You should not use a cursor as an output parameter of a stored
procedure unless you have to. Such a solution is inferior because procedures
are coupled and prone to errors. If you are working with SQL Server 2000,
you should use table-valued user-defined functions instead.
USING IDENTITY VALUES
In previous chapters, we introduced the function of identity values in
a table. They are used to generate surrogate keys—unique identifiers
often based on sequential numbers.
A Standard Problem and Solution
Identity values are similar to the Autocount datatype in Access
tables. But there is one difference that generates many questions in
Usenet newsgroups among developers who are used to Access/DAO
behavior. When a developer uses a resultset to insert a record into a

table, the value of the AutoNumber field is immediately available
in Access. Unfortunately, due to the nature of the client/server
environment, this is not the case with recordsets in SQL Server.
Chapter 10: Advanced Stored Procedure Programming
447
448
SQL Server 2000 Stored Procedure Programming
The best way to insert a record into a SQL Server table and obtain
an identity key is to use a stored procedure. The following stored
procedure prInsertInventory is such a solution. A new record is first
inserted into a table and then the key is read using the @@identity
function/global variable.
Create Procedure prInsertInventory
insert inventory record and return Id
@intEquipmentId int,
@intLocationId int,
@inyStatusId tinyint,
@intLeaseId int,
@intLeaseScheduleId int,
@intOwnerId int,
@mnsRent smallmoney,
@mnsLease smallmoney,
@mnsCost smallmoney,
@inyAcquisitionTypeID int,
@intInventoryId int output
As
Set Nocount On
Declare @intErrorCode int
Select @intErrorCode = @@Error
If @intErrorCode = 0

Begin
Insert into Inventory (EquipmentId, LocationId, StatusId,
LeaseId, LeaseScheduleId, OwnerId,
Rent, Lease, Cost,
AcquisitionTypeID)
Values ( @intEquipmentId, @intLocationId, @inyStatusId,
@intLeaseId, @intLeaseScheduleId, @intOwnerId,
@mnsRent, @mnsLease, @mnsCost,
@inyAcquisitionTypeID)
Select @intErrorCode = @@Error,
@intInventoryId = @@identity
End
Return @intErrorCode
Identity Values and Triggers
Unfortunately, the previous solution does not always work. SQL
Server has a bug/feature that can change a value stored in the
@@identity global variable. If the table in which the record was
inserted (in this case, Inventory) has a trigger that inserts a record
into some other table with an identity key, the value of that key will
be recorded in @@identity.
You can reproduce this behavior using the following script. It
must be executed against the tempdb database.
Create Table a (a_id int identity(1,1),
a_desc varchar(20),
b_desc varchar(20))
Go
Create Table b (b_id int identity(1,1),
b_desc varchar(20))
Go
Create Trigger tr_a_I

On dbo.a
After Insert For Insert
As
If @@Rowcount = 0
Return
Insert Into b (b_desc)
Select b_desc from inserted
Go
Now execute this batch:
Insert into b (b_desc)
Values ('1')
Insert into a (a_desc, b_desc)
Chapter 10: Advanced Stored Procedure Programming
449
450
SQL Server 2000 Stored Procedure Programming
Values ('aaa', 'bbb')
Select @@identity [IdentityValue]
Query Analyzer returns the following result:
(1 row(s) affected)
(1 row(s) affected)
IdentityValue

2
(1 row(s) affected)
The first Insert statement adds the first record to table b. The
second Insert statement adds the first record in a table. Because
there is a trigger on the table, another record (the second one) will
be inserted into table b, and the value of @@identity will be set
to 2. If there was no trigger, the Select statement would return a

value of 1.
Sequence Number Table
Unfortunately, it is not easy to solve this problem. One solution is
to create a table (for example, SequenceNumbers) that contains the
highest sequence numbers for each table. So, each time that you want
to insert a record into a table, you need to obtain a value from the
sequence numbers table and increment that number by one. This
value will then be used as a unique identifier (id) for the record
that you want to insert.
This technique was a standard way to implement surrogate
keys in earlier versions of SQL Server before identity values were
introduced. Unfortunately, this technique is prone to concurrency
contention problems, because there might be more processes
competing to read, lock, and update a sequence key value. In earlier
versions of SQL Server, it was not possible to lock a record, but only
a page. A page could contain more than one record. Therefore, the
process could lock a record even if the intent was to update some
other record.
This problem used to be solved by mechanically increasing the
size of the record so that only one record could fit on a page. Dummy
fields used to be added so that the size of the record became larger
than half of the page (2K / 2 = 1K). This trick is called padding.
In SQL Server 2000 and SQL Server 7.0, there is no need for
this because these versions automatically lock a record. However,
processes can still compete to read, lock, and update a sequence key
value in the same record. This can lead to a deadlock.
The following stored procedure might be used to obtain an
identifier from a table with sequence numbers:
Create Procedure prGetSequenceNumber
return next Id for selected table

and increment the value in SequenceNumbers table
@chvTableName sysname,
@intId int Output
As
read next Id
Select @intId = SequenceNumber
Chapter 10: Advanced Stored Procedure Programming
451
From SequenceNumbers
Where Tablename = 'a table'
increment SequenceNumber
Update SequenceNumbers
Set SequenceNumber = @intId + 1
Where Tablename = 'a table'
Return
For example, it could happen that we have two processes on a
server that need to insert a record into table a. One process might
read a record from the SequenceNumbers table. Let’s assume that the
second process is just a little behind and that it manages to read a
record before the first process can do anything else. Each of them
places a shared lock on the record. Such a lock allows other processes
to read the record but prevents them from updating it until the
originating process finishes. Unfortunately, the first process cannot
update this record any more because of the lock placed on it by the
second process, and the second process cannot update the record
because of the lock by the first process. Each will wait for the other
process to give up. This situation is called a deadlock. SQL Server has
a mechanism that will eventually kill one of the processes so that the
other one can continue. The trouble is that the client application needs
to execute everything again, and that valuable time has been lost.

The standard way to avoid such deadlock is to place a hint in the
From clause of the Select statement that will force SQL Server to
put an update lock instead of a shared lock on a record. An update
lock will prevent other processes from reading and putting locks on
a record until the originating process is complete. Thus, the second
process will wait until the first process is finished. Processes are
thus serialized.
Create Procedure prGetSequenceNumber
return next Id for selected table
and increment the value in SequenceNumbers table
@chvTableName sysname,
@intId int OUTPUT
452
SQL Server 2000 Stored Procedure Programming

454
SQL Server 2000 Stored Procedure Programming
We will demonstrate this solution by changing an earlier example.
You should execute the following against the tempdb database:
Drop Trigger tr_a_I
Drop Table a
Drop Table b
Create Table a (a_id int identity(1,1),
a_desc varchar(20),
b_desc varchar(20))
Go
Create Table b (b_id int identity(1,1),
b_desc varchar(20))
Go
Create Trigger tr_a_I

On dbo.a
After Insert For Insert
As
If @@Rowcount = 0
Return
preserve identity value
Insert Into #ids (TableName, id)
Values ('a', @@identity)
add inserted leases to total
Insert Into b (b_desc)
Select b_desc From inserted
Go
As you can see, the trigger preserves the identity value in
temporary table #ids. This table has to be created from the outer
stored procedure or batch that will insert a record.
Create Table #ids(
TableName sysname,
id int)
Insert Into b (b_desc)
Values ('1')
Insert Into a (a_desc, b_desc)
Values ('aaa', 'bbb')
right identity value
Select id [IdentityValue]
From #ids
Where TableName = 'a'
Drop Table #ids
This time, the result is correct:
IdentityValue


1
TIP:
I do not like either of these solutions. The sequence table is an
archaic approach. It requires many manual steps, and the performance
of the database will suffer because of concurrency problems.
I find the second solution even more distasteful. The trigger is coupled
with the code that is calling it. It depends on the existence of a temporary
table. The developer might forget to create the trigger, or the user might try
to insert records with some other tool. Too many things can go wrong.
Let’s keep this problem in mind and solve it by avoiding such triggers!
Chapter 10: Advanced Stored Procedure Programming
455

Chapter 10: Advanced Stored Procedure Programming
457
You can also generate it in a stored procedure:
Create Procedure prInsertLocation
@Location varchar(50),
@CompanyId int,
@PrimaryContactName varchar(60),
@Address varchar(30) ,
@City varchar(30) ,
@ProvinceId varchar(3) ,
@PostalCode varchar(10),
@Country varchar(20) ,
@Phone varchar(15),
@Fax varchar(15),
@LocationGUID uniqueidentifier OUTPUT
AS
Set @LocationGUID = NewId()

Insert Into Location (Location_id, Location, CompanyId,
PrimaryContactName, Address, City,
ProvinceId, PostalCode, Country,
Phone, Fax)
values (@LocationGUID, @Location, @CompanyId,
@PrimaryContactName, @Address, @City,
@ProvinceId, @PostalCode, @Country,
@Phone, @Fax)
Return @@ERROR
The stored procedure will also return a GUID to the caller.
A WHILE LOOP WITH MIN OR MAX FUNCTIONS
It is possible to iterate through a table or recordset using a While
statement with the aggregate function, which returns extreme
values: MIN and MAX. Take a look at the following batch:
get first value
Select @Value = MIN(Value)
From aTable
loop
While @Value is not null
Begin
do something instead of just displaying a value
Select @Value value
get next value
Select @Value = MIN(Value)
From aTable
And Value > @Value
End
The first Select statement with the Min() function obtains a
first value from the set (table):
Select @Value = MIN(Value)

From aTable
The next value is obtained in a loop as a minimal value bigger
then the previous one:
Select @Value = MIN(Value)
From aTable
And Value > @Value
If no records qualify as members of the set, an aggregate
function will return NULL. We can then use NULL as a criterion to
exit a loop:
While @Value is not null
To demonstrate this method, let’s rewrite prSpaceUsedByTables,
which displays the space used by each user-defined table in the
current database:
Create Procedure prSpaceUsedByTables_4
loop through table names in current database
display info about amount of space used by each table
demonstration of while loop
458
SQL Server 2000 Stored Procedure Programming
As
Set nocount on
Declare @TableName sysname
get first table name
Select @TableName = Min(name)
From sysobjects
Where xtype = 'U'
While @TableName is not null
Begin
display space used
Exec sp_spaceused @TableName

get next table
Select @TableName = Min(name)
From sysobjects
Where xtype = 'U'
And name > @TableName
End
Return 0
This was just an academic example. Naturally, the proper
solution will include a temporary table to collect all results and
display them at the end in one recordset. Note that I am not talking
about a temporary table like we have used for looping using a While
statement in Chapter 4.
You can step backward through the recordset if you use the MAX
function and if you compare the old record and the remainder of the
set using the ‘<‘ operator.
TIP:
This method can be a quick solution for problems that require
iteration. However, solutions based on set operations usually provide
superior performance.
Chapter 10: Advanced Stored Procedure Programming
459
PROPERTY MANAGEMENT
One of the features that I have always wanted to see in SQL Server
is the capability to add descriptions to database objects. Microsoft
Access already has that feature. Naturally, you could be even more
ambitious. It would be perfect on some projects to be able to store
additional attributes such as field formats, input masks, captions,
and the location and size of screen fields in the database as well.
The more things you manage centrally, the fewer maintenance and
deployment issues you will have later in production.

SQL Server 2000 introduces extended properties. Users can define
extended properties, store them in the database, and associate them
with database objects. Each database object can have any number of
extended properties. An extended property can store a
sql_variant value up to 7,500 bytes long.
SQL Server 2000 introduces three stored procedures
and one function for managing extended properties.
sp_addextendedproperty, sp_updateextendedproperty, and
sp_dropextendedproperty are used to create, change, or delete
extended properties. They all have very unusual syntax. We will
examine this syntax in sp_addextendedproperty:
sp_addextendedproperty
[@name =]{'property_name'}
[, [@value =]{'extended_property_value'}
[, [@level0type =]{'level0_object_type'}
, [@level0name =]{'level0_object_name'}
[, [@level1type =]{'level1_object_type'}
, [@level1name =]{'level1_object_name'}
[, [@level2type =]{'level2_object_type'}
, [@level2name =]{'level2_object_name'}
]
]
]
]
Here, @name and @value are the name and value of the extended
property. Other parameters define the name and type of the object
460
SQL Server 2000 Stored Procedure Programming
with which the extended property will be associated. For this reason,
database objects are divided into three levels:

1. User, user-defined type
2. Table, view, stored procedure, function, rule, default
3. Column, index, constraint, trigger, parameter
If you want to assign an extended property to an object of the
second level, you must also specify an object of the first level. If you
want to assign an extended property to an object of the third level,
you must also specify an object of the second level. For example, to
specify an extended property ‘Format’ to associate with the column
‘Phone’ in the table ‘Contact’, you must specify the owner of the table:
Exec sp_addextendedproperty 'Format', '(999)999-9999',
'user', dbo,
'table', Contact,
'column', Phone
The FN_LISTEXTENDEDPROPERTY function is designed to list the
extended properties of an object. It requires that you specify objects
in the same manner as the stored procedures do. You can see the
resultset returned by the function in Figure 10-8.
SUMMARY
In this chapter, we have demonstrated techniques for the dynamic
construction of queries. We have compared different techniques to
see how SQL Server reuses execution plans and what effect such
reuse has on performance.
We have demonstrated the use of timestamp fields to implement
optimistic locking in SQL Server and solutions for loading timestamp
values into client applications.
Microsoft Search Service is a search engine that allows full-text
indexing and querying much like the services we use to query the
Web. The engine is not relational, but it is possible to use it from
stored procedures and Transact-SQL. It gives a new dimension to
documents stored in SQL Server databases.

Chapter 10: Advanced Stored Procedure Programming
461
Special attention was paid to the issue of nested stored procedures
and the use of cursors and temporary tables to transfer information
from outer to inner stored procedures and back.
Some implementations require additional work to generate and
use unique identifiers for records in tables. Several problems,
solutions, and techniques were discussed.
Pay special attention to the implementation of looping using
Min() and Max() functions. This solution is superior to the use of
cursors (better performance, better maintainability, and reduced
chance of errors).
An interesting new feature is the capability to associate additional
attributes called extended properties with database objects. This new
feature provides some interesting opportunities. For example, it is
possible to manage some application behaviors using data from a
single source within the database.
462
SQL Server 2000 Stored Procedure Programming
Figure 10-8. Extended properties of an object

×