SQL RDBMS
SQL RDBMS
RDBMS Concepts............................................................................................................... 3
Introduction..................................................................................................................... 3
What is a Database? ........................................................................................................ 4
Database Tools................................................................................................................ 5
Introduction to Normalization........................................................................................... 14
SQL Server Database Architecture................................................................................... 27
The SQL Server Engine ................................................................................................ 30
SQL Data Types................................................................................................................ 53
Key Data Types............................................................................................................. 53
User-Defined Datatypes :.............................................................................................. 54
Primary key................................................................................................................... 60
Foreign key ................................................................................................................... 65
Constraints .................................................................................................................... 72
Joins .................................................................................................................................. 88
Subqueries....................................................................................................................... 103
Correlated Subqueries................................................................................................. 107
Functions......................................................................................................................... 114
Introduction................................................................................................................. 114
Index ............................................................................................................................... 122
Index Space Requirements.......................................................................................... 131
Managing an Index ..................................................................................................... 134
Using an Index ............................................................................................................ 139
Views .............................................................................................................................. 142
Introduction................................................................................................................. 142
Programming with Transact-SQL................................................................................... 151
Variables ..................................................................................................................... 154
Local Variables ....................................................................................................... 154
Session Variables .................................................................................................... 157
Control-of-Flow Tools ................................................................................................ 159
CASE .......................................................................................................................... 159
Cousins of CASE .................................................................................................... 161
PRINT ......................................................................................................................... 162
RAISERROR .............................................................................................................. 163
FORMATMESSAGE ................................................................................................. 165
Operators..................................................................................................................... 165
Arithmetic Operators .............................................................................................. 165
Bit Operators........................................................................................................... 167
Comparison Operators ............................................................................................ 168
Scalar Functions.......................................................................................................... 172
Conversion Functions ............................................................................................. 172
Day First or Month First ......................................................................................... 176
Date and Time Functions ........................................................................................ 181
Math Functions ....................................................................................................... 183
String Functions ...................................................................................................... 186
SOUNDEX and DIFFERENCE Functions............................................................. 192
System Functions ........................................................................................................ 193
RDBMS Concepts
Introduction
y
You can use Microsoft Access on a client workstation to access databases on a Microsoft *SQL
server.
When you query a database, the SQL server can process the query for you and send the results
to your workstation. In contrast, if you query a database on a file server, the file server must send
the entire database to your workstation so that your workstation can process the query. Thus,
using SQL Server enables you to reduce the traffic on your network.
y
Suppose any company has a customer information database that is 50 MB in size. If you query
the database for a single customer's information and the database is stored on a file server, all 50
MB of data must be sent to your workstation so that your computer can search for the customer.
In contrast, if you query the same database stored on a SQL server, the SQL server processes
your query and sends only the one customer's information to your workstation.
Structured Query Language (SQL) is a standardized set of commands used to work with
databases.
$
Relational Database Management System (RDBMS) uses established relationships between the
data in a database to ensure the integrity of the data. These relationships enable you to prevent
users from entering incorrect data.
That which contains the physical implementation of the schema and the data is called as
database. The *schema conceptually describes the *problem space to the database.
When you install SQL Server, the Setup utility automatically creates several system and sample
user databases. System databases contain information used by SQL Server to operate. You
create user databases-and they can contain any information you need to collect. You can use
*SQL Server Query Analyzer to *query any of your SQL databases, including the system and
sample databases.
y
• Distribution
¾ This database is not created until you configure replication because it contains
history information about replication.
• Master
¾ Information about the operation of SQL Server, including user accounts, other
SQL Servers, environment variables, error messages, databases, storage space
allocated to databases, and the tapes and disk drives on the SQL Server.
• Model
¾ A template for creating new database
• Msdb
¾ Information about all scheduled job on your server. This information is used by
the SQL Server Agent service.
• Northwind
¾ A sample user database for learning SQL Server. It contains information about a
fictitious gourmet food company’s customer, sales and employees.
• Pubs
¾ A sample user database for learning SQL Server. It contains information about a
fictitious publishing company’s authors, publishers, royalties and titles.
• Tempdb
¾ It contains temporary information and is used as a scratchpad by SQL Server.
• Database schema, which is conceptual, not physical is the translation of the conceptual
model into a physical representation that can be implemented using a DBMS.
• Problem space is the well defined part of the real world which is relational databases
analogies, intended to model some aspect of the real world.
• SQL Query Analyzer is used to run SQL queries as well as to optimize the performance
of the queries.
• Query is simply a command consisting of one or more SQL statements, which you send
to SQL server to request data from the server, change data, or delete data.
$
The schema is nothing more than the data model expressed in the terms that you will use to
describe it to the database engine tables and triggers and such creatures.
Database Tools
ADO .NET
ADO
DAO
Database Engine
Database Engines
At the lowest level are the database engines. These are sometimes called "back ends," but that's
a bit sloppy since the term "back end" really refers to a specific physical architecture. These tools
will handle the physical manipulation of storing data onto the disk and feeding it back on demand.
We'll be looking at two: the Jet database engine and SQL Server. You may be surprised not to
see Microsoft Access here. Access is technically a front-end development environment that uses
either Jet or SQL Server natively and can, in fact, use any ODBC-compliant database engine as
its data store. It uses the Jet database engine to manipulate data stored in .mdb files and SQL
Server (or another ODBC data store) for data stored in .adp files. Access has always used the Jet
database engine, although Microsoft didn't expose it as a separate entity until the release of
Microsoft Visual Basic 3.
The Jet database engine and SQL Server, although very different, are both wonderful tools for
storing and manipulating data. The difference between them lies in their architectures and the
Front-End Development
Once the physical definition of your database is in place, you'll need tools to create the forms and
reports your users will interact with. We'll draw our example from two of these: Access and Visual
Studio .NET (specifically, Visual Basic .NET). Again, there are hundreds of front-end tools
The relational model is not the only method available for storing and manipulating data.
Alternatives include the hierarchical, network, and Object/Data models. Each of these models has
its advocates, and each has its advantages for certain tasks.
Because of its efficiency and flexibility, the relational model is by far the most popular database
technique. Both the Microsoft Jet database engine and Microsoft SQL Server implement the
relational model. In general terms, relational database systems have the following characteristics:
If you've worked with Microsoft Access databases at all, you'll recognize a "relation" as a
‘recordset’ or, in SQL Server terms, as a "result set." Dr. Codd, when formulating the relational
model, chose the term "relation" because it was comparatively free of connotations, unlike, for
example, the word "table." It's a common misconception that the relational model is so called
because relationships are established between tables. In fact, the name is derived from the
relations on, which it’s based.
In fact, relations need not have a physical representation at all. A given relation might map to an
actual physical table someplace on a disk, but it can just as well be based on columns drawn
from half a dozen different tables, with a few calculated columns which aren't physically stored
anywhere. A relation is a relation provided that it's arranged in row and column format and its
values are scalar. Its existence is completely independent of any physical representation.
The requirement that all values in a relation be scalar can be somewhat treacherous. The
concept of "one value" is necessarily subjective, based as it is on the semantics of the data
model. To give a common example, a "Name" might be a single value in one model, but another
environment might require that the value be split into "Title", "Given Name", and "Surname", and
another might require the addition of "Middle Name" or "Title of Courtesy". None of these is more
or less correct in absolute terms; it depends on the use to which the data will be put.
The principle of closure that both base tables and the results of operations are represented
conceptually as relation enables the results of one operation to be used as the input to another
operation. Thus, with both the Jet database engine and SQL Server we can use the results of one
query as the basis for another. This provides database designers with functionality similar to a
subroutine in procedural development: the ability to encapsulate complex or commonly performed
operations and reuse them whenever and wherever necessary.
y
You might have created a query called FullNameQuery that concatenates the various attributes
representing an individual's name into a single calculated field called FullName. You can create a
second query using FullNameQuery as a source that uses the calculated FullName field just like
any field that's actually present in the base table. There is no need to recalculate the name.
Relational Terminology
The entire structure is said to be, a relation. Each row of data is a tuple (rhymes with "couple").
Technically, each row is an n-tuple, but the "n-" is usually dropped. The number of tuples in a
relation determines its cardinality. In this case, the relation has a cardinality of 18. Each column in
the tuple is called an attribute. The number of attributes in a relation determines its degree.
The relation is divided into two sections, the heading and the body. The tuples make up the body,
while the heading is composed of, well, the heading. Note that in its relational representation the
label for each attribute is composed of two terms separated by a colon.
y
UnitPrice:Currency. The first part of the label is the name of the attribute, while the second part is
its domain. The domain of an attribute is the "kind" of data it representsin this case, currency. A
domain is not the same as a data type.
Entities
It's difficult to provide a precise formal definition of the term entity, but the concept is intuitively
quite straightforward: An entity is anything about which the system needs to store information.
When you begin to design your data model, compiling an initial list of entities isn't difficult. When
you (or your clients) talk about the problem space, most of the nouns and verbs used will be
candidate entities. "Customers buy products. Employees sell products. Suppliers sell us
products." The nouns "Customers," "Products," "Employees," and "Suppliers" are all clearly
entities.
The events represented by the verbs "buy" and "sell" are also entities, but a couple of traps exist
here. First, the verb "sell" is used to represent two distinct events: the sale of a product to a
customer (Salesman Customer) and the purchase of a product by the organization (Supplier
Company). That's fairly obvious in this example, but it's an easy trap to fall into, particularly if
you're not familiar with the problem space.
The second trap is the inverse of the first: two different verbs ("buy" in the first sentence and "sell"
in the second) are used to describe the same event, the purchase of a product by a customer.
Again, this isn't necessarily obvious unless you're familiar with the problem space. This problem
is often trickier to track down than the first. If a client is using different verbs to describe what
appears to be the same event, they might in fact be describing different kinds of events. If the
client is a tailor, for example, "customer buys suit" and "customer orders suit" might both result in
the sale of a suit, but in the first case it's a prêt-à-porter sale and in the second it's bespoke.
These are very different processes that will probably need to be modeled differently.
In addition to interviewing clients to establish a list of entities, it's also useful to review any
documents that exist in the problem space. Input forms, reports, and procedures manuals are all
good sources of candidate entities. You must be careful with documents, however. Printed
documents have a great deal of inertia input forms particularly are expensive to print and
frequently don't keep up with changes to policies and procedures. If you stumble across an entity
that's never come up in an interview, don't assume the client just forgot to mention it. Chances
are that it's a legacy item that's no longer pertinent to the organization. You'll need to check.
Most entities model objects or events in the physical world: customers, products, or sales calls.
These are concrete entities. Entities can also model abstract concepts. The most common
example of an abstract entity is one that models the relationship between other entities for
example, the fact that a certain sales representative is responsible for a certain client or that a
certain student is enrolled in a certain class.
Sometimes all you need to model is the fact that a relationship exists. Other times you'll want to
store additional information about the relationships, such as the date on which it was established
or some characteristic of the relationship. The relationship between cougars and coyotes is
competitive, that between cougars and rabbits is predatory, and it's useful to know this if you're
planning an open-range zoo.
Whether relationships that do not have attributes ought to be modeled as separate entities is a
matter of some discussion. I personally don't think anything is gained by doing so, and it
complicates the process of deriving a database schema from the data model. However,
understanding that relationships are as important as entities is crucial for designing an effective
data model.
Attributes
Your system will need to keep track of certain facts about each entity. As we've seen, these facts
are the entity's attributes. If your system includes a Customer entity, for example, you'll probably
want to know the names and addresses of the customers and perhaps the businesses they're in.
All of these are attributes.
Determining the attributes to be included in your model is a semantic process. That is, you must
make your decisions based on what the data means and how it will be used. Let's look at one
common example: an address. Do you model the address as a single entity (the Address) or as a
set of entities (HouseNumber, Street, City, State, ZipCode)? Most designers (myself included)
This leads me to strategy two: Find the exceptions. There are two sides to this strategy. First, that
you must identify all the exceptions, and second, that you must design the system to handle as
many exceptions as you can without confusing users. To illustrate what this means, let's walk
through another example: personal names.
Remember that there's a trade-off between flexibility and complexity. While it's important to catch
as many exceptions as possible, it's perfectly reasonable to eliminate some of them as too
unlikely to be worth the cost of dealing with them.
Distinguishing between entities and attributes is sometimes difficult. Again, addresses are a good
example, and again, your decision must be based on the problem space. Some designers
advocate the creation of a single address entity used to store all the addresses modeled by the
system. From an implementation viewpoint, this approach has certain advantages in terms of
encapsulation and code reuse. From a design viewpoint, I have some reservations.
It's unlikely, for example, that addresses for employees and customers will be used in the same
way. Mass mailings to employees, for example, are more likely to be done via internal mail than
the postal service. This being the case, the rules and requirements are different. That awful data
entry screen or something very like it might very well be justified for customer addresses, but by
using a single address entity you're forced to use it for employees as well, where it's unlikely to be
either necessary or appreciated.
Domains
You might recall from the beginning of this chapter that a relation heading contains an
AttributeName:DomainName pair for each attribute. More particularly, a domain is the set of all
possible values that an attribute may validly contain.
Domains are often confused with data types; they are not the same. Data type is a physical
concept while domain is a logical one. "Number" is a data type; "Age" is a domain. To give
another example, "StreetName" and "Surname" might both be represented as text fields, but they
are obviously different kinds of text fields; they belong to different domains. Take, for example,
the domain DegreeAwarded, which represents the degrees awarded by a university. In the
database schema, this attribute might be defined as Text[3], but it's not just any three-character
string, it's a member of the set {BA, BS, MA, MS, PhD, LLD, MD}.
Of course, not all domains can be defined by simply listing their values. Age, for example,
contains a hundred or so values if we're talking about people, but tens of thousands if we're
talking about museum exhibits. In such instances it's easier to define the domain in terms of the
rules that can be used to determine the membership of any specific value in the set of all valid
values rather than listing the values. For example, PersonAge could be defined as "an integer in
the range 0 to 120," whereas ExhibitAge might simply be "an integer equal to or greater than 0."
At this point you might be thinking that a domain is the combination of the data type and the
validation rule. But validation rules are strictly part of the data integrity, not part of the data
description. For example, the validation rule for a zip code might refer to the State attribute,
Unfortunately, neither the Jet database engine nor SQL Server provides strong intrinsic support
for domains beyond data types. And even within data types, neither engine performs strong
checking; both will quietly convert data behind the scenes. For example, if you're using Microsoft
Access and have defined EmployeeID as a long integer in the Employees table and InvoiceTotal
as a currency value in the Invoices, you can create a query linking the two tables on EmployeeID
= InvoiceTotal, and Microsoft Jet will quite happily give you a list of all employees who have an
EmployeeID that matches the total value of an invoice. The two attributes are not type-
compatible, but the Jet database engine doesn't know or care.
So why bother with domains at all? Because, they're extremely useful design tools. "Are these
two attributes interchangeable?" "Are there any rules that apply to one but don't apply to the
other?" These are important questions when you're designing a data model, and domain analysis
helps you think about them.
Relationships
In addition to the attributes of each entity, a data model must specify the relationships between
entities. At the conceptual level, relationships are simply associations between entities. The
statement "Customers buy products" indicates that a relationship exists between the entities
Customers and Products. The entities involved in a relationship are called its participants. The
number of participants is the degree of the relationship. (The degree of a relationship is similar to,
but not the same as, the degree of a relation, which is the number of attributes.)
The vast majority of relationships are binary, like the "Customers buy products" example, but this
is not a requirement. Ternary relationships, those with three participants, are also common. Given
the binary relationships "Employees sell products" and "Customers buy products," there is an
implicit ternary relationship "Employees sell products to customers." However, specifying the two
binary relationships does not allow us to identify which employees sold which products to which
customers; only a ternary relationship can do that.
A special case of a binary relationship is an entity that participates in a relationship with itself.
This is often called the bill of materials relationship and is most often used to represent
hierarchical structures. A common example is the relationship between employees and
managers: Any given employee might both be a manager and have a manager.
The relationship between any two entities can be one-to-one, one-to-many, or many-to-many.
One-to-one relationships are rare, but can be useful in certain circumstances.
One-to-many relationships are probably the most common type. An invoice includes many
products. A salesperson creates many invoices. These are both examples of one-to-many
relationships.
Although not as common as one-to-many relationships, many-to-many relationships are also not
unusual and examples abound. Customers buy many products, and products are bought by many
customers. Teachers teach many students, and students are taught by many teachers. Many-to-
many relationships can't be directly implemented in the relational model, but their indirect
implementation is quite straightforward.
Introduction to Normalization
Basic Principles
The process of structuring the data in the problem space to achieve the two goals i.e. eliminating
redundancy and ensuring flexibility is called normalization. The principles of normalization are
tools for controlling the structure of data in the same way that a paper clip controls sheets of
paper. The normal forms (we'll discuss six) specify increasingly stringent rules for the structure of
relations. Each form extends the previous one in such a way as to prevent certain kinds of update
anomalies.
Bear in mind that the normal forms are not a prescription for creating a "correct" data model. A
data model could be perfectly normalized and still fail to answer the questions asked of it; or, it
might provide the answers, but so slowly and awkwardly that the database system built around it
is unusable. But if your data model is normalized that is, if it conforms to the rules of relational
structure the chances are high that the result will be an efficient, effective data model.
Before we turn to normalization, however, you should be familiar with a couple of underlying
principles.
Lossless Decomposition
The relational model allows relations to be joined in various ways by linking attributes. The
process of obtaining a fully normalized data model involves removing redundancy by dividing
relations in such a way that the resultant relations can be recombined without losing any of the
information. This is the principle of lossless decomposition.
y From the given relation in figure1, you can derive two relations as shown in figure 2
It is sometimes the case although it doesn't happen often that there are multiple possible
candidate keys for a relation. In this case, it is customary to designate one candidate key as a
primary key and consider other candidate keys alternate keys. This is an arbitrary decision and
isn't very useful at the logical level. (Remember that the data model is purely abstract.).
When the only possible candidate key is unwieldy it requires too many attributes or is too large,
for example you can use an artificial unique field data type for creating artificial keys with values
that will be generated by the system.
Called AutoNumber fields in Microsoft Jet and Identity fields in SQL Server, fields based on this
data type are useful tools, provided you don't try to make them mean anything. They're just tags.
They aren't guaranteed to be sequential, you have very little control over how they're generated,
and if you try to use them to mean anything you'll cause more problems than you solve.
Although choosing candidate keys is, as we've seen, a semantic process, don't assume that the
attributes you use to identify an entity in the real world will make an appropriate candidate key.
Individuals, for example, are usually referred to by their names, but a quick look at any phone
book will establish that names are hardly unique.
Of course, the name must provide a candidate key when combined with some other set of
attributes, but this can be awkward to determine. I once worked in an office with about 20 people,
of whom two were named Larry Simon and one was named Lary Simon. All three were Vice
Presidents. Amongst ourselves, they were "Short Lary," "German Larry," and "Blond Larry"; that's
height, nationality, and hair color combined with name, hardly a viable candidate key.
In situations like this, it's probably best to use a system-generated ID number, such as an
Autonumber or Identity field, but remember, don't try to make it mean anything. You need to be
careful, of course, that your users are adding apparent duplicates intentionally, but it's best to do
this as part of the user interface rather than imposing artificial (and ultimately unnecessary)
constraints on the data the system can maintain.
Functional Dependency
The concept of functional dependency is an extremely useful tool for thinking about data
structures. Given any tuple T, with two sets of attributes {X1...Xn} and {Y1...Yn} (the sets need not
be mutually exclusive), then set Y is functionally dependent on set X if, for any legal value of X,
there is only one legal value for Y.
For example, in the relation shown in Figure 3, every tuple that has the same values for
{CategoryName} will have the same value for {Description}. We can therefore say that the
attribute CategoryName functionally determines the attribute Description. Note that functional
dependency doesn't necessarily work the other way: knowing a value for Description won't allow
us to determine the corresponding value for CategoryID.
You can indicate the functional dependency between sets of attributes as shown in figure 4
In text, you can express functional dependencies as X __Y, which reads "X functionally
determines Y."
figure 4 Functional Dependency Diagram Are Largely self - Explanatory
We saw some of the problems involved in determining whether an attribute is scalar when we
looked at the modeling of names and addresses earlier. Dates are another tricky domain. They
consist of three distinct components: the day, the month, and the year. Ought they be stored as
three attributes or as a composite? As always, the answer can be determined only by looking to
the semantics of the problem space you're modeling.
If your system most often uses all three components of a date together, it is scalar. But if your
system must frequently manipulate the individual components of the date, you might be better off
storing them as separate attributes. You might not care about the day, for example, but only the
month and year.
In the specific instance of dates, because date arithmetic is tedious to perform, it will often make
your life easier if you use an attribute defined on the DateTime data type, which combines all
three components of the date and the time. The DateTime data types allow you to offload the
majority of the work involved in, for example, determining the date 37 days from today, to the
development environment.
Another place people frequently have problems with non-scalar values is with codes and flags.
Many companies assign case numbers or reference numbers that are calculated values, usually
something along the lines of REF0010398, which might indicate that this is the first case opened
in March 1998. While it's unlikely that you'll be able to alter company policy, it's not a good idea to
attempt to manipulate the individual components of the reference number in your data model.
It's far easier in the long run to store the values separately: {Reference#, Case#, Month, Year}.
This way, determining the next case number or the number of cases opened in a given year
becomes a simple query against an attribute and doesn't require additional manipulation. This
has important performance implications, particularly in client/server environments, where
extracting a value from the middle of an attribute might require that each individual record be
examined locally (and transferred across the network) rather than by the database server.
Another type of non-scalar attribute that causes problems for people is the bit flag. In
conventional programming environments, it's common practice to store sets of Boolean values as
individual bits in a word, and then to use bitwise operations to check and test them. Windows API
programming relies heavily on this technique, for example. In conventional programming
environments, this is a perfectly sensible thing to do. In relational data models, it is not. Not only
does the practice violate first normal form, but it's extraordinarily tedious and, as a general rule,
inefficient.
There's another kind of non-scalar value to be wary of when checking a relation for first normal
form: the repeating group. Figure 6 shows an Invoice relation. Someone, at some point, decided
that customers are not allowed to buy more than three items. This is almost certainly an artificial
constraint imposed by the system, not the business. Artificial system constraints are evil, and in
this case, just plain wrong as well.
Figure 6 This Data Model Restricts the number of Items a Customer Can Purchase
Another example of a repeating group is shown in figure 7. This isn't as obvious an error, and
many successful systems have been implemented using a model similar to this. But this is really
just a variation of the structure shown in figure 6 and has the same problems. Imagine the query
to determine which products exceeded target by more than 10 percent any time in the first
quarter.
A better model would be that shown in figure 9. Logically, this is an issue of not trying to
represent two distinct entities, Products and Suppliers, in a single relation. By separating the
representation, you're not only eliminating the redundancy, you're also providing a mechanism for
storing information that you couldn't otherwise capture. In the example in figure 9, it becomes
possible to capture information about Suppliers before obtaining any information regarding their
products. That could not be done in the first relation, since neither component of a primary key
can be empty.The other way that people get into trouble with second normal form is in confusing
constraints that happen to be true at any given moment with those that are true for all time.
The relation shown in figure 10, for example, assumes that a supplier has only one address,
which might be true at the moment but will not necessarily remain true in the future.
Figure 10 Suppliers might have more than one Address
It's possible to get really pedantic about third normal form. In most places, for example, you can
determine a PostalCode value based on the City and Region values, so the relation shown in
figure 12 is not strictly in third normal form.
The two relations shown in figure 13 are technically more correct, but in reality the only benefit
you're gaining is the ability to automatically look up the PostalCode when you're entering new
records, saving users a few keystrokes. This isn't a trivial benefit, but there are probably better
ways to implement this functionality, ones that don't incur the overhead of a relation join every
time the address is referenced.
As with every other decision in the data modeling process, when and how to implement third
normal form can only be determined by considering the semantics of the model. It's impossible to
give fixed rules, but there are some guidelines:
Postal codes do change, but not often; and they aren't intrinsically important in most systems. In
addition, a separate postal code table is impractical in most real-world applications because of
the varying rules for how postal codes are defined.
Further Normalisation
The first three normal forms were included in Codd's original formulation of relational theory, and
in the vast majority of cases they're all you'll need to worry about. The further normal
formsBoyce/Codd, fourth, and fifth have been developed to handle special cases, most of which
are rare.
The easiest way to understand Boyce/Codd normal form is to use functional dependencies.
Boyce/Codd normal form states, essentially, that there must be no functional dependencies
between candidate keys. Take, for example, the relation shown in figure 14. The relation is in
third normal form (assuming supplier names are unique), but it still contains significant
redundancy.
Figure 14 This Relation is in Third Normal form but not in Boyce/Codd normal Form
The two candidate keys in this case are {SupplierID, ProductID} and {SupplierName, ProductID},
and the functional dependency diagram is shown in figure 15
As you can see, there is a functional dependency {SupplierID} ____{ SupplierName}, which is in
violation of Boyce/Codd normal form. A correct model is shown in figure 16. Figure 16
multiple package sizes, that they are sourced from multiple suppliers, and that all suppliers
provide all pack sizes.
Now, the first step in normalizing this relation is to eliminate the non-scalar PackSize attribute,
resulting in the relation shown in
Figure 18 The version of relationship Shown in figure 17 is in Boyce/Codd Normal Form.
A multi-valued dependency pair is two mutually-independent sets of attributes. In figure 17, the
multi-valued dependency is {Product-Name} {PackSize}| {SupplierName}, which is read "Product
multi-determines PackSize and Supplier." Fourth normal form states, informally, that multi-valued
dependencies must be divided into separate relations. Formally, a relation is in fourth normal form
if it is in Boyce/Codd normal form, and in addition, all the multi-valued dependencies are also
functional dependencies out of the candidate keys.
Decomposing the relation into three distinct relations (SupplierProduct, ProductCustomer, and
SupplierCustomer) eliminates this problem but causes problems of its own; in re-creating the
original relation, all three relations must be joined. Interim joins of only two relations will result in
invalid information.
From a system designer's point of view, this is a terrifying situation, since there's no intrinsic
method for enforcing the three-table join except through security restrictions. Further, if a user
should create an interim result set, the results will seem perfectly reasonable, and it's unlikely that
the user will be able to detect the error by inspection.
Introduction
Because SQL Server is a client/server database management system, you will find components
of its architecture on both the client and the server itself.
Client-Server Architecture
Client Architecture
On the client, the SQL Server architecture consists of the client application, a database interface,
and a Net-Library. Clients use applications such as the SQL Server utilities, Microsoft Access, or
a custom application to access the SQL Server. The client application uses a database interface
to connect to and access resources on the SQL server. The database interface consists of an
application programming interface (API) and a data object interface. SQL Server supports two
classes of APIs: Object Linking And Embedding Database (OLE DB) and Open Database
Connectivity (ODBC). The OLE DB API uses the Component Object Model (COM)-based
interface and is modular. In contrast, the ODBC API uses calls to access the SQL Server directly
via the Tabular Data Stream (TDS) protocol. While OLE DB can be used to access many different
types of data sources, ODBC can be used to access data only in relational databases.
SQL Server supports two data object interfaces: ActiveX Data Objects and Remote Data Objects.
ActiveX Data Objects (ADO) enable you to encapsulate the OLE DB API commands in order to
reduce application development time. You can use ADO from Visual Basic, Visual Basic for
Applications, Active Server Pages, and the Microsoft Internet Explorer scripting object model.
Remote Data Objects (RDO) enable you to encapsulate the ODBC API. You can use RDO from
Server Architecture
On the server, the SQL Server architecture consists of the SQL Server database engine, Open
Data Services, and the server's Net-Library. The SQL Server database engine actually processes
client requests. The database engine consists of two components: the relational engine and the
storage engine. The relational engine is responsible for parsing and optimizing queries; the
relational engine retrieves data from the storage engine. The storage engine is responsible for
retrieving and modifying data on your server's hard drives. Open Data Services manages clients'
connections to the server. Specifically, the server receives client requests and responds through
Open Data Services. The server's Net-Library accepts connection requests from a client's Net-
Library.
Administration Architecture
SQL Server uses the SQL Distributed Management Objects (SQL-DMO) to write all of its
administrative utilities. SQL-DMO is a collection of Component Object Model (COM)-based
objects that are used by the administrative tools. SQL-DMO essentially enables you to create
administrative tools that hide the Transact SQL statements from the user. All of the SQL Server
graphical administrative tools use SQL-DMO. For example, you can create a database within
SQL Server Enterprise Manager without ever having to know or understand the CREATE
DATABASE Transact SQL-statement.
Application Architecture
When you design an application, there are several different client/server architectures you can
choose from. These architectures vary as to how much of the data processing is done by the SQL
server as compared to the client. Before you look at the architectures, it is important that you
understand that all client/server applications consist of three layers:
• Presentation- The user interface (this layer usually resides on the client).
• Business- The application's logic and rules for working with the data (this layer can be on
the server, client, or both).
±1. What components make up the database interface in the SQL Server architecture?
³ The database interface consists of an API and a data object interface. SQL Server supports
the OLE DB and ODBC APIs, and the ActiveX Data Objects and Remote Data Objects data
object interfaces.
Logical Architecture
The Net-Library
The Net-Library (often called Net-Lib, but in this book I'll use Net-Library) abstraction layer
enables SQL Server to read from and write to many different network protocols, and each such
protocol (such as TCP/IP sockets) can have a specific driver. The Net-Library layer makes it
relatively easy to support many different network protocols without having to change the core
server code.
SQL Server uses the Net-Library abstraction layer on both the server and client machines,
making it possible to support several clients simultaneously on different networks. Microsoft
Windows NT/2000 and Windows 98 support the simultaneous use of multiple protocol stacks.
Net-Libraries are paired. For example, if a client application is using a Named Pipes Net-Library,
SQL Server must also be listening on a Named Pipes Net-Library. The client application
determines which Net-Library is actually used for the communication, and you can control the
client application's choice by using a tool called the Client Network Utility. You can easily
configure SQL Server to listen on multiple Net-Libraries by using the Server Network Utility, which
is available under Programs\Microsoft SQL Server on the Start menu.
SQL Server 2000 has two primary Net-Libraries: Super Socket and Shared Memory. TCP/IP,
Named Pipes, IPX/SPX, and so on are referred to as secondary Net-Libraries. The OLE DB
Provider for SQL Server, SQL Server ODBC driver, DB-Library, and the database engine
communicate directly with these two primary network libraries. Intercomputer connections
communicate through the Super Socket Net-Library. Local connections between an application
and a SQL Server instance on the same computer use the Shared Memory Net-Library if Shared
Memory support has been enabled (which it is, by default). SQL Server 2000 supports the Shared
Memory Net-Library on all Windows platforms.
Communication path
If the client is configured to communicate over TCP/IP Sockets or NWLink IPX/SPX connection,
the Super Socket Net-Library directly calls the Windows Socket 2 API for the communication
between the application and the SQL Server instance.
If the client is configured to communicate over a Named Pipes, Multiprotocol, AppleTalk, or
Banyan VINES connection, a subcomponent of the Super Socket Net-Library called the Net-
Library router loads the secondary Net-Library for the chosen protocol and routes all Net-Library
calls to it.
Encryption layer
The encryption is implemented using the Secure Sockets Layer (SSL) API. The level of
encryption, 40-bit or 128-bit, depends on the Windows version on the application and SQL
Connect events
When a connect event occurs, SQL Server initiates a security check to determine whether a
connection is allowed. Other ODS applications, such as a gateway to DB/2, have their own logon
handlers that determine whether connections are allowed. Events also exist that close a
connection, allowing the proper connection cleanup to occur.
Language events
ODS also generates events based on certain client activities and application activities. These
events allow an ODS server application to respond to changes to the status of the client
connection or of the ODS server application.
In addition to handling connections, ODS manages threads (and fibers) for SQL Server. It takes
care of thread creation and termination and makes the threads available to the User Mode
Scheduler (UMS). Since ODS is an open interface with a full programming API and toolkit,
independent software vendors (ISVs) writing server applications with ODS get the same benefits
that SQL Server derives from this component, including SMP-capable thread management and
pooling, as well as network handling for multiple simultaneous networks. This multithreaded
operation enables ODS server applications to maintain a high level of performance and
availability and to transparently use multiple processors under Windows NT/2000 because the
operating system can schedule any thread on any available processor.
The optimizer
The optimizer takes the query tree from the command parser and prepares it for execution. This
module compiles an entire command batch, optimizes queries, and checks security. The query
optimization and compilation result in an execution plan.
The first step in producing such a plan is to normalize each query, which potentially breaks down
a single query into multiple, fine-grained queries. After the optimizer normalizes a query, it
optimizes it, which means that the optimizer determines a plan for executing that query. Query
optimization is cost-based; the optimizer chooses the plan that it determines would cost the least
based on internal metrics that include estimated memory requirements, estimated CPU utilization,
This query can be parameterized as if it were a stored procedure with a parameter for the value
of type:
The expression manager copies the value of qty from the row set returned by the storage engine,
multiplies it by 10, and stores the result in @myqty.
The relational engine implements the table scan by requesting one row set containing all the rows
from ScanTable. This next SELECT statement needs only information available in an index:
The relational engine implements the index scan by requesting one row set containing the leaf
rows from the index that was built on the LastName column. The following SELECT statement
needs information from two indexes:
SELECT CompanyName, OrderID, ShippedDate
FROM Northwind.dbo.Customers AS Cst
JOIN Northwind.dbo.Orders AS Ord
ON (Cst.CustomerID = Ord.CustomerID)
The relational engine requests two row sets: one for the nonclustered index on Customers and
the other for one of the clustered indexes on Orders.
The relational engine uses the OLE DB API to request that the storage engine open the row sets.
As the relational engine works through the steps of the execution plan and needs data, it uses
OLE DB to fetch the individual rows from the row sets it asked the storage engine to open. The
storage engine transfers the data from the data buffers to the relational engine.
A session opens a table, requests and evaluates a range of rows against the conditions in the
WHERE clause, and then closes the table. A session descriptor data structure (SDES) keeps
track of the current row and the search conditions for the object being operated on (which is
identified by the object descriptor data structure, or DES).
In-place mode
This mode is used to update a heap or clustered index when none of the clustering keys change.
The update can be done in place, and the new data is written to the same slot on the data page.
Since SQL Server maintains ordering in index leaf levels, you do not need to unload and reload
data to maintain clustering properties as data is added and moved. SQL Server always inserts
Uncommitted Read
Uncommitted Read, or dirty read (not to be confused with "dirty page," which I'll discuss later) lets
your transaction read any data that is currently on a data page, whether or not that data has been
committed. For example, another user might have a transaction in progress that has updated
data, and even though it's holding exclusive locks on the data, your transaction can read it
anyway. The other user might then decide to roll back his or her transaction, so logically those
changes were never made. If the system is a single-user system and everyone is queued up to
access it, the changes will not have been visible to other users. In a multiuser system, however,
you read the changes and take action based on them. Although this scenario isn't desirable, with
Uncommitted Read you can't get stuck waiting for a lock, nor do your reads issue share locks
(described in the next section) that might affect others.
When using Uncommitted Read, you give up the assurance of strongly consistent data in favor of
high concurrency in the system without users locking each other out. So when should you choose
Uncommitted Read? Clearly, you don't want to use it for financial transactions in which every
number must balance. But it might be fine for certain decision-support analyses—for example,
Committed Read
Committed Read is SQL Server's default isolation level. It ensures that an operation never reads
data that another application has changed but not yet committed. (That is, it never reads data that
logically never existed.) With Committed Read, if a transaction is updating data and consequently
has exclusive locks on data rows, your transaction must wait for those locks to be released before
you can use that data (whether you're reading or modifying). Also, your transaction must put
share locks (at a minimum) on the data that will be visited, which means that data might be
unavailable to others to use. A share lock doesn't prevent others from reading the data, but it
makes them wait to update the data. Share locks can be released after the data has been sent to
the calling client—they don't have to be held for the duration of the transaction.
Although a transaction can never read uncommitted data when running with Committed
Read isolation, if the transaction subsequently revisits the same data, that data might have
changed or new rows might suddenly appear that meet the criteria of the original query. If data
values have changed, we call that a non-repeatable read. New rows that appear are called
phantoms.
Repeatable Read
The Repeatable Read isolation level adds to the properties of Committed Read by ensuring that if
a transaction revisits data or if a query is reissued, the data will not have changed. In other words,
issuing the same query twice within a transaction will not pick up any changes to data values
made by another user's transaction. However, Repeatable Read isolation level does allow
phantom rows to appear.
Preventing nonrepeatable reads from appearing is a desirable safeguard. But there's no free
lunch. The cost of this extra safeguard is that all the shared locks in a transaction must be held
until the completion (COMMIT or ROLLBACK) of the transaction. (Exclusive locks must always be
held until the end of a transaction, no matter what the isolation level, so that a transaction can be
rolled back if necessary. If the locks were released sooner, it might be impossible to undo the
work.) No other user can modify the data visited by your transaction as long as your transaction is
outstanding. Obviously, this can seriously reduce concurrency and degrade performance. If
transactions are not kept short or if applications are not written to be aware of such potential lock
contention issues, SQL Server can appear to "hang" when it's simply waiting for locks to be
released.
You can control how long SQL Server waits for a lock to be released by using the session option
LOCK_TIMEOUT.
Serializable
The Serializable isolation level adds to the properties of Repeatable Read by ensuring that if a
query is reissued, rows will not have been added in the interim. In other words, phantoms will not
appear if the same query is issued twice within a transaction. More precisely, Repeatable Read
and Serializable affect sensitivity to another connection's changes, whether or not the user ID of
the other connection is the same. Every connection within SQL Server has its own transaction
and lock space. I use the term "user" loosely so as not to obscure the central concept.
Other Managers
Also included in the storage engine are managers for controlling utilities such as bulk load, DBCC
commands, backup and restore operations, and the Virtual Device Interface (VDI). VDI allows
ISVs to write their own backup and restore utilities and to access the SQL Server data structures
Data Integrity
Creating a model of the entities in the problem space and the relationships between them is only
part of the data modeling process. You must also capture the rules that the database system will
use to ensure that the actual physical data stored in it is, if not correct, at least plausible. In other
words, you must model the data integrity.
It's important to understand that the chances of being able to guarantee the literal correctness of
the data are diminishingly small. Take, for example, an order record showing that Mary Smith
purchased 17 hacksaws on July 15, 1999. The database system can ensure that Mary Smith is a
customer known to the system, that the company does indeed sell hacksaws, and that it was
taking orders on July 15, 1999. It can even check that Mary Smith has sufficient credit to pay for
the 17 hacksaws. What it can't do is verify that Ms. Smith actually ordered 17 hacksaws and not 7
or 1, or 17 screwdrivers instead. The best the system might do is notice that 17 is rather a lot of
hacksaws for an individual to purchase and notify the person entering the order to that effect.
Even having the system do this much is likely to be expensive to implement, probably more
expensive than its value warrants.
My point is that the system can never verify that Mary Smith did place the order as it's recorded; it
can verify only that she could have done so. Of course, that's all any record-keeping system can
do, and a well-designed database system can certainly do a better job than the average manual
system, if for no other reason than its consistency in applying the rules. But no database system,
and no database system designer, can guarantee that the data in the database is true, only that it
could be true. It does this by ensuring that the data complies with the integrity constraints that
have been defined for it.
Integrity Constraints
Some people refer to integrity constraints as business rules. However, the concept of business
rules is much broader; it includes all of the constraints on the system rather than just the
constraints concerning the integrity of the data. In particular, system securitythe definition of
which users can do what and under what circumstances they can do itis part of system
administration, not data integrity. But certainly security is a business requirement and will
constitute one or more business rules.
Data integrity is implemented at several levels of granularity. Domain, transition, and entity
constraints define the rules for maintaining the integrity of the individual relations. Referential
integrity constraints ensure that necessary relationships between relations are maintained.
Database integrity constraints govern the database as a whole, and transaction integrity
constraints control the way data is manipulated either within a single database or between
multiple databases.
Domain Integrity
A domain is the set of all possible values for a given attribute. A domain integrity constraintusually
just called a domain constraintis a rule that defines these legal values. It might be necessary to
define more than one domain constraint to describe a domain completely.
That being said, however, data type can be a convenient shorthand in the data model, and for
this reason choosing a logical data type is often the first step in determining the domain
constraints in a system Dates are probably the best example of the benefits of this approach. I'd
recommend against defining the domain TransactionDate as "DateTime", which is a physical
representation. But defining it as "a date" allows you to concentrate on it being "between the
commencement of business and the present date, inclusive" and ignore all those rather tedious
rules about leap years.
Having chosen a logical data type, it might be appropriate to narrow the definition by, for
example, indicating the scale and precision of a numeric type, or the maximum length of string
values. This is very close to specifying a physical data type, but you should still be working at the
logical level.
The next aspect of domain integrity to consider is whether a domain is permitted to contain
unknown or nonexistent values. The handling of these values is contentious, and we'll be
discussing them repeatedly as we examine various aspects of database system design. For now,
it's necessary to understand only that there is a difference between an unknown value and a
nonexistent value, and that it is often (although not always) possible to specify whether either or
both of these is permitted for the domain.
The first point here, that "unknown" and "nonexistent" are different, doesn't present too many
problems at the logical level. My father does not have a middle name; I do not know my
neighbor's. These are quite different issues. There are implementation issues that need not yet
concern us, but the logical distinction is quite straightforward.
The second point is that, having determined whether a domain is allowed to include unknown or
nonexistent values, you'll need to decide whether either of these can be accepted by the system.
To return to our TransactionDate example, it's certainly possible for the date of a transaction to
be unknown, but if it occurred at all it occurred at some fixed point in time and therefore cannot be
nonexistent. In other words, there must be a transaction date; we just might not know it.
Now, obviously, we can be ignorant of anything, so any value can be unknown. That's not a
useful distinction. What we're actually defining here is not so much whether a value can be
unknown as whether an entity with an unknown value in this attribute should be stored. It might
be that it's not worth storing data unless the value is known, or it might be that we can't identify an
entity without knowing the value. In either case, you would prevent a record containing an
unknown value in the specified field from being added to the database.
This decision can't always be made at the domain level, but it's always worth considering since
doing so can make the job a little easier down the line. To some extent, your decision depends on
how generic your domains are. As an example, say that you have defined a Name domain and
declared the attributes GivenName, MiddleName, Surname, and CompanyName against it. You
might just as well have defined these attributes as separate domains, but there are some
advantages to using the more general domain definition because doing so allows you to capture
the overlapping rules (and in this case, there are probably a lot of them) in a single place.
However, in this case you won't be able to determine whether empty or unknown values are
acceptable at the domain level; you will have to define these properties at the entity level.
The final aspect of domain integrity is that you'll want to define the set of values represented by a
domain as specifically as possible. Our TransactionDate domain, for example, isn't just the set of
all dates; it's the set of dates from the day the company began trading until the current date. It
might be further restricted to eliminate Sundays, public holidays, and any other days on which the
Sometimes the easiest way to describe a domain is to simply list the domain values. The domain
of Weekends is completely described by the set {"Saturday", "Sunday"}. Sometimes it will be
easier to list one or more rules for determining membership, as we did for TransactionDate. Both
techniques are perfectly acceptable, although a specific design methodology might dictate a
particular method of documenting constraints. The important thing is that the constraints be
captured as carefully and completely as possible.
Transition Integrity
Transition integrity constraints define the states through which a tuple can validly pass. The
State-Transition diagram , for example, shows the states through which an order can pass.
State Diagram for order processing
You would use transitional integrity constraints, for instance, to ensure that the status of a given
order never changed from "Entered" to "Completed" without passing through the interim states, or
to prevent a canceled order from changing status at all.
The status of an entity is usually controlled by a single attribute. In this case, transition integrity
can be considered a special type of domain integrity. Sometimes, however, the valid transitions
are controlled by multiple attributes or even multiple relations. Because transition constraints can
exist at any level of granularity, it's useful to consider them a separate type of constraint during
preparation of the data model.
y
The status of a customer might only be permitted to change from "Normal" to "Preferred" if the
customer's credit limit is above a specified value and he or she has been doing business with the
company for at least a year. The credit limit requirement would most likely be controlled by an
attribute of the Customers relation, but the length of time the customer has been doing business
with the company might not be explicitly stored anywhere. It might be necessary to calculate the
value based on the oldest record for the customer in the Orders relation.
Entity Integrity
Entity constraints ensure the integrity of the entities being modeled by the system. At the simplest
level, the existence of a primary key is an entity constraint that enforces the rule "every entity
must be uniquely identifiable."
In a sense, this is the entity integrity constraint; all others are technically entity-level integrity
constraints. The constraints defined at the entity level can govern a single attribute, multiple
attributes, or the relation as a whole.
The integrity of an individual attribute is modeled first and foremost by defining the attribute
against a specific domain. An attribute within a relation inherits the integrity constraints defined for
its domain. At the entity level, these inherited constraints can properly be made more rigorous,
but they cannot be relaxed. Another way of thinking about this is that the entity constraint can
specify a subset of the domain constraints but not a superset. For example, an OrderDate
attribute defined against the TransactionDate domain might specify that the date must be in the
current year, whereas the TransactionDate domain allows any date between the date business
commenced and the current date. An entity constraint cannot, however, allow OrderDate to
contain dates in the future, since the attribute's domain prohibits these.
Similarly, a CompanyName attribute defined against the Name domain might prohibit empty
values, even though the Name domain permits them. Again, this is a narrower, more rigorous
definition of permissible values than that specified in the domain.
In addition to narrowing the range of values for a single attribute, entity constraints can also affect
multiple attributes. A requirement that Shipping-Date be on or after OrderDate is an example of
such a constraint. Entity constraints can't reference other relations, however. It wouldn't be
appropriate, for example, to define an entity constraint that sets a customer DiscountRate (an
attribute of the Customer relation) based on the customer's TotalSales (which is based on
multiple records in the OrderItems relation). Constraints that depend on multiple relations are
database-level constraints; we'll discuss them later in this chapter.
Be careful of multiple-attribute constraints; they often indicate that your data model isn't fully
normalized. If you are restricting or calculating the value of one attribute based on another, you're
probably OK. An entity constraint that says "Status is not allowed to be 'Preferred' unless the
Customer record is at least one year old" would be fine. But if the value of one attribute
determines the value of anotherfor example, "If the customer record is older than one year, then
Status = 'Preferred'"then you have a functional dependency and you're in violation of third normal
form.
Referential Integrity
We already looked at the decomposition of relations to minimize redundancy and at foreign keys
to implement the links between relations. If these links are ever broken, the system will be
unreliable at best and unusable at worst. Referential integrity constraints maintain and protect
these links.
There is really only one referential integrity constraint: Foreign keys cannot become orphans. In
other words, no record in the foreign table can contain a foreign key that doesn't match a record
in the primary table. Tuples that contain foreign keys that don't have a corresponding candidate
key in the primary relation are called orphan entities.
All four of these cases must be handled if the integrity of a relationship is to be maintained. The
first case, the addition of an unmatched foreign key, is usually simply prohibited. But note that
unknown and nonexistent values don't countthat's a separate rule. If the relationship is declared
as optional, any number of unknown and nonexistent values can be entered without
compromising referential integrity.
The second and third causes of orphaned entities changing the candidate key value in the
referenced table shouldn't occur very often. (This would be an entity constraint, by the way:
"Candidate keys are not allowed to change.")
If your model does allow candidate keys to be changed, you must ensure that these changes are
made in the foreign keys as well. This is known as a cascading update. Both Microsoft Jet and
Microsoft SQL Server provide mechanisms for easily implementing cascading updates.
If your model allows foreign keys to change, in effect allowing the entity to be re-assigned, you
must ensure that the new value is valid. Again, Microsoft Jet and Microsoft SQL Server both allow
this constraint to be easily implemented, even though it doesn't have a specific name.
The final cause of orphan foreign keys is the deletion of the record containing the primary entity. If
one deletes a Customer record, for example, what becomes of that customer's orders? As with
candidate key changes, you can simply prohibit the deletion of tuples in the primary relation if
they are referenced in a foreign relation. This is certainly the cleanest solution if it is a reasonable
restriction for your system. When it's not, both the Jet database engine and SQL Server provide a
simple means of cascading the operation, known as a cascading delete.
But in this case, you also have a third option that's a little harder to implement because it can't be
implemented automatically. You might want to re-assign the dependent records. This isn't often
appropriate, but it is sometimes necessary. Say, for example, that CustomerA purchases
CustomerB. It might make sense to delete CustomerB and reassign all of CustomerB's orders to
CustomerA.
A special kind of referential integrity constraint is the maximum cardinality issue. In the data
model, rules such as "Managers are allowed to have a maximum of five individuals reporting to
them" are defined as referential constraints.
Database Integrity
The most general form of integrity constraint is the database constraint. Database constraints
reference more than one relation: "All Products sold to Customers with the status of 'Government'
must have more than one Supplier." The majority of database constraints take this form, and, like
this one, may require calculations against multiple relations.
It isn't always clear whether a given business rule is an integrity constraint or a work process (or
something else entirely). The difference might not be desperately important. All else being equal,
implement the rule where it's most convenient to do so. If it's a straightforward process to express
a rule as a database constraint, do so. If that gets tricky (as it often can, even when the rule is
clearly an integrity constraint), move it to the middle tier or the front end, where it can be
implemented procedurally.
Transaction Integrity
The final form of database integrity is transaction integrity. Transaction integrity constraints
govern the ways in which the database can be manipulated. Unlike other constraints, transaction
constraints are procedural and thus are not part of the data model per se.
Transactions are closely related to work processes. The concepts are, in fact, orthogonal,
inasmuch as a given work process might consist of one or more transactions and vice versa. It
isn't quite correct, but it's useful to think of a work process as an abstract construct ("add an
order") and a transaction as a physical one ("update the OrderDetail table").
A transaction is usually defined as a "logical unit of work," which I've always found to be a
particularly unhelpful bit of rhetoric. Essentially, a transaction is a group of actions, all of which (or
none of which) must be completed. The database must comply with all of the defined integrity
constraints before the transaction commences and after it's completed, but might be temporarily
in violation of one or more constraints during the transaction.
The classic example of a transaction is the transfer of money from one bank account to another.
If funds are debited from Account A but the system fails to credit them to Account B, money has
been lost. Clearly, if the second command fails, the first must be undone. In database parlance, it
must be "rolled back." Transactions can involve multiple records, multiple relations, and even
multiple databases.
To be precise, all operations against a database are transactions. Even updating a single existing
record is a transaction. Fortunately, these low-level transactions are implemented transparently
by the database engine, and you can generally ignore this level of detail.
Both the Jet database engine and SQL Server provide a means of maintaining transactional
integrity by way of the BEGIN TRANSACTION, COMMIT TRANSACTION, and ROLLBACK
TRANSACTION statements. As might be expected, SQL Server's implementation is more robust
and better able to recover from hardware failure as well as certain kinds of software failure.
However, these are implementation issues and are outside the scope of this book. What is
important from a design point of view is to capture and specify transaction dependencies, those
infamous "logical units of work."
Domain Integrity
SQL Server provides a limited kind of support for domains in the form of user-defined data types
(UDDTs). Fields defined against a UDDT will inherit the data type declaration as well as domain
constraints defined for the UDDT.
Equally importantly, SQL Server will prohibit comparison between fields declared against different
UDDTs, even when the UDDTs in question are based on the same system data type. For
example, even though the CityName domain and the CompanyName domain are both defined as
being char(30), SQL Server would reject the expression CityName = CompanyName. This can be
explicitly overridden by using the convert function CityName = CONVERT(char(30),
CompanyName), but the restriction forces you to think about it before comparing fields declared
against different domains. This is a good thing, since these comparisons don't often make sense.
UDDTs can be created either through the SQL Server Enterprise Manager or through the system
stored procedure sp_addtype. Either way, UDDTs are initially declared with a name or a data
type and by whether they are allowed to accept Nulls. Once a UDDT has been created, default
values and validation rules can be defined for it. A SQL Server rule is a logical expression that
defines the acceptable values for the UDDT (or for a field, if it is bound to a field rather than a
UDDT). A default is simply thata default value to be inserted by the system into a field that would
otherwise be Null because the user did not provide a value.
Binding a rule or default to a UDDT is a two-step procedure. First you must create the rule or
default, and then bind it to the UDDT (or field). The "it's not a bug, it's a feature" justification for
this two-step procedure is that, once defined, the rule or default can be reused elsewhere. I find
this tedious since in my experience these objects are reused only rarely. When defining a table,
SQL Server provides the ability to declare defaults and CHECK constraints directly, as part of the
table definition. (CHECK constraints are similar to rules, but more powerful.) Unfortunately this
one-step declaration is not available when declaring UDDTs, which must use the older "create-
and-then-bind" methodology. It is heartily to be wished that Microsoft add support for default and
CHECK constraint declarations to UDDTs in a future release of SQL Server.
A second way of implementing a kind of deferred domain integrity is to use lookup tables. This
technique can be used in both Microsoft Jet and SQL Server. As an example, take the domain of
USStates. Now, theoretically, you can create a rule listing all 50 states. In reality, this would be a
painful process, particularly with the Jet database engine, where the rule would have to be
retyped for every field declared against the domain
Entity Integrity
In the database schema, entity constraints can govern individual fields, multiple fields, or the table
as a whole. Both the Jet database engine and SQL Server provide mechanisms for ensuring
integrity at the entity level. Not surprisingly, SQL Server provides a richer set of capabilities, but
the gap is not as great as one might expect.
At the level of individual fields, the most fundamental integrity constraint is of course the data
type. Both the Jet database engine and SQL Server provide a rich set of data types, as shown in
Table 1 The physical Data Types Suported by Microsoft Jet and SQL Server
• Data stored in a relational database can be stored using a variety of data types. The
primary ORACLE data types are NUMBER, VARCHAR, and CHAR for storing numbers
anda text data; however, there are additional data types that are supported to support
backward compatability with products.given below:
• DATE • Valid dates range from Jan 1, 4712 B.C. to Dec 31, 4712 A.D.
• For NUMBER column with space for 40 digits, plus space for
a decimal point and sign. Numbers may be expressed in two
ways: first, with numbers 0 to 9, the signs + and -, and a
• NUMBER decimal point(.); second, in scientific notation, e.g. 1.85E3 for
1850. Valid values are 0 and positive and negative numbers
from 1.0E-130 to 9.99…E125.
MISCELLANEOUS
DATA TYPES AND
VARIATIONS
• DECIMAL • Same as NUMBER.
• LONG RAW • Raw binary data; otherwise the same as LONG (used for
images).
• NUMBER(size,d) • For NUMBER column of specified size with d digits after the
decimal point, e.g. NUMBER(5,2) could contain nothing larger
than 999.99 without an error being generated.
• RAW(size) • Raw binary data, size bytes long, maximum size=255 bytes.
User-Defined Datatypes :
A user-defined datatype (UDDT) provides a convenient way for you to guarantee consistent use
of underlying native datatypes for columns known to have the same domain of possible values.
For example, perhaps your database will store various phone numbers in many tables. Although
no single, definitive way exists to store phone numbers, in this database consistency is important.
You can create a phone_number UDDT and use it consistently for any column in any table that
keeps track of phone numbers to ensure that they all use the same datatype.
And here's how to use the new UDDT when you create a table:
When the table is created, internally the cust_phone datatype is known to be varchar(20). Notice
that both cust_phone and cust_fax are varchar(20), although cust_phone has that declaration
through its definition as a UDDT.
Here's how the customer table appears in the entries in the syscolumns table for this table:
You can see that both the cust_phone and cust_fax columns have the same xtype (datatype),
although the cust_phone column shows that the datatype is a UDDT (xusertype = 261). The type
is resolved when the table is created, and the UDDT can't be dropped or changed as long as one
or more tables are currently using it. Once declared, a UDDT is static and immutable, so no
inherent performance penalty occurs in using a UDDT instead of the native datatype.
The use of UDDTs can make your database more consistent and clear. SQL Server implicitly
converts between compatible columns of different types (either native types or UDDTs of different
types).
Currently, UDDTs don't support the notion of subtyping or inheritance, nor do they allow a
DEFAULT value or CHECK constraint to be declared as part of the UDDT itself.
Changing a Datatype
By using the ALTER COLUMN clause of ALTER TABLE, you can modify the datatype or NULL
property of an existing column. But be aware of the following restrictions:
• The modified column can't be a text, image, ntext, or rowversion (timestamp) column.
• If the modified column is the ROWGUIDCOL for the table, only DROP ROWGUIDCOL is
allowed; no datatype changes are allowed.
• The modified column can't be a computed or replicated column.
• The modified column can't have a PRIMARY KEY or FOREIGN KEY constraint defined
on it.
• The modified column can't be referenced in a computed column.
• The modified column can't have the type changed to timestamp.
• If the modified column participates in an index, the only type changes that are allowed
are increasing the length of a variable-length type (for example, VARCHAR(10) to
VARCHAR(20)), changing nullability of the column, or both.
SYNTAX
ALTER TABLE table-name ALTER COLUMN column-name
{ type_name [ ( prec [, scale] ) ] [COLLATE <collation name> ]
[ NULL | NOT NULL ]
| {ADD | DROP} ROWGUIDCOL }
y
/* Change the length of the emp_name column in the employee
table from varchar(30) to varchar(50) */
ALTER TABLE employee
ALTER COLUMN emp_name varchar(50)
Identity Property
It is common to provide simple counter-type values for tables that don't have a natural or efficient
primary key. Columns such as cust_id are usually simple counter fields. The IDENTITY property
makes generating unique numeric values easy. IDENTITY isn't a datatype; it's a column property
that you can declare on a whole-number datatype such as tinyint, smallint, int, or numeric/decimal
(having a scale of zero). Each table can have only one column with the IDENTITY property. The
table's creator can specify the starting number (seed) and the amount that this value increments
or decrements. If not otherwise specified, the seed value starts at 1 and increments by 1.
y
CREATE TABLE customer
(
cust_id smallint IDENTITY NOT NULL,
cust_name varchar(50) NOT NULL
)
To find out which seed and increment values were defined for a table, you can use the
IDENT_SEED(tablename) and IDENT_INCR(tablename) functions.
Statement:
Output:
1 1
for the customer table because values weren't explicitly declared and the default values were
used.
y explicitly starts the numbering at 100 (seed) and increments the value by 20:
CREATE TABLE customer
(
cust_id smallint IDENTITY(100, 20) NOT NULL,
cust_name varchar(50) NOT NULL
)
The value automatically produced with the IDENTITY property is normally unique, but that isn't
guaranteed by the IDENTITY property itself. Nor is it guaranteed to be consecutive. For
efficiency, a value is considered used as soon as it is presented to a client doing an INSERT
operation. If that client doesn't ultimately commit the INSERT, the value never appears, so a
break occurs in the consecutive numbers. An unacceptable level of serialization would exist if the
next number couldn't be parceled out until the previous one was actually committed or rolled
back.
If you need exact sequential values without gaps, IDENTITY isn't the appropriate feature to use.
Instead, you should implement a next_number-type table in which you can make the operation of
bumping the number contained within it part of the larger transaction (and incur the serialization
of queuing for this value).
To temporarily disable the automatic generation of values in an identity column, you use the SET
IDENTITY_INSERT tablename ON option. In addition to filling in gaps in the identity sequence,
this option is useful for tasks such as bulk-loading data in which the previous values already exist.
For example, perhaps you're loading a new database with customer data from your previous
system. You might want to preserve the previous customer numbers but have new ones
automatically assigned using IDENTITY. The SET option was created exactly for cases like this.
Because of the SET option's ability to allow you to determine your own values for an IDENTITY
column, the IDENTITY property alone doesn't enforce uniqueness of a value within the table.
Although IDENTITY will generate a unique number if IDENTITY_INSERT has never been
enabled, the uniqueness is not guaranteed once you have used the SET option. To enforce
uniqueness (which you'll almost always want to do when using IDENTITY), you should also
declare a UNIQUE or PRIMARY KEY constraint on the column. If you insert your own values for
an identity column (using SET IDENTITY_INSERT), when automatic generation resumes, the
next value is the next incremented value (or decremented value) of the highest value that exists
in the table, whether it was generated previously or explicitly inserted.
$
If you're using the bcp utility for bulk loading data, be aware of the -E (uppercase) parameter if
your data already has assigned values that you want to keep for a column that has the IDENTITY
The keyword IDENTITYCOL automatically refers to the specific column in a table that has the
IDENTITY property, whatever its name. If that column is cust_id, you can refer to the column as
IDENTITYCOL without knowing or using the column name or you can refer to it explicitly as
cust_id.
y
The following two statements work identically and return the same data:
The column name returned to the caller is cust_id, not IDENTITYCOL, in both of these cases.
When inserting rows, you must omit an identity column from the column list and VALUES section.
(The only exception is when the IDENTITY_INSERT option is on.) If you do supply a column list,
you must omit the column for which the value will be automatically supplied.
Here are two valid INSERT statements for the customer table shown earlier:
cust_id cust_name
------- ---------
1 ACME Widgets
2 AAA Gadgets
(2 row(s) affected)
Sometimes in applications, it's desirable to immediately know the value produced by IDENTITY
for subsequent use. For example, a transaction might first add a new customer and then add an
order for that customer. To add the order, you probably need to use the cust_id. Rather than
selecting the value from the customer table, you can simply select the special system function
@@IDENTITY, which contains the last identity value used by that connection. It doesn't
necessarily provide the last value inserted in the table, however, because another user might
have subsequently inserted data. If multiple INSERT statements are carried out in a batch on the
same or different tables, the variable has the value for the last statement only. In addition, if there
is an INSERT trigger that fires after you insert the new row and if that trigger inserts rows into a
table with an identity column, @@IDENTITY will not have the value inserted by the original
INSERT statement.
It might look like you're inserting and then immediately checking the value:
However, if a trigger was fired for the INSERT, the value of @@IDENTITY might have changed.
There are two other functions that you might find useful when working with identity columns.
SCOPE_IDENTITY returns the last identity value inserted into a table in the same scope, which
could be a stored procedure, trigger, or batch. So if we replace @@IDENTITY with the
In other cases, you might want to know the last identity value inserted in a specific table, from any
application or user. You can get this value using the IDENT_CURRENT function, which takes a
table name as an argument:
SELECT IDENT_CURRENT('customer')
This doesn't always guarantee that you can predict the next identity value to be inserted, because
another process could insert a row between the time you check the value of IDENT_CURRENT
and the time you execute your INSERT statement.
You can't define the IDENTITY property as part of a UDDT, but you can declare the IDENTITY
property on a column that uses a UDDT. A column that has the IDENTITY property must always
be declared NOT NULL (either explicitly or implicitly); otherwise, error number 8147 will result
from the CREATE TABLE statement and CREATE won't succeed. Likewise, you can't declare the
IDENTITY property and a DEFAULT on the same column. To check that the current identity value
is valid based on the current maximum values in the table, and to reset it if an invalid value is
found (which should never be the case), use the DBCC CHECKIDENT(tablename) statement.
Identity values are fully recoverable. If a system outage occurs while insert activity is taking place
with tables that have identity columns, the correct value will be recovered when SQL Server is
restarted. SQL Server accomplishes this during the checkpoint processing by flushing the current
identity value for all tables. For activity beyond the last checkpoint, subsequent values are
reconstructed from the transaction log during the standard database recovery process. Any
inserts into a table that have the IDENTITY property are known to have changed the value, and
the current value is retrieved from the last INSERT statement (post-checkpoint) for each table in
the transaction log. The net result is that when the database is recovered, the correct current
identity value is also recovered.
In rare cases, the identity value can get out of sync. If this happens, you can use the DBCC
CHECKIDENT command to reset the identity value to the appropriate number. In addition, the
RESEED option to this command allows you to set a new starting value for the identity sequence.
Constraints
Constraints provide a powerful yet easy way to enforce the data integrity in your database. Data
integrity comes in three forms:
• Entity integrity ensures that a table has a primary key. In SQL Server 2000, you can
guarantee entity integrity by defining PRIMARY KEY or UNIQUE constraints or by
building unique indexes. Alternatively, you can write a trigger to enforce entity integrity,
but this is usually far less efficient.
• Domain integrity ensures that data values meet certain criteria. In SQL Server 2000,
domain integrity can be guaranteed in several ways. Choosing appropriate datatypes can
ensure that a data value meets certain conditions—for example, that the data represents
a valid date. Other approaches include defining CHECK constraints or FOREIGN KEY
constraints or writing a trigger. You might also consider DEFAULT constraints as an
aspect of enforcing domain integrity.
• Referential integrity enforces relationships between two tables, a referenced table, and a
referencing table. SQL Server 2000 allows you to define FOREIGN KEY constraints to
enforce referential integrity, and you can also write triggers for enforcement. It's crucial to
note that there are always two sides to referential integrity enforcement. If data is
Constraints are also called declarative data integrity because they are part of the actual table
definition. This is in contrast to programmatic data integrity enforcement, which uses stored
procedures or triggers. Here are the five types of constraints:
• PRIMARY KEY
• UNIQUE
• FOREIGN KEY
• CHECK
• DEFAULT
You might also sometimes see the IDENTITY property and the nullability of a column described
as constraints. I typically don't consider these attributes to be constraints; instead, I think of them
as properties of a column, for two reasons. First, as we'll see, each constraint has its own row in
the sysobjects system table, but IDENTITY and nullability information is not stored in sysobjects,
only in syscolumns. This makes me think that these properties are more like datatypes, which are
also stored in syscolumns. Second, when you use the special command SELECT INTO, a new
table can be created that is a copy of an existing table. All column names and datatypes are
copied, as well as IDENTITY information and column nullability. However, constraints are not
copied to the new table. This makes me think that IDENTITY and nullability are more a part of the
actual table structure than constraints are.
Primary key
Nullability
All columns that are part of a primary key must be declared (either explicitly or implicitly) as NOT
NULL. Columns that are part of a UNIQUE constraint can be declared to allow NULL. However,
y If the constraint contains two int columns, exactly one row of each of these combinations
will be allowed:
NULL NULL
0 NULL
NULL 0
1 NULL
NULL 1
This behavior is questionable: NULL represents an unknown, but using it this way clearly implies
that NULL is equal to NULL. As you'll recall, avoid using NULLs, especially in key columns.
Index Attributes
You can explicitly specify the index attributes CLUSTERED or NONCLUSTERED when you
declare a constraint. If you don't, the index for a UNIQUE constraint will be nonclustered and the
index for a PRIMARY KEY constraint will be clustered (unless CLUSTERED has already been
explicitly stated for a unique index, because only one clustered index can exist per table). You
can specify the index FILLFACTOR attribute if a PRIMARY KEY or UNIQUE constraint is added
to an existing table using the ALTER TABLE command. FILLFACTOR doesn't make sense in a
CREATE TABLE statement because the table has no existing data, and FILLFACTOR on an
index affects how full pages are only when the index is initially created. FILLFACTOR isn't
maintained when data is added.
Choosing Keys
Try to keep the key lengths as compact as possible. Columns that are the primary key or that are
unique are most likely to be joined and frequently queried. Compact key lengths allow more index
entries to fit on a given 8-KB page, thereby reducing I/O, increasing cache hits, and speeding up
character matching. Clustered index keys are used as bookmarks in all your nonclustered
indexes, so a long clustered key will increase the size and decrease the I/O efficiency of all your
indexes. So if your primary key has a clustered index, you've got plenty of good reasons to keep
it short. When no naturally efficient compact key exists, it's often useful to manufacture a
surrogate key using the IDENTITY property on an int column. If int doesn't provide enough range,
a good second choice is a numeric column with the required precision and with scale 0.
Alternatively, you can consider a bigint for an identity column You might use this surrogate as the
primary key, use it for most join and retrieval operations, and declare a UNIQUE constraint on the
natural but inefficient columns that provide the logical unique identifier in your data. Or you might
dispense with creating the UNIQUE constraint altogether if you don't need to have SQL Server
enforce the uniqueness. Indexes slow performance of data modification statements because the
index as well as the data must be maintained.
Although it's permissible to do so, don't create a PRIMARY KEY constraint on a column of type
float or real. Because these are approximate datatypes, the uniqueness of such columns is also
approximate, and the results can sometimes be unexpected.
EXAMPLE 1
>>>>
Object Name
-----------
customer
constraint_type constraint_name
----------------------- ------------------------------
PRIMARY KEY (clustered) PK__customer__68E79C55
EXAMPLE 2
>>>>
Object Name
-----------
customer
constraint_type constraint_name
----------------------- ---------------
PRIMARY KEY (clustered) cust_pk
EXAMPLE 3
>>>>
Object Name
-----------
customer
constraint_type constraint_name
--------------- ---------------
PRIMARY KEY (clustered) PK__customer__59063A47
EXAMPLE 4
>>>>
Object Name
-----------
customer
constraint_type constraint_name
--------------- ---------------
PRIMARY KEY (clustered) customer_PK
In Examples 1 and 3, an explicit name for the constraint is not provided, so SQL Server comes up
with the names. The names, PK__customer__68E79C55 and PK__customer__59063A47, seem
cryptic, but there is some method to this apparent madness. All types of single-column
constraints use this same naming scheme, which will be discussed later in the chapter, and multi-
column constraints use a similar scheme. Whether you choose a more intuitive name, such as
cust_pk in Example 2 or customer_PK in Example 4, or the less intuitive (but information-
packed), system-generated name is up to you. Here's an example of creating a multi-column,
UNIQUE constraint on the combination of multiple columns. (The primary key case is essentially
identical.)
>>>
Object Name
-----------------
customer_location
As noted earlier, a unique index is created to enforce either a PRIMARY KEY or a UNIQUE
constraint. The name of the index is the same as the constraint name, whether the name was
explicitly defined or system-generated. The index used to enforce the
CUSTOMER_LOCATION_UNIQUE constraint in the above example is also named
customer_location_unique. The index used to enforce the column-level, PRIMARY KEY
constraint of the customer table in Example 1 is named PK__customer__68E79C55, which is the
system-generated name of the constraint. You can use the sp_helpindex stored procedure to see
information for all indexes of a given table.
y
EXEC sp_helpindex customer
>>>
index_name index_description index_keys
------------------------------ ------------------ ----------
PK__customer__68E79C55 clustered, unique, cust_id
primary key
located on default
Foreign key
• NO ACTION The delete is prevented. This default mode, per the ANSI standard, occurs if
no other action is specified. NO ACTION is often referred to as RESTRICT, but this
usage is slightly incorrect in terms of how ANSI defines RESTRICT and NO ACTION.
ANSI uses RESTRICT in DDL statements such as DROP TABLE, and it uses NO
SQL Server 2000 allows you to specify either NO ACTION or CASCADE when you define your
foreign key constraints. If you want to implement either SET DEFAULT or SET NULL, you can
use a trigger. Implementing the SET NULL action is very straightforward, which discusses
triggers in detail. One reason it's so straightforward is that while a FOREIGN KEY requires any
value in the referencing table to match a value in the referenced table, NULLs are not considered
to be a value, and having a NULL in the referencing table doesn't break any relationships.
Implementing the SET DEFAULT action is just a little more involved. Suppose you want to
remove a row from the referenced table and set the foreign key value in all referencing rows to -
9999. If you want to maintain referential integrity, you have to have a "dummy" row in the
referenced table with the primary key value of -9999.
Keep in mind that enforcing a foreign key implies that SQL Server will check data in both the
referenced and the referencing tables. The referential actions just discussed apply only to actions
that are taken on the referenced table. On the referencing table, the only action allowed is to not
allow an update or an insert if the relationship will be broken as a result.
Because a constraint is checked before a trigger fires, you can't have both a FOREIGN KEY
constraint to enforce the relationship when a new key is inserted into the referencing table and a
trigger that performs an operation such as SET NULL on the referenced table. If you do have
both, the constraint will fail and the statement will be aborted before the trigger to cascade the
delete fires. If you want to allow the SET NULL action, you have to write two triggers. You have to
have a trigger for delete and update on the referenced table to perform the SET NULL action and
a trigger for insert and update on the referencing table that disallows new data that would violate
the referential integrity.
If you do decide to enforce your referential integrity with triggers, you might still want to declare
the foreign key relationship largely for readability so that the relationship between the tables is
clear. You can then use the NOCHECK option of ALTER TABLE to disable the constraint, and
then the trigger will fire.
This note is just referring to AFTER triggers. SQL Server 2000 provides an additional type of
trigger called an INSTEAD OF trigger.
A referential action can be specified for both DELETE and UPDATE operations on the referenced
table, and the two operations can have different actions.
y
You can choose to CASCADE any updates to the cust_id in customer but not allow (NO
ACTION) any deletes of referenced rows in customer.
y
Adding a FOREIGN KEY constraint to the orders table that does just that: it cascades any
updates to the cust_id in customer but does not allow any deletes of referenced rows in
customer:
The previous examples show the syntax for a single-column constraint—both the primary key and
foreign key are single columns. This syntax uses the keyword REFERENCES, and the term
"foreign key" is implied but not explicitly stated. The name of the FOREIGN KEY constraint is
generated internally, following the same general form described earlier for PRIMARY KEY and
UNIQUE constraints. Here's a portion of the output of sp_helpconstraint for both the customer
and orders tables. (The tables were created in the pubs sample database.)
Object Name
------------
customer
Table is referenced by
----------------------------------------------
pubs.dbo.orders: FK__orders__cust_id__0AD2A005
Object Name
-----------
orders
constraint_keys
---------------
cust_id REFERENCES pubs.dbo.customer(cust_id)
order_id
>>>
Object Name
-----------
customer
Table is referenced by
---------------------------
pubs.dbo.orders: FK_ORDER_CUSTOMER
Object Name
------------
orders
y
The following example shows the following variations on how you can create constraints:
• You can use a FOREIGN KEY constraint to reference a UNIQUE constraint (an alternate
key) instead of a PRIMARY KEY constraint. (Note, however, that referencing a PRIMARY
KEY is much more typical and is generally better practice.)
• You don't have to use identical column names in tables involved in a foreign key
reference, but doing so is often good practice. The cust_id and location_num column
names are defined in the customer table. The orders table, which references the
customer table, uses the names cust_num and cust_loc.
• The datatypes of the related columns must be identical, except for nullability and
variable-length attributes. (For example, a column of char(10) NOT NULL can reference
one of varchar(10) NULL, but it can't reference a column of char(12) NOT NULL. A
column of type smallint can't reference a column of type int.) Notice in the preceding
example that cust_id and cust_num are both int NOT NULL and that location_num and
cust_loc are both smallint NULL.
When a FOREIGN KEY constraint exists on the orders table, the same operation has more steps
in the execution plan:
1. Check for the existence of a related record in the customer table (based on the updated
order record) using a clustered index.
2. If no related record is found, raise an exception and terminate the operation.
3. Find a qualifying order record using a clustered index.
4. Update the order record.
The execution plan is more complex if the orders table has many FOREIGN KEY constraints
declared. Internally, a simple update or insert operation might no longer be possible. Any such
operation requires checking many other tables for matching entries. Because a seemingly simple
operation might require checking as many as 253 other tables (see the next paragraph) and
possibly creating multiple worktables, the operation might be much more complicated than it
looks and much slower than expected.
A table can have a maximum of 253 FOREIGN KEY references. This limit is derived from the
internal limit of 256 tables in a single query. In practice, an operation on a table with 253 or fewer
FOREIGN KEY constraints might still fail with an error because of the 256-table query limit if
worktables are required for the operation.
A database designed for excellent performance doesn't reach anything close to this limit of 253
FOREIGN KEY references. For best performance results, use FOREIGN KEY constraints
judiciously. Some sites unnecessarily use too many FOREIGN KEY constraints because the
constraints they declare are logically redundant. Take the following example. The orders table
declares a FOREIGN KEY constraint to both the master_customer and customer_location tables:
In the case just described, declaring a foreign key improves the readability of the table definition,
but you can achieve the same result by simply adding comments to the CREATE TABLE
command. It's perfectly legal to add a comment practically anywhere—even in the middle of a
CREATE TABLE statement. A more subtle way to achieve this result is to declare the constraint
so that it appears in sp_helpconstraint and in the system catalogs but then disable the constraint
by using the ALTER TABLE NOCHECK option. Because the constraint will then be unenforced,
an additional table isn't added to the execution plan.
The CREATE TABLE statement shown in the following example for the orders table omits the
redundant foreign key and, for illustrative purposes, includes a comment. Despite the lack of a
FOREIGN KEY constraint in the master_customer table, you still can't insert a cust_id that
doesn't exist in the master_customer table because the reference to the customer_location table
will prevent it.
When using constraints, you should consider triggers, performance, and indexing. Let's take a
look at the ramifications of each.
The owner of a table isn't allowed to declare a foreign key reference to another table unless the
owner of the other table has granted REFERENCES permission to the first table owner. Even if
the owner of the first table is allowed to select from the table to be referenced, that owner must
have REFERENCES permission. This prevents another user from changing the performance of
operations on your table without your knowledge or consent. You can grant any user
REFERENCES permission even if you don't also grant SELECT permission, and vice-versa. The
only exception is that the DBO, or any user who is a member of the db_owner role, has full
default permissions on all objects in the database.
Performance When deciding on the use of foreign key relationships, you must weigh the
protection provided against the corresponding performance overhead. Be careful not to add
constraints that form logically redundant relationships. Excessive use of FOREIGN KEY
constraints can severely degrade the performance of seemingly simple operations.
Indexing The columns specified in FOREIGN KEY constraints are often strong candidates for
index creation. You should build the index with the same key order used in the PRIMARY KEY or
UNIQUE constraint of the table that it references so that joins can be performed efficiently. Also
be aware that a foreign key is often a subset of the table's primary key. In the customer_location
table used in the preceding two examples, cust_id is part of the primary key as well as a foreign
key in its own right. Given that cust_id is part of a primary key, it's already part of an index. In this
example, cust_id is the lead column of the index, and building a separate index on it alone
probably isn't warranted. However, if cust_id is not the lead column of the index B-tree, it might
make sense to build an index on it.
Constraints
Constraint-Checking Solutions
Sometimes two tables reference one another, which creates a bootstrap problem. Suppose
Table1 has a foreign key reference to Table2, but Table2 has a foreign key reference to Table1.
Even before either table contains any data, you'll be prevented from inserting a row into Table1
because the reference to Table2 will fail. Similarly, you can't insert a row into Table2 because the
reference to Table1 will fail.
ANSI SQL has a solution: deferred constraints, in which you can instruct the system to postpone
constraint checking until the entire transaction is committed. Using this elegant remedy puts both
INSERT statements into a single transaction that results in the two tables having correct
references by the time COMMIT occurs. Unfortunately, no mainstream product currently provides
the deferred option for constraints. The deferred option is part of the complete SQL-92
specification, which no product has yet fully implemented.
SQL Server 2000 provides immediate constraint checking; it has no deferred option. SQL Server
offers three options for dealing with constraint checking: it allows you to add constraints after
adding data, it lets you temporarily disable checking of foreign key references, and it allows you
to use the bcp (bulk copy) program or BULK INSERT command to initially load data and avoid
checking FOREIGN KEY constraints. (You can override this default option with bcp or the BULK
INSERT command and force FOREIGN KEY constraints to be validated.) To add constraints after
adding data, don't create constraints via the CREATE TABLE command. After adding the initial
data, you can add constraints by using the ALTER TABLE command.
With the second option, the table owner can temporarily disable checking of foreign key
references by using the ALTER TABLE table NOCHECK CONSTRAINT statement. Once data
exists, you can reestablish the FOREIGN KEY constraint by using ALTER TABLE table CHECK
Self-Referencing Tables
A table can be self-referencing—that is, the foreign key can reference one or more columns in the
same table. The following example shows an employee table in which a column for managers
references another employee entry:
The employee table is a perfectly reasonable table. However, in this case, a single INSERT
command that satisfies the reference is legal. For example, if the CEO of the company has an
emp_id of 1 and is also his own manager, the following INSERT will be allowed and can be a
useful way to insert the first row in a self-referencing table:
Although SQL Server doesn't currently provide a deferred option for constraints, self-referencing
tables add a twist that sometimes makes SQL Server use deferred operations internally. Consider
the case of a nonqualified DELETE statement that deletes many rows in the table. After all rows
are ultimately deleted, you can assume that no constraint violation will occur. However, violations
might occur during the DELETE operation because some of the remaining referencing rows might
be orphaned before they are actually deleted. SQL Server handles such interim violations
automatically and without any user intervention. As long as the self-referencing constraints are
valid at the end of the data modification statement, no errors are raised during processing.
To gracefully handle these interim violations, however, additional processing and worktables are
required to hold the work-in-progress. This adds substantial overhead and can also limit the
actual number of foreign keys that can be used. An UPDATE statement can also cause an interim
violation. For example, if all employee numbers are to be changed by multiplying each by 1000,
the following UPDATE statement would require worktables to avoid the possibility of raising an
error on an interim violation:
The additional worktables and the processing needed to handle the worktables are made part of
the execution plan. Therefore, if the optimizer sees that a data modification statement could
cause an interim violation, the additional temporary worktables will be created even if no such
interim violations ever actually occur. These extra steps are needed only in the following
situations:
• A table is self-referencing (it has a FOREIGN KEY constraint that refers back to itself).
• A single data modification statement (UPDATE, DELETE, or INSERT based on a
SELECT) is performed and can affect more than one row. (The optimizer can't determine
a priori, based on the WHERE clause and unique indexes, whether more than one row
could be affected.) Multiple data modification statements within the transaction don't
apply—this condition must be a single statement that affects multiple rows.
• Both the referencing and referenced columns are affected (which is always the case for
DELETE and INSERT operations, but might or might not be the case for UPDATE).
If a data modification statement in your application meets the preceding criteria, you can be sure
that SQL Server is automatically using a limited and special-purpose form of deferred constraints
to protect against interim violations.
CHECK Constraints
Enforcing domain integrity (that is, ensuring that only entries of expected types, values, or ranges
can exist for a given column) is also important. SQL Server provides two ways to enforce domain
integrity: CHECK constraints and rules. CHECK constraints allow you to define an expression for
a table that must not evaluate to FALSE for a data modification statement to succeed. The
constraint will allow the row if it evaluates to TRUE or to unknown. The constraint evaluates to
unknown when NULL values are present, and this introduces three-value logic. SQL Server
provides a similar mechanism to CHECK constraints, called rules, which are provided basically
Object Name
---------------
employee
Table is referenced by
--------------------------------
pubs.dbo.employee: FK__employee__mgr_id__2E1BDC42
This example illustrates the following points:
• CHECK constraints can be expressed at the column level with abbreviated syntax
(leaving naming to SQL Server), such as the check on entered_date; at the column level
with an explicit name, such as the NO_NUMS constraint on emp_name; or as a table-
level constraint, such as the VALID_MGR constraint.
• Table-level CHECK constraints can refer to more than one column in the same row. For
example, VALID_MGR means that no employee can be his own boss, except employee
number 1, who is assumed to be the CEO. SQL Server currently has no provision that
allows you to check a value from another row or from a different table.
• You can make a CHECK constraint prevent NULL values—for example, CHECK
(entered_by IS NOT NULL). Generally, you simply declare the column NOT NULL.
• A NULL column might make the expression logically unknown. For example, a NULL
value for entered_date gives CHECK entered_date >= CURRENT_TIMESTAMP an
unknown value. This doesn't reject the row, however. The constraint rejects the row only
if the expression is clearly false, even if it isn't necessarily true.
• You can use system functions, such as GETDATE, APP_NAME, DATALENGTH, and
SUSER_ID, as well as niladic functions, such as SYSTEM_USER,
CURRENT_TIMESTAMP, and USER, in CHECK constraints. This subtle feature is
powerful and can be useful, for example, to ensure that a user can change only records
that she has entered by comparing entered_by to the user's system ID, as generated by
SUSER_ID (or by comparing emp_name to SYSTEM_USER). Note that niladic functions
such as CURRENT_TIMESTAMP are provided for ANSI SQL conformance and simply
map to an underlying SQL Server function, in this case GETDATE. So while the DDL to
create the constraint on entered_date uses CURRENT_TIMESTAMP, sp_helpconstraint
shows it as GETDATE, which is the underlying function. Either expression is valid, and
they are equivalent in the CHECK constraint. The VALID_ENTERED_BY constraint
ensures that the entered_by column can be set only to the currently connected user's ID,
and it ensures that users can't update their own records.
• A constraint defined as a separate table element can call a system function without
referencing a column in the table. In the example preceding this list, the
END_OF_MONTH CHECK constraint calls two date functions, DATEPART and
GETDATE, to ensure that updates can't be made after day 27 of the month (which is
when the business's payroll is assumed to be processed). The constraint never
references a column in the table. Similarly, a CHECK constraint might call the
APP_NAME function to ensure that updates can be made only from an application of a
certain name, instead of from an ad hoc tool such as SQL Query Analyzer.
As with FOREIGN KEY constraints, you can add or drop CHECK constraints by using ALTER
TABLE. When adding a constraint, by default the existing data is checked for compliance; you
can override this default by using the WITH NOCHECK syntax. You can later do a dummy update
to check for any violations. The table or database owner can also temporarily disable CHECK
constraints by using NOCHECK in the ALTER TABLE statement.
Default Constraints
A default allows you to specify a constant value, NULL, or the run-time value of a system function
if no known value exists or if the column is missing in an INSERT statement. Although you could
argue that a default isn't truly a constraint (because a default doesn't enforce anything), you can
y
CHECK constraint discussion, now modified to include several defaults:
>>>
Object Name
---------------
employee
[entered_by] =
suser_id(null)
and
[entered_by]
<> [emp_id])
[emp_id] or
[emp_id] = 1)
Table is referenced by
--------------------------------
pubs.dbo.employee: FK__employee__mgr_id__2E1BDC42
• A default value can clash with a CHECK constraint. This problem appears only at
runtime, not when you create the table or when you add the default using ALTER TABLE.
For example, a column with a default of 0 and a CHECK constraint that states that the
value must be greater than 0 can never insert or update the default value.
• Although you can assign a default to a column that has a PRIMARY KEY or a UNIQUE
constraint, it doesn't make much sense to do so. Such columns must have unique values,
so only one row could exist with the default value in that column. The preceding example
sets a DEFAULT on a primary key column for illustration, but in general, this practice is
unwise.
• You can write a constant value within parentheses, as in DEFAULT (1), or without them,
as in DEFAULT 1. A character or date constant must be enclosed in either single or
double quotation marks.
• One tricky concept is knowing when a NULL is inserted into a column as opposed to a
default value. A column declared NOT NULL with a default defined uses the default only
under one of the following conditions:
o The INSERT statement specifies its column list and omits the column with the
default.
The INSERT statement specifies the keyword DEFAULT in the values list (whether the column is
explicitly specified as part of the column list or implicitly specified in the values list and the column
list is omitted, meaning "All columns in the order in which they were created"). If the values list
explicitly specifies NULL, an error is raised and the statement fails; the default value isn't used. If
the INSERT statement omits the column entirely, the default is used and no error occurs. (This
behavior is in accordance with ANSI SQL.) The keyword DEFAULT can be used in the values list,
and this is the only way the default value will be used if a NOT NULL column is specified in the
column list of an INSERT statement (either, as in the following example, by omitting the column
list—which means all columns—or by explicitly including the NOT NULL column in the columns
list).
Table 6-10 summarizes the behavior of INSERT statements based on whether a column is
declared NULL or NOT NULL and whether it has a default specified. It shows the result for the
column for three cases:
Declaring a default on a column that has the IDENTITY property doesn't make sense, and SQL
Server will raise an error if you try it. The IDENTITY property acts as a default for the column. But
the DEFAULT keyword cannot be used as a placeholder for an identity column in the values list
of an INSERT statement. You can use a special form of INSERT statement if a table has a
default value for every column (an identity column does meet this criteria) or allows NULL. The
following statement uses the DEFAULT VALUES clause instead of a column list and values list:
INSERT employee DEFAULT VALUES
$
You can generate some test data by putting the IDENTITY property on a primary key column and
declaring default values for all other columns and then repeatedly issuing an INSERT statement
of this form within a Transact-SQL loop.
The constraint produced from this simple statement bears the nonintuitive name
PK__customer__68E79C55. The advantage of explicitly naming your constraint rather than using
the system-generated name is greater clarity. The constraint name is used in the error message
for any constraint violation, so creating a name such as CUSTOMER_PK probably makes more
sense to users than a name such as PK__customer__cust_i__0677FF3C. You should choose
your own constraint names if such error messages are visible to your users. The first two
characters (PK) show the constraint type—PK for PRIMARY KEY, UQ for UNIQUE, FK for
FOREIGN KEY, and DF for DEFAULT. Next are two underscore characters, which are used as a
$
The hexadecimal value 0x68E79C55 is equal to the decimal value 1760009301, which is the
value of constid in sysconstraints and of id in sysobjects.
These sample queries of system tables show the following:
A constraint is an object. A constraint has an entry in the sysobjects table in the xtype column of
C, D, F, PK, or UQ for CHECK, DEFAULT, FOREIGN KEY, PRIMARY KEY, and UNIQUE,
respectively.
Sysconstraints relates to sysobjects. The sysconstraints table is really just a view of the
sysobjects system table. The constid column in the view is the object ID of the constraint, and the
id column of sysconstraints is the object ID of the base table on which the constraint is declared.
If the constraint is a column-level CHECK, FOREIGN KEY, or DEFAULT constraint,
sysconstraints.colid has the column ID of the column. This colid in sysconstraints is related to the
colid of syscolumns for the base table represented by id. A table-level constraint or any
PRIMARY KEY/UNIQUE constraint (even if column level) always has 0 in this column.
$
To see the names and order of the columns in a PRIMARY KEY or UNIQUE constraint, you can
query the sysindexes and syscolumns tables for the index being used to enforce the constraint.
The name of the constraint and that of the index enforcing the constraint are the same, whether
the name was user-specified or system-generated. The columns in the index key are somewhat
cryptically encoded in the keys1 and keys2 fields of sysindexes. The easiest way to decode these
values is to simply use the sp_helpindex system stored procedure; alternatively, you can use the
code of that procedure as a template if you need to decode them in your own procedure.
Decoding the status Field
The status field of the sysconstraints view is a pseudo-bit-mask field packed with information. We
could also call it a bitmap, because each bit has a particular meaning. If you know how to crack
this column, you can essentially write your own sp_helpconstraint-like procedure. Note that the
documentation is incomplete regarding the values of this column. One way to start decoding this
column is to look at the definition of the sysconstraints view using the sp_helptext system
procedure.
The lowest four bits, obtained by AND'ing status with 0xF (status & 0xF), contain the constraint
type. A value of 1 is PRIMARY KEY, 2 is UNIQUE, 3 is FOREIGN KEY, 4 is CHECK, and 5 is
DEFAULT. The fifth bit is on (status & 0x10 <> 0) when the constraint is a nonkey constraint on a
Some of the documentation classifies constraints as either table-level or column-level. This
implies that any constraint defined on the line with a column is a column-level constraint and any
constraint defined as a separate line in the table or added to the table with the ALTER TABLE
command is a table-level constraint. However, this distinction does not hold true when you look at
sysconstraints. Although it is further documented that the fifth bit is for a column-level constraint,
you can see for yourself that this bit is on for any single column constraint except PRIMARY KEY
and UNIQUE and that the sixth bit, which is documented as indicating a table-level constraint, is
on for all multi-column constraints, as well as PRIMARY KEY and UNIQUE constraints.
Some of the higher bits are used for internal status purposes, such as noting whether a
nonclustered index is being rebuilt, and for other internal states. Table 6-11 shows some of the
other bit-mask values you might be interested in:
Bitmap
Description
Value
The constraint is a "column-level" constraint, which means that it's a single column
16
constraint and isn't enforcing entity integrity
The constraint is a "table-level" constraint, which means that it's either a multi-
32
column constraint, a PRIMARY KEY, or a UNIQUE constraint
512 The constraint is enforced by a clustered index.
1024 The constraint is enforced by a nonclustered index.
16384 The constraint has been disabled.
32767 The constraint has been enabled.
131072 SQL Server has generated a name for the constraint
Using this information, and not worrying about the higher bits used for internal status, you could
use the following query to show constraint information for the employee table:
SELECT
OBJECT_NAME(constid) 'Constraint Name',
constid 'Constraint ID',
CASE (status & 0xF)
WHEN 1 THEN 'Primary Key'
WHEN 2 THEN 'Unique'
WHEN 3 THEN 'Foreign Key'
WHEN 4 THEN 'Check'
WHEN 5 THEN 'Default'
ELSE 'Undefined'
END 'Constraint Type',
CASE (status & 0x30)
WHEN 0x10 THEN 'Column'
WHEN 0x20 THEN 'Table'
ELSE 'NA'
END 'Level'
FROM sysconstraints
WHERE id=OBJECT_ID('employee')
y
A simple transaction that tries to insert three rows of data. The second row contains a duplicate
key and violates the PRIMARY KEY constraint. Some developers believe that this example
wouldn't insert any rows because of the error that occurs in one of the statements; they think that
this error will cause the entire transaction to be aborted. However, this doesn't happen—instead,
the statement inserts two rows and then commits that change. Although the second INSERT fails,
the third INSERT is processed because no error checking has occurred between the statements,
and then the transaction does a COMMIT. Because no instructions were provided to take some
other action after the error other than to proceed, SQL Server does just that. It adds the first and
third INSERT statements to the table and ignores the second statement.
BEGIN TRANSACTION
COMMIT TRANSACTION
GO
col1 col2
---- ----
1 1
2 2
Here's a modified version of the transaction. This example does some simple error checking
using the system function @@ERROR and rolls back the transaction if any statement results in
an error. In this example, no rows are inserted because the transaction is rolled back.
BEGIN TRANSACTION
INSERT show_error VALUES (1, 1)
IF @@ERROR <> 0 GOTO TRAN_ABORT
INSERT show_error VALUES (1, 2)
if @@ERROR <> 0 GOTO TRAN_ABORT
INSERT show_error VALUES (2, 2)
if @@ERROR <> 0 GOTO TRAN_ABORT
COMMIT TRANSACTION
GOTO FINISH
TRAN_ABORT:
ROLLBACK TRANSACTION
FINISH:
col1 col2
---- ----
(0 row(s) affected)
Because many developers have handled transaction errors incorrectly and because it can be
tedious to add an error check after every command, SQL Server includes a SET statement that
aborts a transaction if it encounters any error during the transaction. (Transact-SQL has no
WHENEVER statement, although such a feature would be useful for situations like this.) Using
SET XACT_ABORT ON causes the entire transaction to be aborted and rolled back if any error is
encountered. The default setting is OFF, which is consistent with ANSI-standard behavior. By
setting the option XACT_ABORT ON, we can now rerun the example that does no error checking,
and no rows will be inserted:
SET XACT_ABORT ON
BEGIN TRANSACTION
COMMIT TRANSACTION
GO
col1 col2
---- ----
A final comment about constraint errors and transactions: a single data modification statement
(such as an UPDATE statement) that affects multiple rows is automatically an atomic operation,
even if it's not part of an explicit transaction. If such an UPDATE statement finds 100 rows that
meet the criteria of the WHERE clause but one row fails because of a constraint violation, no
rows will be updated.
The modification of a given row will fail if any constraint is violated or if a trigger aborts the
operation. As soon as a failure in a constraint occurs, the operation is aborted, subsequent
checks for that row aren't performed, and no trigger fires for the row. Hence, the order of these
checks can be important, as the following list shows.
Joins
You gain much more power when you join tables, which typically results in combining columns of
matching rows to project and return a virtual table. Usually, joins are based on the primary and
foreign keys of the tables involved, although the tables aren't required to explicitly declare keys.
The pubs database contains a table of authors (authors) and a table of book titles (titles). An
obvious query would be, "Show me the titles that each author has written and sort the results
alphabetically by author. I'm interested only in authors who live outside California." Neither the
authors table nor the titles table alone has all this information. Furthermore, a many-to-many
relationship exists between authors and titles; an author might have written several books, and a
book might have been written by multiple authors. So an intermediate table, titleauthor, exists
expressly to associate authors and titles, and this table is necessary to correctly join the
information from authors and titles. To join these tables, you must include all three tables in the
FROM clause of the SELECT statement, specifying that the columns that make up the keys have
the same values:
SELECT
'Author'=RTRIM(au_lname) + ', ' + au_fname,
'Title'=title
FROM authors AS A JOIN titleauthor AS TA
ON A.au_id=TA.au_id -- JOIN CONDITION
JOIN titles AS T
ON T.title_id=TA.title_id -- JOIN CONDITION
WHERE A.state <> 'CA'
ORDER BY 1
Author Title
------------------------ ----------------------------------
Blotchet-Halls, Reginald Fifty Years in Buckingham Palace
Kitchens
DeFrance, Michel The Gourmet Microwave
del Castillo, Innes Silicon Valley Gastronomic Treats
Panteley, Sylvia Onions, Leeks, and Garlic: Cooking
Secrets of the Mediterranean
Ringer, Albert Is Anger the Enemy?
Ringer, Albert Life Without Fear
Ringer, Anne The Gourmet Microwave
Ringer, Anne Is Anger the Enemy?
Before discussing join operations further, let's study the preceding example. The author's last and
first names have been concatenated into one field. The RTRIM (right trim) function is used to strip
off any trailing whitespace from the au_lname column. Then we add a comma and a space and
concatenate on the au_fname column. This column is then aliased as simply Author and is
returned to the calling application as a single column.
The RTRIM function isn't needed for this example. Because the column is of type varchar, trailing
blanks won't be present. RTRIM is shown for illustration purposes only.
Instead of using ORDER BY 1, you can repeat the same expression used in the select list and
specify ORDER BY RTRIM(au_lname) + ', ' + au_fname instead. Alternatively, SQL Server
provides a feature supported by the ANSI SQL-99 standard that allows sorting by columns not
included in the select list. So even though you don't individually select the columns au_lname or,
au_fname, you can nonetheless choose to order the query based on these columns by specifying
columns ORDER BY au_lname, au_fname. We'll see this in the next example. Notice also that
the query contains comments (-- JOIN CONDITION). A double hyphen (--) signifies that the rest
of the line is a comment (similar to // in C++). You can also use the C-style /* comment block */,
which allows blocks of comment lines.
$
Comments can be nested, but you should generally try to avoid this. You can easily introduce a
bug by not realizing that a comment is nested within another comment and misreading the code.
Now let's examine the join in the example above. The ON clauses specify how the tables relate
and set the join criteria, stating that au_id in authors must equal au_id in titleauthor, and title_id in
titles must equal title_id in titleauthor. This type of join is referred to as an equijoin, and it's the
most common type of join operation. To remove ambiguity, you must qualify the columns. You
can do this by specifying the columns in the form table.column, as in authors.au_id =
titleauthor.au_id. The more compact and common way to do this, however, is by specifying a
table alias in the FROM clause, as was done in this example. By following the titles table with the
word AS and the letter T, the titles table will be referred to as T anywhere else in the query where
the table is referenced. Typically, such an alias consists of one or two letters, although it can be
much longer (following the same rules as identifiers).
After a table is aliased, it must be referred to by the alias, so now we can't refer to authors.au_id
because the authors table has been aliased as A. We must use A.au_id. Note also that the state
column of authors is referred to as A.state. The other two tables don't contain a state column, so
qualifying it with the A. prefix isn't necessary; however, doing so makes the query more
readable—and less prone to subsequent bugs.
The join is accomplished using the ANSI JOIN SQL syntax, which was introduced in SQL Server
version 6.5. Many examples and applications continue to use an old-style JOIN syntax, which is
shown below. (The term "old-style JOIN" is actually used by the SQL-92 specification.) The ANSI
JOIN syntax is based on ANSI SQL-92. The main differences between the two types of join
formulations are
The ANSI JOIN syntax specifies the JOIN conditions in the ON clauses (one for each pair of
tables), and the search conditions are specified in the WHERE clause—for example, WHERE
state <> 'CA'. Although slightly more verbose, the explicit JOIN syntax is more readable. There's
no difference in performance; behind the scenes, the operations are the same. Here's how you
can respecify the query using the old-style JOIN syntax:
SELECT
This query produces the same output and the same execution plan as the previous query.
One of the most common errors that new SQL users make when using the old-style JOIN syntax
is not specifying the join condition. Omitting the WHERE clause is still a valid SQL request and
causes a result set to be returned. However, that result set is likely not what the user wanted. In
the query above, omitting the WHERE clause would return the Cartesian product of the three
tables: it would generate every possible combination of rows between them. Although in a few
unusual cases you might want all permutations, usually this is just a user error. The number of
rows returned can be huge and typically doesn't represent anything meaningful.
y
The Cartesian product of the three small tables here (authors, titles, and titleauthor each have
less than 26 rows) generates 10,350 rows of (probably) meaningless output.
Using the ANSI JOIN syntax, it's impossible to accidentally return a Cartesian product of the
tables—and that's one reason to use ANSI JOINs almost exclusively. The ANSI JOIN syntax
requires an ON clause for specifying how the tables are related. In cases where you actually do
want a Cartesian product, the ANSI JOIN syntax allows you to use a CROSS JOIN operator,
which we'll examine in more detail later
You might want to try translating some of them into queries using old-style JOIN syntax, because
you should be able to recognize both forms. If you have to read SQL code written earlier than
version 7, you're bound to come across queries using this older syntax.
The most common form of join is an equijoin, which means that the condition linking the two
tables is based on equality. An equijoin is sometimes referred to as aninner join to differentiate it
from an outer join, which I'll discuss shortly. Strictly speaking, an inner join isn't quite the same as
an equijoin; an inner join can use an operator such as less than (<) or greater than (>). So all
equijoins are inner joins but not all inner joins are equijoins. To make this distinction clear in your
code, you can use the INNER JOIN syntax in place of JOIN
y
FROM authors AS A INNER JOIN titleauthor TA ON A.au_id=TA.au_id
Other than making the syntax more explicit, there's no difference in the semantics or the
execution. By convention, the modifier INNER generally isn't used.
ANSI SQL-92 also specifies the natural join operation, in which you don't have to specify the
tables' column names. By specifying syntax such as FROM authors NATURAL JOIN titleauthor,
The ANSI specification calls for the natural join to be resolved based on identical column names
between the tables. Perhaps a better way to do this would be based on a declared primary key-
foreign key relationship, if it exists. Admittedly, declared key relationships have issues, too,
because there's no restriction that only one such foreign key relationship be set up. Also, if the
natural join were limited to only such relationships, all joins would have to be known in advance—
as in the old CODASYL days.
The FROM clause in this example shows an alternative way to specify a table alias—by omitting
the AS. The use of AS preceding the table alias, as used in the previous example, conforms to
ANSI SQL-92. From SQL Server's standpoint, the two methods are equivalent (stating FROM
authors AS A is identical to stating FROM authors A). Commonly, the AS formulation is used in
ANSI SQL-92 join operations and the formulation that omits AS is used with the old-style join
formulation. However, you can use either formulation—it's strictly a matter of preference.
Outer Joins
Inner joins return only rows from the respective tables that meet the conditions specified in the
ON clause. In other words, if a row in the first table doesn't match any rows in the second table,
that row isn't returned in the result set. In contrast, outer joins preserve some or all of the
unmatched rows. To illustrate how easily subtle semantic errors can be introduced, let's refer
back to the previous two query examples, in which we want to see the titles written by all authors
not living in California. The result omits two writers who do not, in fact, live in California. Are the
queries wrong? No! They perform exactly as written—we didn't specify that authors who currently
have no titles in the database should be included. The results of the query are as we requested.
In fact, the authors table has four rows that have no related row in the titleauthor table, which
means these "authors" actually haven't written any books. Two of these authors don't contain the
value CA in the state column, as the following result set shows.
The titles table has one row for which there is no author in our database, as shown here. (Later,
you'll see the types of queries used to produce these two result sets.)
title_id title
-------- ----------------------------------
MC3026 The Psychology of Computer Cooking
To find out authors who live outside of California," the query would use an outer join so that
authors with no matching titles would be selected:
SELECT
'Author'=RTRIM(au_lname) + ', ' + au_fname,
'Title'=title
FROM
( -- JOIN CONDITIONS
Author Title
------------------------ ----------------------------------
NULL The Psychology of Computer Cooking
Blotchet-Halls, Reginald Fifty Years in Buckingham Palace
Kitchens
DeFrance, Michel The Gourmet Microwave
del Castillo, Innes Silicon Valley Gastronomic Treats
Greene, Morningstar NULL
Panteley, Sylvia Onions, Leeks, and Garlic: Cooking
Secrets of the Mediterranean
Ringer, Albert Is Anger the Enemy?
Ringer, Albert Life Without Fear
Ringer, Anne The Gourmet Microwave
Ringer, Anne Is Anger the Enemy?
Smith, Meander NULL
The query demonstrates a full outer join. Rows in the authors and titles tables that don't have a
corresponding entry in titleauthor are still presented, but with a NULL entry for the title or author
column. (The data from the authors table is requested as a comma between the last names and
the first names. Because we have the option CONCAT_NULL_YIELDS_NULL set to ON, the
result is NULL for the author column. If we didn't have that option set to ON, SQL Server would
have returned the single comma between the nonexistent last and first names.) A full outer join
preserves nonmatching rows from both the lefthand and righthand tables. In the example above,
the authors table is presented first, so it is the lefthand table when joining to titleauthor. The result
of that join is the lefthand table when joining to titles.
You can generate missing rows from one or more of the tables by using either a left outer join or
a right outer join. So if we want to preserve all authors and generate a row for all authors who
have a missing title, but we don't want to preserve titles that have no author, we can reformulate
the query using LEFT OUTER JOIN, as shown below. This join preserves entries only on the
lefthand side of the join. Note that the left outer join of the authors and titleauthor columns
generates two such rows (for Greene and Smith). The result of the join is the lefthand side of the
join to titles; therefore, the LEFT OUTER JOIN must be specified again to preserve these rows
with no matching titles rows.
Author Title
------------------------ ----------------------------------
Blotchet-Halls, Reginald Fifty Years in Buckingham Palace
Kitchens
DeFrance, Michel The Gourmet Microwave
del Castillo, Innes Silicon Valley Gastronomic Treats
Greene, Morningstar NULL
Panteley, Sylvia Onions, Leeks, and Garlic: Cooking
Secrets of the Mediterranean
Ringer, Albert Is Anger the Enemy?
Ringer, Albert Life Without Fear
Ringer, Anne The Gourmet Microwave
Ringer, Anne Is Anger the Enemy?
Smith, Meander NULL
The query produces the same rows as the full outer join, except for the row for The Psychology of
Computer Cooking. Because we specified only LEFT OUTER JOIN, there was no request to
preserve titles (righthand) rows with no matching rows in the result of the join of authors and
titleauthor.
You must use care with OUTER JOIN operations because the order in which tables are joined
affects which rows are preserved and which aren't. In an inner join, the symmetric property holds
(if A equals B, then B equals A) and no difference results, whether something is on the left or the
right side of the equation, and no difference results in the order in which joins are specified. This
is definitely not the case for OUTER JOIN operations.
y
Consider the following two queries and their results:
QUERY 1
SELECT
'Author'= RTRIM(au_lname) + ', ' + au_fname,
'Title'=title
FROM (titleauthor AS TA
RIGHT OUTER JOIN authors AS A ON (A.au_id=TA.au_id))
Author Title
------------------------ ----------------------------------
NULL The Psychology of Computer Cooking
Blotchet-Halls, Reginald Fifty Years in Buckingham Palace
Kitchens
DeFrance, Michel The Gourmet Microwave
del Castillo, Innes Silicon Valley Gastronomic Treats
Greene, Morningstar NULL
Panteley, Sylvia Onions, Leeks, and Garlic: Cooking
Secrets of the Mediterranean
Ringer, Albert Is Anger the Enemy?
Ringer, Albert Life Without Fear
Ringer, Anne The Gourmet Microwave
Ringer, Anne Is Anger the Enemy?
Smith, Meander NULL
This query produces results semantically equivalent to the previous FULL OUTER JOIN
formulation, although we've switched the order of the authors and titleauthor tables. This query
and the previous one preserve both authors with no matching titles and titles with no matching
authors. This might not be obvious because RIGHT OUTER JOIN is clearly different than FULL
OUTER JOIN. However, in this case we know it's true because a FOREIGN KEY constraint
exists on the titleauthor table to ensure that there can never be a row in the titleauthor table that
doesn't match a row in the authors table, and the FOREIGN KEY columns in titleauthor are
defined to not allow NULL values. So we can be confident that the titleauthor RIGHT OUTER
JOIN to authors can't produce any fewer rows than would a FULL OUTER JOIN.
But if we modify the query ever so slightly by changing the join order again, look what happens:
QUERY 2
SELECT
'Author'=rtrim(au_lname) + ', ' + au_fname,
'Title'=title
FROM (titleauthor AS TA
FULL OUTER JOIN titles AS T ON TA.title_id=T.title_id)
RIGHT OUTER JOIN authors AS A ON A.au_id=TA.au_id
WHERE
A.state <> 'CA' or A.state is NULL
ORDER BY 1
Author Title
------------------------ ----------------------------------
Blotchet-Halls, Reginald Fifty Years in Buckingham Palace
Kitchens
DeFrance, Michel The Gourmet Microwave
del Castillo, Innes Silicon Valley Gastronomic Treats
Greene, Morningstar NULL
Panteley, Sylvia Onions, Leeks, and Garlic: Cooking
Secrets of the Mediterranean
Ringer, Albert Is Anger the Enemy?
At a glance, Query 2 looks equivalent to Query 1, although the join order is slightly different. But
notice how the results differ. Query 2 didn't achieve the goal of preserving the titles rows without
corresponding authors, and the row for The Psychology of Computer Cooking is again excluded.
This row would have been preserved in the first join operation:
But then the row is discarded because the second join operation preserves only authors without
matching titles:
Because the title row for The Psychology of Computer Cooking is on the lefthand side of this join
operation and only a RIGHT OUTER JOIN operation is specified, this title is discarded.
Just as INNER is an optional modifier, so is OUTER. Hence, LEFT OUTER JOIN can be
equivalently specified as LEFT JOIN, and FULL OUTER JOIN can be equivalently expressed as
FULL JOIN. However, although INNER is seldom used by convention and is usually only implied,
OUTER is almost always used by convention when specifying any type of outer join.
Because join order matters, use the parentheses and indentation carefully when specifying
OUTER JOIN operations. Indentation, of course, is always optional, and use of parentheses is
often optional. But as this example shows, it's easy to make mistakes that result in your queries
returning the wrong answers. As is true with almost all programming, simply getting into the habit
of using comments, parentheses, and indentation often results in such bugs being noticed and
fixed by a developer or database administrator before they make their way into your applications.
For inner joins, the symmetric property holds, so the issues with old-style JOIN syntax don't exist.
You can use either new-style or old-style syntax with inner joins. For outer joins, you should
consider the *= operator obsolete and move to the OUTER JOIN syntax as quickly as possible—
the *= operator might be dropped entirely in future releases of SQL Server.
ANSI's OUTER JOIN syntax, which was adopted in SQL Server version 6.5, recognizes that for
outer joins, the conditions of the join must be evaluated separately from the criteria applied to the
rows that are joined. ANSI gets it right by separating the JOIN criteria from the WHERE criteria.
The old SQL Server *= and =* operators are prone to ambiguities, especially when three or more
tables, views, or subqueries are involved. Often the results aren't what you'd expect, even though
you might be able to explain them. But sometimes you simply can't get the result you want. These
aren't implementation bugs; more accurately, these are inherent limitations in trying to apply the
outer-join criteria in the WHERE clause.
When *= was introduced, no ANSI specification existed for OUTER JOIN or even for INNER
JOIN. Just the old-style join existed, with operators such as =* in the WHERE clause. So the
designers quite reasonably tried to fit an outer-join operator into the WHERE clause, which was
the only place where joins were ever stated. However, efforts to resolve this situation helped spur
the ANSI SQL committee's specification of proper OUTER JOIN syntax and semantics.
Implementing outer joins correctly is difficult, and SQL Server is one of the few mainstream
products that has done so.
To illustrate the semantic differences and problems with the old *= syntax, a series of examples
will be discussed using both new and old outer-join syntax. The following is essentially the same
outer-join query shown earlier, but this one returns only a count. It correctly finds the 11 rows,
preserving both authors with no titles and titles with no authors.
There's really no way to write this query—which does a full outer join—using the old syntax,
because the old syntax simply isn't expressive enough. Here's what looks to be a reasonable
try—but it generates several rows that you wouldn't expect:
(
Cust_ID int PRIMARY KEY,
Cust_Name char(20)
)
At a glance, in the simplest case, the new-style and old-style syntax appear to work the same.
Here's the new syntax:
SELECT
'Customers.Cust_ID'=Customers.Cust_ID, Customers.Cust_Name,
'Orders.Cust_ID'=Orders.Cust_ID
FROM Customers LEFT JOIN Orders
ON Customers.Cust_ID=Orders.Cust_ID
But as soon as you begin to add restrictions, things get tricky. What if you want to filter out Cust
2? With the new syntax it's easy, but remember not to filter out the row with NULL that the outer
join just preserved!
Now try to do this query using the old-style syntax and filter out Cust 2:
Notice that this time, we don't get rid of Cust 2. The check for NULL occurs before the JOIN, so
the outer-join operation puts Cust 2 back. This result might be less than intuitive, but at least we
can explain and defend it. That's not always the case, as you'll see in a moment.
If you look at the preceding query, you might think that we should have filtered out
Customers.Cust_ID rather than Orders.Cust_ID. How did we miss that? Surely this query will fix
the problem:
Oops! Same result. The problem here is that Orders.Cust_ID IS NULL is now being applied after
the outer join, so the row is presented again. If we're careful and understand exactly how the old
outer join is processed, we can get the results we want with the old-style syntax for this query.
We need to understand that the OR Orders.Cust_ID IS NULL puts back Cust_ID 2, so just take
that out. Here is the code:
Finally! This is the result we want. And if you really think about it, the semantics are even
understandable (although different from the new style). Besides the issues of joins with more than
two tables and the lack of a full outer join, we also can't effectively deal with subqueries and
views (virtual tables). For example, let's try creating a view with the old-style outer join:
Cust_ID Cust_Name
------- ---------
1 Cust 1
2 Cust 2
NULL Cust 3
The output shows NULLs in the Cust_ID column, even though we tried to filter them out:
If we expand the view to the full select and we realize that Cust_ID is Orders.Cust_ID, not
Customers.Cust_ID, perhaps we can understand why this happened. But we still can't filter out
those rows! In contrast, if we create the view with the new syntax and correct semantics, it works
exactly as expected:
SELECT * FROM Cust_new_OJ WHERE Cust_ID <> 2 AND Cust_ID IS NOT NULL
Cust_ID Cust_Name
------- ---------
1 Cust 1
y
The new syntax performed the outer join and then applied the restrictions in the WHERE clause
to the result. In contrast, the old style applied the WHERE clause to the tables being joined and
then performed the outer join, which can reintroduce NULL rows. This is why the results often
seemed bizarre. However, if that behavior is what you want, you could apply the criteria in the
JOIN clause instead of in the WHERE clause.
The following example uses the new syntax to mimic the old behavior. The WHERE clause is
shown here simply as a placeholder to make clear that the statement Cust_ID <> 2 is in the JOIN
section, not in the WHERE section.
With the improvements in outer-join support, you can now use outer joins where you couldn't
previously. A bit later, you'll see how to use an outer join instead of a correlated subquery in a
common type of query.
Cross Joins
In addition to INNER JOIN, OUTER JOIN, and FULL JOIN, the ANSI JOIN syntax allows a
CROSS JOIN. Earlier, you saw that the advantage of using the ANSI JOIN syntax was that you
wouldn't accidentally create a Cartesian product. However, in some cases, creating a Cartesian
product might be exactly what you want to do. SQL Server allows you to specify a CROSS JOIN
with no ON clause to produce a Cartesian product.
For example, one use for CROSS JOINs is to generate sample or test data. For example, to
generate 10,000 names for a sample employees table, you don't have to come up with 10,000
individual INSERT statements. All you need to do is build a first_names table and a last_names
table with 26 names each (perhaps one starting with each letter of the English alphabet), and a
middle_initials table with the 26 letters. When these three small tables are joined using the
CROSS JOIN operator, the result is well over 10,000 unique names to insert into the employees
table. The SELECT statement used to generate these names looks like this:
To summarize the five types of ANSI JOIN operations, consider two tables, TableA and TableB:
The INNER JOIN returns rows from either table only if they have a corresponding row in the other
table. In other words, the INNER JOIN disregards any rows in which the specific join condition, as
specified in the ON clause, isn't met.
The LEFT OUTER JOIN returns all rows for which a connection exists between TableA and
TableB; in addition, it returns all rows from TableA for which no corresponding row exists in
TableB. In other words, it preserves unmatched rows from TableA. TableA is sometimes called
the preserved table. In result rows containing unmatched rows from TableA, any columns
selected from TableB are returned as NULL.
The RIGHT OUTER JOIN returns all rows for which a connection exists between TableA and
TableB; in addition, it returns all rows from TableB for which no corresponding row exists in
TableA. In other words, it preserves unmatched rows from TableB, and in this case TableB is the
preserved table. In result rows containing unmatched rows from TableB, any columns selected
from TableA are returned as NULL.
The FULL OUTER JOIN returns all rows for which a connection exists between TableA and
TableB. In addition, it returns all rows from TableA for which no corresponding row exists in
TableB, with any values selected from TableB returned as NULL. In addition, it returns all rows
from TableB for which no corresponding row exists in TableA, with any values selected from
TableA returned as NULL. In other words, FULL OUTER JOIN acts as a combination of LEFT
OUTER JOIN and RIGHT OUTER JOIN.
CROSS JOIN
TableA CROSS JOIN TableB
The CROSS JOIN returns all rows from TableA combined with all rows from TableB. No ON
clause exists to indicate any connecting column between the tables. A CROSS JOIN returns a
Cartesian product of the two tables.
Subqueries
SQL Server has an extremely powerful capability for nesting queries that provides a natural and
efficient way to express WHERE clause criteria in terms of the results of other queries. You can
express most joins as subqueries, although this method is often less efficient than performing a
join operation. For example, to use the pubs database to find all employees of the New Moon
Books publishing company, you can write the query as either a join (using ANSI join syntax) or as
a subquery.
You can write a join (equijoin) as a subquery (subselect), but the converse isn't necessarily true.
The equijoin offers an advantage in that the two sides of the equation equal each other and the
order doesn't matter. In certain types of subqueries, it does matter which query is the nested
query. However, if the query with the subquery can be rephrased as a semantically equivalent
JOIN query, the optimizer will do the conversion internally and the performance will be the same
whether you write your queries as joins or with subqueries.
Relatively complex operations are simple to perform when you use subqueries. For example,
earlier you saw that the pubs sample database has four rows in the authors table that have no
related row in the titleauthor table (which prompted our outer-join discussion).
Each of these formulations is equivalent to testing the value of au_id in the authors table to the
au_id value in the first row in the titleauthor table, and then OR'ing it to a test of the au_id value of
the second row, and then OR'ing it to a test of the value of the third row, and so on. As soon as
one row evaluates to TRUE, the expression is TRUE, and further checking can stop because the
row in authors qualifies. However, it's an easy mistake to conclude that NOT IN must be
equivalent to <> ANY, and some otherwise good discussions of the SQL language have made
Careful reading of the ANSI SQL-92 specifications also reveals that NOT IN is equivalent to <>
ALL but is not equivalent to <> ANY. Section 8.4 of the specifications shows that R NOT IN T is
equivalent to NOT (R = ANY T). Furthermore, careful study of section 8.7 <quantified comparison
predicate> reveals that NOT (R = ANY T) is TRUE if and only if R <> ALL T is TRUE. In other
words, NOT IN is equivalent to <> ALL.
By using NOT IN, you're stating that none of the corresponding values can match. In other words,
all of the values must not match (<> ALL), and if even one does match, it's FALSE. With <> ANY,
as soon as one value is found to be not equivalent, the expression is TRUE. This, of course, is
also the case for every row of authors: rows in titleauthor will always exist for other au_id values,
and hence all authors rows will have at least one nonmatching row in titleauthor. That is, every
row in authors will evaluate to TRUE for a test of <> ANY row in titleauthor.
The following query using <> ALL returns the same four rows as the earlier one that used NOT
IN:
If you had made the mistake of thinking that because IN is equivalent to = ANY, then NOT IN is
equivalent to <> ANY, you would have written the query as follows. This returns all 23 rows in the
authors table!
The examples just shown use IN, NOT IN, ANY, and ALL to compare values to a set of values
from a subquery. This is common. However, it's also common to use expressions and compare a
set of values to a single, scalar value. For example, to find titles whose royalties exceed the
average of all royalty values in the roysched table by 25 percent or more, you can use this simple
query:
This query is perfectly good because the aggregate function AVG (expression) stipulates that the
subquery must return exactly one value and no more. Without using IN, ANY, or ALL (or their
negations), a subquery that returns more than one row will result in an error. If you incorrectly
rewrote the query as follows, without the AVG function, you'd get run-time error 512:
y
The subquery in the following code returns only one row, so the query is valid and returns four
rows:
However, this sort of query can be dangerous, and you should avoid it or use it only when you
know that a PRIMARY KEY or UNIQUE constraint will ensure that the subquery returns only one
value. The query here appears to work, but it's a bug waiting to happen. As soon as another row
is added to the roysched table—say, with a title_id of MC3021 and a lorange of 0—the query
returns an error. No constraint exists to prevent such a row from being added.
You might argue that SQL Server should determine whether a query formation could conceivably
return more than one row regardless of the data at the time and then disallow such a subquery
formulation. The reason it doesn't is that such a query might be quite valid when the database
relationships are properly understood, so the power shouldn't be limited to try to protect naïve
users. Whether you agree with this philosophy or not, it's consistent with SQL in general—and
you should know by now that you can easily write a perfectly legal, syntactically correct query that
answers a question in a way that's entirely different from what you thought you were asking!
Correlated Subqueries
You can use powerful correlated subqueries to compare specific rows of one table to a condition
in a matching table. For each row otherwise qualifying in the main (or top) query, the subquery is
evaluated. Conceptually, a correlated subquery is similar to a loop in programming, although it's
entirely without procedural constructs such as do-while or for. The results of each execution of
the subquery must be correlated to a row of the main query. In the next example, for every row in
the titles table that has a price of $19.99 or less, the row is compared with each sales row for
stores in California for which the revenue (price × qty) is greater than $250. In other words, "Show
me titles with prices of under $20 for which any single sale in California was more than $250."
title_id title
-------- -----------------------------
BU7832 Straight Talk About Computers
PS2091 Is Anger the Enemy?
TC7777 Sushi, Anyone?
Correlated subquery, like many subqueries, could have been written as a join (here using the old-
style JOIN syntax):
It becomes nearly impossible to create alternative joins when the subquery isn't doing a simple IN
or when it uses aggregate functions. For example, suppose we want to find titles that lag in sales
for each store. This could be defined as "Find any title for every store in which the title's sales in
that store are below 80 percent of the average of sales for all stores that carry that title and ignore
titles that have no price established (that is, the price is NULL)." An intuitive way to do this is to
first think of the main query that will give us the gross sales for each title and store, and then for
each such result, do a subquery that finds the average gross sales for the title for all stores. Then
we correlate the subquery and the main query, keeping only rows that fall below the 80 percent
standard.
y
For clarity, notice the two distinct queries, each of which answers a separate question. Then
notice how they can be combined into a single correlated query to answer the specific question
posed here. All three queries use the old-style JOIN syntax.
-- This query computes 80% of the average gross revenue for each
-- title for all stores carrying that title:
SELECT T2.title_id, .80*AVG(price*qty)
When the newer ANSI JOIN syntax was first introduced, it wasn't obvious how to use it to write a
correlated subquery. It could be that the creators of the syntax forgot about the correlated
subquery case, because using the syntax seems like a hybrid of the old and the new: the
correlation is still done in the WHERE clause rather than in the JOIN clause.
y
Examine the two equivalent formulations of the above query using the ANSI JOIN syntax:
To completely avoid the old-style syntax with the join condition in the WHERE clause, we could
write this using a subquery with a GROUP BY in the FROM clause (by creating a derived table).
However, although this gets around having to use the old syntax, it might not be worth it. The
query is much less intuitive than either of the preceding two formulations, and it takes twice as
many logical reads to execute it.
Often, correlated subqueries use the EXISTS statement, which is the most convenient syntax to
use when multiple fields of the main query are to be correlated to the subquery. (In practice,
EXISTS is seldom used other than with correlated subqueries.) EXISTS simply checks for a
nonempty set. It returns (internally) either TRUE or NOT TRUE (which we won't refer to as
FALSE, given the issues of three-valued logic and NULL). Because no column value is returned
and the only thing that matters is whether any rows are returned, convention dictates that a
column list isn't specified.
A common use for EXISTS is to answer a query such as "Show me the titles for which no stores
have sales."
title_id title
-------- ----------------------------------
MC3026 The Psychology of Computer Cooking
PC9999 Net Etiquette
Conceptually, this query is pretty straightforward. The subquery, a simple equijoin, finds all
matches of titles and sales. Then NOT EXISTS correlates titles to those matches, looking for
titles that don't have even a single row returned in the subquery.
Another common use of EXISTS is to determine whether a table is empty. The optimizer knows
that as soon as it gets a single hit using EXISTS, the operation is TRUE and further processing is
unnecessary. For example, here's how you determine whether the authors table is empty:
Here's an outer-join formulation for the problem described earlier: "Show me the titles for which
no stores have sales."
$
Depending on your data and indexes, the outer-join formulation might be faster or slower than a
correlated subquery. But before deciding to write your query one way or the other, you might want
to come up with a couple of alternative formulations and then choose the one that's fastest in
your situation.
In this example, for which little data exists, both solutions run in subsecond elapsed time. But the
outer-join query requires fewer than half the number of logical I/Os than does the correlated
subquery. With more data, that difference would be significant.
This query works by joining the stores and titles tables and by preserving the titles for which no
store exists. Then, in the WHERE clause, it specifically chooses only the rows that it preserved in
the outer join. Those rows are the ones for which a title had no matching store.
At other times, a correlated subquery might be preferable to a join, especially if it's a self-join
back to the same table or some other exotic join. Here's an example. Given the following table
(and assuming that the row_num column is guaranteed unique), suppose we want to identify the
rows for which col2 and col3 are duplicates of another row:
We can do this in two standard ways. The first way uses a self-join. In a self-join, the table (or
view) is used multiple times in the FROM clause and is aliased at least once. Then it can be
treated as an entirely different table and you can compare columns between two "instances" of
the same table. A self-join to find the rows having duplicate values for col2 and col3 is easy to
understand:
But in this case, a correlated subquery using aggregate functions provides a considerably more
efficient solution, especially if many duplicates exist:
This correlated subquery has another advantage over the self-join example—the row_num
column doesn't need to be unique to solve the problem at hand.
You can take a correlated subquery a step further to ask a seemingly simple question that's
surprisingly tricky to answer in SQL: "Show me the stores that have sold every title." Even though
it seems like a reasonable request, relatively few people can come up with the correct SQL query,
especially if I throw in the restrictions that you aren't allowed to use an aggregate function like
COUNT(*) and that the solution must be a single SELECT statement (that is, you're not allowed
to create temporary tables or the like).
The previous query already revealed two titles that no store has sold, so we know that with the
existing dataset, no stores can have sales for all titles. For illustrative purposes, let's add sales
records for a hypothetical store that does, in fact, have sales for every title. Following that, we'll
see the query that finds all stores that have sold every title (which we know ahead of time is only
the phony one we're entering here):
Although this query might be difficult to think of immediately, you can easily understand why it
works. In English, it says, "Show me the store(s) such that no titles exist that the store doesn't
sell." This query consists of the two subqueries that are applied to each store. The bottommost
subquery produces all the titles that the store has sold. The upper subquery is then correlated to
that bottom one to look for any titles that are not in the list of those that the store has sold. The
top query returns any stores that aren't in this list. This type of query is known as a relational
division, and unfortunately, it isn't as easy to express as we'd like. Although the query shown is
quite understandable, once you have a solid foundation in SQL, it's hardly intuitive. As is almost
always the case, you could probably use other formulations to write this query.
If you think of the query in English as "Find the stores that have sold as many unique titles as
there are total unique titles," you'll find the following formulation somewhat more intuitive:
The following formulation runs much more efficiently than either of the previous two. The syntax is
similar to the preceding one but its approach is novel because it's just a standard subquery, not a
join or a correlated subquery. You might think it's an illegal query, since it does a GROUP BY and
a HAVING without an aggregate in the select list of the first subquery. But that's OK, both in
terms of what SQL Server allows and in terms of the ANSI specification
Here's a formulation that uses a derived table—a feature that allows you to use a subquery in a
FROM clause. This capability lets you alias a virtual table returned as the result set of a SELECT
statement, and then lets you use this result set as if it were a real table. This query also runs
efficiently.
Functions
Introduction
The SQL language includes many functions that you can use to summarize data from a column
within a table. Collectively, the functions that enable you to summarize data are referred to as
aggregate functions. You might also hear aggregate functions referred to as group functions
because they operate on groups of rows to provide you with a single result.
USE database
SELECT FUNCTION(expression)
FROM table
You will typically replace expression with a column name. You can optionally include an AS
clause after the function so that SQL Server can display a heading for the column in the result
set. If you do not specify an alias when you use a function, SQL Server does not display a column
heading. When you use an aggregate function in your SELECT statement, you cannot include
other columns in the SELECT clause unless you use a GROUP BY clause.
The following query shows you how to find the highest rental price for a movie in the movie table:
USE movies
SELECT MAX(rental_price) AS `Highest Rental Fee'
FROM movie
y
It enables you to count the number of rows in the customer table in order to determine the total
number of customers:
USE movies
SELECT COUNT(*)
FROM customer
y
You can use the MAX( ) function against character-based columns as follows:
USE movies
SELECT MAX(title)
FROM movie
• You can use the COUNT function against all data types. COUNT is the only aggregate
function you can use against text, ntext, and image data types.
• You can use only the int, smallint, decimal, numeric, float, real, money, and smallmoney
data types in the SUM( ) and AVG( ) functions.
Null values
Other than the COUNT function, the aggregate functions ignore null values in columns. All of the
aggregate functions base their calculations on the premise that the values in columns are
significant only if those values are not null. If you count the number of rows based on a column
with null values (such as COUNT(zip), SQL Server skips any rows that have null values in that
column. If you use COUNT(*), you will get the actual row count-even if a row has nothing but null
values in all columns.
You are logged on to Windows NT as user#. You have created a database named movies and
tables within it named movie, category, customer, rental, and rental_detail. You have defined
primary key, foreign key, default, and check constraints on the tables, and created nonclustered
indexes based on your primary keys. You have imported data into the tables. You have created
database diagrams for both the movies and pubs databases.
±1. Write a query to find the average price of movies with a G rating.
³
USE movies
SELECT AVG(rental_price) AS `Average Rental Fee'
FROM movie
WHERE rating = `G'
±2. What query would you use to find the title and price of the highest priced movie? (Hint:
You must use a subquery to find this information.)
³
USE movies
SELECT title, rental_price
FROM movie
WHERE rental_price = (SELECT MAX(rental_price) FROM movie)
y
You might want to count the number of movies you have in stock for each rating (G, PG, an so
on). To find this information, you must use a GROUP BY clause to group the movies by rating-
and then count the number of movies in each group. Use the following syntax:
USE movies
SELECT rating, COUNT(movie_num)
FROM movie
GROUP BY rating
In this example, you can use COUNT(movie_num) because the structure of the movie table does
not permit null values in the movie_num column.
The GROUP BY clause requires a one-to-one relationship between the columns you specify in
the SELECT statement (other than the aggregate function) and the GROUP BY clause. For
example, the following query is invalid because the rating column is not included in the SELECT
clause:
USE movies
SELECT COUNT(movie_num)
FROM movie
GROUP BY rating
This query is also invalid because the rating column is not referenced in a GROUP BY clause:
USE movies
SELECT rating, COUNT(movie_num)
FROM movie
y
You can use a GROUP BY clause to enable you to calculate the total rental fee collected for each
invoice in the rental_detail table:
USE movies
SELECT invoice_num, SUM(rental_price)
FROM rental_detail
GROUP BY invoice_num
USE movies
SELECT rating, AVG(rental_price)
FROM movie
The WHERE clause must precede the GROUP BY clause. If you reverse the order of these
clauses, you will get a syntax error.
y
You could use the following query to display the rating and average price of all movies for each
rating as long as the average price of those movies is greater than $2.50:
USE movies
SELECT rating, AVG(rental_price)
FROM movie
GROUP BY rating
HAVING AVG(rental_price) >= 2.50
One other difference between a WHERE clause and a HAVING clause is that the WHERE clause
restricts the groups of rows on which the aggregate function calculates its results; in contrast, the
aggregate function calculates values for all groups of rows but only displays those that meet the
HAVING clause's criteria in the result set.
±1. Design a query based on the movies database that shows the total rental price collected
for each invoice in the rental_detail table.
³
USE movies
SELECT invoice_num, SUM(rental_price) AS `Total Rental Price'
FROM rental_detail
GROUP BY invoice_num
±2. Design a query on the movies database that shows all invoices and their total rental price
where the total price was more than $4.00. (You should get 22 rows in the result set.)
³
USE movies
SELECT invoice_num, SUM(rental_price) AS `Total Rental Price'
FROM rental_detail
±3. Design a query that lists the category and average rental price of movies in the Comedy
category. (The category_num for Comedy is 1.)
³
SELECT category_num, AVG(rental_price) AS 'Average Rental Price'
FROM movie
GROUP BY category_num
HAVING category_num = 1
y
Consider the following query:
USE northwind
SELECT orderid, productid, SUM(quantity) AS `Total Quantity'
FROM [order details]
GROUP BY orderid, productid WITH ROLLUP
ORDER BY orderid, productid
This query displays the order ID, product ID, and the total quantity of each product purchased for
each order ID. In addition, because the query includes the WITH ROLLUP operator, SQL Server
Query Analyzer not only totals the quantity purchased for each product of each order, but also
generates a total for each order. SQL Server adds a row to the result set to display the running
totals. You can identify running totals because SQL Server Query Analyzer indicates them by
NULL in the result set. (In this example, the running total for all products for a particular orderid
displays NULL in the productid column.)
y
USE northwind
SELECT orderid, productid, SUM(quantity) AS `Total Quantity'
FROM [order details]
GROUP BY orderid, productid WITH CUBE
ORDER BY orderid, productid
In this example, SQL Server calculates not only the total quantity of products sold in each order,
but also the total quantity of all products sold (regardless of order ID and product ID), as well as
the total quantity of each product sold (regardless of the order ID). SQL Server displays NULL in
the columns with calculations resulting from the WITH CUBE operator. In this example, you will
see additional rows in the result set over what you saw when you used the WITH ROLLUP
operator.
y
Use the GROUPING function along with the WITH CUBE operator, use the following syntax:
USE northwind
SELECT orderid, GROUPING(orderid), productid, GROUPING(productid), SUM(quantity) AS
`Total Quantity'
FROM [order details]
GROUP BY orderid, productid
WITH CUBE
ORDER BY orderid, productid
In this example, SQL Server will display a `1' in the column it is using to group the results on, and
a `0' if it is not using the column. The GROUPING thus enables you to identify which columns
SQL Server used to generate its summary rows.
y
You can use COMPUTE to display the total quantity of all products purchased in the order details
table by using the following query:
This query adds a row to the end of the result set with the total quantity of all products purchased.
In contrast, you can use the COMPUTE BY clause to calculate a total quantity purchased in each
order by using the following query:
USE northwind
SELECT orderid, productid, quantity
FROM [order details]
ORDER BY orderid, productid
COMPUTE SUM(quantity)BY orderid
±1. By using the movies database, write a query to list the titles of the top three movies based
on rental price. (Hint: make sure you use the DISTINCT keyword so that you get three unique
titles.)
³
USE movies
SELECT DISTINCT TOP 3 title, rental_price
FROM movie
ORDER BY rental_price DESC
±2. Use the movies database to write a query listing the top three invoice numbers based on
total money spent renting movies.
³
USE movies
SELECT TOP 3 invoice_num, SUM(rental_price)
FROM rental_detail
GROUP BY invoice_num
ORDER BY SUM(rental_price) DESC
±3. Use the pubs database to write a query listing the titles and prices of the top five most
expensive books.
³
USE pubs
SELECT TOP 5 title, price
FROM titles
ORDER BY price DESC
Index
Indexes are the other significant user-defined, on-disk data structure besides tables. An index
provides fast access to data when the data can be searched by the value that is the index key.
In this chapter, you'll understand the physical organization of index pages for both types of SQL
Server indexes, clustered and nonclustered, the various options available when you create and
re-create indexes, when, and why to rebuild your indexes, SQL Server 2000's online index
defragmentation utility and about a tool for determining whether your indexes need
defragmenting.
Indexes allow data to be organized in a way that allows optimum performance when you access
or modify it. SQL Server does not need indexes to successfully retrieve results for your SELECT
statements or to find and modify the specified rows in your data modification statements. As your
tables get larger, the value of using proper indexes becomes obvious. You can use indexes to
quickly find data rows that satisfy conditions in your WHERE clauses, to find matching rows in
your JOIN clauses, or to efficiently maintain uniqueness of your key columns during INSERT and
UPDATE operations. In some cases, you can use indexes to help SQL Server sort, aggregate, or
group your data or to find the first few rows as indicated in a TOP clause.
It is the job of the query optimizer to determine which indexes, if any, are most useful in
processing a specific query. The final choice of which indexes to use is one of the most important
components of the query optimizer's execution plan. In this chapter, we’ll understand what
indexes look like and how they can speed up your queries.
Index Organization
Think of the indexes that you see in your everyday life—those in books and other documents.
Suppose you're trying to write a SELECT statement in SQL Server using the CASE expression,
and you're using two SQL Server documents to find out how to write the statement. One
document is the Microsoft SQL Server 2000 Transact-SQL Language Reference Manual. The
other is this book, Inside Microsoft SQL Server 2000. You can quickly find information in either
book about CASE, even though the two books are organized differently.
In the T-SQL language reference, all the information is organized alphabetically. You know that
CASE will be near the front, so you can just ignore the last 80 percent of the book. Keywords are
shown at the top of each page to tell you what topics are on that page. So you can quickly flip
through just a few pages and end up at a page that has BREAK as the first keyword and
CEILING as the last keyword, and you know that CASE will be on this page. If CASE is not on
this page, it's not in the book. And once you find the entry for CASE, right between BULK
INSERT and CAST, you'll find all the information you need, including lots of helpful examples.
Next, you try to find CASE in this book. There are no helpful key words at the top of each page,
but there's an index at the back of the book and all the index entries are organized alphabetically.
So again, you can make use of the fact that CASE is near the front of the alphabet and quickly
find it between "cascading updates" and "case-insensitive searches." However, unlike in the
reference manual, once you find the word CASE, there are no nice neat examples right in front of
you. Instead, the index gives you pointers. It tells you what pages to look at—it might list two or
three pages that point you to various places in the book. If you look up SELECT in the back of the
book, however, there might be dozens of pages listed, and if you look up SQL Server, there might
be hundreds.
Both clustered and nonclustered indexes in SQL Server store their information using standard B-
trees, as shown in figure 21. A B-tree provides fast access to data by searching on a key value of
the index. B-trees cluster records with similar keys. The B stands for balanced, and balancing the
tree is a core feature of a B-tree's usefulness. The trees are managed, and branches are grafted
as necessary, so that navigating down the tree to find a value and locate a specific record takes
only a few page accesses. Because the trees are balanced, finding any record requires about the
same amount of resources, and retrieval speed is consistent because the index has the same
depth throughout.
An index consists of a tree with a root from which the navigation begins, possible intermediate
index levels, and bottom-level leaf pages. You use the index to find the correct leaf page. The
number of levels in an index will vary depending on the number of rows in the table and the size
of the key column or columns for the index. If you create an index using a large key, fewer entries
will fit on a page, so more pages (and possibly more levels) will be needed for the index. On a
qualified select, update, or delete, the correct leaf page will be the lowest page of the tree in
which one or more rows with the specified key or keys reside. A qualified operation is one that
affects only specific rows that satisfy the conditions of a WHERE clause, as opposed to
accessing the whole table. In any index, whether clustered or nonclustered, the leaf level contains
every key value, in key sequence.
Clustered Indexes
Because the actual page chain for the data pages can be ordered in only one way, a table can
have only one clustered index. The query optimizer strongly favors a clustered index because
such an index allows the data to be found directly at the leaf level. Because it defines the actual
order of the data, a clustered index allows especially fast access for queries looking for a range of
values. The query optimizer detects that only a certain range of data pages must be scanned.
Most tables should have a clustered index. If your table will have only one index, it generally
should be clustered. Many documents describing SQL Server indexes will tell you that the
clustered index physically stores the data in sorted order. This can be misleading if you think of
physical storage as the disk itself. If a clustered index had to keep the data on the actual disk in a
particular order, it could be prohibitively expensive to make changes. If a page got too full and
had to be split in two, all the data on all the succeeding pages would have to be moved down.
Sorted order in a clustered index simply means that the data page chain is in order. If SQL Server
follows the page chain, it can access each row in clustered index order, but new pages can be
added by simply adjusting the links in the page chain. In SQL Server 2000, all clustered indexes
are unique. If you build a clustered index without specifying the unique keyword, SQL Server
forces uniqueness by adding a uniqueifier to the rows when necessary. This uniqueifier is a 4-
byte value added as an additional sort key to only the rows that have duplicates of their primary
sort key.
Nonclustered Indexes
In a nonclustered index, the lowest level of the tree (the leaf level) contains a bookmark that tells
SQL Server where to find the data row corresponding to the key in the index. A bookmark can
take one of two forms. If the table has a clustered index, the bookmark is the clustered index key
for the corresponding data row. If the table is a heap (in other words, it has no clustered index),
the bookmark is a RID, which is an actual row locator in the form File#:Page#:Slot#. (In contrast,
in a clustered index, the leaf page is the data page.)
The presence or absence of a nonclustered index doesn't affect how the data pages are
organized, so you're not restricted to having only one nonclustered index per table, as is the case
with clustered indexes. Each table can include as many as 249 nonclustered indexes, but you'll
usually want to have far fewer than that.
When you search for data using a nonclustered index, the index is traversed and then SQL
Server retrieves the record or records pointed to by the leaf-level indexes. For example, if you're
looking for a data page using an index with a depth of three—a root page, one intermediate page,
and the leaf page—all three index pages must be traversed. If the leaf level contains a clustered
index key, all the levels of the clustered index then have to be traversed to locate the specific row.
The clustered index will probably also have three levels, but in this case remember that the leaf
level is the data itself. There are two additional index levels separate from the data, typically one
less than the number of levels needed for a nonclustered index. The data page still must be
retrieved, but because it has been exactly identified there's no need to scan the entire table. Still,
it takes six logical I/O operations to get one data page. You can see that a nonclustered index is a
win only if it's highly selective.
Figure22 SQL server traverse both clusteres and non clustered Indexes to find the first
name for the employee names Anson
Creating an Index
The typical syntax for creating an index is straightforward:
When you create an index, you must specify a name for it. You must also specify the table on
which the index will be built, and then one or more columns. For each column, you can specify
that the leaf level will store the key values sorted in either ascending (ASC) or descending
(DESC) order. The default is ascending. You can specify that SQL Server must enforce
uniqueness of the key values by using the keyword UNIQUE. If you don't specify UNIQUE,
duplicate key values will be allowed. You can specify that the index be either clustered or
nonclustered. Nonclustered is the default.
CREATE INDEX has some additional options available for specialized purposes. You can add a
WITH clause to the CREATE INDEX command:
[WITH
[FILLFACTOR = fillfactor]
[[,] [PAD_INDEX]
[[,] IGNORE_DUP_KEY]
[[,] DROP_EXISTING]
[[,] STATISTICS_NORECOMPUTE]
FILLFACTOR is probably the most commonly used of these options. FILLFACTOR lets you
reserve some space on each leaf page of an index. In a clustered index, since the leaf level
contains the data, you can use FILLFACTOR to control how much space to leave in the table
itself. By reserving free space, you can later avoid the need to split pages to make room for a new
entry. But remember that FILLFACTOR is not maintained; it indicates only how much space is
reserved with the existing data at the time the index is built. If you need to, you can use the
DBCC DBREINDEX command to rebuild the index and reestablish the original FILLFACTOR
specified.
If you plan to rebuild all of a table's indexes, simply specify the clustered index with DBCC
DBREINDEX. Doing so internally rebuilds the entire table and all nonclustered indexes.
FILLFACTOR isn't usually specified on an index-by-index basis, but you can specify it this way for
fine-tuning. If FILLFACTOR isn't specified, the serverwide default is used. The value is set for the
server via sp_configure, fillfactor. This value is 0 by default, which means that leaf pages of
indexes are made as full as possible. FILLFACTOR generally applies only to the index's leaf
page (the data page for a clustered index). In specialized and high-use situations, you might want
to reserve space in the intermediate index pages to avoid page splits there, too. You can do this
by specifying the PAD_INDEX option, which uses the same value as FILLFACTOR.
The DROP_EXISTING option specifies that a given index should be dropped and rebuilt as a
single transaction. This option is particularly useful when you rebuild clustered indexes. Normally,
when a clustered index is dropped, every nonclustered index has to be rebuilt to change its
bookmarks to RIDs instead of the clustering keys. Then, if a clustered index is built (or rebuilt), all
the nonclustered indexes need to be rebuilt again to update the bookmarks. The
DROP_EXISTING option of the CREATE INDEX command allows a clustered index to be rebuilt
without having to rebuild the nonclustered indexes twice. If you are creating the index on the
exact same keys that it had previously, the nonclustered indexes do not need to be rebuilt at all. If
you are changing the key definition, the nonclustered indexes are rebuilt only once, after the
clustered index is rebuilt.
You can ensure the uniqueness of an index key by defining the index as UNIQUE or by defining a
PRIMARY KEY or UNIQUE constraint. If an UPDATE or INSERT statement would affect multiple
rows, and if even one row is found that would cause duplicate keys defined as unique, the entire
statement is aborted and no rows are affected. Alternatively, when you create the unique index,
you can use the IGNORE_DUP_KEY option so that a duplicate key error on a multiple-row
INSERT won't cause the entire statement to be rolled back. The nonunique row will be discarded,
and all other rows will be inserted or updated. IGNORE_DUP_KEY doesn't allow the uniqueness
of the index to be violated; instead, it makes a violation in a multiple-row data modification
nonfatal to all the nonviolating rows.
The SORT_IN_TEMPDB option allows you to control where SQL Server performs the sort
operation on the key values needed to build an index. The default is that SQL Server uses space
from the filegroup on which the index is to be created. While the index is being built, SQL Server
scans the data pages to find the key values and then builds leaf-level index rows in internal sort
buffers. When these sort buffers are filled, they are written to disk. The disk heads for the
database can then move back and forth between the base table pages and the work area where
the sort buffers are being stored. If, instead, your CREATE INDEX command includes the option
When you create a table that includes PRIMARY KEY or UNIQUE constraints, you can specify
whether the associated index will be clustered or nonclustered, and you can also specify the
fillfactor. Since the fillfactor applies only at the time the index is created, and since there is no
data when you first create the table, it might seem that specifying the fillfactor at that time is
completely useless. However, if after the table is populated you decide to use DBCC
DBREINDEX to rebuild all your indexes, you can specify a fillfactor of 0 to indicate that SQL
Server should use the fillfactor that was specified when the index was created. You can also
specify a fillfactor when you use ALTER TABLE to add a PRIMARY KEY or UNIQUE constraint to
a table, and if the table already had data in it, the fillfactor value is applied when you build the
index to support the new constraint.
If you check the documentation for CREATE TABLE and ALTER TABLE, you'll see that the
SORT_IN_TEMPDB option is not available for either command. It really doesn't make sense to
specify a sort location when you first create the table because there's nothing to sort. However,
the fact that you can't specify this alternate location when you add a PRIMARY KEY or UNIQUE
constraint to a table with existing data seems like an oversight. Also note that
SORT_IN_TEMPDB is not an option when you use DBCC DBREINDEX. Again, there's no reason
why it couldn't have been included, but it isn't available in this release.
The biggest difference between indexes created using the CREATE INDEX command and
indexes that support constraints is in how you can drop the index. The DROP INDEX command
allows you to drop only indexes that were built with the CREATE INDEX command. To drop
indexes that support constraints, you must use ALTER TABLE to drop the constraint. In addition,
to drop a PRIMARY KEY or UNIQUE constraint that has any FOREIGN KEY constraints
referencing it, you must first drop the FOREIGN KEY constraint. This can leave you with a
window of vulnerability while you redefine your constraints and rebuild your indexes. While the
FOREIGN KEY constraint is gone, an INSERT statement can add a row to the table that violates
your referential integrity.
One way to avoid this problem is to use DBCC DBREINDEX, which drops and rebuilds all your
indexes on a table in a single transaction, without requiring the auxiliary step of removing
FOREIGN KEY constraints. Alternatively, you can use the CREATE INDEX command with the
y
The following command attempts to rebuild the index on the title_id column of the titles table in
the pubs database:
How would you have known that this index supported a constraint? First of all, the name includes
UPKCL, which is a big clue. However, the output of sp_helpindex tells us only the names of the
indexes, the property (clustered or unique), and the columns the index is on. It doesn't tell us if
the index supports a constraint. However, if we execute sp_help on a table, the output will tell us
that UPKCL_titleidind is a PRIMARY KEY constraint. The error message indicates that we can't
use the DROP_EXISTING clause to rebuild this index because the new definition doesn't match
the current index. We can use this command as long as the properties of the new index are
exactly the same as the old. In this case, we can rephrase the command as follows so that the
CREATE INDEX is successful:
The header information for index pages is almost identical to the header information for data
pages. The only difference is the value for type, which is 1 for data and 2 for index. The header of
an index page also has nonzero values for level and indexId. For data pages, these values are
both always 0.
USE pubs
GO
CREATE TABLE Clustered_Dupes
(Col1 char(5) NOT NULL,
Col2 int NOT NULL,
Col3 char(3) NULL,
Col4 char(6) NOT NULL,
Col5 float NOT NULL)
GO
CREATE CLUSTERED INDEX Cl_dupes_col1 ON Clustered_Dupes(col1)
The rows in syscolumns are without the clustered index. However, if you look at the sysindexes
row for this table, you'll notice something different.
RESULT:
The column called keycnt, which indicates the number of keys an index has, is 2. If we had
created this index using the UNIQUE qualifier, the keycnt value would be 1. If we had looked at
the sysindexes row before adding a clustered index, when the table was still a heap, the row for
the table would have had a keycnt value of 0. Add the initial row and then look at sysindexes to
find the first page of the table:
RESULT:
The first column tells me that the first page of the table is 1461 (0x05B5), so we can use DBCC
PAGE to examine that page. Remember to turn on trace flag 3604 prior to executing this
undocumented DBCC command.
We can reproduce it again here, in Figure 23. When you read the row output from DBCC PAGE,
remember that each two displayed characters represents a byte and that the bytes are displayed
from right to left within each group of 4 bytes in the output. For character fields, you can just treat
each byte as an ASCII code and convert that to the associated character. Numeric fields are
stored with the low-order byte first, so within each numeric field, we must swap bytes to
determine what value is being stored. Right now, there are no duplicates of the clustered key, so
no extra information has to be provided. However, if we add two additional rows, with duplicate
values in col1, the row structure changes:
The first difference in the second two rows is that the bits in the first byte (TagA) are different. Bit
5 is on, giving TagA a value of 0x30, which means the variable block is present in the row.
Without this bit on, TagA would have a value of 0x10. The "extra" variable-length portions of the
second two rows are shaded in the figure. You can see that 8 extra bytes are added when we
have a duplicate row. In this case, the first 4 extra bytes are added because the uniqueifier is
considered a variable-length column. Since there were no variable-length columns before, SQL
Server adds 2 bytes to indicate the number of variable-length columns present. These bytes are
Now you understand the structure and number of bytes in individual index rows, but you really
need to be able to translate that into overall index size. In general, the size of an index is based
on the size of the index keys, which determines how many index rows can fit on an index page
and the number of rows in the table.
B-Tree Size
When we talk about index size, we usually mean the size of the index tree. The clustered index
does include the data, but since you still have the data even if you drop the clustered index, we're
usually just interested in how much additional space the nonleaf levels require. A clustered
index's node levels typically take up very little space. You have one index row for each page of
table data, so the number of index pages at the level above the leaf (level 1) is the bytes available
y
Consider a table of 10,000 pages and 500,000 rows with a clustered key of a 5-byte fixed-length
character. As you saw in Figure 8-5, the index key row size is 12 bytes, so we can fit 674 (8096
bytes available on a page / 12 bytes per row) rows on an index page. Since we need index rows
to point to all 10,000 pages, we'll need 15 index pages (10000 / 674) at level 1. Now, all these
index pages need pointers at level 2, and since one page can contain 15 index rows to point to
these 15 pages, level 2 is the root. If our 10,000-page table also has a 5-byte fixed-length
character nonclustered index, the leaf-level rows (level 0) will be 11 bytes long as they will each
contain a clustered index key (5 bytes), a nonclustered index key (5 bytes) and 1 byte for the
status bits. The leaf level will contain every single nonclustered key value along with the
corresponding clustered key values. An index page can hold 736 index rows at this leaf level. We
need 500,000 index rows, one for each row of data, so we need 680 leaf pages. If this
nonclustered index is unique, the index rows at the higher levels will be 12 bytes each, so 674
index rows will fit per page, and we'll need two pages at level 1, with level 2 as the one root page.
So how big are these indexes compared to the table? For the clustered index, we had 16 index
pages for a 10,000-page table, which is less than 1 percent of the size of the table. We
frequently use 1 percent as a ballpark estimate for the space requirement of a clustered index,
even though you can see that in this case it's an overly large estimate. On the other hand, our
nonclustered index needed 683 pages for the 10,000-page table, which is about 6 percent
additional space. For nonclustered indexes, it is much harder to give a good ballpark figure.
Nonclustered index keys are frequently much larger, or even composite, so it's not unusual to
have key sizes of over 100n bytes. In that case, we'd need a lot more leaf level pages, and the
total nonclustered index size could be 30 or 40 percent of the size of the table. Sometime we can
see nonclustered indexes that are as big or bigger than the table itself. Once you have two or
three nonclustered indexes, you need to double the space to support these indexes. Remember
that SQL Server allows you to have up to 249 nonclustered indexes! Disk space is cheap, but is it
that cheap? You still need to plan your indexes carefully.
Finally, there is a column in sysindexes called used. For clustered indexes or heaps, it contains
the total number of pages used for all index and table data. For LOB data, used is the number of
LOB pages. For nonclustered indexes, used is the count of index pages. Because no specific
value is just the number of nonleaf, or nondata, pages in a clustered index, you must do
additional computations to determine the total number of index pages for a table, not including the
data.
You can force SQL Server to update the sysindexes data in two ways. The simplest way to force
the space used values to be updated is to ask for this when you execute the sp_spaceused
procedure. The procedure takes two optional parameters: the first is called @objname and is a
table name in the current database, and the second is called @updateusage, which can have a
value of TRUE or FALSE. If you specify TRUE, the data in sysindexes will be updated. The
default is FALSE.
y
Forcing SQL Server to update the space used data for a table called charge:
Remember that after the first 8 pages are allocated to a table, SQL Server grants all future
allocations in units of 8 pages, called extents. The sum of all the allocations is the value of
reserved. It might be that only a few of the 8 pages in some of the extents actually have data in
them and are counted as used, so that the reserved value is frequently higher than the used
value. The data value is computed by looking at the data column in sysindexes for the clustered
index or heap and adding any LOB pages. The index_size value is computed by taking the used
value from sysindexes for the clustered index or heap, which includes all index pages for all
indexes. Because this value includes the number of data pages, which are at the leaf level of the
clustered index, we must subtract the number of data pages from used pages to get the total
number of index pages. The procedure sp_spaceused does all of its space reporting in KB, which
we can divide by 8 to get the number of pages. The unused value is then the leftover amount
after we subtract the data value and the index_size value from the reserved value.
You can also use the DBCC UPDATEUSAGE command to update the space information for
every table and index in sysindexes. Or, by supplying additional arguments, you can have the
values updated for only a single table, view, or index. In addition, DBCC UPDATEUSAGE gives
you a report of the changes it made. You might want to limit the command to only one table
because the command works by actually counting all the rows and all the pages in each table it's
interested in. For a huge database, this can take a lot of time. In fact, sp_spaceused with the
@updateusage argument set to TRUE actually calls DBCC UPDATEUSAGE and suppresses the
report of changes made.
The following command updates the space used information for every table in the current
database:
Here's some of the output I received when I ran this in the Northwind database:
DBCC UPDATEUSAGE: sysindexes row updated for table 'Order Details' (index ID 3):
USED pages: Changed from (2) to (6) pages.
RSVD pages: Changed from (2) to (6) pages.
You can use this command to update both the used and reserved values in sysindexes. It's great
to have a way to see how much space is being used by a particular table or index, but what if
we're getting ready to build a database and we want to know how much space our data and
indexes will need? For planning purposes, it would be nice to know this information ahead of
time.
If you've already created your tables and indexes, even if they're empty, SQL Server can get
column and datatype information from the syscolumns system table, and it should be able to see
any indexes you've defined in the sysindexes table. There shouldn't be a need for you to type all
this information into a spreadsheet. The CD also includes a set of stored procedures you can use
to calculate estimated space requirements when the table and indexes have already been built.
The main procedure is called sp_EstTableSize, and it requires only two parameters: the table
name and the anticipated number of rows. The procedure calculates the storage requirements for
the table and all indexes by extracting information from the sysindexes, syscolumns, and
systypes tables. The result is only an estimate when you have variable-length fields. The
procedure has no way of knowing whether a variable-length field will be completely filled, half full,
or mostly empty in every row, so it assumes that variable-length columns will be filled to the
maximum size. If you know that this won't be the case with your data, you can create a second
table that more closely matches the expected data. For example, if you have a varchar(1000)
field that you must set to the maximum because a few rare entries might need it but 99 percent of
your rows will use only about 100 bytes, you can create a second table with a varchar(100) field
and run sp_EstTableSize on that table.
Managing an Index
SQL Server maintains your indexes automatically. As you add new rows, it automatically inserts
them into the correct position in a table with a clustered index, and it adds new leaf-level rows to
your nonclustered indexes that will point to the new rows. When you remove rows, SQL Server
automatically deletes the corresponding leaf-level rows from your nonclustered indexes. Here
you'll see some specific examples of the changes that take place within an index as data
modification operations take place.
Types of Fragmentation
Internal fragmentation means that the index is taking up more space than it needs to. Scanning
the entire table involves more read operations than if no free space were available on your pages.
However, internal fragmentation is sometimes desirable—in fact, you can request internal
fragmentation by specifying a low fillfactor value when you create an index. Having room on a
page means that there is space to insert more rows without having to split a page. Splitting is a
relatively expensive operation and can lead to external fragmentation because when a page is
split, a new page must be linked into the indexes page chain, and usually the new page is not
contiguous to the page being split.
External fragmentation is truly bad only when SQL Server is doing an ordered scan of all or part
of a table or index. If you're seeking individual rows through an index, it doesn't matter where
those rows are physically located—SQL Server can find them easily. If SQL Server is doing an
unordered scan, it can use the IAM pages to determine which extents need to be fetched, and the
IAM pages list the extents in disk order, so the fetching can be very efficient. Only if the pages
need to be fetched in logical order, according to their index key values, do you need to follow the
page chain. If the pages are heavily fragmented, this operation is more expensive than if there
were no fragmentation.
Detecting Fragmentation
You can use the DBCC SHOWCONTIG command to report on the fragmentation of an index.
Here's the syntax:
DBCC SHOWCONTIG
[ ( { table_name | table_id | view_name | view_id }
[ , index_name | index_id ]
)
]
[ WITH { ALL_INDEXES
| FAST [ , ALL_INDEXES ]
| TABLERESULTS [ , { ALL_INDEXES } ]
[ , { FAST | ALL_LEVELS } ]
}
]
You can ask to see the information for all the indexes on a table or view or just one index, and
you can specify the object and index by name or by ID number. Alternatively, you can use the
ALL_INDEXES option, which provides an individual report for each index on the object,
regardless of whether a specific index is specified.
Here's some sample output from running a basic DBCC SHOWCONTIG on the order details table
in the Northwind database:
By default, DBCC SHOWCONTIG scans the page chain at the leaf level of the specified index
and keeps track of the following values:
• Average number of bytes free on each page (Avg. Bytes Free per Page)
• Number of pages accessed (Pages scanned)
• Number of extents accessed (Extents scanned)
• Number of times a page had a lower page number than the previous page in the scan
(This value for Out of order pages is not displayed, but it is used for additional
computations.)
• Number of times a page in the scan was on a different extent than the previous page in
the scan (Extent Switches)
SQL Server also keeps track of all the extents that have been accessed, and then it determines
how many gaps are in the used extents. An extent is identified by the page number of its first
page. So, if extents 8, 16, 24, 32, and 40 make up an index, there are no gaps. If the extents are
8, 16, 24, and 40, there is one gap. The value in DBCC SHOWCONTIG's output called Extent
Scan Fragmentation is computed by dividing the number of gaps by the number of extents, so in
this example the Extent Scan Fragmentation is ¼, or 25 percent. A table using extents 8, 24, 40,
and 56 has 3 gaps, and its Extent Scan Fragmentation is ¾, or 75 percent. The maximum
number of gaps is the number of extents - 1, so Extent Scan Fragmentation can never be 100
percent.
The value in DBCC SHOWCONTIG's output called Logical Scan Fragmentation is computed by
dividing the number of Out Of Order Pages by the number of pages in the table. This value is
meaningless in a heap.
You can use either the Extent Scan Fragmentation value or the Logical Scan Fragmentation
value to determine the general level of fragmentation in a table. The lower the value, the less
fragmentation there is. Alternatively, you can use the value called Scan Density, which is
computed by dividing the optimum number of extent switches by the actual number of extent
switches. A high value means that there is little fragmentation. Scan Density is not valid if the
table spans multiple files, so all in all it is less useful than the other values.
You can use DBCC SHOWCONTIG to have the report returned in a format that can be easily
inserted into a table. If you have a table with the appropriate columns and datatypes, you can
execute the following:
Many more columns are returned if you specify TABLERESULTS. In addition to the values that
the nontabular output returns, you also get the values listed in Table 8-3.
Column Meaning
ObjectName Name of the table or view processed.
ObjectId ID of the object processed.
IndexName Name of the index processed (NULL for a heap).
IndexId ID of the index (0 for a heap).
Level of the index. Level 0 is the leaf (or data) level of the index. The level
number increases moving up the tree toward the index root. The level is 0
Level
for a heap. By default, only level 0 is reported. If you specify ALL_LEVELS
along with TABLERESULTS, you get a row for each level of the index.
Pages Number of pages comprising that level of the index or the entire heap.
Number of data or index records at that level of the index. For a heap, this
Rows
is the number of data records in the entire heap.
MinimumRecordSize Minimum record size in that level of the index or the entire heap.
MaximumRecordSize Maximum record size in that level of the index or the entire heap.
AverageRecordSize Average record size in that level of the index or the entire heap.
ForwardedRecords Number of forwarded records in that level of the index or the entire heap.
Extents Number of extents in that level of the index or the entire heap.
One last option available to DBCC SHOWCONTIG is the FAST option. The command takes less
time to run when you specify this option, as you might imagine, because it gathers only data that
is available from the IAM pages and the nonleaf levels of the index, and it returns only these
values:
• Pages Scanned
• Extent Switches
• Scan Density
• Logical Scan Fragmentation
Since the level above the leaf has pointers to every page, SQL Server can determine all the page
numbers and determine the Logical Scan Fragmentation. In fact, it can also use the IAM pages to
determine the Extent Scan Fragmentation. However, the purpose of the FAST option is to
determine whether a table would benefit from online defragmenting, and since online
defragmenting cannot change the Extent Scan Fragmentation, there is little benefit in reporting it.
Removing Fragmentation
Several methods are available for removing fragmentation from an index. First, you can rebuild
the index and have SQL Server allocate all new contiguous pages for you. You can do this by
using a simple DROP INDEX and CREATE INDEX, but I've already discussed some reasons why
this is not optimal. In particular, if the index supports a constraint, you can't use the DROP INDEX
The drawback of these methods is that the table is unavailable while the index is being rebuilt. If
you are rebuilding only nonclustered indexes, there is a shared lock on the table, which means
that no modifications can be made, but other processes can SELECT from the table. Of course,
they cannot take advantage of the index you're rebuilding, so the query might not perform as well
as it should. If you're rebuilding the clustered index, SQL Server takes an exclusive lock, and no
access is allowed at all.
SQL Server 2000 allows you to defragment an index without completely rebuilding it. In this
release, DBCC INDEXDEFRAG reorders the leaf level pages into physical order as well as
logical order, but using only the pages that are already allocated to the leaf level. It basically does
an in-place ordering, similar to a sorting technique called bubble-sort. This can reduce the logical
fragmentation to 0 to 2 percent, which means that an ordered scan through the leaf level will be
much faster. In addition, DBCC INDEXDEFRAG compacts the pages of an index, based on the
original fillfactor, which is stored in the sysindexes table. This doesn't mean that the pages always
end up with the original fillfactor, but SQL Server uses that as a goal. The process does try to
leave at least enough space for one average-size row after the defragmentation takes place. In
addition, if a lock cannot be obtained on a page during the compaction phase of DBCC
INDEXDEFRAG, SQL Server skips the page and doesn't go back to it. Any empty pages created
as a result of this compaction are removed.
y
DBCC INDEXDEFRAG(0, 'charge', 1)
The algorithm SQL Server uses for DBCC INDEXDEFRAG finds the next physical page in a file
belonging to the leaf level and the next logical page in the leaf level with which to swap it. It finds
the next physical page by scanning the IAM pages for that index. The single-page allocations are
not included in this release. Pages on different files are handled separately. The algorithm finds
the next logical page by scanning the leaf level of the index. After each page move, all locks and
latches are dropped and the key of the last page moved is saved. The next iteration of the
algorithm uses the key to find the next logical page. This allows other users to update the table
and index while DBCC INDEXDEFRAG is running.
y
Suppose the leaf level of an index consists of these pages, in this order:
47 22 83 32 12 90 64
The first step is to find the first physical page, which is 12, and the first logical page, which is 47.
These pages are then swapped, using a temporary buffer as a holding area. After the first swap,
the leaf level looks like this:
The next physical page is 22, which is the same as the next logical page, so no work is done. The
next physical page is 32, which is swapped with the next logical page, 83, to look like this:
12 22 32 83 47 90 64
After the next swap of 47 with 83, the leaf level looks like this:
12 22 32 47 83 90 64
12 22 32 47 64 90 83
12 22 32 47 64 83 90
Keep in mind that DBCC INDEXDEFRAG uses only pages that are already part of the index leaf
level. No new pages are allocated. In addition, the defragmenting can take quite a while on a
large table. You get a report every five minutes on the estimated percentage completed.
Using an Index
Hopefully, you're aware of at least some of the places that indexes can be useful in your SQL
Server applications. I'll list a few of the situations in which you can benefit greatly from indexes
Joining
A typical join tries to find all the rows in one table that match rows in anther table. Of course, you
can have joins that aren't looking for exact matching, but you're looking for some sort of
relationship between tables, and equality is by far the most common relationship. A query plan for
a join frequently starts with one of the tables, finds the rows that match the search conditions, and
then uses the join key in the qualifying rows to find matches in the other table. An index on the
join column in the second table can be used to quickly find the rows that match.
Sorting
If you have a nonclustered index on the column you're sorting by, the sort keys themselves are in
order at the leaf level. For example, consider the following query:
The nonclustered index will have all the zip codes in sorted order. However, we want the data
pages themselves to be accessed to find the firstname and lastname values associated with the
zipcode. The query optimizer will decide if it's faster to traverse the nonclustered index leaf level
and from there access each data row or to just perform a sort on the data. If you're more
concerned with getting the first few rows of data as soon as possible and less concerned with the
overall time to return all the results, you can force SQL Server to use the nonclustered index with
the FAST hint.
Inverse Indexes
SQL Server allows you to sort in either ascending or descending order, and if you have a
clustered index on the sort column, SQL Server can use that index and avoid the actual sorting.
The pages at the leaf level of the clustered index are doubly linked, so SQL Server can use a
clustered index on lastname to satisfy this query
SQL Server 2000 allows you to create descending indexes. Why would you need them if an
ascending index can be scanned in reverse order? The real benefit is when you want to sort with
a combination of ascending and descending order. For example, take a look at this query:
The default sort order is ascending, so the query wants the names ordered alphabetically by last
name, and within duplicate last names, the rows should be sorted with the highest salary first and
lowest salary last. The only kind of index that can help SQL Server avoid actually sorting the data
is one with the lastname and salary columns sorted in opposite orders, such as this one:
If you execute sp_help or sp_helpindex on a table, SQL Server will indicate whether the index is
descending by placing a minus (-) after the index key. Here's a subset of some sp_helpindex
output:
Grouping
One way that SQL Server can perform a GROUP BY operation is by first sorting the data by the
grouping column. For example, if you want to find out how many customers live in each state, you
can write a query with a GROUP BY state clause. A clustered index on state will have all the rows
with the same value for state in logical sequence, so the sorting and grouping operations will be
very fast.
Maintaining Uniqueness
Creating a unique index (or defining a PRIMARY KEY or UNIQUE constraint that builds a unique
index) is by far the most efficient method of guaranteeing that no duplicate values are entered
into a column. By traversing an index tree to determine where a new row should go, SQL Server
can detect within a few page reads that a row already has that value. Unlike all the other uses of
indexes described in this section, using unique indexes to maintain uniqueness isn't just one
option among others. Although SQL Server might not always traverse a particular unique index to
determine where to try to insert a new row, SQL Server will always use the existence of a unique
index to verify whether a new set of data is acceptable.
Summary
We now know how SQL Server indexes organize the data on disk and help you access your data
more quickly than if no indexes existed. Indexes are organized as B-trees, which means that you
will always traverse through the same number of index levels when you traverse from the root to
any leaf page. To use an index to find a single row of data, SQL Server never has to read more
pages than there are levels in an appropriate index.
You also learned about all the options available when you create an index, how to determine the
amount of space an index takes up, and how to predict the size of an index that doesn't have any
data yet.
Indexes can become fragmented in SQL Server 2000, but the performance penalty for
fragmentation is usually much less than in earlier versions of SQL Server. When you want to
defragment your indexes, you have several methods to choose from, one of which allows the
index to continue to be used by other operations, even while the defragmentation operation is
going on.
Finally, you learned about some of the situations in which you can get the greatest performance
gains by having the appropriate indexes on your tables.
Views
Introduction
You can use a view to save almost any SELECT statement as a separate database object. This
SELECT statement enables you to create a result set that you can use just as you would any
table. In a sense, you can think of a view as a "virtual" table. Views do not actually contain data-
they simply consist of SELECT statements for extracting data from the actual tables in your
database.
The tables on which you base a view are referred to as base tables. You can use a view to create
a subset of a base table by selecting only some of its columns, or you can use a view to display
columns from multiple base tables by using a join statement.
Creating a View
You create a view by using the CREATE VIEW Transact-SQL statement. You can include a total
of 1,024 columns in a view. You cannot combine the CREATE VIEW with other SQL statements
in the same batch. If you want to use other statements (such as USE database) with the CREATE
VIEW statement, you must follow those statements with the GO keyword.
USE database
GO
CREATE VIEW view_name AS
SELECT column_list
FROM table_name
Replace view_name with the name you want to assign to the view. You should come up with a
naming convention for your views that makes it easier for you to differentiate between tables and
views. For example, you might try using "view" as part of all of your view names. Replace
column_list with the list of columns you want to include in the view, and table_name with the
name of the table on which you want to base the view.
y
If you want to create a view that consists only of each customer's name and phone number, use
the following syntax:
USE movies
GO
CREATE VIEW dbo.CustView AS
SELECT lname, fname, phone
FROM customer
You can optionally specify a list of column names so that SQL Server will use these names for
the columns in the view instead of the column names from the table in the SELECT portion of the
statement. For example, in the following query, the (lname, fname) clause assigns these names
to the columns in the view instead of the names au_lname, au_fname:
USE pubs
GO
CREATE VIEW dbo.PracticeView
(lname, fname)
AS
SELECT au_lname, au_fname
FROM authors
Restrictions
You cannot include the ORDER BY, COMPUTE, or COMPUTE BY clauses in the SELECT
statement you use to create a view. In addition, you cannot use the SELECT INTO keywords.
Your view cannot refer to temporary tables. For example, the following SQL statement is invalid:
Permissions
If your users have permissions to the database in which you create the view, they will inherit
permissions to the view itself. However, if your users do not inherit permissions to the view, you
must assign them permissions or they will not be able to access the view. You do not have to give
users permissions to the base tables on which you create a view-you just have to give users
permissions to the view itself provided you are both the owner of the table and the view.
Ownership
The views that you create depend on the base tables (or other views). SQL Server refers to
objects that depend on other objects as dependent. Objects can have either the same or different
owners. If the same owner owns both the view and the table, that owner (typically you) needs
only to assign users permissions to the view. Likewise, when users access your view, SQL
Server need check users permissions only for that view.
If you (or another user with sufficient permissions) create a view based on a table for which you
are not the owner, SQL Server considers the ownership chain to be "broken." Each object's
owner can change users' permissions; thus, SQL Server must check users' permissions for the
view and all objects on which the view depends. Checking users' permissions for each object
hurts your server's performance. Microsoft recommends that you do not break the ownership
chain (meaning create views with different owners from the base tables) in order to avoid
degrading the performance of your server.
You make the dbo user the owner of a view by using the following syntax:
Nested views
SQL Server enables you to create a view based on another view (this is also called a nested
view). However, nested views can be much more difficult to troubleshoot because you must
search through multiple view definitions to find a problem. For this reason, Microsoft recommends
that you create separate views instead.
y
If you want to create a view that contains the title of each book in the pubs database, along with
the author's royalty percentage for that book, you could use the following query:
USE pubs
GO
CREATE VIEW dbo.TitleRoyaltyView
AS
SELECT t.title, r.royalty
FROM titles AS t JOIN roysched AS r
ON t.title_id JOIN r.title_id
You should base views only on inner joins, not outer joins. While SQL Server enables you to
specify an outer join in the SELECT statement, you will get unpredictable results.
y
SQL Server frequently returns null values for the inner table in the outer join.
Step 2:
Step 3:
Step 4:
Step 6:
y
To view a list of views defined in a database, you can use the following syntax:
SELECT *
FROM information_schema.tables
WHERE table_type = `view'
To view the SELECT statement that makes up a view, use the sp_helptext stored procedure. Use
the following syntax:
sp_helptext view_name
You can optionally add the WITH ENCRYPTION operator to prevent users from reading a view's
definition, as follows:
Modifying a view
You can alter a view by either dropping and re-creating it, or by using the ALTER VIEW
statement. If you drop a view, you must re-create any permissions assignments when you re-
create the view. In contrast, if you change a view by using the ALTER VIEW statement, the view
retains whatever permissions you had assigned to your users. You can use the following syntax
to change an existing view:
If you created the view with the WITH ENCRYPTION operator, you must include that option in
the ALTER VIEW statement.
Dropping a view
You drop a view by using the DROP VIEW statement. When you delete a view, SQL Server
automatically deletes the view definition and any permissions you have assigned to users for it. If
you delete a table that is referenced by a view, SQL Server does not automatically drop the view.
Thus, you must manually drop the view. You can use the sp_depends stored procedure to
determine if a table has any dependent views by using the following syntax:
sp_depends object_name
You must be the owner of a view to delete it. However, if you are a member of the sysadmins
server role or the database owner database role, you can drop a view that is owned by another
user by specifying the owner's name in the DROP VIEW statement.
Use the following syntax to insert data into a table by using a view:
Replace the value_list with a list of values you want to insert into the columns contained in the
view.
UPDATE view_name
SET column_name = value
WHERE condition
Likewise, you can use the following syntax to delete rows through a view:
±2. You would like to prevent anyone from reading the statement you used to build a view.
What should you do?
³You can prevent users from displaying a view definition by encrypting it. You encrypt a view
definition by adding the WITH ENCRYPTION clause after the CREATE VIEW statement as
follows:
Figure 24 shows the two-tier client/server model, and Figure 25 shows the three-tier model, which
some programmers find preferable for some applications.
Figure 24 Two Tier Data Model figure 25 Three tier Data Model
The two-tier model has a client (first tier) and a server (second tier); the client handles application
and presentation logic, and the server handles data services and business services. The two-tier
model uses the so-called fat client—the client does a large amount of application processing
locally. Many solutions are deployed using this topology; certainly the lion's share of today's non-
Internet client/server solutions are two-tier.
Presumably, far fewer application servers exist than clients; therefore, server costs and issues
should be reduced in the three-tier model. (The three tiers are logical concepts; the actual
number of computers involved might be more or less than three.) Typically, you're part of a three-
tier model when you use the Internet with a browser and access a Web page residing on an
Internet server that performs database work. An example of a three-tier application is a mail-order
company's Web page that checks current availability by using ODBC calls to access its inventory
in SQL Server.
Both two-tier and three-tier solutions have advantages and disadvantages, and each can be
viewed as a potential solution to a given problem. The specifics of the problem and the
constraints imposed by the environment (including costs, hardware, and expertise of both the
development staff and the user community) should drive the solution's overall design. However,
some people claim that a three-tier solution means that such capabilities as stored procedures,
rules, constraints, or triggers are no longer important or shouldn't be used. In a three-tier solution,
the middle tier doing application logic is still a client from the perspective of the database services
(that is, SQL Server). Consequently, for that tier, it's still preferable that remote work be done
close to the data. If the data can be accessed by servers other than the application server,
integrity checking must be done at the database server as well as in the application. Otherwise,
you've left a gaping hole through which the integrity checks in the application can be
circumvented.
The client sends the commands and processes the results, and it makes no difference to SQL
Server whether an application server process does this or whether the process that the end user
is running directly does it. The benefits of the programmable server provide much more efficient
network use and better abstraction of data services from the application and application server.
However, you typically use SELECT when the values to be assigned are found in a column of a
table. A SELECT statement used to assign values to one or more variables is called an
assignment SELECT. You can't combine the functionality of the assignment SELECT and a
"regular" SELECT in the same statement. That is, if a SELECT statement is used to assign
values to variables, it can't also return values to the client as a result set. In the following simple
example, two variables are declared, assigned values to them from the roysched table, and then
we would select their values as a result set:
A single DECLARE statement can declare multiple variables. When you use a SELECT
statement for assigning values, you can assign more than one value at a time. When you use
SET to assign values to variables, you must use a separate SET statement for each variable.
y
The following SET statement returns an error:
y
Suppose that the following stor_name values exist in the stores table of the pubs database:
stor_name
---------
Eric the Read Books
Barnum's
News & Brews
Doc-U-Mat: Quality Laundry and Books
Fricative Bookshop
Bookbeat
The resulting value of @stor_name is the last row, Bookbeat. But consider the order returned,
without an ORDER BY clause, as a chance occurrence. Assigning a variable to a SELECT
statement that returns more than one row usually isn't intended, and it's probably a bug
introduced by the developer. To avoid this situation, you should qualify the SELECT statement
with an appropriate WHERE clause to limit the result set to the one row that meets your criteria.
Alternatively, you can use an aggregate function, such as MAX, MIN, or SUM, to limit the number
of rows returned. Then you can select only the value you want. If you want the assignment to be
to only one row of a SELECT statement that might return many rows (and it's not important which
row that is), you should at least use SELECT TOP 1 to avoid the effort of gathering many rows,
with all but one row thrown out. (In this case, the first row would be returned, not the last. You
could use ORDER BY to explicitly indicate which row should be first.) You might also consider
checking the value of @@ROWCOUNT immediately after the assignment and, if it's greater than
1, branch off to an error routine.
Also be aware that if the SELECT statement assigning a value to a variable doesn't return any
rows, nothing will be assigned. The variable will keep whatever value it had before the
assignment SELECT statement was run. For example, suppose we use the same variable twice
to find the first names of authors with a particular last name:
If you run the previous code, you'll see the same first name returned both times because no
authors with the last name of Ben-Gan exist, at least not in the pubs database!
UPDATE title
SET @old_price = price = price * 0.8
WHERE title_id = 'PC2091'
A variable can be assigned a value from a subquery, and in some cases it might look like there's
no difference between using a subquery and a straight SELECT. For example, the following two
batches return exactly the same result set:
RESULT:
Akiko
This one row is returned because the assignment to the variable is happening for every row that
the query would return. With no WHERE clause, every row in the table is returned, and for every
row, the first name value is assigned to the variable, overriding whatever value was assigned by
the previous row returned. We are left with the value from the last row because there are no more
values to override that one.
However, when a subquery is used and the WHERE clause is ignored, the result is very
different:
RESULT:
--------------------
NULL
In this case, the query returns an error. When you use a subquery, the entire subquery is
processed before any assignment to the variable is made. The subquery's result set contains 23
rows, and then all 23 rows are used in the assignment to the variable. Since we can assign only a
single value to a variable, the error message is generated.
Session Variables
SQL Server 2000 allows you to store and retrieve session-level context information by directly
querying the sysprocesses table in the master database. Normally it is recommended that you
never access the system tables directly, but for this release, direct access of sysprocesses is the
only way to retrieve this information. A construct is supplied for setting this information, but you
have nothing other than direct system table access for retrieving it.
The sysprocesses table in the master database in SQL Server 2000 has a 128-byte field called
context_info of type binary, which means you have to manipulate the data using hexadecimal
values. You can use the entire column to store a single value or, if you're comfortable with
manipulating binary values, you can use different groups of bytes for different purposes. The
following statements store the price of one particular book in the context_info column:
If we stored this value in a local variable, it would have the scope of only a single query batch and
we wouldn't be able to come back after executing several more batches and check the value. But
storing it in the sysprocesses table means that the value will be available as long as my
connection is active. The sysprocesses table has one row for each active process in SQL Server,
and each row is assigned a unique Server Process ID (spid) value. A system function @@spid
returns the process ID for the current connection. Using @@spid, we can find the row in
sysprocesses that contains the context_info value.
Using SET context_info, we can assign a money value to the binary column. If you check the
documentation for the CONVERT function, you'll see that money is implicitly convertible to binary.
For convenience, in Figure 26 we have reproduced the chart that shows which datatypes are
implicitly and explicitly convertible.
If, instead, we had tried to assign a character value to CONTEXT_INFO, we would have received
an error message because the chart in Figure 10-3 tells us that conversion from character to
binary must be done explicitly. The SET CONTEXT_INFO statement stores the assigned value in
the fewest number of bytes, starting with the first byte. So we can use the SUBSTRING function
to extract a value from the context_info column, and since the money datatype is stored in 8
bytes, we need to get the first 8 bytes of the column. we can then convert those first 8 bytes to
money:
Since the context_info column is 128 bytes long, there's lot of room to work with. We can store
another price in the second set of 8 bytes if we are careful. We need to take the first 8 bytes and
concatenate them to the second price value after converting it to hexadecimal. Concatenation
works for hexadecimal values the same way as it does for character strings, and the operator is
the plus (+):
Control-of-Flow Tools
Like any programming language worth its salt, Transact-SQL provides a decent set of control-of-
flow tools. (True, Transact-SQL has only a handful of tools—perhaps not as many as you'd like—
but they're enough to make it dramatically more powerful than standard SQL.) The control-of-flow
tools include conditional logic (IF…ELSE and CASE), loops (only WHILE, but it comes with
CONTINUE and BREAK options), unconditional branching (GOTO), and the ability to return a
status value to a calling routine (RETURN). Table 10-1 presents a quick summary of these
control-of-flow constructs.
CASE
The CASE expression is enormously powerful. Although CASE is part of the ANSI SQL-92
specification, it's not required for ANSI compliance certification, and few database products other
than Microsoft SQL Server have implemented it. If you have experience with an SQL database
system other than Microsoft SQL Server, chances are you haven't used CASE. If that's the case
(pun intended), you should get familiar with it now. It will be time well spent. CASE was added in
version 6, and once you get used to using it, you'll wonder how SQL programmers ever managed
without it.
CASE is a conceptually simpler way to do IF-ELSE IF-ELSE IF-ELSE IF-ELSE_type operations.
It's roughly equivalent to a switch statement in C. However, CASE is far more than shorthand for
IF—in fact, it's not really even that. CASE is an expression, not a control-of-flow keyword, which
means that it can be used only inside other statements. You can use CASE in a SELECT
statement in the SELECT list, in a GROUP BY clause, or in an ORDER BY clause. You can use it
in the SET clause of an UPDATE statement. You can include CASE in the values list for an
SELECT
title,
price,
'classification'=CASE
WHEN price < 10.00 THEN 'Low Priced'
WHEN price BETWEEN 10.00 AND 20.00 THEN 'Moderately Priced'
WHEN price > 20.00 THEN 'Expensive'
ELSE 'Unknown'
END
FROM titles
A NULL price won't fit into any of the category buckets.
SELECT
title,
price,
'Type' = CASE
WHEN type = 'mod_cook' THEN 'Modern Cooking'
WHEN type = 'trad_cook' THEN 'Traditional Cooking'
WHEN type = 'psychology' THEN 'Psychology'
WHEN type = 'business' THEN 'Business'
WHEN type = 'popular_comp' THEN 'Popular Computing'
ELSE 'Not yet decided'
END
FROM titles
SELECT
title,
price,
'Type' = CASE type
WHEN 'mod_cook' THEN 'Modern Cooking'
WHEN 'trad_cook' THEN 'Traditional Cooking'
WHEN 'psychology' THEN 'Psychology'
WHEN 'business' THEN 'Business'
ELSE 'Not yet decided'
END
FROM titles
You can use CASE in some unusual places; because it's an expression, you can use it anywhere
that an expression is legal. Using CASE in a view is a nice technique that can make your
database more usable to others. Using CASE in an UPDATE statement can make the update
easier. More important, CASE can allow you to make changes in a single pass of the data that
would otherwise require you to do multiple UPDATE statements, each of which would have to
scan the data.
Cousins of CASE
SQL Server provides three nice shorthand derivatives of CASE: COALESCE, NULLIF, and
ISNULL. COALESCE and NULLIF are both part of the ANSI SQL-92 specification; they were
added to version 6 at the same time that CASE was added. ISNULL is a longtime SQL Server
function.
COALESCE This function is equivalent to a CASE expression that returns the first NOT NULL
expression in a list of expressions.
COALESCE(expression1, expression2, ... expressionN)
If no non-null values are found, COALESCE returns NULL (which is what expressionN was, given
that there were no non-null values). Written as a CASE expression, COALESCE looks like this:
CASE
WHEN expression1 IS NOT NULL THEN expression1
WHEN expression2 IS NOT NULL THEN expression2
ELSE expressionN
END
NULLIF(expression1, expression2)
If the expressions aren't equal, expression1 is returned. NULLIF can be handy if you use dummy
values instead of NULL, but you don't want those dummy values to skew the results produced by
aggregate functions. Written using CASE, NULLIF looks like this:
CASE
WHEN expression1=expression2 THEN NULL
ELSE expression1
END
ISNULL allows you to easily substitute an alternative expression or value for an expression that's
NULL. The use of ISNULL to substitute the string 'ALL' for a GROUPING NULL when using
CUBE and to substitute a string of question marks ('????') for a NULL value. The results were
more clear and intuitive to users. The functionality of ISNULL written instead using CASE looks
like this:
CASE
WHEN expression1 IS NULL THEN expression2
ELSE expression1
END
The second argument to ISNULL (the value to be returned if the first argument is NULL) can be
an expression or even a SELECT statement. Say, for example, that we want to query the titles
table. If a title has NULL for price, we want to instead use the lowest price that exists for any
book. Without CASE or ISNULL, this isn't an easy query to write. With ISNULL, it is easy:
SELECT title, pub_id, ISNULL(price, (SELECT MIN(price)
FROM titles)) FROM titles
For comparison, here's an equivalent SELECT statement written with the longhand CASE
formulation:
PRINT
Transact-SQL, like other programming languages, provides printing capability through the PRINT
and RAISERROR statements. I'll discuss RAISERROR momentarily, but first we'll take a look at
the PRINT statement.
PRINT is the most basic way to display any character string of up to 8000 characters. You can
display a literal character string or a variable of type char, varchar, nchar, nvarchar, datetime, or
smalldatetime. You can concatenate strings and use functions that return string or date values. In
addition, PRINT can print any expression or function that is implicitly convertible to a character
datatype. (Refer to Figure 10-3 to determine which datatypes are implicitly convertible.)
It seems like every time you learn a new programming language, the first assignment is to write
the "Hello World" program. Using Transact-SQL, it couldn't be easier:
PRINT 'Hello World'
Even before you saw PRINT, you could've produced the same output in your SQL Query
Analyzer screen with the following:
SELECT 'Hello World'
So what's the difference between the two? The PRINT statement returns a message, nothing
more.
The SELECT statement returns output like this:
-----------
Hello World
(1 row(s) affected)
The SELECT statement returns the string as a result set. That's why we get the (1 row(s)
affected) message. When you use SQL Query Analyzer to submit your queries and view your
results, you might not see much difference between the PRINT and the SELECT statements. But
for other client tools (besides ISQL, OSQL, and SQL Query Analyzer), there could be a big
difference. PRINT returns a message of severity 0, and you need to check the documentation for
RAISERROR
RAISERROR is similar to PRINT but can be much more powerful. RAISERROR also sends a
message (not a result set) to the client; however, RAISERROR allows you to specify the specific
error number and severity of the message. It also allows you to reference an error number, the
text of which you can add to the sysmessages table. RAISERROR makes it easy for you to
develop international versions of your software. Rather than using multiple versions of your SQL
code when you want to change the language of the text, you need only one version. You can
simply add the text of the message in multiple languages. The user will see the message in the
language used by the particular connection.
Unlike PRINT, RAISERROR accepts printf-like parameter substitution as well as formatting
control. RAISERROR can also allow messages to be logged to the Windows NT or Windows
2000 event service, making the messages visible to the Windows NT or Windows 2000 Event
Viewer, and it allows SQL Server Agent alerts to be configured for these events. In many cases,
RAISERROR is preferable to PRINT, and you might choose to use PRINT only for quick-and-dirty
situations. For production-caliber code, RAISERROR is usually a better choice. Here's the
syntax:
RAISERROR ({msg_id | msg_str}, severity, state[, argument1
[, argumentn]])
[WITH LOG]|[WITH NOWAIT]
After you call RAISERROR, the system function @@ERROR returns the value passed as
msg_id. If no ID is passed, the error is raised with error number 50000, and @@ERROR is set to
that number. Error numbers lower than 50000 are reserved for SQL Server use, so choose a
higher number for your own messages.
You can also set the severity level; an informational message should be considered severity 0 (or
severity 10; 0 and 10 are used interchangeably for a historical reason that's not important). Only
someone with the system administrator (SA) role can raise an error with a severity of 19 or
higher. Errors of severity 20 and higher are considered fatal, and the connection to the client is
automatically terminated.
By default, if a batch includes a WAITFOR statement, results or messages are not returned to the
client until the WAITFOR has completed. If you want the messages generated by your
RAISERROR statement to be returned to the client immediately, even if the batch contains a
subsequent WAITFOR, you can use the WITH NOWAIT option
You must use the WITH LOG option for all errors of severity 19 or higher. For errors of severity
20 or higher, the message text isn't returned to the client. Instead, it's sent to the SQL Server
error log, and the error number, text, severity, and state are written to the operating system event
log. A SQL Server system administrator can use the WITH LOG option with any error number or
any severity level.
Errors written to the operating system event log from RAISERROR go to the application event
log, with MSSQLServer as the source if the server is the default server, and
$
There's a little-known way to cause isql.exe or OSQL.EXE (the command-line query tools) to
terminate. Raising any error with state 127 causes isql or OSQL to immediately exit, with a return
code equal to the error number. (You can determine the error number from a batch command file
by inspecting the system ERRORLEVEL value.) You could write your application so that it does
something similar—it's a simple change to the error-handling logic. This trick can be a useful way
to terminate a set of scripts that would be run through isql or OSQL. You could get a similar result
by raising a high-severity error to terminate the connection. But using a state of 127 is actually
simpler, and no scary message will be written to the error log or event log for what is a planned,
routine action.
You can add your own messages and text for a message in multiple languages by using the
sp_addmessage stored procedure. By default, SQL Server uses U.S. English, but you can add
languages. SQL Server distinguishes U.S. English from British English because of locale settings,
such as currency. The text of messages is the same. The procedure sp_helplanguage shows you
what languages are available on your SQL Server instance. If a language you are interested in
doesn't appear in the list returned by sp_helplanguage, take a look at the script instlang.sql,
which can be found in the INSTALL subdirectory of your SQL Server instance installation files.
This script adds locale settings for additional languages but does not add the actual translated
error messages. Suppose we want to rewrite "Hello World" to use RAISERROR, and we want the
text of the message to be able to appear in both U.S. English and German (language ID of 1).
Here's how:
When RAISERROR is executed, the text of the message becomes dependent on the SET
LANGUAGE setting of the connection. If the connection does a SET LANGUAGE Deutsch, the
text returned for RAISERROR (50001, 10, 1) will be Hallo Welt. If the text for a language doesn't
exist, the U.S. English text (by default) is returned. For this reason, the U.S. English message
must be added before the text for another language is added. If no entry exists, even in U.S.
English, error 2758 results:
FORMATMESSAGE
If you have created your own error messages with parameter markers, you might need to inspect
the entire message with the parameters replaced by actual values. RAISERROR makes the
message string available to the client but not to your SQL Server application. To construct a full
message string from a parameterized message in the sysmessages table, you can use the
function FORMATMESSAGE.
If we consider the last example from the preceding RAISERROR section, we can use
FORMATMESSAGE to build the entire message string and save it in a local variable for further
processing. Here's an example of the use of FORMATMESSAGE:
The message is: Hello World, from: kalend, process id: Oxc
Note that no error number or state information is returned.
Operators
Transact-SQL provides a large collection of operators for doing arithmetic, comparing values,
doing bit operations, and concatenating strings. These operators are similar to those you might
be familiar with in other programming languages. You can use Transact-SQL operators in any
expression, including in the select list of a query, in the WHERE or HAVING clauses of queries, in
UPDATE and IF statements, and in CASE expressions.
Arithmetic Operators
We won't go into much detail here because arithmetic operators are pretty standard stuff. If you
need more information, please refer to the online documentation. Table 10-2 shows the arithmetic
operators.
As with other programming languages, in Transact-SQL you need to consider the datatypes
you're using when you perform arithmetic operations. Otherwise, you might not get the results
you expect.
sql_variant (highest)
datetime
smalldatetime
float
real
decimal
money
smallmoney
bigint
int
smallint
tinyint
bit
ntext
text
image
timestamp (rowversion)
uniqueidentifier
nvarchar
nchar
varchar
char
varbinary
binary (lowest)
As you can see from the precedence list, an int multiplied by a float produces a float. If you need
to use data of type sql_variant in an arithmetic operation, you must first convert the sql_variant
data to its base datatype. You can explicitly convert to a given datatype by using the CAST
function or its predecessor, CONVERT.
author
------
White, Johnson
Green, Marjorie
Carson, Cheryl
O'Leary, Michael
Bit Operators
Table 10-3 shows the SQL Server bit operators.
The operands for the two-operand bitwise operators can be any of the datatypes of the integer or
binary string datatype categories (except for the image datatype), with the exception that both
operands cannot be a type of binary string. So, one of the operands can be binary or varbinary,
but they can't both be binary or varbinary. In addition, the right operand can be of the bit datatype
only if the left operand is of type bit. Table 10-4 shows the supported operand datatypes.
The single operand for the bitwise NOT operator must be one of the integer datatypes.
Often, you must keep a lot of indicator-type values in a database, and bit operators make it easy
to set up bit masks with a single column to do this. The SQL Server system tables use bit masks.
For example, the status field in the sysdatabases table is a bit mask. If we wanted to see all
databases marked as read-only, which is the 11th bit or decimal 1024 (210), we could use this
query:
This example is for illustrative purposes only. You shouldn't query the system catalogs directly.
Sometimes the catalogs need to change between product releases, and if you query directly, your
applications can break because of these required changes. Instead, you should use the provided
system catalog stored procedures and object property functions, which return catalog information
in a standard way. If the underlying catalogs change in subsequent releases, the property
functions are also updated, insulating your application from unexpected changes.
So, for this example, to see whether a particular database was marked read-only, we could use
the DATABASEPROPERTY function:
A return value of 1 would mean that mydb was set to read-only status.
Keeping such indicators as bit datatype columns can be a better approach than using an int as a
bit mask. It's more straightforward—you don't always need to look up your bit-mask values for
each indicator. To write the "read only databases" query we just looked at, you'd need to check
the documentation for the bit-mask value for "read only." And many developers, not to mention
end users, aren't that comfortable using bit operators and are likely to use them incorrectly. From
a storage perspective, bit columns don't require more space than equivalent bit-mask columns
require. (Eight bit fields can share the same byte of storage.)
But you wouldn't typically want to do that with a "yes/no" indicator field anyway. If you have a
huge number of indicator fields, using bit-mask columns of integers instead of bit columns might
be a better alternative than dealing with hundreds of individual bit columns. However, there's
nothing to stop you from having hundreds of bit columns. You'd have to have an awful lot of bit
fields to actually exceed the column limit because a table in SQL Server can have up to 1024
columns. If you frequently add new indicators, using a bit mask on an integer might be better. To
add a new indicator, you can make use of an unused bit of an integer status field rather than do
an ALTER TABLE and add a new column (which you must do if you use a bit column). The case
for using a bit column boils down to clarity.
Comparison Operators
Table 9 shows the SQL Server comparison operators.
Comparison operators are straightforward. Only a few related issues might be confusing to a
novice SQL Server developer:
• When sql_variant values of different base datatypes are compared and the base
datatypes are in different datatype families, the value whose datatype family is higher in
the hierarchy chart is considered the higher of the two values.
SELECT *
FROM variant2
WHERE a > b
Result:
a b
----- -----
333 444
The second row inserted had a value of base type int and a value of base type char. Even though
you would normally think that 333 is not greater than 444 because 333 is of datatype int and the
family exact numeric, it is considered higher than 444, of datatype char, and of the family
Unicode.
• When sql_variant values of different base datatypes are compared and the base
datatypes are in the same datatype family, the value whose base datatype is lower in the
hierarchy chart is implicitly converted to the other datatype and the comparison is then
made.
SELECT *
Result:
a b
-------------------- -----------------
222.0000
In this case, the two base types are int and money, which are in the same family, so 111 is
considered less than 222.0000.
• When sql_variant values of the char, varchar, nchar, or varchar datatypes are compared,
they are evaluated based on additional criteria, including the locale code ID (LCID), the
LCID version, the comparison flags use for the column's collation, and the sort ID.
EXAMPLE 1
SELECT au_lname, au_fname, city, state FROM authors
WHERE
city <> 'OAKLAND' AND state <> 'CA'
OR
city <> 'Salt Lake City' AND state <> 'UT'
EXAMPLE 2
SELECT au_lname, au_fname, city, state FROM authors
WHERE
(city <> 'OAKLAND' AND state <> 'CA')
OR
(city <> 'Salt Lake City' AND state <> 'UT')
EXAMPLE 3
SELECT au_lname, au_fname, city, state FROM authors
WHERE
(city <> 'OAKLAND' AND state <> 'CA')
AND
(city <> 'Salt Lake City' AND state <> 'UT')
EXAMPLE 4
SELECT au_lname, au_fname, city, state FROM authors
WHERE
NOT
(
(city='OAKLAND' AND state='CA')
OR
(city='Salt Lake City' AND state='UT')
)
you can see that only Example 4 operates correctly. This query would be much more difficult to
write without some combination of parentheses, NOT, OR (including IN, a shorthand for OR), and
AND. You can also use parentheses to change the order of precedence in mathematical and
logical operations. These two statements return different results:
The order of precedence is similar to what you learned back in algebra class (except for the bit
operators). Operators of the same level are evaluated left to right. You use parentheses to
change precedence levels to suit your needs, when you're unsure, or when you simply want to
make your code more readable. Groupings with parentheses are evaluated from the innermost
grouping outward. Table 11 shows the order of precedence.
Table 11 Order of precedence in mathematical and logical operations, from highest to lowest.
Operation Operators
Bitwise NOT ~
Multiplication/division/modulo * / %
Addition/subtraction +/-
Bitwise exclusive OR ^
Bitwise AND &
Bitwise OR |
Logical NOT NOT
Logical AND AND
Logical OR OR
If you didn't pick the correct example, you can easily see the flaw in Examples 1, 2, and 3 by
examining the output. Examples 1 and 2 both return every row. Every row is either not in CA or
not in UT because a row can be in only one or the other. A row in CA isn't in UT, so the
expression returns TRUE. Example 3 is too restrictive—for example, what about the rows in San
Francisco, CA? The condition (city <> 'OAKLAND' AND state <> 'CA') would evaluate to (TRUE
and FALSE) for San Francisco, CA, which, of course, is FALSE, so the row would be rejected
when it should be selected, according to the desired semantics.
Scalar Functions
Aggregate functions, such as MAX, SUM, and COUNT operate on a set of values to produce a
single aggregated value. In addition to aggregate functions, SQL Server provides scalar
functions, which operate on a single value. Scalar is just a fancy term for single value. You can
also think of the value in a single column of a single row as scalar. Scalar functions are
enormously useful—the Swiss army knife of SQL Server. You've probably noticed that scalar
functions have been used in several examples already—we'd all be lost without them.
You can use scalar functions in any legal expression, such as:
SQL Server provides too many scalar functions to remember. The best you can do is to
familiarize yourself with those that exist, and then, when you encounter a problem and a light bulb
goes on in your head to remind you of a function that will help, you can look at the online
documentation for details. Even with so many functions available, you'll probably find yourself
wanting some functions that don't exist. You might have a specialized need that would benefit
from a specific function. The following sections describe the scalar functions that are supplied
with SQL Server 2000. We'll first look at the CAST function because it's especially important.
Conversion Functions
SQL Server provides three functions for converting datatypes: the generalized CAST function, the
CONVERT function—which is analogous to CAST but has a slightly different syntax—and the
more specialized STR function.
The CAST function is a synonym for CONVERT and was added to the product to comply with
ANSI-92 specifications. You'll see CONVERT used in older documentation and older code.
CAST is possibly the most useful function in all of SQL Server. It allows you to change the
datatype when you select a field, which can be essential for concatenating strings, joining
columns that weren't initially envisioned as related, performing mathematical operations on
columns that were defined as character but which actually contain numbers, and other similar
operations. Like C, SQL is a fairly strongly typed language.
Some languages, such as Visual Basic or PL/1 (Programming Language 1), allow you to almost
indiscriminately mix datatypes. If you mix datatypes in SQL, however, you'll often get an error.
Some conversions are implicit, so, for example, you can add or join a smallint and an int without
any such error; trying to do the same between an int and a char, however, produces an error.
Without a way to convert datatypes in SQL, you would have to return the values back to the client
application, convert them there, and then send them back to the server. You might need to create
temporary tables just to hold the intermediate converted values. All of this would be cumbersome
and inefficient.
Recognizing this inefficiency, the ANSI committee added the CAST operator to SQL-92. SQL
Server's CAST—and the older CONVERT—provides a superset of ANSI CAST functionality. The
CAST syntax is simple:
Suppose we want to concatenate the job_id (smallint) column of the employee table with the
lname (varchar) column. Without CAST, this would be impossible (unless it was done in the
application). With CAST, it's trivial:
Accorti-13
Afonso-14
Ashworth-6
Bennett-12
Brown-7
Chang-4
Cramer-2
Cruz-10
Devon-3
Specifying a length shorter than the column or expression in your call to CAST is a useful way to
truncate the column. You could use the SUBSTRING function for this with equal results, but you
might find it more intuitive to use CAST. Specifying a length when you convert to char, varchar,
decimal, or numeric isn't required, but it's recommended. Specifying a length shields you better
from possible behavioral changes in newer releases of the product.
Although behavioral changes generally don't occur by design, it's difficult to ensure 100
percent consistency of subtle behavioral changes between releases. The development
team strives for such consistency, but it's nearly impossible to ensure that no behavioral
side effects result when new features and capabilities are added. There's little point in
You might want to avoid CAST when you convert float or numeric/decimal values to character
strings if you expect to see a certain number of decimal places. CAST doesn't currently provide
formatting capabilities for numbers. The older CONVERT function has some limited formatting
capabilities, which we'll see shortly. Formatting floating-point numbers and such when converting
to character strings is another area that's ripe for subtle behavior differences. So if you need to
transform a floating-point number to a character string in which you expect a specific format, you
can use the other conversion function, STR. STR is a specialized conversion function that always
converts from a number (float, numeric, and so on) to a character datatype, but it allows you to
explicitly specify the length and number of decimal places that should be formatted for the
character string. Here's the syntax:
STR(float_expression, character_length, number_of_decimal_places)
Remember that character_length must include room for a decimal point and a negative sign if
they might exist. Also be aware that STR rounds the value to the number of decimal places
requested, while CAST simply truncates the value if the character length is smaller than the size
required for full display. Here's an example of STR:
discounttype Discount
------------ --------
Initial Customer 10.500
Volume Discount 6.700
Customer Discount 5.000
You can think of the ASCII and CHAR functions as special type-conversion functions, but we'll
discuss them later in this chapter along with string functions.
If all you're doing is converting an expression to a specific datatype, you can actually use either
CAST or the older CONVERT. However, the syntax of CONVERT is slightly different and allows
for a third, optional argument. Here's the syntax for CONVERT:
CONVERT has an optional third parameter, style. This parameter is most commonly used when
you convert an expression of type datetime or smalldatetime to type char or varchar. You can
also use it when you convert float, real, money, or smallmoney to a character datatype.
When you convert a datetime expression to a character datatype, if the style argument isn't
specified, the conversion will format the date with the default SQL Server date format (for
example, Oct 3 1999 2:25PM). By specifying a style type, you can format the output as you want.
You can also specify a shorter length for the character buffer being converted to, in order to
perhaps eliminate the time portion or for other reasons. Table 10-8 shows the various values you
can use as the style argument.
Table 12 Values for the style argument of the CONVERT function when you convert a datetime
expression to a character expression.
Although SQL Server uses style 0 as the default when converting a datetime value to a
character string, SQL Query Analyzer (and OSQL) use style 121 when displaying a
datetime value.
Since we just passed a change in the century, using two-character formats for the year could be a
bug in your application just waiting to happen! Unless you have a compelling reason not to, you
should always use the full year (yyyy) for both the input and output formatting of dates to prevent
any ambiguity.
SQL Server has no problem dealing with the year 2000—in fact, the change in century isn't even
a boundary condition in SQL Server's internal representation of dates. But if you represent a year
by specifying only the last two digits, the inherent ambiguity might cause your application to make
an incorrect assumption. When a date formatted that way is entered, SQL Server's default
behavior is to interpret the year as 19yy if the value is greater than or equal to 50 and as 20yy if
the value is less than 50. That might be OK now, but by the year 2050 you won't want a two-digit
year of 50 to be interpreted as 1950 instead of 2050. (If you simply assume that your application
will have been long since retired, think again. A lot of COBOL programmers in the 1960s didn't
worry about the year 2000.)
SQL Server 2000 allows you to change the cutoff year that determines how a two-digit year is
interpreted. A two-digit year that is less than or equal to the last two digits of the cutoff year is in
the same century as the cutoff year. A twodigit year that is greater than the last two digits of the
cutoff year is in the century that precedes the cutoff year. You can change the cutoff year by
using the Properties dialog box for your SQL server in SQL Server Enterprise Manager and
selecting the Server Settings tab. Or you can use the sp_configure stored procedure:
EXEC sp_configure 'two digit year cutoff', '2000'
RESULTS:
FORMAT is mdy
---------------------------
2000-07-04 00:00:00.000
FORMAT is dmy
---------------------------
2000-04-07 00:00:00.000
You might consider using ISO style when you insert dates. This format has the year followed by
month and then the day, with no punctuation. This style is always recognizable, no matter what
the SQL Server default language and no matter what the DATEFORMAT setting. Here's the
above conversion command with dates supplied in ISO format:
The first statement assumes that the date is style 1, mm.dd.yy, so the string can be converted to
the corresponding datetime value and returned to the client. The client then returns the datetime
value according to its own display conventions. The second statement assumes that the date is
represented as dd.mm.yy. If we had tried to use style 102 or 104, both of which require a four-
digit year, we would have received a conversion error because we specified only a two-digit year.
CONVERT can also be useful if you insert the current date using the GETDATE function but don't
consider the time elements of the datetime datatype to be meaningful. (Remember that SQL
Server doesn't currently have separate date and time datatypes.) You can use CONVERT to
format and insert datetime data with only a date element. Without the time specification in the
GETDATE value, the time will consistently be stored as 12:00AM for any given date. That
eliminates problems that can occur when you search among columns or join columns of datetime.
For example, if the date elements are equal between columns but the time element is not, an
equality search will fail. You can eliminate this equality problem by making the time portion
consistent. Consider this:
In this example, the CONVERT function converts the current date and time value to a character
string that doesn't include the time. However, because the table we're inserting into expects a
datetime value, the new character string is converted back to a datetime datatype using the
default time of midnight.
You can also use the optional style argument when you convert float, real, money, or smallmoney
to a character datatype. When you convert from float or real, the style allows you to force the
output to appear in scientific notation and also allows you to specify either 8 or 16 digits. When
you convert from a money type to a character type, the style allows you to specify whether you
want a comma to appear every three digits and whether you want two digits to the right of the
decimal point or four.
Table 13 Values for the style argument of the CONVERT function when you convert a floating-
point expression to a character datatype.
In Table 14, the column on the left represents the style value for money or smallmoney
conversion to character data.
Table 14. Values for the style argument of the CONVERT function when you convert a money
expression to a character datatype.
Style
Output
Value
0 (the No commas to the left of the decimal point. Two digits appear to the right of the
default) decimal point. Example: 4235.98.
Commas every three digits to the left of the decimal point. Two digits appear to the
1
right of the decimal point. Example: 3,510.92.
No commas to the left of the decimal point. Four digits appear to the right of the
2
decimal point. Example: 4235.9819.
Here's an example from the pubs database. The advance column in the titles table lists the
advance paid by the publisher for each book, and most of the amounts are greater than $1000. In
the titles table, the value is stored as a money datatype. The following query returns the value of
advance as a money value, a varchar value with the default style, and as varchar with each of the
two optional styles:
In addition to determining how to display a value of a particular datatype, the client
program determines whether the output should be left-justified or right-justified and how
NULLs are displayed. It is frequently asked how to change this default output to, for
example, print money values with four decimal digits in SQL Query Analyzer.
Unfortunately, SQL Query Analyzer has its own set of predefined formatting
specifications, and, for the most part, you can't change them. Other client programs, such
as report writers, might give you a full range of capabilities for specifying the output
format, but SQL Query Analyzer isn't a report writer.
Here's another example of how the display can be affected by the client. If we use the GETDATE
function to select the current date, it is returned to the client application as a datetime datatype.
The client application will likely convert that internal date representation to a string for display. On
the other hand, if we use GETDATE but also explicitly use CONVERT to change the datetime to
character data, the column is sent to the calling application already as a character string.
So let's see what one particular client will do with that returned data. The command-line program
ISQL is a DB-Library program and uses functions in DBLibrary for binding columns to character
strings. DB-Library allows such bindings to automatically pick up locale styles based on the
settings of the workstation running the application. (You must select the Use International
Settings option in the SQL Server Client Network Utility in order for this to occur by default.) So a
column returned internally as a datetime datatype is converted by isql.exe into the same format
as the locale setting of Windows. A column containing a date representation as a character string
is not reformatted, of course.
When we issue the following SELECT statement with SQL Server configured for U.S. English as
the default language and with Windows NT and Windows 2000 on the client configured as
English (United States), the date and string look alike:
SELECT
'returned as date'=GETDATE(),
'returned as string'=CONVERT(varchar(20), GETDATE())
But if we change the locale in the Regional Options application of the Windows 2000 Control
Panel to French (Standard), the same SELECT statement returns the following:
You can see that the value returned to the client in internal date format was converted at the
client workstation in the format chosen by the application. The conversion that uses CONVERT
was formatted at the server.
This discussion is also relevant to formatting numbers and currency with different regional
settings. We cannot use the command-line OSQL program to illustrate the same behavior. OSQL,
like SQL Query Analyzer, is an ODBC-based program, and datetime values are converted to
ODBC Canonical format, which is independent of any regional settings.
The rest of the book primarily uses CAST instead of CONVERT when converting between
datatypes. CONVERT is used only when the style argument is needed.
Some conversions are automatic and implicit, so using CAST is unnecessary (but OK). For
example, converting between numbers with types int, smallint, tinyint, float, numeric, and so on
happens automatically and implicitly as long as an overflow doesn't occur, in which case you'd
get an error for arithmetic overflow. Converting numbers with decimal places to integer datatypes
results in the truncation of the values to the right of the decimal point—without warning.
(Converting between decimal or numeric datatypes requires that you explicitly use CAST if a loss
of precision is possible.)
Even though Figure 10-3 indicates that conversion from character strings to datetime values can
happen implicitly, this conversion is possible only when SQL Server can figure out what the
datetime value needs to be. If you use wildcards in your character strings, SQL Server might not
be able to do a proper conversion.
Suppose we use the following query to find all orders placed in August 1996 and stored in the
orders table in the Northwind database:
USE Northwind
SELECT * FROM orders WHERE OrderDate BETWEEN '8/1/96' and '8/31/96'
Because all the dates have a time of midnight in the orders table, SQL Server will correctly
interpret this query and return 25 rows. It converts the two string constants to datetime values,
and then it can do a proper chronological comparison. However, if your string has a wildcard in it,
SQL Server cannot convert it to a datetime, so instead it converts the datetime into a string. To do
this, it uses its default date format, which, as you've seen, is mon dd yyyy hh:miAM (or PM). So
this is the format you have to match in the character string with wildcards. Obviously, %1996%
will match but '8/1/%' won't. Although SQL Server is usually very flexible in how dates can be
entered, once you compare a datetime to a string with wildcards, you can assume only the default
format.
Operations on datetime values are common, such as "get current date and time," "do date
arithmetic—50 days from today is what date," or "find out what day of the week falls on a specific
date." Programming languages such as C or Visual Basic are loaded with functions for operations
like these. Transact-SQL also provides a nice assortment to choose from, as shown in Table 10-
11. The datetime parameter is any expression with a SQL Server datetime datatype or one that
can be implicitly converted, such as an appropriately formatted character string like 1999.10.31.
The datepart parameter uses the encodings shown in Table 10-12. Either the full name or the
abbreviation can be passed as an argument.
Quarter Qq 1—4
Month Mm 1—12
dayofyear Dy 1—366
Day Dd 1—31
Week Wk 1—53
weekday Dw 1—7 (Sunday-Saturday)
Hour Hh 0—23
Minute Mi 0—59
Second Ss 0—59
millisecond Ms 0—999
Like other functions, the date functions provide more than simple convenience. Suppose we need
to find all records entered on the second Tuesday of every month and all records entered within
48 weeks of today. Without SQL Server date functions, the only way we could accomplish such a
query would be to select all the rows and return them to the application and then filter them there.
With a lot of data and a slow network, this can get ugly. With the date functions, the process is
simple and efficient, and only the rows that meet these criteria are selected. For this example,
let's assume that the records table includes these columns:
Record_number int
Entered_on datetime
The day of the week considered "first" is locale-specific and depends on the DATEFIRST
setting.SQL Server 2000 also allows you to add or subtract an integer to or from a datetime
value. This is actually just a shortcut for the DATEADD function, with a datepart of day. For
example, the following two queries are equivalent. They each return the date 14 days from today:
SELECT GETDATE() + 14
The date functions don't do any rounding. The DATEDIFF function just subtracts the components
from each date that correspond to the datepart specified. For example, to find the number of
years between New Year's Day and New Year's Eve of the same year, this query would return a
value of 0. Because the datepart specifies years, SQL Server subtracts the year part of the two
dates, and because they're the same, the result of subtracting them is 0:
However, if we want to find the difference in years between New Year's Eve of one year and New
Year's Day (the next day), the following query would return a value of 1 because the difference in
the year part is 1:
There's no built-in mechanism for determining whether two date values are actually the same day
unless you've forced all datetime values to use the default time of midnight. (We saw a technique
for doing this earlier.) If you want to compare two datetime values (@date1 and @date2) to
determine whether they're on the same day, regardless of the time, one technique is to use a
three-part condition like this:
But there is a much simpler way. Even though these two queries both return the same dd value, if
we try to find the difference in days between these two dates, SQL Server is smart enough to
know that they are different dates:
So, the following query returns the message that the dates are different:
IF DATEDIFF(dd, '7/5/99','7/5/00') = 0
PRINT 'The dates are the same'
ELSE PRINT 'The dates are different'
And the following general form allows us to indicate whether two dates are really the same date,
irrespective of the time:
Math Functions
Transact-SQL math functions are straightforward and typical. Many are specialized and of the
type you learned about in trigonometry class. If you don't have an engineering or mathematical
application, you probably won't use those. A handful of math functions are useful in general types
of queries in applications that aren't mathematical in nature. ABS, CEILING, FLOOR, and
ROUND are the most useful functions for general queries to find values within a certain range.
The random number function, RAND, is useful for generating test data or conditions. You'll see
examples of RAND later in this chapter.
Table 17 shows the complete list of math functions and a few simple examples. Some of the
examples use other functions in addition to the math ones to illustrate how to use functions within
functions.
EXAMPLE 1
Produce a table with the sine, cosine, and tangent of all angles in multiples of 10, from 0 through
180. Format the return value as a string of eight characters, with the value rounded to five
decimal places. (We need one character for the decimal point and one for a negative sign, if
needed.)
This example actually produces 19 different result sets because the SELECT statement is issued
once for each iteration of the loop. These separate result sets are concatenated and appear as
one result set in this example. That works fine for illustrative purposes, but you should avoid
doing an operation like this in the real world, especially on a slow network. Each result set carries
with it metadata to describe itself to the client application.
EXAMPLE 2
Express in whole-dollar terms the range of prices (non-null) of all books in the titles table. This
example combines the scalar functions FLOOR and CEILING inside the aggregate functions MIN
and MAX:
EXAMPLE 3
Use the same records table that was used in the earlier date functions example. Find all records
within 150 days of September 30, 1997. Without the absolute value function ABS, you would
have to use BETWEEN or provide two search conditions and AND them to account for both 150
days before and 150 days after that date. ABS lets you easily reduce that to a single search
condition.
SELECT Record_number, Entered_on
FROM records
WHERE
ABS(DATEDIFF(DAY, Entered_on, '1997.09.30')) <= 150
-- Plus or minus 150 days
String Functions
String functions make it easier to work with character data. They let you slice and dice character
data, search it, format it, and alter it. Like other scalar functions, string functions allow you to
perform functions directly in your search conditions and SQL batches that would otherwise need
SELECT ASCII('Ä')
The CHAR function is handy for generating test data, especially when combined with RAND and
REPLICATE. CHAR is also commonly used for inserting control characters such as tabs and
carriage returns into your character string. Suppose, for example, that we want to return authors'
last names and first names concatenated as a single field, but with a carriage return (0x0D, or
decimal 13) separating them so that without further manipulation in our application, the names
occupy two lines when displayed. The CHAR function makes this simple:
NAME
--------
Johnson
White
Marjorie
Green
Cheryl
Carson
Michael
O'Leary
This query finds no rows in the new table even though author name Cheryl Carson is included in
that table:
If the value to be searched might be of mixed case, you need to use the function on both sides of
the equation. This query will find the row in the case-sensitive table:
In these examples, even though we might have an index on au_lname, it won't be useful because
the index keys aren't uppercase. However, we could create a computed column on
UPPER(au_lname) and build an index on the computed column.
You'll also often want to use UPPER or LOWER in stored procedures in which character
parameters are used. For example, in a procedure that expects Y or N as a parameter, you'll
likely want to use one of these functions in case y is entered instead of Y.
TRIM functions
The functions LTRIM and RTRIM are handy for dealing with leading or trailing blanks. Recall that
by default (and if you don't enable the ANSI_PADDING setting) a varchar datatype is
automatically right-trimmed of blanks, but a fixed-length char isn't. Suppose that we want to
concatenate the type column and the title_id column from the titles table, with a colon separating
them but with no blanks. The following query doesn't work because the trailing blanks are
retained from the type column:
business :BU1032
business :BU1111
business :BU2075
business :BU7832
mod_cook :MC2222
mod_cook :MC3021
UNDECIDED :MC3026
popular_comp:PC1035
popular_comp:PC8888
popular_comp:PC9999
business:BU1032
business:BU1111
business:BU2075
business:BU7832
mod_cook:MC2222
mod_cook:MC3021
UNDECIDED:MC3026
popular_comp:PC1035
popular_comp:PC8888
popular_comp:PC9999
You can also use CHARINDEX or PATINDEX as a replacement for LIKE. For example, instead of
saying WHERE name LIKE '%Smith%', you can say WHERE CHARINDEX('Smith', name) > 0.
Suppose we want to change occurrences of the word "computer" within the notes field of the titles
table and replace it with "Hi-Tech Computers." Assume that SQL Server is case sensitive and, for
simplicity, that we know that "computer" won't appear more than once per column. The regular
expression computer[^s] always finds the word "computer" and ignores "computers," so it'll work
perfectly with PATINDEX.
title_id notes
-------- ------------------------------------------------------
BU7832 Annotated analysis of what computers can do for you: a
no-hype guide for the critical user.
PC8888 Muckraking reporting on the world's largest computer
hardware and software manufacturers.
PC9999 A must-read for computer conferencing.
PS1372 A must for the specialist, this book examines the
difference between those who hate and fear computers
and those who don't.
PS7777 Protecting yourself and your loved ones from undue
emotional stress in the modern world. Use of computer
and nutritional aids emphasized.
You might consider using the REPLACE function to make the substitution. However, REPLACE
requires that we search for a specific string that can't include wildcards like [^s]. Instead, we can
use the older STUFF function, which has a slightly more complex syntax. STUFF requires that we
specify the starting location within the string to make a substitution. We can use PATINDEX to
find the correct starting location, and PATINDEX allows wildcards:
UPDATE titles
SET notes=STUFF(notes, PATINDEX('%computer[^s]%', notes),
DATALENGTH('computer'), 'Hi-Tech Computers')
WHERE PATINDEX('%computer[^s]%', notes) > 0
title_id notes
-------- ------------------------------------------------------
BU7832 Annotated analysis of what computers can do for you: a
no-hype guide for the critical user.
PC8888 Muckraking reporting on the world's largest Hi-Tech
Computers hardware and software manufacturers.
PC9999 A must-read for Hi-Tech Computers conferencing.
PS1372 A must for the specialist, this book examines the
difference between those who hate and fear computers
and those who don't.
PS7777 Protecting yourself and your loved ones from undue
emotional stress in the modern world. Use of Hi-Tech
Computers and nutritional aids emphasized.
Of course, we could have simply provided 8 as the length parameter of the string "computer" to
be replaced. But we used yet another function, LEN, which would be more realistic if we were
creating a general-purpose, search-and-replace procedure. Note that DATALENGTH returns
NULL if the expression is NULL, so in your applications, you might go one step further and use
DATALENGTH inside the ISNULL function to return 0 in the case of NULL.
The REPLICATE function is useful for adding filler characters—such as for test data. (In fact,
generating test data seems to be about the only practical use for this function.) The SPACE
function is a special-purpose version of REPLICATE: it's identical to using REPLICATE with the
space character. REVERSE reverses a character string. You can use REVERSE in a few cases
y
The names "Smythe" and "Smith" both have a SOUNDEX value of S530, so their difference level
is 4, even though the spellings differ. If the first character (a letter, not a number) of the
SOUNDEX value sx_a1 is the same as the first character of sx_a2, the starting level is 1. If the
first character is different, the starting level is 0. Then each character in sx_a2 is successively
compared to all characters in sx_a1. When a match is found, the level is incremented and the
next scan on sx_a1 starts from the location of the match. If no match is found, the next sx_a2
character is compared to the entire four-character list of sx_a1.
The preceding description of the algorithms should make it clear that SOUNDEX at best provides
an approximation. Even so, sometimes it works extremely well. Suppose we want to query the
authors table for names that sound similar to "Larsen." We'll define similar to mean "having a
SOUNDEX value of 3 or 4":
SELECT au_lname,
Soundex=SOUNDEX(au_lname),
Diff_Larsen=DIFFERENCE(au_lname, 'Larson')
FROM authors
WHERE
DIFFERENCE(au_lname, 'Larson') >= 3
In this case, we found two names that rhyme with "Larsen" and didn't get any bad hits of names
that don't seem close. For example, do you think "Bennet" and "Smith" sound similar? Well,
SOUNDEX does. When you investigate, you can see why the two names have a similar
SOUNDEX value. The SOUNDEX values have a different first letter, but the m and n sounds are
converted to the same digit, and both names include a t, which is converted to a digit for the third
position. "Bennet" has nothing to put in the fourth position, so it gets a 0. For Smith, the h
becomes a 0. So, except for the initial letter, the rest of the SOUNDEX strings are the same.
Hence, the match.
This type of situation happens often with SOUNDEX—you get hits on values that don't seem
close, although usually you won't miss the ones that are similar. So if you query the authors table
for similar values (with a DIFFERENCE value of 3 or 4) as "Smythe," you get a perfect hit on
"Smith" as you'd expect, but you also get a close match for "Bennet," which is far from obvious:
SELECT au_lname,
Soundex=SOUNDEX(au_lname),
Diff_Smythe=DIFFERENCE(au_lname, 'Smythe')
FROM authors
WHERE
DIFFERENC3E(au_lname, 'Smythe') >= 3
Sometimes SOUNDEX misses close matches altogether. This happens when consonant blends
are present. For example, you might expect to get a close match between "Knight" and "Nite." But
you get a low value of 1 in the DIFFERENCE between the two:
SELECT
"SX_KNIGHT"=SOUNDEX('Knight'),
"SX_NITE"=SOUNDEX('Nite'),
"DIFFERENCE"=DIFFERENCE('Nite', 'Knight')
System Functions
System functions are most useful for returning certain metadata, security, or configuration
settings. Table 19 lists of some of the more commonly used system functions, and it's followed by
a brief discussion of the interesting features of a few of them. For complete details, see the SQL
Server online documentation.
The DATALENGTH function is most often used with variable-length data, especially character
strings, and it tells you the actual storage length of the expression (typically a column name). A
fixed-length datatype always returns the storage size of its defined type (which is also its actual
storage size), which makes it identical to COL_LENGTH for such a column. The DATALENGTH
of any NULL expression returns NULL. The OBJECT_ID, OBJECT_NAME, SUSER_SNAME,
SUSER_SID, USER_ID, USER_NAME, COL_NAME, DB_ID, and DB_NAME functions are
commonly used to more easily eliminate the need for joins between system catalogs and to get
run-time information dynamically.
y
Recognizing that an object name is unique within a database for a specific user, we can use the
following statement to determine whether an object by the name "foo" exists for our current user
ID; if so, we can drop the object (assuming that we know it's a table).
System functions can be handy with constraints and views. Recall that a view can benefit from
the use of system functions. For example, if the accounts table includes a column that is the
system login ID of the user to whom the account belongs, we can create a view of the table that
allows the user to work only with his or her own accounts. We do this simply by making the
WHERE clause of the view something like this:
WHERE system_login_id=SUSER_SID()
Or if we want to ensure that updates on a table occur only from an application named
CS_UPDATE.EXE, we can use a CHECK constraint in the following way:
CONSTRAINT APP_CHECK(APP_NAME()='CS_UPDATE.EXE')
For this particular example, the check is by no means foolproof because the application must set
the app_name in its login record. A rogue application could simply lie. To prevent casual use by
someone running an application like isql.exe, the preceding constraint might be just fine for your
needs. But if you're worried about hackers, this formulation isn't appropriate. Other functions,
such as SUSER_SID, have no analogous way to be explicitly set, so they're better for security
operations like this.
The ISDATE and ISNUMERIC functions can be useful for determining whether data is
appropriate for an operation. For example, suppose your predecessor didn't know much about
using a relational database and defined a column as type char and then stored values in it that
were naturally of type money. Then she compounded the problem by sometimes encoding letter
codes in the same field as notes. You're trying to work with the data as is (eventually you'll clean
it up, but you have a deadline to meet), and you need to get a sum and average of all the values
whenever a value that makes sense to use is present. (If you think this example is contrived, talk
to a programmer in an MIS shop of a Fortune 1000 company with legacy applications.) Suppose
the table has a varchar(20) column named acct_bal with these values:
acct_bal
--------
205.45
If you try to simply use the aggregate functions directly, you'll get error 409:
But if you use both CONVERT() in the select list and ISNUMERIC() in the WHERE clause to
choose only those values for which a conversion would be possible, everything works great:
SELECT
"SUM"=SUM(CONVERT(money, acct_bal)),
"AVG"=AVG(CONVERT(money, acct_bal))
FROM bad_column_for_money
WHERE ISNUMERIC(acct_bal)=1
SUM AVG
-------- --------
9,470 1.352.95
Metadata Functions
In earlier versions of SQL Server, the only way to determine which options or properties an object
had was to actually query a system table and possibly even decode a bitmap status column. SQL
Server 2000 provides a set of functions that return information about databases, files, filegroups,
indexes, objects, and datatypes.
For example, to determine what recovery model a database is using, you can use the
DATABASEPROPERTYEX function:
Many of the properties that you can check for using either the DATABASEPROPERTY and
DATABASEPROPERTYEX functions correspond to database options that you can set using the
sp_dboption stored procedure. One example is the AUTO_CLOSE option, which determines the
value of the IsAutoClose property. Other properties, such as IsInStandBy, are set internally by
SQL Server. The documentation also provides the complete list of properties and possible values
for use with the following functions:
• COLUMNPROPERTY
• FILEGROUPPROPERTY
• FILEPROPERTY
• INDEXPROPERTY
Niladic Functions
ANSI SQL-92 has a handful of what it calls niladic functions—a fancy name for functions that
don't accept any parameters. Niladic functions were first implemented in version 6, and each one
maps directly to one of SQL Server's system functions. They were actually added to SQL Server
6 for conformance with the ANSI SQL-92 standard; all of their functionality was already provided,
however. Table 20 lists the niladic functions and their equivalent SQL Server system functions.
Table 20. Niladic functions and their equivalent SQL Server functions.
If you execute the following two SELECT statements, you'll see that they return identical results:
However, these values aren't variables because you can't declare them and you can't assign
them values. These functions are global only in the sense that any connection can access their
values. However, in many cases the value returned by these functions is specific to the
connection. For example, @@ERROR represents the last error number generated for a specific
connection, not the last error number in the entire system. @@ROWCOUNT represents the
number of rows selected or affected by the last statement for the current connection.
Many of these parameterless system functions keep track of performance-monitoring information
for your SQL Server. These include @@CPU_BUSY, @@IO_BUSY, @@PACK_SENT, and
@@PACK_RECEIVED.
Some of these functions are extremely static, and others are extremely volatile. For example,
@@version represents the build number and code freeze date of the SQL Server executable
(Sqlservr.exe) that you're running. It will change only when you upgrade or apply a service pack.
Functions like @@ROWCOUNT are extremely volatile. Take a look at the following code and its
results.
USE pubs
SELECT * FROM publishers
SELECT @@ROWCOUNT
SELECT @@ROWCOUNT
(8 row(s) affected)
-----------
8
(1 row(s) affected)
-----------
1
(1 row(s) affected)
Note that the first time @@ROWCOUNT was selected, it returned the number of rows in the
publishers table (8). The second time @@ROWCOUNT was selected, it returned the number of
rows returned by the previous SELECT @@ROWCOUNT (1). Any query you execute that affects
rows will change the value of @@ROWCOUNT.
Table-Valued Functions
SQL Server 2000 provides a set of system functions that return tables; because of this, the
functions can appear in the FROM clause of a query. To invoke these functions, you must use the
special signifier :: as a prefix to the function name and then omit any database or owner name.
Several of these table-valued functions return information about traces you have defined. Another
one of the functions can also be useful during monitoring and tuning, as shown in this example:
SELECT *
FROM ::fn_virtualfilestats(5,1)
The function fn_virtualfilestats takes two parameters, a database ID and a file ID, and it returns
statistical information about the I/O on the file. Here is some sample output from running this
query on the pubs database:
Because the function returns a table, you can control the result set by limiting it to only certain
columns or to rows that meet certain conditions. As another example, the function
fn_helpcollations lists all the collations supported by SQL Server 2000. An unqualified SELECT
from this function would return 753 rows in two columns.
SELECT name
FROM ::fn_helpcollations()
WHERE name LIKE 'SQL%latin%CI%'
AND name NOT LIKE '%pref%'
name
---------------------------------------
SQL_Latin1_General_CP1_CI_AI
SQL_Latin1_General_CP1_CI_AS
SQL_Latin1_General_CP1250_CI_AS
SQL_Latin1_General_CP1251_CI_AS
SQL_Latin1_General_CP1253_CI_AI
SQL_Latin1_General_CP1253_CI_AS
SQL_Latin1_General_CP1254_CI_AS
SQL_Latin1_General_CP1255_CI_AS
SQL_Latin1_General_CP1256_CI_AS
SQL_Latin1_General_CP1257_CI_AS
SQL_Latin1_General_CP437_CI_AI
SQL_Latin1_General_CP437_CI_AS
SQL_Latin1_General_CP850_CI_AI
SQL_Latin1_General_CP850_CI_AS
Transactions
You can use transactions to specify that you want SQL Server to process a series of SQL
statements as a single unit rather than individually. When SQL Server processes the statements
as a single unit, all of them must be completed successfully-or they will all fail. This capability is
referred to as atomicity.
Types of Transactions
SQL Server supports two types of transactions: explicit and implicit. An explicit transaction is a
group of one or more Transact-SQL statements that begin with a BEGIN TRANSACTION
statement and end with the COMMIT TRANSACTION statement. SQL Server does not commit
the changes made in an explicit transaction's SQL statements until it processes the COMMIT
TRANSACTION statement. Thus, you can roll back the transaction at any time prior to the
COMMIT TRANSACTION statement. Remember, however, that you must always use COMMIT
TRANSACTION after you use BEGIN TRANSACTION. If you enter the BEGIN TRANSACTION
before an INSERT statement, for example, the INSERT transaction will not be committed to the
table without a closing COMMIT TRANSACTION statement.
You use an implicit transaction when you use Transact-SQL statements by themselves without
the BEGIN TRANSACTION. SQL Server considers all statements you execute part of a
transaction until you issue either a COMMIT TRAN, COMMIT WORK, or ROLLBACK TRAN
statement. SQL Server does not enable implicit transactions by default.
By default, SQL Server enables the autocommit transaction mode. This mode configures SQL
Server to treat each individual SQL statement (along with its parameters) as a separate
transaction. For example, if you execute a query and you do not use the BEGIN TRAN and
COMMIT TRAN statements, nor do you turn on implicit transactions, SQL Server autocommits
the transaction.
SQL Server first records the changes you make by using an INSERT, UPDATE, or DELETE
statement to a table in the transaction log for the database. SQL Server marks the beginning of
each transaction so that it can return to the start of the transaction if a failure occurs. SQL Server
then posts the changes from the transaction log to the database itself. The process of writing the
changes from the transaction log to the database is referred to as the checkpoint process. When
you use explicit transactions, SQL Server does not begin the checkpoint process until you use the
COMMIT TRANSACTION statement. Once SQL Server has written your transaction to the
database itself, the transaction is considered committed.
When SQL Server posts the changes in the transaction log to your database, it creates a
checkpoint in the transaction log. SQL Server uses this checkpoint to identify all uncommitted
transactions-these are the changes that have occurred since the last checkpoint process. In the
event of a failure (such as loss of power), SQL Server makes sure that all committed transactions
in the transaction log are written to the database (in other words, the transactions are rolled
SQL Server temporarily caches committed transactions. When the cache fills up, it then writes the
information to your server's hard disk. The copies of your database on the server's hard drive and
in RAM are identical at this point. To prevent database corruption, do not use a write-caching
hard drive controller on a SQL server. With this type of controller, it is possible that SQL Server
will think it has written information to the hard disk when it fact the information is still cached on
the hard drive controller. If you have a power failure at this point, SQL Server will not be able to
roll back or roll forward the necessary transactions to repair your database.
Designing Transactions
You should try to keep your transactions short to minimize the amount of work SQL Server must
do to roll back the transaction in the event of a problem. In addition, SQL Server locks resources
whenever a transaction is open. When a resource (such as an entire table) is locked, other users
cannot access it. To keep your transaction short, try not to use control-of-flow statements such as
WHILE. You also should not use Data Definition Language statements such as CREATE TABLE
within a transaction. You also should not require user input from within a transaction. Instead,
gather all of the necessary information from user data entry first, write the statements to begin the
transaction, perform whatever task is necessary, and then end the transaction.
You should also try to avoid nesting transactions. In other words, do not begin a transaction and
then begin a second transaction within the first. If you do, SQL Server ignores the innermost
BEGIN TRANSACTION and COMMIT TRANSACTION statements. One of the most common
situations that leads to nested transactions is when you have nested triggers or stored
procedures. You can use the @@trancount global variable to view a count of open transactions.
This information helps you to determine if you have nested transactions. The @@trancount
variable is set to zero when you do not have any open transactions. When SQL Server processes
the first BEGIN TRANSACTION statement, it increases @@trancount by one; if SQL Server
processes a ROLLBACK TRANSACTION statement, it sets @@trancount to zero.
You begin an explicit transaction by using the BEGIN TRANSACTION statement. You can
optionally name a transaction; you can then use this name when you either commit or rollback the
transaction. Use the following syntax to begin an explicit transaction:
Because you use a transaction when you are making changes to data, you will most commonly
use the INSERT, UPDATE, and DELETE statements within a transaction. You cannot include the
following statements within an explicit transaction:
• Any system stored procedure that creates a temporary table (such as sp_dboption).
• ALTER DATABASE
• BACKUP LOG
• CREATE DATABASE
• RECONFIGURE
• RESTORE DATABASE
• RESTORE LOG
• UPDATE STATISTICS
Step 1:
Step 2:
Step 3:
Step 4:
Step 5:
Step 6:
Microsoft recommends that you explicitly define transactions wherever possible. However, you
might run into situations such as when you migrate an application from another environment into
the SQL Server environment where you must maintain implicit transactions. To use implicit
transactions, you must first configure SQL Server to support them by using the following syntax:
SET IMPLICIT_TRANSACTIONS ON
Once you have enabled implicit transactions, you start a transaction whenever you issue a query
that begins with any of the following SQL statements:
• ALTER TABLE
• CREATE
• DELETE
• DROP
• FETCH
• GRANT
• INSERT
Once you have begun an implicit transaction, SQL Server does not commit the transaction's
changes until you execute one of the following statements: COMMIT TRAN, COMMIT WORK, or
ROLLBACK TRAN.
To avoid conflicts, SQL Server uses locks to protect the integrity of your databases during
transactions. For example, locks prevent lost updates-a scenario in which two users update the
same row at the same time. Without locks, one user's change would overwrite the other user's
change. Locks also prevent inconsistent analysis. Inconsistent analysis (nonrepeatable read)
occurs when a transaction in SQL Server reads the same row twice, yet the values in the row
change between each read. SQL Server uses locks to prevent phantoms as well. A phantom can
occur when two transactions are executed at the same time. For example, you might have one
user updating the rows in the customer table to reflect a zip code change, while at the same time
another user inserts a new row into the customer table. If these two transactions occur
simultaneously, the transaction for changing the zip codes might change the necessary customer
rows-but then find a new row when the other user's transaction is completed. This new row is
referred to as a phantom. Finally, locks can be used to prevent dirty reads. A dirty read occurs
when a transaction attempts to read uncommitted data.
SQL Server locks resources based on the type of action you are performing. SQL Server tries to
implement locking that affects the smallest amount of a resource while still maintaining the
integrity of your data. For example, one of the new features in SQL Server 7.0 is the ability to lock
a table at the row level rather than at the page level. SQL Server can place locks on the following
resources:
Types of locks
SQL Server implements several types of locks. These locks can be divided into two categories:
basic and special use.
Basic locks
SQL Server implements two types of basic locks: shared and exclusive. SQL Server uses shared
locks during read transactions, and exclusive locks during write transactions. SQL Server uses
shared locks (S) only if your transaction does not modify data. This lock is referred to as shared
because other transactions can also place a shared lock on the same resource as well. Thus,
multiple transactions can use the resource at the same time. SQL Server releases a shared lock
on a row as soon as it reads the next row in a table. If you issue a query that returns multiple
rows, SQL Server maintains the shared lock until it has retrieved all rows that satisfy the query.
SQL Server uses exclusive locks (X) whenever you issue transactions that change
data. For example, SQL Server uses an exclusive lock if your transaction contains an
INSERT, UPDATE, or DELETE statement. This lock is referred to as exclusive because
only one transaction can use the resource. In addition, other transactions cannot
place a shared lock on a resource that has an exclusive lock on it. Likewise, your
transaction cannot place an exclusive lock on a resource if it has shared locks on it.
SQL Server uses an update lock (U) on a table when it plans to modify one of the table's pages.
SQL Server places an update lock on a page when it first reads the page. SQL Server changes
the update lock to an exclusive lock when it writes the changes to the page. An update lock on a
page prevents user transactions from obtaining exclusive locks on rows within the same page.
However, user transactions can still place shared locks on the page.
If a table or index is in use, SQL Server places a schema lock on that table or index. A schema
stability lock (Sch-S) prevents the table or index from being dropped while it is currently in use. A
schema modification lock (Sch-M) prevents users from accessing a table or index while you are
modifying its structure.
Finally, SQL Server uses bulk update locks to prevent other transactions from accessing a table
when you are importing data into the table. SQL server places a bulk update lock on a table
whenever you configure a table to use the "table lock on bulk load option." You can configure this
option by using the sp_tableoption stored procedure.
Coexistence of locks
Some locks cannot be placed on the same resource at the same time.
*The Sch-M lock is incompatible with all other locks; the Sch-S lock is compatible with all other
locks except Sch-M.
You can also use the Current Activity object in SQL Server Enterprise Manager to view
information about locks held by a process or user, as well as any locks that are currently being
blocked (due to an exclusive lock) and locks that are blocking other processes or users. SQL
Server Enterprise Manager enables you to view locks by process ID or by object. Of the different
types of locks, you should be most concerned about exclusive locks as these locks can block
other processes from performing their tasks. When you view the locks within SQL Server
Enterprise Manager, you can determine whether a lock is blocking other processes or not.
Managing locks
SQL Server includes options that enable you to control how it locks resources. These options are
referred to as isolation levels. You can set the isolation level at either the session- or table-level.
Session-level locking
You can control locking at the session level by setting the transaction isolation level. SQL Server
uses the transaction isolation level to determine to what degree it will isolate a transaction. If you
change the transaction isolation level, SQL Server applies this setting to all transactions in your
current session. (You can later override these settings by specifying the transaction isolation level
on a statement.)
Use the following syntax to set the transaction isolation level for your current session:
By default, SQL Server sets the lock timeout to -1 which means it is disabled. Thus, a transaction
will wait indefinitely for a blocking transaction to clear its lock. If you set the lock timeout to 0, SQL
Server immediately cancels the transaction if the resource it needs to access is locked.
Table-level locking
By default, SQL Server automatically tunes locking for your environment. If necessary, however,
you can specify how you want SQL Server to lock resources by specifying table-level locking as
part of your transaction. You specify table-locking by adding lock hints to SELECT and UPDATE
queries (just as you can add index hints). You must use lock hints as part of a transaction. For
example, the following query contains a lock hint to make the transaction SERIALIZABLE:
BEGIN TRAN
UPDATE movie(SERIALIZABLE)
SET rented = `Y'
WHERE movie_num = 110
COMMIT TRAN
You can use the same options to set table locking that you can with session-level locking:
READUNCOMMITTED, READCOMMITTED, REPEATABLEREAD, and SERIALIZABLE. In
Deadlocks
A deadlock occurs when two users or processes have locks on different objects and each needs
to also place a lock on the other's object. In this situation, the SQL Server will choose a deadlock
victim and abort that user's process, thus allowing the other user or process to continue. The SQL
server then informs the deadlock victim of the termination. SQL Server notifies the deadlock
victim's application by using error message number 1205.
A deadlock can occur when you run several long transactions simultaneously in a database or
when the query optimizer designs a query execution plan for a complex query. You can minimize
deadlocks by designing your transactions such that they use resources in the same order. You
should also try to keep your transactions as short as possible to avoid deadlocks.
±1. List the different types of locks that can be implemented by SQL Server.
³
There are two categories of locks: basic and special use. Basic locks include shared and
exclusive locks whereas special use locks include the intent locks, update locks, schema locks
and bulk update locks.
Stored Procedures
Introduction
While at its core a stored procedure is simply a series of SQL statements just like any other
query, it offers several distinct advantages over queries-even client applications such as those
written in Microsoft Access or Visual Basic. First, once SQL Server parses and compiles a stored
procedure, it caches the execution plans in its procedure cache. Thus, when you run the same
stored procedure again, it executes faster than an equivalent ad hoc query. Second, if you call
stored procedures from your applications instead of explicitly writing the queries into your
applications, it is much easier for you to change a query within a stored procedure than it is to
search for and change the code in an application.
Another advantage of stored procedures is that you will typically batch your SQL statements
within them. Batches offer you enhanced performance because SQL Server can process all of
the statements together instead of individually. You can also see that executing a stored
procedure that contains SQL batches will reduce your network traffic. This is because SQL
Server can send the result set for all of the statements in the batch at the same time rather than
individually.
SQL Server supports three types of stored procedures. The first two types of stored procedures,
system and extended, are built-in-which means they are installed automatically when you install
SQL Server itself. The third type of stored procedure is user-defined-you create these yourself.
User-defined stored procedures can be local, temporary, or remote.
SQL Server also includes extended stored procedures. These are implemented as dynamic link
libraries (DLLs). Extended stored procedures primarily have names that begin with xp_. For
example, the xp_cmdshell extended stored procedure enables you to shell out to the operating
system (typically Windows NT) to run an operating system command. Another extended stored
procedure, xp_sendmail, enables SQL Server to send email messages. You must execute
extended stored procedures either from within the master database or by using a fully qualified
name (such as master.dbo.xp_cmdshell).
You can create two types of temporary stored procedures: local or global. You identify a
temporary stored procedure as either local or global by preceding its name with a # if it is local,
and ## if it is global. A local stored procedure is available only to you during your current session.
A global stored procedure is available to all current sessions on your server.
Step 1:
Step 2:
Step 3:
Step 4:
Step 5:
Step 6:
master.owner. extended_procedure_name.
You can leave out the owner name if you are the owner of the procedure. You can view the name
of the DLL file that makes up an extended stored procedure by executing the following query:
sp_helptext extended_procedure_name
You can create your own extended stored procedures but you must store them within the master
database. You can use extended stored procedures to call your own external programs in
programming languages such as C++ and Visual Basic.
Step 1:
Step 3:
Step 4:
Step 5:
Parse
When you run a stored procedure for the first time, SQL Server parses the SQL statements to
test their accuracy. SQL Server does support delayed name resolution, which enables your
stored procedures to reference objects that do not already exist (this scenario typically occurs
when you create objects when the stored procedure executes). It then translates the SQL
statements into an internal format for processing; this format is called the query tree or sequence
tree. Finally, SQL Server updates the sysobjects system table with the name of your stored
procedure. It also writes a row to the syscomments system table with the text of the stored
procedure.
Compile
Once SQL Server has the sequence tree for your stored procedure, it can compile an execution
plan. SQL Server checks your permissions as well as determines how to optimize the query as
part of creating the execution plan. The execution plan contains step-by-step instructions on how
SQL Server will process the query.
y
The execution plan includes the steps for checking any constraints you might have on referenced
tables.
Execute
SQL Server can process the stored procedure once it completes creating the execution plan.
SQL Server sends statements within the stored procedure to the appropriate manager for those
statements.
y
If your stored procedure contains Data Definition Language (DDL) statements for creating objects
such as tables, SQL Server sends those statements to the DDL manager.
When you run a stored procedure for the first time, SQL Server must begin by parsing the stored
procedure. Next, it compiles your stored procedure. Finally, SQL Server executes the procedure.
When you run a stored procedure for the first time, SQL Server must begin by parsing the stored
procedure. Next, it compiles your stored procedure. Finally, SQL Server executes the procedure.
If any of these conditions occurs, SQL Server must retrieve your stored procedure's definition
from the syscomments table, re-compile its execution plan, and then cache it in the procedure
cache. The advantage to a stored procedure is that once the execution plan is cached, SQL
Server can simply retrieve it from RAM, rather than parsing and re-compiling the stored
procedure each time it is run. Thus, you will see the benefits of stored procedures the second
time you run them (but not the first).
Notice that if you restart your server, SQL Server clears the procedure cache. SQL Server must
parse and re-compile the execution plans of stored procedures whenever you reboot. If you have
stored procedures that you use frequently, you can create a stored procedure to execute them-
and then configure this stored procedure to run automatically when you start up your server.
±1. List the three types of stored procedures supported in SQL Server.
³SQL Server supports system, extended, and user-defined stored procedures.
±2. Describe each stored procedure type.
³System stored procedures enable you to perform many of the administrative tasks on your
server. System stored procedures typically have names that begin with sp_. Extended stored
procedures are actually DLLs that extend the functionality of SQL Server. Extended stored
procedures have names that begin with xp_. User-defined stored procedures are ones that you
define to perform virtually any task on the server.
Replace procedure_name with the name you want to assign to your stored procedure. You can
abbreviate CREATE PROCEDURE as CREATE PROC. You cannot use the CREATE
PROCEDURE statement along with other statements in a batch-you need to follow it with the GO
keyword.
y
You might create the following stored procedure to list all of the rented movies that are due today:
USE movies
GO
CREATE PROC dbo.MoviesDue
AS
SELECT m.title, m.movie_num, rd.invoice_num, r.due_date
FROM rental AS r JOIN rental_detail AS rd
ON r.invoice_num = rd.invoice_num
JOIN movie AS m
ON rd.movie_num = m.movie_num
WHERE convert(char(10), r.due_date, 101) = convert(char(10), getdate(), 101)
GO
Because the CREATE PROC statement cannot be executed along with other statements, you
must end it with the GO keyword. As you can see in the previous example, the GO keyword
comes after the SQL statements that make up your stored procedure. Note: In this stored
procedure, you must convert the due_date and getdate() values to character strings because
both contain not just a date but also a time. Both due_date and getdate() use the smalldatetime
data type-which means both contain not only a date but also the time. You must strip out the
month, day, and year information from the time information in order for this WHERE condition to
work. Otherwise, you would be able to see a list of movies due only if they happened to be due
today and at the exact current time.
Permissions
To create a stored procedure, you must be either a member of the sysadmins server role, or a
member of the db_owner or ddl_admin database roles for the database in which you are
attempting to create the procedure. If you have users who you want to create procedures, but you
do not want them to be a member of either the server or database roles, you can explicitly grant
these users the CREATE PROCEDURE statement permission.
You should try to avoid breaking the ownership chain between a stored procedure and the tables
or views on which it is based. Microsoft recommends that you make the dbo user the owner of all
objects in the database (including stored procedures) to avoid this problem.
Limitations
Your stored procedures can be up to 128 MB in size-but can be further limited by the amount of
available RAM in your server. You can nest up to 32 levels of stored procedures. You nest stored
procedures when one procedure calls another.
Recommendations
Once you have debugged your stored procedure on the SQL server, you should always test it
from a client computer. This test will enable you to detect any communication problems between
the client and the server. You should also test it logged on as a typical user, not as a system
administrator to verify that you have given users sufficient permissions.
You can view the text of a stored procedure by using the sp_helptext system stored procedure.
y
To view the text of the MoviesDue stored procedure, use the following syntax:
sp_helptext MoviesDue
You can also use the sp_help stored procedure to view information about who owns a stored
procedure, as well as when the stored procedure was created. Use the following syntax:
sp_help procedure_name
The sp_depends stored procedure enables you to see a list of objects on which a stored
procedure depends.
sp_depends procedure_name
Finally, you can use the sp_stored_procedures procedure to list all of the defined stored
procedures in your current database.
You will see a list of both user-defined and system stored procedures.
sp_stored_procedures
Step 1:
Step 2:
Step 3:
Step 4:
Step 5:
Step 6:
Step 7:
SQL Server requires that you run a stored procedure either as the first line of a query or that you
precede it with the EXECUTE (or EXEC) keyword. For example, the following query will return an
error:
USE movies
SELECT *
FROM movie
sp_help movie
This query will not work because the sp_help statement is not the first line of the query or you
have not preceded it with the EXECUTE keyword. You can rewrite this query so that it will run
successfully by using the following syntax:
USE movies
SELECT *
FROM movie
EXEC sp_help movie
TIP: Always call a stored procedure with the EXEC keyword whenever you
are developing programs and script files. By getting into this habit, you
can avoid having stored procedures fail.
If you choose to run a stored procedure simply by typing its name (and not the EXECUTE or
EXEC keywords), you must make it the first statement in a batch.
You must use a fully qualified name to run a stored procedure that is stored in a database other
than your current database. For example, if you want to run the MoviesDue example stored
procedure, but your current database is pubs, you can use the name movies.dbo.MoviesDue to
run it.
USE database
INSERT INTO table_name
EXEC procedure_name
Step 1:
Step 3:
Step 5:
Just as in views, SQL Server stores the definition of your stored procedure in the syscomments
system table. You should never edit this table directly-especially if you want to hide the definition
of a stored procedure. Instead, you should use encrypt the stored procedure.
You can change a stored procedure by using the ALTER PROCEDURE statement. (You can also
use ALTER PROC.) When you change a stored procedure, SQL Server replaces its previous
definition with the new definition (SQL statements) you specify. Modifying a stored procedure
instead of dropping and recreating it enables you to retain the permissions you have assigned to
users for the stored procedure. If you want to modify an encrypted stored procedure, you must
include the WITH ENCRYPTION option in the ALTER PROCEDURE statement.
You must be the owner of the stored procedure, a member of the system administrators server
role, or a member of either the db_owner or db_ddladmin database roles in order to modify a
stored procedure. You cannot assign the permission for editing a stored procedure.
You can use ALTER PROCEDURE to modify only one stored procedure at a time. If you want to
modify several nested stored procedures, you must modify each individually.
Microsoft strongly recommends that you do not modify any of the system stored procedures that
come with SQL Server. If you want to modify them, you should copy their definitions to a new
stored procedure, then make the necessary changes.
Ans.
You can view a stored procedure's definition by executing sp_helptext procedure_name.
2. How can you determine which objects a stored procedure depends on?
Ans.
You can list the objects on which a stored procedure runs by executing sp_depends
procedure_name.
Introduction
You can use parameters in stored procedures to make them more interactive. You can use both
input and output parameters. Input parameters enable you to pass a value to a variable within the
stored procedure. In contrast, output parameters return a value after you run a stored procedure.
You can use output parameters to return information to a calling stored procedure. You can
define a total of 1,024 parameters in a stored procedure.
Input Paramters
You begin implementing parameters in stored procedures by first defining the name of the
parameter as well as its data type. You can optionally assign a default value to the parameter.
y
You might want to create a stored procedure that enables you to list all customers who live in a
specific zip code. Instead of hard-coding a specific zip code into the stored procedure, you can
define a parameter as part of the stored procedure-and then specify a zip code whenever you
execute the stored procedure.
Replace @parameter_name with the name you want to assign to the parameter, and data_type
with the data type (such as char, varchar, and so on). You should typically define a default value
for the parameter so that the stored procedure will run successfully in the event no value is
supplied. You can use either constants (character strings or numeric values) or null for the default
value.
This parameter, @rating, enables you to pass a particular movie rating to the stored procedure.
The SELECT statement then displays all movies in the movie table that have a rating equal to the
rating supplied in the parameter.
TIP: Indenting is not required in this statement. We've used indenting to make the SQL
statements easier to read.
If you run this stored procedure without providing a value for the @rating parameter, you will not
see any rows in the result set.
You can use an input parameter only within the stored procedure where it is defined.
y
To pass a value to the @rating parameter by reference, you could use this syntax:
Notice that you must specify the parameter's value by using the appropriate syntax for its data
type. In other words, values for character-based parameters must be enclosed in quotes.
y
To use the MovieByRating stored procedure to list all movies with a G rating, you could also use
this syntax:
MovieByRating `G'
Step 1:
Step 2:
Step 3:
Step 4:
Step 5:
Step 6:
Step 7:
Output Parameters
You can use output parameters to return a value from a stored procedure. That value can then be
used by whatever method you used to call the stored procedure. For example, you might have
two stored procedures: the first stored procedure calls the second, and the second procedure
then returns a value to the first procedure. You might also simply call a stored procedure from a
SQL statement, and then use the value in the output parameter in a subsequent SQL statement.
You will typically use output parameters in other stored procedures. Output parameters thus
enable you to use the results of one stored procedure in another stored procedure.
Identify an output parameter by adding the OUTPUT keyword to its definition in the stored
procedure. In addition, you must also identify the output parameter as part of the EXECUTE
statement you use to call its stored procedure. Use the following syntax to define an output
parameter:
y
The following stored procedure uses five output parameters to store the row counts of each of the
tables in the movies database:
y
To call the count_rows stored procedure from the previous example, you should use the following
syntax:
DECLARE @movie_count int, @cust_count int, @cat_count int, @rental_count int, @rd_count
int
EXEC count_rows @movie_count OUTPUT, @cust_count OUTPUT,
@cat_count OUTPUT, @rental_count OUTPUT, @rd_count OUTPUT
SELECT @movie_count AS movie, @cust_count AS customer,
@cat_count AS category, @rental_count AS rental,
@rd_count AS rental_detail
Managing errors
SQL Server provides you with several tools you can use to manage errors in your stored
procedures. These tools include the RETURN SQL statement, sp_addmessage stored
procedure, the @@error global variable, and the RAISERROR statement.
y
The RETURN statement is used simply to exit the stored procedure.
USE pubs
GO
CREATE PROCEDURE dbo.ListAuthors
@author_id varchar(10) = null
AS
IF @author_id IS NULL
BEGIN
PRINT "Please enter a valid author ID number."
PRINT "Use the format 999-99-9999."
RETURN -- Ends running the stored procedure
END
SELECT au_lname, au_fname, au_id
FROM authors
WHERE au_id = @author_id
GO
If you have the RETURN keyword return a status code, then these return codes actually function
as output parameters. You must save the return code into a variable in order to use it for further
processing.
y
The following stored procedure returns the total number of rows in the result set as a RETURN
status code:
Just as you must declare a variable for SQL Server to store the values of output parameters, so
must you declare a variable for return status codes. Continuing with the previous example, you
could use the following query to run the NumRentals stored procedure, store the return status
code in the variable @answer, and then display it on the screen:
You can use the sp_addmessage stored procedure to create your own custom error messages.
SQL Server stores all error messages, including both system and user-defined, in the
sysmessages table in the master database. You can then call these messages by using the
RAISERROR statement.
EXEC sp_addmessage
@msgnum = number,
@severity = severity_level,
@msgtext = `Text of error message.',
@with_log = `true' or `false'
You can use message numbers 50000 and higher and you can use a severity level from 0 to 25.
Note: Only system administrators can create error messages with a severity level greater than 19.
The following table explains the differences between the various severity levels.
As a general rule, you should use either 0 or 10 for informational messages. SQL Server
considers error messages with a severity greater than 20 as fatal and terminates the client's
connection to the server. Use 15 as the severity level for warning messages, and 16 and higher
as the severity level for errors.
y
The following syntax enables you to add a custom error message:
EXEC sp_addmessage
@msgnum = 50001,
@severity = 10,
@msgtext = `Movie number cannot be found.',
@with_log = `true'
You can view a list of existing error messages by executing the following query:
USE master
SELECT *
FROM sysmessages
This query displays each message's error number in the error column, the severity level in the
severity column, and the text of the error message in the description column.
Replace message_number with the number of the custom error message you want to delete from
the sysmessages table.
Replace msg_id with the ID number of the custom error message you have already created and
stored in the sysmessages table. You can optionally have RAISERROR display a new message
by specifying a message text instead. If you specify a message text, SQL Server does not store
this message for later use-it simply displays it if the necessary error occurs. Because you can
display a new error message by using the RAISERROR statement, you must also specify the
severity level and state. The state is an arbitrary number from 1 to 127 that you can use to
provide information about what actions invoked the error. The syntax for the RAISERROR
statement requires that you specify the severity level and state parameters even if you are calling
a custom error message you have stored in the sysmessages table.
You can optionally use the WITH LOG keywords with the RAISERROR statement to force SQL
Server to write the error message to the Windows NT Application log. You can view these
messages by using the Windows NT Event Viewer. Note: If you set the @with_log option equal to
true when you defined the error message by using the sp_addmessage stored procedure, SQL
Server will automatically write the message to the Application log regardless of whether you
specify the WITH LOG keywords with the RAISERROR statement or not.
y
To create a custom error message by using the sp_addmessage stored procedure:
sp_addmessage
@msgnum = 50001,
@severity = 10,
@msgtext = `Cannot delete customer. Customer has rentals on file.',
with_log = `true'
You can call this error message by using the following syntax:
You can then incorporate the RAISERROR statement into a stored procedure. For example, the
following stored procedure is used to delete a customer from the customer table in the movies
Finally, you can run this stored procedure by using the following syntax (where 101 represents a
customer number for the input parameter):
y
You could configure your message to identify the user who performed the action. To do so, you
must add the parameter %s to your error message as follows:
sp_addmessage
@msgnum = 50002,
@severity = 10,
@msgtext = 'Customer record deleted by %s.' ,
@with_log = 'true'
Next, you must populate %s with the user's name by declaring a variable and then storing the
user name in it. You can then reference the variable as part of the RAISERROR statement by
using the following syntax:
±1. What is the difference between an input parameter and an output parameter?
³An input parameter enables you to specify a value for use within the stored procedure. An
output parameter enables you to return a value from a stored procedure to either the calling
stored procedure or a SQL batch.
±2. Give an example of when you might use an input and an output parameter.
³ You can use an input parameter to accept a value for a movie rating. You can then use this
value in a WHERE clause to display only those movies with a specific rating. You can use an
output parameter to return the number of rows in a table to the calling stored procedure.
You might find that you need to force SQL Server to recompile a stored procedure whenever you
run it. For example, it is possible for you have an index that, depending on your input parameter,
varies widely in its selectivity. Thus, when you run the stored procedure, some of the time it will
be more efficient if SQL Server performs a table scan rather than using an index. In this scenario,
you should create the stored procedure so that SQL Server will recompile it each time it is run by
using the following syntax:
This statement prevents SQL Server from caching a plan for the stored procedure-thus, SQL
Server must recompile it every time you run the stored procedure.
If you do not want to force SQL Server to recompile a stored procedure every time you run it, but
you have had enough changes to your data that the stored procedure's execution plan might be
inefficient, you can have SQL Server recompile it by using the following syntax:
By adding the WITH RECOMPILE option when you run the stored procedure, SQL Server
generates a new execution plan-and then executes the stored procedure.
You can also mark a stored procedure to be recompiled without running it by using the following
syntax:
SQL Server does not recompile the stored procedure when you run this command. Instead, it
marks it for recompiling the next time you run it. If you use sp_recompile with a table name
instead of a stored procedure name, SQL Server automatically marks all stored procedures that
reference the table for recompiling.
EXEC sp_recompile MovieByRating. This query marks the stored procedure for recompiling.
You can use Performance Monitor to analyze the performance of stored procedures.
±1. You would like SQL Server to recompile a stored procedure the next time you run it. What
should you do?
³
You should run the following query: EXEC sp_recompile procedure_name.
User-Defined Functions
You've seen parameterized functions such as CONVERT, RTRIM, and ISNULL as well as
parameterless functions such as @@SPID. SQL Server 2000 lets you create functions of your
own. However, whereas user-defined procedures are managed similarly to system stored
procedures, the functions you create are managed very differently from the supplied functions. As
mentioned, the ANSI SCHEMA VIEW called ROUTINES allows you to see all the procedures and
functions in a database. This view shows system-defined stored procedures but not built-in
functions such as the ones you've seen in earlier chapters. You should think of the built-in
functions as almost like built-in commands in the SQL language; they are not listed in any system
table, and there is no way for you to see how they are defined.
Table Variables
To make full use of user-defined functions, it's useful to understand a special type of variable that
SQL Server 2000 provides. You can declare a local variable as a table or use a table value as the
result set of a user-defined function. You can think of table as a special kind of datatype. Note
that you cannot use table variables as stored procedure or function parameters, nor can a column
in a table be of type table.
Table variables have a well-defined scope, which is limited to the procedure, function, or batch in
which they are declared. Here's a simple example:
The definition of a table variable looks almost like the definition of a normal table, except that you
use the word DECLARE instead of CREATE, and the name of the table variable comes before
the word TABLE.
• A column list defining the datatypes for each column and specifying the NULL or NOT
NULL property
• PRIMARY KEY, UNIQUE, CHECK, or DEFAULT constraints
Within their scope, table variables can be treated like any other table. All data manipulation
statements (SELECT, INSERT, UPDATE, and DELETE) can be performed on the data in a table
variable, with two exceptions:
• You cannot use SELECT INTO to add data to a table variable. This is because SELECT
INTO creates a table, and table variable must be created using DECLARE. For example,
you cannot do the following:
• SELECT select_list INTO table_variable statements
Scalar-Valued Functions
You can use a scalar-valued function anywhere that your Transact-SQL command is expecting a
value. It can take up to 1024 input parameters but no output parameters. The function can return
a value of any datatype except rowversion (or timestamp), cursor, or table.
Unlike a stored procedure, a function must include the RETURN statement. (In stored
procedures, the RETURN is optional.) You can declare a scalar local variable to hold the return
value of the function and then use this variable with the RETURN statement, or the RETURN
statement can include the computation of the return value. The following two function definitions
are equivalent; both will return the average price for books in the titles table for which the type is
the same as the type specified as the input parameter:
RETURN @avg
END
To invoke a user-defined scalar function, you must specify the owner name. The following query
will return an error:
RESULT:
RESULT:
title_id price
-------- ---------------------
BU1032 19.9900
BU7832 19.9900
You can invoke user-defined scalar functions by simply SELECTing their value:
You can also use the keyword EXECUTE to invoke a user-defined function as if it were a stored
procedure. You might want to do this to assign the return value to a variable. In this case, though,
the syntax is different. You must not specify the parameter list in parentheses, as in the examples
above. Instead, you should just list them after the name of the function:
Additional Restrictions
The SQL statements inside your scalar-valued functions cannot include any nondeterministic
system functions.
y
Suppose weI want a function that will format today's date using any separator that is supplied
then we can write the function like this:
We can change the function definition to accept a datetime value as an input parameter and then
call the function with GETDATE as an argument. Not only does this allow us to accomplish the
goal, but also it makes the function much more versatile. Here's the function:
y
SELECT dbo.MyDateFormat(GETDATE(), '*')
RESULT:
--------------------
18*7*2000
y
We can go only one value higher using the iterative solution instead of the recursive solution.
Since functions work very nicely recursively, we can rewrite it as a recursive function. The
function will call itself by multiplying the input parameter by the factorial of the number one less
than the parameter. So 10 is computed by finding the factorial of 9 and multiplying by 10. To
avoid overflow problems, the function simply returns a 0 for the result of any input value that is
out of range. Here's the function:
AS
BEGIN
IF (@param < 0 OR @param > 32) RETURN (0)
RETURN (CASE
WHEN @param > 1 THEN @param * dbo.fn_factorial(@param - 1)
ELSE 1
Unlike the factorial procedure, which lists all the factorial values up to and including the factorial
of the input parameter, the function fn_factorial simply returns the value that is the factorial for the
argument. Remember that you must always specify a two-part name when you call a user-
defined scalar function:
RESULT:
factorial
----------------------------------------
3628800
Table-Valued Functions
Table-valued functions return a rowset. You can invoke them in the FROM clause of a SELECT
statement, just as you would a view. In fact, you can think of a table-valued function as if it were a
parameterized (or parameterizable) view. A table-valued function is indicated in its definition
using the word TABLE in the RETURNS clause. There are two ways to write table-valued
function: as inline functions or as multistatement functions. This difference is relevant only to the
way the function is written; all table-valued functions are invoked in the same way. There is a
difference in the way that the query plans for inline and multistatement functions are cached.
Inline Functions
If the RETURNS clause specifies TABLE with no additional table definition information, the
function is an inline function and there should be a single SELECT statement as the body of the
function. Here's a function that will return the names and quantities of all the books sold by a
particular store for which the store ID is passed as an input parameter:
RESULT:
title qty
------------------------------------------------------ ------
The Gourmet Microwave 15
The Busy Executive's Database Guide 10
Cooking with Computers: Surreptitious Balance Sheets 25
But Is It User Friendly? 30
y
This example is of previous inline function rewritten as a multistatement function:
The following statements are the only ones allowed in a multistatement table-valued function:
• Assignment statements
• Control-of-flow statements
• DECLARE statements that define data variables and cursors that are local to the function
• SELECT statements containing select lists with expressions that assign values to
variables that are local to the function
• Cursor operations referencing local cursors that are declared, opened, closed, and
deallocated in the function. Only FETCH statements that assign values to local variables
using the INTO clause are allowed; FETCH statements that return data to the client are
not allowed.
• INSERT, UPDATE, and DELETE statements that modify table variables that are local to
the function
Also, like for scalar functions, table-valued functions cannot contain any built-in nondeterministic
functions.
Side Effects
Basically, the statements that aren't allowed are ones that return data other than the function's
return value and the ones that product side effects. A side effect is a change to some persisted
state that is not local to the function. Invoking a function should not change your database in any
way; it should only return a value (scalar or table-valued) to the client. Thus, the following are not
allowed:
fn_helpcollations
fn_listextendedproperty
fn_servershareddrives
fn_trace_geteventinfo
fn_trace_getfilterinfo
fn_trace_getinfo
fn_trace_gettable
fn_virtualfilestats
fn_virtualservernodes
The function fn_virtualfilestats returns I/O statistics for a database file and requires a database ID
and a file ID as parameters. The following example calls this function for the data file of the pubs
database.
RESULT:
DbId FileId TimeStamp NumberReads NumberWrites BytesRead
------ ------ ----------- ------------ ------------ ------------
5 1 34138899 115 9 942080
BytesWritten IoStallMS
------------- -----------------
90112 4585
• The user-defined functions and views referenced by the function are also schema-bound.
• All objects referenced by the function are referred to by a two-part name.
• The function is in the same database as all the referenced objects.
• Any user referencing the function has REFERENCES permission on all the database
objects that the function references.
SQL Server keeps track of objects that are dependent on other objects in the sysdepends table.
The data in the table looks very cryptic—it's all numbers. To get meaningful information from the
sysdepends table, you have to get the names of the objects stored in the id column, which are the
dependent objects, and the names of the objects stored in the depid column, which are the
referenced objects.
y
In my SalesByStore function above, the function itself is the dependent object, and the objects it
depends on are the titles and sales tables. The depnumber column refers to a column in the
referenced object that the dependent object depends on. My SalesByStore functions depend on
the qty, stor_id, and title_id columns from sales and the title_id and title columns from titles.
Finally, the column deptype indicates whether the dependency is schema-bound—that is,
whether a change to the referenced column should be prevented. By accessing the syscolumns
and sysdepends tables and using the OBJECT_NAME function to extract the name from
sysobjects, we can see what columns are schema-bound in the SalesByStore functions:
RESULTS:
obj_name dep_obj col_name IsSchemaBound
-------------------- -------------------- --------------- -------------
SalesByStore sales stor_id Schema Bound
SalesByStore sales qty Schema Bound
SalesByStore sales title_id Schema Bound
SalesByStore titles title_id Schema Bound
SalesByStore titles title Schema Bound
SalesByStore_MS sales stor_id Free
SalesByStore_MS sales qty Free
SalesByStore_MS sales title_id Free
SalesByStore_MS titles title_id Free
To see the impact of schema binding, we'll try to change one of the referenced columns. The
following ALTER will attempt to change the datatype of qty from smallint to int:
On the other hand, changing the datatype of the ord_date column from datetime to smalldatetime
will succeed because the ord_date column is not referenced by a schema-bound object.
SQL Server 2000 provides many utilities for getting information about your functions. To see the
definition of a function, you can use the system procedure sp_helptext or look in the ANSI
SCHEMA VIEW called ROUTINES. Both commands shown below will return the definition of my
SalesByStore function:
SELECT routine_definition
FROM INFORMATION_SCHEMA.routines
WHERE routine_name = 'SalesByStore'
Although you can use either sp_helptext or ROUTINES to get information about your own
functions, you can't get the definition of the system table-valued functions using sp_helptext. You
must use the master database and look in the ROUTINES view. In fact, for some of the supplied
system table-valued functions, you won't get the entire definition. You'll get only the declaration
section because the rest of the code is hidden. The query below shows the complete definition of
a system function:
USE master
SELECT routine_definition
FROM INFORMATION_SCHEMA.ROUTINES
WHERE routine_name = 'fn_helpcollations'
You can also get information about your functions from the ANSI SCHEMA VIEWS called
ROUTINE_COLUMNS and PARAMETERS. For details about using these views, see SQL Server
Books Online.
The metadata function OBJECTPROPERTY also has quite a few property values that are
relevant to user-defined functions. Alternatively, you can use the OBJECTPROPERTY function
with the IsTableFunction or 'IsInlineFunction' argument:
Calling the OBJECTPROPERTY function with the 'IsInlineFunction' property parameter returns
one of three values. The value 1 means TRUE, the function is an inline table-valued function and
0 means FALSE, the function isn't an inline table-valued function. A NULL will be returned if you
typed something wrong, for example, if you supply an invalid object ID, the ID of an object that is
not a function, or if you misspell the property name. If you call the OBJECTPROPERTY function
with the 'IsTableFunction' property parameter, it returns a 1 if the function is a table-valued
function that is not also an inline function. Here are the other parameters of OBJECTPROPERTY
that can give you useful information about your functions:
IsScalarFunction
IsSchemaBound
IsDeterministic
Triggers
A trigger is a special type of stored procedure that is fired on an event-driven basis rather than by
a direct call. Here are some common uses for triggers:
• To maintain data integrity rules that extend beyond simple referential integrity
• To implement a referential action, such as cascading deletes
• To maintain an audit record of changes
• To invoke an external action, such as beginning a reorder process if inventory falls below
a certain level or sending e-mail or a pager notification to someone who needs to perform
an action because of data changes
You can set up a trigger to fire when a data modification statement is issued—that is, an INSERT,
UPDATE, or DELETE statement. SQL Server 2000 provides two types of triggers: after triggers
and instead-of triggers.
After Triggers
After triggers are the default type of trigger in SQL Server 2000, so you don't have to use the
word AFTER in the CREATE TRIGGER statement. In addition, if any documentation discusses
"triggers" without specifying after or instead-of triggers, you can assume that the discussion is
referring to after triggers.
You can define multiple after triggers on a table for each event, and each trigger can invoke many
stored procedures as well as carry out many different actions based on the values of a given
column of data. However, you have only a minimum amount of control over the order in which
triggers are fired on the same table.
y
If you have four INSERT triggers on the inventory table, you can determine which trigger will be
the first and which will be the last, but the other two triggers will fire in an unpredictable order.
Controlling trigger order is a feature that is needed for some of SQL Server's own internal
functionality. For example, when you set up merge replication, you can designate a trigger as the
first trigger to fire. In general, we recommend that you don't create multiple triggers with any
expectation of a certain execution order. If it's crucial that certain steps happen in a prescribed
order, you should reconsider whether you really need to split the steps into multiple triggers.
You can create a single after trigger to execute for any or all the INSERT, UPDATE, and DELETE
actions that modify data. Currently, SQL Server offers no trigger on a SELECT statement
because SELECT does not modify data. In addition, after triggers can exist only on base tables,
not on views. Of course, data modified through a view does cause an after trigger on the
underlying base table to fire. As you'll see, you can define instead-of triggers on views.
An after trigger is executed once for each UPDATE, INSERT, or DELETE statement, regardless
of the number of rows it affects. Although some people assume that a trigger is executed once
per row or once per transaction, these assumptions are incorrect, strictly speaking. Let's see a
simple example of this. The script below creates and populates a small table with three rows. It
then builds a trigger on the table to be fired when a DELETE statement is executed for this table:
Now let's put the trigger to the test. What do you think will happen when the following statement is
executed?
DELETE test_trigger
WHERE col1 = 0
The message appears because the DELETE statement is a legal DELETE from the test_trigger
table. The trigger is fired once no matter how many rows are affected, even if the number of rows
is 0! To avoid executing code that is meaningless, it is not uncommon to have the first statement
in a trigger check to see how many rows were affected. We have access to the @@ROWCOUNT
function, and if the first thing the trigger does is check its value, it will reflect the number of rows
affected by the data modification statement that caused the trigger to be fired. So we could
change the trigger to something like this:
The trigger will still fire, but it will end almost as soon as it begins. You can also inspect the
trigger's behavior if many rows are affected. What do you think will happen when the following
statement is executed?
DELETE test_trigger
A DELETE without a WHERE clause means that all the rows will be removed, and we get the
following message:
(5 row(s) affected)
If a statement affects only one row or is a transaction unto itself, the trigger will exhibit the
characteristics of per-row or per-transaction execution. For example, if you set up a WHILE loop
to perform an UPDATE statement repeatedly, an update trigger would execute each time the
UPDATE statement was executed in the loop.
An after trigger fires after the data modification statement has performed its work but before that
work is committed to the database. Both the statement and any modifications made in the trigger
are implicitly a transaction (whether or not an explicit BEGIN TRANSACTION was declared).
Therefore, the trigger can roll back the data modifications that caused the trigger to fire. A trigger
has access to the before image and after image of the data via the special pseudotables inserted
and deleted. These two tables have the same set of columns as the underlying table being
changed. You can check the before and after values of specific columns and take action
depending on what you encounter. These tables are not physical structures—SQL Server
constructs them from the transaction log. In fact, you can think of the inserted and deleted tables
as views of the transaction log. For regular logged operations, a trigger will always fire if it exists
and has not been disabled. You can disable a trigger by using the DISABLE TRIGGER clause of
the ALTER TABLE statement.
You cannot modify the inserted and deleted pseudotables directly because they don't actually
exist. As I mentioned earlier, the data from these tables can only be queried. The data they
appear to contain is based entirely on modifications made to data in an actual, underlying base
table. The inserted and deleted pseudotables will contain as many rows as the insert, update, or
delete statement affected. Sometimes it is necessary to work on a row-by-row basis within the
pseudotables, although, as usual, a set-based operation is generally preferable to row-by-row
operations. You can perform row-by-row operations by executing the underlying insert, update, or
delete in a loop so that any single execution affects only one row, or you can perform the
operations by opening a cursor on one of the inserted or deleted tables within the trigger.
In most cases you shouldn't manipulate the trigger firing order. However, if you do want a
particular trigger to fire first or fire last, you can use the sp_settriggerorder procedure to indicate
this ordering. This procedure takes a trigger name, an order value (FIRST, LAST, or NONE), and
an action (INSERT, UPDATE, or DELETE) as arguments. Setting a trigger's order value to NONE
removes the ordering from a particular trigger.
y
sp_settriggerorder delete_test, first, 'delete'
Note that you'll get an error if you specify an action that is not associated with the particular
trigger—
Information about trigger type and firing order is stored in the status column of the sysobjects
table. Nine of the bits are needed to keep track of this information. Three bits are used to indicate
whether the trigger is an insert, update, or delete trigger, three are used for the first triggers for
each action, and three are used for the last triggers for each action. Table 21 shows which bits
correspond to which properties.
To check to see whether a trigger has a particular functionality, you can decode the
sysobjects.status value or use the OBJECTPROPERTY function. There are nine property
functions, which correspond to the nine bits indicated in Table 12-2.
A value of 1 means that the trigger has this property, a value of 0 means it doesn't, and a value of
NULL means you typed something wrong or the object is not a trigger. However, to find all the
properties that a trigger has would mean executing this statement nine times, checking nine
different property values. To simplify the process, I created a new version of the sp_helptrigger
procedure called sp_helptrigger2(available on the companion CD), which provides ordering
information for each of the triggers.
y
EXEC sp_helptrigger2 test_trigger
PARTIAL RESULTS:
The IsUpdate column has a 1 if the trigger is an update trigger, and it has a 0 otherwise. If the
trigger is not an update trigger, the order of the update trigger is n/a. If the trigger is an update
trigger, values for the trigger order are First, Last, and Unspecified. You can interpret the IsDelete
and DeleteOrd columns in the same way, as well as the IsInsert and InsertOrd columns. This
procedure is available on the companion CD.
Executing a ROLLBACK from within a trigger is different from executing a ROLLBACK from within
a nested stored procedure. In a nested stored procedure, a ROLLBACK will cause the outermost
transaction to abort, but the flow of control continues. However, if a trigger results in a
ROLLBACK (because of a fatal error or an explicit ROLLBACK command), the entire batch is
aborted.
Suppose the following pseudocode batch is issued from SQL Query Analyzer:
begin tran
delete....
update....
insert.... -- This starts some chain of events that fires a trigger
-- that rolls back the current transaction
update.... -- Execution never gets to here - entire batch is
-- aborted because of the rollback in the trigger
if....commit -- Neither this statement nor any of the following
-- will be executed
else....rollback
begin tran....
insert....
if....commit
else....rollback
As you can see, once the trigger in the first INSERT statement aborts the batch, SQL Server not
only rolls back the first transaction but skips the second transaction completely and continues
execution following the GO.
Misconceptions about triggers include the belief that the trigger cannot do a SELECT statement
that returns rows and that it cannot execute a PRINT statement. Although you can use SELECT
and PRINT in a trigger, doing these operations is usually a dangerous practice unless you control
all the applications that will work with the table that includes the trigger. Otherwise, applications
not written to expect a result set or a print message following a change in data might fail because
that unexpected behavior occurs anyway. For the most part, you should assume that the trigger
will execute invisibly and that if all goes well, users and application programmers will never even
know that a trigger was fired.
Be aware that if a trigger modifies data in the same table on which the trigger exists, using the
same operation (INSERT, UPDATE, or DELETE) won't, by default, fire that trigger again. That is,
if you have an UPDATE trigger for the inventory table that updates the inventory table within the
trigger, the trigger will not be fired a second time. You can change this behavior by allowing
triggers to be recursive. You control this on a database-by-database basis by setting the option
recursive triggers to TRUE. It's up to the developer to control the recursion and make sure that it
terminates appropriately. However, it will not cause an infinite loop if the recursion isn't terminated
because, just like stored procedures, triggers can be nested only to a maximum level of 32. Even
if recursive triggers have not been enabled, if separate triggers exist for INSERT, UPDATE, and
DELETE statements, one trigger on a table could cause other triggers on the same table to fire
(but only if sp_configure 'nested triggers' is set to 1, as you'll see in a moment).
A trigger can also modify data on some other table. If that other table has a trigger, whether that
trigger also fires depends on the current sp_configure value for the nested triggers option. If that
option is set to 1 (TRUE), which is the default, triggers will cascade to a maximum chain of 32. If
an operation would cause more than 32 triggers to fire, the batch will be aborted and any
transaction will be rolled back. This prevents an infinite cycle from occurring. If your operation is
hitting the limit of 32 firing triggers, you should probably look at your design—you've reached a
point at which there are no longer any simple operations, so you're probably not going to be
ecstatic with the performance of your system. If your operation truly is so complex that you need
to perform further operations on 32 or more tables to modify any data, you could call stored
procedures to perform the actions directly rather than enabling and using cascading triggers.
Although cascading triggers can be of value, overusing them can make your system a nightmare
to maintain.
Instead-of Triggers
SQL Server 2000 allows you create a second kind of trigger, called an instead-of trigger. An
instead-of trigger, rather than the data modification operation that fires the triggers, specifies the
action to take. Instead-of triggers are different from after triggers in several ways:
• You can have only one instead-of trigger for each action (INSERT, UPDATE, and
DELETE).
• You cannot combine instead-of triggers and foreign keys that have been defined with
CASCADE on a table. For example, if Table2 has a FOREIGN KEY constraint that
references Table1 and specifies CASCADE as the response to DELETE operations, you
Instead-of triggers are intended to allow updates to views that are not normally updateable. For
example, a view that is based on a join normally cannot have DELETE operations executed on it.
However, you can write an instead-of DELETE trigger. The trigger has access to the rows of the
view that would have been deleted had the view been a real table. The deleted rows are available
in a worktable, which is accessed with the name deleted, just like for after triggers. Similarly, in an
UPDATE or INSERT instead-of trigger, you can access the new rows in the inserted table.
y
A Table1 and Table2 and builds a view on a join of these tables:
USE pubs
SET NOCOUNT ON
-- drop table Table1
CREATE TABLE Table1
(a int PRIMARY KEY,
b datetime default getdate(),
c varchar(10))
AS
DELETE Table1
WHERE a IN (SELECT a1 FROM deleted)
DELETE Table2
WHERE a IN (SELECT a2 FROM deleted)
In this case, the view contained values from each table that could be used to determine which
rows in the base table needed to be removed.
In addition to views based on joins, another kind of view that has severe limitations on its
updateability is a view based on a UNION. Some UNION views are updateable when we look at
partitioned views. But for views that don't meet the conditions for partitioning, direct updates
might not be possible.
y
Create a contacts list view in the pubs database consisting of the name, city, state, and country of
all the authors, stores, and publishers:
USE pubs
GO
CREATE VIEW contact_list
AS
SELECT ID = au_id, name = au_fname + ' ' + au_lname,
city, state, country = 'USA'
FROM authors
UNION ALL
SELECT stor_id, stor_name, city, state, 'USA'
FROM stores
UNION ALL
SELECT pub_id, pub_name, city, state, country
FROM publishers
We want to be able to insert a new contact into this list, and to do that we'll use an instead-of
INSERT trigger. The inserted table in the trigger will have values only for the columns included in
the view, so all other columns in all three tables will have to have default values or allow NULLS.
The only column not meeting this requirement is the contract column of the authors table, which
is a bit column. I'll alter the column to give it a default value:
You can write similar instead-of triggers for updates and deletes.
Managing Triggers
The options available for managing triggers are similar to those for managing stored procedures.
Here are some of the options:
• You can see the definition of a trigger by executing sp_helptext <trigger name>.
• You can create a trigger WITH ENCRYPTION so that its text cannot be exposed.
• You can change a trigger's definition using ALTER TRIGGER so that it keeps its same
internal ID number.
WARNING
If you ALTER a trigger that has had any firing order properties set, all such order values will be
lost when the ALTER is done. You must reexecute sp_settriggerorder procedures to reestablish
the firing order.
A FOREIGN KEY constraint can of course be directed toward a UNIQUE constraint, not just a
PRIMARY KEY constraint. But there is no performance difference if the referenced table is
declared using UNIQUE. In this section, we'll refer only to PRIMARY KEY in the referenced table
for simplicity.
Keep in mind that a FOREIGN KEY constraint affects two tables, not just one. It affects the
referenced table as well as the referencing table. While the constraint is declared on the
referencing table, a modification that affects the primary key of the referenced table checks to see
whether the constraint has been violated. The change to the referenced table will be either
disallowed or cascaded to the referencing table, depending on how the FOREIGN KEY constraint
was defined.
The SQL-92 standard specifies four referential actions for modifying the primary key side of a
relationship: NO ACTION, SET NULL, SET DEFAULT, and CASCADE. The four referential
actions have the following characteristics.
NO ACTION Disallows the action if the FOREIGN KEY constraint would be violated. This is the
only referential action implemented by SQL Server 7 for a declared FOREIGN KEY constraint,
and it's the only one that must be implemented in order for a product to claim to be "SQL-92
conformant."
• SET NULL Updates the referencing table so that the foreign key columns are set to
NULL. Obviously, this requires that the columns be defined to allow NULL.
• SET DEFAULT Updates the referencing table so that the foreign key columns are set to
their DEFAULT values. The columns must have DEFAULT values defined.
• CASCADE Updates the referencing table so that the foreign key columns are set to the
same values that the primary key was changed to, or deletes the referencing rows
entirely if the primary key row is deleted.
A declared FOREIGN KEY constraint, rather than a trigger, is generally the best choice for
implementing NO ACTION. The constraint is easy to use and eliminates the possibility of writing a
trigger that introduces a bug. In SQL Server 2000, you can also use FOREIGN KEY constraints
for the CASCADE action if you want to simply cascade any changes. You can use triggers to
implement either of the other referential actions not currently available with declared FOREIGN
KEY constraints.
Recall that constraint violations are tested before triggers fire. If a constraint violation is detected,
the statement is aborted and execution never gets to the trigger (and the trigger never fires).
Therefore, you cannot use a declared FOREIGN KEY constraint to ensure that a relationship is
never violated by the update of a foreign key and then also use a trigger to perform a SET NULL
or SET DEFAULT action when the primary key side of the same relationship is changed. If you
use triggers to implement SET NULL or SET DEFAULT behavior for changes to the referenced
table, you must also use triggers to restrict invalid inserts and updates to the referencing table.
You can and should still declare PRIMARY KEY or UNIQUE constraints on the table to be
referenced, however. It's relatively easy to write triggers to perform referential actions, as you'll
see in the upcoming examples. For the sake of readability, you can still declare the FOREIGN
KEY constraint in your CREATE TABLE scripts but then disable the constraint using ALTER
TABLE NOCHECK so that it is not enforced. In this way, you ensure that the constraint still
appears in the output of sp_help <table> and similar procedures.
Although you're about to see triggers that perform referential actions, you might want to consider
another option entirely. If you want to define constraints and still have update and delete
capability beyond NO ACTION and CASCADE, a good alternative is using stored procedures that
exclusively perform the update and delete operations. A stored procedure can easily perform the
referential action (within the scope of a transaction) before the constraint would be violated. No
violation occurs because the corresponding "fix-up" operation is already performed in the stored
procedure. If an INSERT or UPDATE statement is issued directly (and you can prevent that in the
first place by not granting such permissions), the FOREIGN KEY constraint still ensures that the
relationship is not violated.
Here's an example of using triggers to implement a referential action. Rather than simply
implementing NO ACTION, we want to implement ON DELETE SET NULL and ON UPDATE
SET DEFAULT for the foreign key relationship between titleauthor and titles. We must create the
following three triggers (or suitable alternatives):
SET @COUNTER=@@ROWCOUNT
-- If the trigger resulted in modifying rows of
-- titleauthor, raise an informational message
IF (@counter > 0)
RAISERROR('%d rows of titleauthor were updated to
DEFAULT title_id as a result of an update to titles table',
10, 1, @counter)
END
GO
IF (@counter > 0)
RAISERROR('%d rows of titleauthor were set to a
NULL title_id as a result
of a delete to the titles table', 10, 1, @counter)
GO
In order for these examples to run, you must drop or suspend the existing FOREIGN KEY
constraints on titleauthor that reference titles as well as FOREIGN KEY constraints on two other
tables (sales and roysched) that reference titles. In addition, you must modify the title_id column
of titleauthor to allow NULLS, and you must re-create the PRIMARY KEY constraint on the
The following trigger is just an example of how you can use triggers to implement a cascade
action. This trigger is actually unnecessary because you can define a FOREIGN KEY constraint
to cascade any changes. It is a cascading update trigger on the titles (referenced) table that
updates all rows in the titleauthor table with matching foreign key values. The cascading update is
tricky, which is why I'm presenting it. It requires that you associate both the before and after
values (from the inserted and deleted pseudotables) to the referencing table. In a cascading
update, you by definition change that primary key or unique value. This means that typically the
cascading update works only if one row of the referenced table is updated. If you change the
values of multiple primary keys in the same update statement, you lose the ability to correctly
associate the referencing table. You can easily restrict an update of a primary key to not affect
more than one row by checking the value of the @@ROWCOUNT function, as follows:
IF UPDATE(title_id)
BEGIN
IF (@num_affected=1)
BEGIN
SELECT @title_id=title_id FROM inserted
SELECT @old_title_id=title_id FROM deleted
UPDATE titleauthor
SET title_id=@title_id
FROM titleauthor
WHERE titleauthor.title_id=@old_title_id
SELECT @num_affected=@@ROWCOUNT
RAISERROR ('Cascaded update in titles of Primary Key
from %s to %s to %d rows in titleauthor', 10, 1,
@old_title_id, @title_id, @num_affected)
END
ELSE
BEGIN
RAISERROR ('Cannot update multiple Primary Key values
in a single statement due to Cascading Update
trigger in existence.', 16, 1)
ROLLBACK TRANSACTION
END
END
Again, this trigger is shown just as an example. If you define a FOREIGN KEY constraint to
cascade updates, you can update multiple primary key values in a single update. But you should
carefully evaluate whether this is something you really want to do. The inability to update more
than one primary key value at a time might save you from making a mistake. You typically won't
want to make mass updates to a primary key. If you declare a PRIMARY KEY or UNIQUE
constraint (as you should), mass updates usually won't work because of the constraint's need for
Another situation in which you might want to update multiple primary keys in a single statement is
if you have a mail-order customer list that uses the customer phone number as the primary key.
As phone companies add numbers to accommodate fax machines, server remote access, and
cellular phones, some area codes run out of possible numbers, which leads to the creation of new
area codes. If a mail-order company needs to keep up with such changes, it must update the
primary key for all customers who get new area codes. If you assume that the phone number is
stored as a character string of 10 digits, you must update the first three characters only. If the
new area code is 425.
y
Your UPDATE statement would look something like this:
UPDATE customer_list
SET phone = '425' + substring(phone, 4, 7)
WHERE zip IN (list of affected zip codes)
This UPDATE would affect the primary key of multiple rows, and it would be difficult to write a
trigger to implement a cascade operation (perhaps into an orders table).
You might think that you can write a trigger to handle multiple updates to the primary key by using
two cursors to step through the inserted and deleted tables at the same time. However, this won't
work. There's no guarantee that the first row in the deleted table will correspond to the first row in
the inserted table, so you cannot determine which old foreign key values in the referencing tables
should be updated to which new values.
Although it is logically the correct thing to do, you are not required to create a PRIMARY KEY or
UNIQUE constraint on a referenced table. (Unless, of course, you are going to actually declare a
FOREIGN KEY constraint to reference a column in the referenced table.) And there is no hard-
and-fast requirement that the constraint be unique. (However, not making it unique violates the
logical relationship.) In fact, the "one row only" restriction in the previous cascading trigger is a
tad more restrictive than necessary. If uniqueness were not required on title_id, the restriction
could be eased a bit to allow the update to affect more than one row in the titles table, as long as
all affected rows were updated to the same value for title_id. When uniqueness on titles.title_id is
required (as should usually be the case), the two restrictions are actually redundant. Since all
affected rows are updated to the same value for title_id, you no longer have to worry about
associating the new value in the referencing table, titleauthor—there is only one value.
IF UPDATE(title_id)
BEGIN
SELECT @num_distinct=COUNT(DISTINCT title_id) FROM inserted
IF (@num_distinct=1)
BEGIN
-- Temporarily make it return just one row
SET ROWCOUNT 1
SELECT @title_id=title_id FROM inserted
SET ROWCOUNT 0 -- Revert ROWCOUNT back
UPDATE titleauthor
SET titleauthor.title_id=@title_id
FROM titleauthor, deleted
WHERE titleauthor.title_id=deleted.title_id
SELECT @num_affected=@@ROWCOUNT
RAISERROR ('Cascaded update of Primary Key to value in
titles to %d rows in titleauthor', 10, 1,
@title_id, @num_affected)
END
ELSE
BEGIN
RAISERROR ('Cannot cascade a multirow update that
changes title_id to multiple different values.', 16, 1)
ROLLBACK TRANSACTION
END
END
Using COUNT(DISTINCT title_id) FROM inserted ensures that even if multiple rows are affected,
they are all set to the same value. So @title_id can pick up the value of title_id for any row in the
inserted table. You use the SET ROWCOUNT 1 statement to limit the subsequent SELECT to
only the first row. This is not required, but it's good practice and a small optimization. If you don't
use SET ROWCOUNT 1, the assignment still works correctly but every inserted row is selected
and you end up with the value of the last row (which, of course, is the same value as any other
row). Note that you can't use the TOP clause in a SELECT statement that assigns to a variable,
unless the assignment is done by a subquery.
y
You can do this:
It's not good practice to do a SELECT or a variable assignment that assumes that just one row is
returned unless you're sure that only one row can be returned. You can also ensure a single
returned row by doing the assignment like this:
Because the previous IF allows only one distinct title_id value, you are assured that the preceding
SELECT statement returns only one row.
In the previous two triggers, you first determine whether any rows were updated; if none were,
you do a RETURN. This illustrates something that might not be obvious: a trigger fires even if no
rows were affected by the update. Recall that the plan for the trigger is appended to the rest of
the statements' execution plan. You don't know the number of rows affected until execution. This
is a feature, not a bug; it allows you to take action when you expect some rows to be affected but
none are. Having no rows affected is not an error in this case. However, you can use a trigger to
return an error when this occurs. The previous two triggers use RAISERROR to provide either a
message that indicates the number of rows cascaded or an error message. If no rows are
affected, we don't want any messages to be raised, so we return from the trigger immediately.
Recursive Triggers
SQL Server allows triggers to recursively call themselves if the database option recursive triggers
is set to TRUE. This means that if a trigger modifies the table on which the trigger is based, the
trigger might fire a second time. But then, of course, the trigger modifies the table and fires the
trigger again, which modifies the table and fires the trigger again…and so on. This process does
not result in an infinite loop because SQL Server has a maximum nesting depth of 32 levels; after
the 32nd call to the trigger, SQL Server generates the following error. The batch stops executing,
and all data modifications since the original one that caused the trigger to be fired are rolled back.
Writing recursive routines is not for the faint of heart. You must be able to determine what further
actions to take based on what has already happened, and you must make sure that the recursion
stops before the nesting reaches its limit. Let's look at a simple example. Suppose we have a
table that keeps track of budgets for various departments. Departments are members (or
children) of other departments, so the budget for a parent department includes the budgets for all
its child departments. If a child department's budget increases or decreases, the change must be
propagated to the parent department, the parent's parent, and so forth.
The following code creates a small budget table and inserts three rows into it:
The following trigger is fired if a single row in the budget table is updated. If the department is not
the highest-level department, its parent name is not NULL, so you can adjust the budget for the
parent department by the same amount that the current row was just updated. (The amount of the
change is equal to the new amount in the inserted table minus the old amount in the deleted