Chapter 2

Protecting Against User Errors and Conflicts

IN THIS CHAPTER

Bullet Dealing with human errors: More trouble in paradise

Bullet Resolving operational errors

Bullet Taking the ACID test

Bullet Preventing mix-ups with transactions

Bullet Understanding lock types

Bullet Getting locks and the recovery system in tune

Bullet Using timestamps

After you have done everything you can do to minimize the possibility of problems due to hardware faults and Internet threats (which I discuss in Book 4, Chapter 1), you still have to worry about yet other things that can damage the data in your database or degrade performance.

Several additional potential sources of error exist. Here are a few of the most important:

  • The database design could be faulty, leading to modification anomalies or poor performance.
  • The database application written to access the data in the database could be buggy, leading to database corruption, erroneous results, poor performance, or all of the above.
  • The data entered into the database could be wrong.
  • Users accessing the same (or nearby) data at the same time could interfere with one another, affecting performance or even corrupting the data.

Reducing Data-Entry Errors

All the things you do to protect your database from harm are to no avail if bad data is entered in the first place. Even though nowadays quite a bit of data is recorded automatically by instrumentation and delivered to databases electronically, much of the data stored in the world’s databases was initially entered by hand by a fallible human at a computer keyboard. Humans are notorious for making typing errors, even if they are very conscientious and are excellent spellers. Even data collected automatically could contain errors produced by electronic noise or a thousand other unpredictable causes. You can’t eliminate all these problems before they put bad data in a database, but you can discover and eliminate some of them.

Data types: The first line of defense

SQL is a strongly typed language. That means that if a data entry is supposed to be of a particular type, the database engine will not accept any entry that does not conform to the rules of that type. The BOOLEAN type, for example, accepts only values of TRUE and FALSE; it rejects any and all other entries. The INTEGER type accepts only integers, the CHAR type accepts only valid alphanumeric characters, and so on. The strongly typed nature of SQL prevents a lot of bad stuff from being added to databases accidentally. Strong typing, however, does not prevent data of the correct type but with an incorrect value from being entered.

Constraints: The second line of defense

By applying constraints to your database tables, you can prevent incorrect data of the correct type from being accepted. (I cover constraints in Book 1, Chapter 5.) Several kinds of constraints exist, each of which prevents a certain class of problems. Probably the most flexible is the CHECK constraint because it enables you to specify exactly what values are acceptable for entry in a specific database field.

Here’s another look at an example of the use of a CHECK constraint that first appears in Book 1, Chapter 5:

CREATE TABLE TESTS (

TestName CHARACTER (30) NOT NULL,

StandardCharge NUMERIC (6,2)

CHECK (StandardCharge >= 0.00

AND StandardCharge <= 200.00)

) ;

This code guarantees that any entry in the StandardCharge field is of the NUMERIC type, with two decimal places, and that the value entered in the StandardCharge field must be no less than 0 and no more than 200.00. This kind of protection can prevent many errors due to a slip of the finger or the slip of a decimal point.

Sharp-eyed humans: The third line of defense

Strong typing can ensure that data being entered is of the proper type, and constraints can ensure that it is in the proper range of values. These defenses cannot ensure that the data is right, however. The only way to make sure that the data entered is indeed the data that is supposed to be entered is to have it checked by a human who knows what the data should be. In most cases, human checking is too costly and time-consuming, so it is not done. We accept a certain amount of bad data because having a human check it is not feasible in all but the most critical cases.

Coping with Errors in Database Design

In Book 1, I talk quite a bit about modeling a database before you start creating its tables. People who don’t put in a full effort at modeling are likely to build databases with inherent design flaws. People who do put in a full effort at modeling, however, still may have problems if their models are not normalized adequately. Unnormalized or incompletely normalized models are susceptible to modification anomalies that introduce inconsistencies into the data. Those inconsistencies ultimately lead to incorrect results, which could snowball into disastrous executive decisions. Database design is a topic worthy of a book of its own, and many books have been written on the subject. I give a brief introduction to normalization in Book 2, Chapter 2. I cover the topic more extensively in my SQL For Dummies (Wiley).

Handling Programming Errors

Even if a database is carefully modeled and designed in such a way that it accurately and efficiently structures the data, and even if the data entered in is 100 percent correct, you still could draw incorrect conclusions based on information you retrieve from that database. How is that so?

Although you can certainly retrieve the information you want from a relational database by entering SQL statements at the system console, this is not the way retrieval is usually done. It is too tedious, time-consuming, and boring — not to mention error-prone — to think up complex SQL queries and enter them on the fly while you sit in front of your computer. Instead, database owners hire people like you to write database applications that manage and query their databases for them. Those database applications are other potential sources of error.

People who write database applications must not only be masters of SQL, but also be experts in the procedural languages in which they embed their SQL statements. Unfortunately, many people who don’t have the requisite background are drafted into writing such applications, with predictable results. The applications never really do all the things that the database owner expected them to do, and even worse, the applications may provide misleading results. In recent years, business intelligence (BI) tools that query databases have become widespread. If not used properly, these tools can produce misleading results.

Tip Although you can’t completely eliminate the chance that your database may have design flaws or that your database application may contain hidden bugs, you can do one thing to minimize the chance that such problems will do you significant harm: Hire experienced professionals who understand solid database design and database application design, and who have a good understanding of the system that you want to build. People with this kind of expertise do not come cheap, but in the long run, they are worth what they cost. You get a system that does what you want it to do — and does it reliably and expeditiously.

Solving Concurrent-Operation Conflicts

Suppose that your database is well designed and contains correct data, and that all the applications that access it are bug-free. You still may have a problem. Databases are typically central repositories of important data for businesses, government agencies, and academic institutions, and as such, they are likely to be accessed by multiple people at the same time. If two people attempt to access the same database record at the same time, one could be given precedence, locking out the other. The users could even interfere with each other in such a way that both are locked out and neither gets the job done. Even more problematic, both users could be given access, and their operations could be mixed, corrupting the database.

Protecting the database from corruption is the number-one priority. After that, making sure that people are able to get their jobs done, even when traffic to and from the database is heavy, is very important. In the following paragraphs, I take a look at some of the problems involved here and suggest ways to solve them.

Here’s a look at how two perfectly legitimate operations by two authorized users can cause a major problem. Suppose that you and your friend Calypso have a joint savings account at Medieval Savings and Loan. Currently, the account has a balance of $47.17. To meet upcoming expenses, you decide to deposit $100 into the account. Calypso has the same thought at about the same time. You go to the nearest ATM machine, and Calypso, who works in another part of the city, goes to a different ATM machine. A problem arises because two operations are being performed on the same account at the same time. Here’s what happens:

  1. You insert your ATM card into your ATM.
  2. Your ATM pulls up your account and notes that you have a balance of $47.17.
  3. Calypso inserts her ATM card into her ATM.
  4. Calypso’s ATM pulls up your account and notes that you have a balance of $47.17.
  5. You insert $100 in cash into your ATM.
  6. Your ATM adds $100 to the balance it originally read, producing a total of $147.17.
  7. Calypso inserts $100 in cash into her ATM.
  8. Calypso’s ATM adds $100 to the balance it originally read, producing a total of $147.17.

If you don’t compare notes with Calypso at a later time, you have just been victimized to the tune of $100. The write of Calypso’s transaction wiped out the fact that you previously deposited $100.

What is the root cause of this problem? The bank tried to handle two operations at the same time and mixed them up. In this case, the mistake was to the bank’s advantage, but it could just as easily have gone the other way. At any rate, any bank that loses its customers’ money doesn’t keep those customers very long.

Problems such as the lost-update scenario described here caused database architects to introduce the idea of a transaction. A transaction is an indivisible unit of work that cannot be mixed up with anything else the database might be doing.

Well-designed transaction architectures have four essential characteristics. You find out all about this quartet in the next section.

Passing the ACID Test: Atomicity, Consistency, Isolation, and Durability

The four characteristics of an effective transaction — atomicity, consistency, isolation, and durability — are commonly known by the acronym ACID. To ensure that a transaction will protect your data, no matter what unlucky event might occur, it should have ACID. What do those four magic words mean?

  • Atomicity: Database transactions should be atomic, in the classic sense of the word: The entire transaction is treated as an indivisible unit. Either it is executed in its entirety (committed), or the database is restored (rolled back) to the state it would have been in if the transaction had not been executed.
  • Consistency: Oddly enough, the meaning of consistency is not consistent; it varies from one application to another. When you transfer funds from one account to another in a banking application, for example, you want the total amount of money in both accounts at the end of the transaction to be the same as it was at the beginning of the transaction. In a different application, your criterion for consistency may be different.
  • Isolation: Ideally, database transactions should be isolated from other transactions that execute at the same time. If the transactions are serializable, total isolation is achieved. A serializable set of transactions produces the same results as though they were executed serially, one after another. Serializable transactions do not need to be executed serially; they just need to give the same results that they would give if they had been executed serially. Insisting on serializability can cause performance problems, so if a system has to process transactions at top speed, lower levels of isolation are sometimes used. If you serialize two transactions, it is as if a two lane highway suddenly merges into one lane. You are not going to be able to go as fast.
  • Durability: After a transaction has either committed or rolled back, you should be able to count on the database to be in the proper state: well stocked with uncorrupted, reliable, up-to-date data. Even if your system suffers a hard crash after a commit — but before the transaction is stored to disk — a durable database management system (DBMS) can guarantee that upon recovery from the crash, the database can be restored to its proper state.

Operating with Transactions

Any operation that reads data from or writes data to a database should be enclosed in a transaction. As a result, whenever the database engine encounters an SQL statement that either reads or writes, it automatically starts a transaction, called the default transaction. Thus, you do not have to explicitly tell the database engine to start a transaction, but you can do so if you want to modify the default parameters. After you start a transaction, it either completes successfully or it does not. What happens in either case is discussed in the following sections.

Using the SET TRANSACTION statement

Whatever DBMS you are using has default settings for how the transaction will be run. Although the default settings are perfectly fine most of the time, you can override them, if you want, with a SET TRANSACTION statement. Here’s the syntax:

<set transaction statement> ::=

SET [ LOCAL ] TRANSACTION <mode> [ , <mode> ] …

<mode> ::=

<isolation level>

| <access mode>

| <diagnostics size>

<isolation level> ::=

READ UNCOMMITTED

| READ COMMITTED

| REPEATABLE READ

| SERIALIZABLE

<access mode> ::=

READ ONLY

| READ WRITE

<diagnostics size> ::=

DIAGNOSTICS SIZE <simple value expression>

With the SET TRANSACTION statement, you can set the isolation level, the access mode, and the diagnostics size. Any one of the modes that you do not specify assumes the default value for that mode. If you specify the READ ONLY access mode, for example, any statements that change the database — such as UPDATE, INSERT, and DELETE — cannot execute. The default access mode is READ WRITE unless the isolation level is READ UNCOMMITTED. When you are running at the READ UNCOMMITTED isolation level, the default access mode is READ ONLY. The default isolation level is SERIALIZABLE. (I cover isolation levels in more detail in the next section. I defer discussion of DIAGNOSTICS SIZE to Book 4, Chapter 4.)

Remember You can’t start a new transaction while an existing transaction is still active. If you do execute a SET TRANSACTION statement while a transaction is active, the modes specified in the statement apply only to the next transaction, not the current one. The LOCAL keyword restricts the mode settings specified to the local transaction included in a transaction that encompasses multiple databases.

Starting a transaction

As I mention earlier in this section, a transaction is started automatically when the database engine senses that the next statement to execute either reads from or writes to the database. Default modes are assumed unless a SET TRANSACTION statement has been executed. If one has, the modes specified in it are used rather than the default modes. The modes specified by a SET TRANSACTION statement are active only for the next transaction to execute. Any following transactions once again use the default modes unless another SET TRANSACTION statement is executed or a START TRANSACTION statement is executed.

With a START TRANSACTION statement, you can specify modes the same way you can with a SET TRANSACTION statement. The difference is that a START TRANSACTION statement starts a transaction, whereas a SET TRANSACTION statement sets up the modes for a transaction but does not actually start one.

Access modes

There is not much mystery about the access modes, READ ONLY and READ WRITE. In either mode, you can read the contents of database records by using the SELECT statement. You can make changes to the database only in READ WRITE mode, however.

Isolation levels

In the Medieval Savings and Loan example (refer to “Solving Concurrent-Operation Conflicts,” earlier in this chapter), I outline one of the potential problems when two database operations are not sufficiently isolated from each other and interact in an undesirable way. Transactions provide four levels of protection from such harmful interactions, ranging from the fairly weak protection of READ UNCOMMITTED to the level of protection you would get if transactions never ran concurrently (SERIALIZABLE).

READ UNCOMMITTED

The weakest level of isolation is called READ UNCOMMITTED, which allows the sometimes-problematic dirty read — a situation in which a change made by one user can be read by a second user before the first user commits (that is, finalizes) the change. The problem arises when the first user aborts and rolls back his transaction. Now the second user’s subsequent operations are based on an incorrect value.

The classic example of this foul-up can appear in an inventory application: One user decrements inventory, and a second user reads the new (lower) value. The first user rolls back her transaction (restoring the inventory to its initial value), but the second user, thinking that inventory is low, orders more stock and possibly creates a severe overstock. And that’s if you’re lucky.

Warning Don’t use the READ UNCOMMITTED isolation level unless you don’t care about accurate results.

You can use READ UNCOMMITTED if you want to generate approximate statistical data, such as the following:

  • Maximum delay in filling orders
  • Average age of salespeople who don’t make quota
  • Average age of new employees

In many such cases, approximate information is sufficient; the extra (performance) cost of the concurrency control required to produce an exact result may not be worthwhile.

READ COMMITTED

The next-highest level of isolation is READ COMMITTED. At this level, a change made by another transaction isn’t visible to your transaction until the other user has committed the other transaction. This level gives you a better result than you can get from READ UNCOMMITTED, but it’s still subject to a nonrepeatable read — a serious problem that creates a comedy of errors.

To illustrate, consider the classic inventory example. User 1 queries the database to see how many items of a particular product are in stock. That number is 10. At almost the same time, User 2 starts — and then commits — a transaction that records an order for 10 units of that same product, decrementing the inventory and leaving none. Now User 1, having seen that 10 units are available, tries to order 5 of them — but 5 units are no longer left, because User 2 has in effect raided the pantry. User 1’s initial read of the quantity available is not repeatable. The quantity has changed out from under User 1; any assumptions made on the basis of the initial read are not valid.

REPEATABLE READ

An isolation level of REPEATABLE READ guarantees that the nonrepeatable-read problem doesn’t happen. When running at this isolation level, the DBMS simply will not allow a change by a second user to take place after the first user has read a set of records but has not completed the transaction. This isolation level, however, is still haunted by the phantom read — a problem that arises when the data a user is reading changes in response to another transaction (and does not show the change onscreen) while the user is reading it.

Suppose that User 1 issues a command whose search condition (the WHERE clause or HAVING clause) selects a set of rows, and immediately afterward, User 2 performs and commits an operation that changes the data in some of those rows while User 1’s read operation is still running. Those data items met User 1’s search condition at the start of this snafu, but now they no longer do. Maybe some other rows that first did not meet the original search condition now do meet it. User 1, whose transaction is still active, has no inkling of these changes; the application behaves as though nothing has happened. The hapless User 1 issues another SQL statement with the same search conditions as the original one, expecting to retrieve the same rows. Instead, the second operation is performed on rows other than those used in the first operation. Reliable results go out the window, spirited away by the phantom read.

SERIALIZABLE

An isolation level of SERIALIZABLE is not subject to any of the problems that beset the other three levels. At this level, concurrent transactions can (in principle) be run serially — one after the other — rather than in parallel, and the results come out the same. If you’re running at this isolation level, hardware or software problems can still cause your transaction to fail, but at least you don’t have to worry about the validity of your results if you know that your system is functioning properly.

Superior reliability may come at the price of slower performance, of course, so you’re back in Trade-Off City. Table 2-1 sums up the trade-off terms, showing the four isolation levels and the problems they solve.

TABLE 2-1 Isolation Levels and Problems Solved

Isolation Level

Problems Solved

READ UNCOMMITTED

None

READ COMMITTED

Dirty read

REPEATABLE READ

Dirty read

Nonrepeatable read

SERIALIZABLE

Dirty read

Nonrepeatable read

Phantom read

Committing a transaction

Although SQL doesn’t require an explicit transaction-starting keyword, it has two that terminate a transaction: COMMIT and ROLLBACK. Use COMMIT when you have come to the end of the transaction and want to make permanent the changes that you have made in the database (if any). You may include the optional keyword WORK (COMMIT WORK), if you want. If an error is encountered or the system crashes while a COMMIT is in progress, you may have to roll the transaction back and try it again.

Rolling back a transaction

When you come to the end of a transaction, you may decide that you don’t want to make permanent the changes that have occurred during the transaction. In fact, you want to restore the database to the state it was in before the transaction began. To do this, issue a ROLLBACK statement. ROLLBACK is a fail-safe mechanism. Even if the system crashes while a ROLLBACK is in progress, you can restart the ROLLBACK after the system is restored, and it restores the database to its pre-transaction state.

Why roll back a transaction?

It may be necessary to roll back a transaction if some kind of system failure occurs while the transaction is active. Such a failure has several possible causes, including the following:

  • Power failure
  • Application crash
  • Operating-system crash
  • Failed peripheral device
  • Failed processor
  • System shutdown due to overheating
  • Hurricane, wildfire, or other weather damage
  • Electromagnetic storms due to solar coronal mass ejections
  • Bit flips due to cosmic rays
  • Terrorist attack

In most of the cases cited, although system operation is interrupted and everything in volatile main memory is lost, information stored on a nonvolatile hard disk is still intact, particularly if it’s stored in a RAID array physically removed from the main system box. (For details on RAID, see Book 4, Chapter 1.) The good information on your hard disk forms the basis for a rollback operation that takes the system back to the condition it was in before the start of any of the transactions that were active when the service interruption occurred.

How can you roll back changes that have already been made? How can you undelete records that you have deleted? How can you restore fields that you have overwritten with new data? How can you remove new records that you have added? The answers to all these questions lie in the log file.

The log file

Because volatile semiconductor memory is so much faster than hard disk storage, when changes are made to a data file, they are not immediately written to hard disk, which is a relatively slow process. Instead, they are written to a page buffer in semiconductor memory. If those same logical memory locations must be accessed again fairly soon, the retrieval is much quicker from the page buffer than it would be from hard disk.

Eventually, the page buffer fills, and when a new page is needed, one of the existing pages in the buffer must be swapped out to make room for the new page. During the period when information has been written to memory but is still in the page buffer, it is vulnerable to a failure of the volatile page buffer memory. When a power failure occurs, everything in the page buffer is lost, as well as everything in the system’s main memory. The log file is the primary tool for recovering what has been lost and rendering the system able to redo the incomplete transactions.

The log file, primarily located on disk but also necessarily partly in the page buffer, records every change made to the database. Log-file entries pertaining to a transaction are always flushed from the page buffer to disk before the actual changes themselves are flushed. If a failure occurs between the time the log-file entries are flushed to disk and the time the changes themselves would have been flushed, the changes can be reconstructed from the log-file entries. The main idea is to make the window of vulnerability as small as possible. When log-file entries are frequently flushed to disk, that window is open only a crack. For all but the most ill-timed and severe failures, a minimum of data is lost, and it can be re-entered without too much trouble.

The write-ahead log protocol

The entries in the log file are made according to a formula known as the write-ahead log protocol. When a transaction prepares to write to a page containing some target record, it obtains an exclusive lock on the page. (I discuss locks extensively in the next section of this chapter.) Before the transaction makes the modification to a record, it writes a log record containing the contents of the record both before and after the change. After the log record has been successfully written, the modification itself is written. Now the change is sitting in the page buffer, where it is vulnerable to a power outage or other mischance that may require a reboot that erases all volatile storage.

If a failure occurs before the log record and modification are flushed to disk, they are lost. In that case, you must go back to the last good version on disk and redo everything from that point on. If a failure occurs after the log file has been written to disk but before the modification has been, the data in the log file enables full reconstitution of the change.

After a failure, the log file may contain information on the following:

  • Transactions that were committed but not yet written to disk
  • Transactions that were rolled back
  • Transactions that were still active and (of course) not yet written to disk

Transactions that were committed and not yet written to disk need to be redone. Transactions that were rolled back need to be undone. Transactions that were still active need to be restarted. You can figure out which of these three actions to take by scanning the log file backward in time, undoing actions as you go. When you come to a COMMIT statement, put that transaction in the redo list. When you come to a ROLLBACK statement, put that transaction in the undo list. Put the rest of the transactions in the restart list.

When you reach the beginning of the log, you have undone all the transactions. Now scan forward to redo all the transactions in the redo list. Skip the transactions in the undo list, because you have already undone them. Finally, submit the restart list to the DBMS to start those transactions from scratch.

Checkpoints

A log file may accumulate records of transactions for months or years, becoming quite large in the process. Scanning back through the log file can be time-consuming. There is no point in scanning back beyond a point at which all transactions are guaranteed to have been safely stored on disk. To shorten the portion of the log that must be scanned, checkpoints are established at intervals. These intervals may be fixed units of time, such as 15 minutes, or they may come after a specific number of entries have been made in the log. In either case, at a checkpoint, all log entries in the page buffer are flushed to disk. This checkpoint establishes a point beyond which you can be assured that all log entries and all committed transactions are safely on disk.

When a problem occurs that requires recovery, you need concern yourself only with transactions that were active at, or that started later than, the checkpoint. Transactions that were committed before the checkpoint will have been flushed to disk at the checkpoint, if not before. The same is true for transactions that were rolled back. Transactions that were active at the checkpoint have to be undone back to the point at which they started and then restarted.

Implementing deferrable constraints

Ensuring the validity of the data in your database means doing more than just making sure that the data is of the right type. Perhaps some columns, for example, should never hold a null value — and maybe others should hold only values that fall within a certain range. Such restrictions are constraints, as discussed in Book 1, Chapter 5.

Constraints are relevant to transactions because they can conceivably prevent you from doing what you want. Suppose that you want to add data to a table that contains a column with a NOT NULL constraint. One common method of adding a record is to append a blank row to your table and then insert values into it later. The NOT NULL constraint on one column, however, causes the append operation to fail. SQL doesn’t allow you to add a row that has a null value in a column with a NOT NULL constraint, even though you plan to add data to that column before your transaction ends. To address this problem, SQL enables you to designate constraints as DEFERRABLE or NOT DEFERRABLE.

Constraints that are NOT DEFERRABLE are applied immediately. You can set DEFERRABLE constraints to be initially DEFERRED or IMMEDIATE. If a DEFERRABLE constraint is set to IMMEDIATE, it acts like a NOT DEFERRABLE constraint: It is applied immediately. If a DEFERRABLE constraint is set to DEFERRED, it is not enforced.

To append blank records or perform other operations that may violate constraints, ISO/IEC standard SQL allows you to use a statement similar to the following:

SET CONSTRAINTS ALL DEFERRED ;

This statement puts all DEFERRABLE constraints in the DEFERRED condition. It does not affect the NOT DEFERRABLE constraints. After you have performed all operations that could violate your constraints, and the table reaches a state that doesn’t violate them, you can reapply them. The statement that reapplies your constraints looks like this:

SET CONSTRAINTS ALL IMMEDIATE ;

If you made a mistake, and any of your constraints are still being violated, you find out as soon as this statement takes effect.

If you do not explicitly set your DEFERRED constraints to IMMEDIATE, SQL does it for you when you attempt to COMMIT your transaction. If a violation is still present at that time, the transaction does not COMMIT; instead, SQL gives you an error message.

SQL’s handling of constraints protects you from entering invalid data (or an invalid absence of data, which is just as important) while giving you the flexibility to violate constraints temporarily while a transaction is still active.

Consider a payroll example to see why being able to defer the application of constraints is important.

Assume that an EMPLOYEE table has columns EmpNo, EmpName, DeptNo, and Salary. EMPLOYEE.DeptNo is a foreign key referencing the DEPT table. Assume also that the DEPT table has columns DeptNo and DeptName. DEPT.DeptNo is the primary key.

In addition, you want to have a table like DEPT that also contains a Payroll column that holds the sum of the Salary values for employees in each department.

You can create the equivalent of this table with the following view:

CREATE VIEW DEPT2 AS

SELECT D.*, SUM(E.Salary) AS Payroll

FROM DEPT D, EMPLOYEE E

WHERE D.DeptNo = E.DeptNo

GROUP BY D.DeptNo ;

You can also define this same view as follows:

CREATE VIEW DEPT3 AS

SELECT D.*,

(SELECT SUM(E.Salary)

FROM EMPLOYEE E

WHERE D.DeptNo = E.DeptNo) AS Payroll

FROM DEPT D ;

But suppose that for efficiency, you don’t want to calculate the sum every time you reference DEPT3.Payroll. Instead, you want to store an actual Payroll column in the DEPT table. Then you will update that column every time you change a salary.

To make sure that the Salary column is accurate, you can include a CONSTRAINT in the table definition, as follows:

CREATE TABLE DEPT

(DeptNo CHAR(5),

DeptName CHAR(20),

Payroll DECIMAL(15,2),

CHECK (Payroll = (SELECT SUM(Salary)

FROM EMPLOYEE E WHERE E.DeptNo= DEPT.DeptNo)));

Now suppose that you want to increase the salary of employee 123 by 100. You can do it with the following update:

UPDATE EMPLOYEE

SET Salary = Salary + 100

WHERE EmpNo = '123' ;

You must remember to do the following as well:

UPDATE DEPT D

SET Payroll = Payroll + 100

WHERE D.DeptNo = (SELECT E.DeptNo

FROM EMPLOYEE E

WHERE E.EmpNo = '123') ;

(You use the subquery to reference the DeptNo of employee 123.)

But there’s a problem: Constraints are checked after each statement. In principle, all constraints are checked. In practice, implementations check only the constraints that reference the values modified by the statement.

After the first preceding UPDATE statement, the implementation checks all constraints that reference values that the statement modifies. This check includes the constraint defined in the DEPT table, because that constraint references the Salary column of the EMPLOYEE table, and the UPDATE statement is modifying that column. After the first UPDATE statement, that constraint is violated. You assume that before you execute the UPDATE statement, the database is correct, and each Payroll value in the DEPT table equals the sum of the Salary values in the corresponding columns of the EMPLOYEE table. When the first UPDATE statement increases a Salary value, this equality is no longer true. The second UPDATE statement corrects this problem and again leaves the database values in a state for which the constraint is True. Between the two updates, the constraint is False.

The SET CONSTRAINTS DEFERRED statement lets you temporarily disable or suspend all constraints or only specified constraints. The constraints are deferred until you execute a SET CONSTRAINTS IMMEDIATE statement or a COMMIT or ROLLBACK statement. So you surround the previous two UPDATE statements with SET CONSTRAINTS statements. The code looks like this:

SET CONSTRAINTS DEFERRED ;

UPDATE EMPLOYEE

SET Salary = Salary + 100

WHERE EmpNo = '123' ;

UPDATE DEPT D

SET Payroll = Payroll + 100

WHERE D.DeptNo = (SELECT E.DeptNo

FROM EMPLOYEE E

WHERE E.EmpNo = '123') ;

SET CONSTRAINTS IMMEDIATE ;

This procedure defers all constraints. If you insert new rows into DEPT, the primary keys won’t be checked; you have removed protection that you may want to keep. Specifying the constraints that you want to defer is preferable. To do this, name the constraints when you create them, as follows:

CREATE TABLE DEPT

(DeptNo CHAR(5),

DeptName CHAR(20),

Payroll DECIMAL(15,2),

CONSTRAINT PayEqSumSal

CHECK (Payroll = SELECT SUM(Salary)

FROM EMPLOYEE E

WHERE E.DeptNo = DEPT.DeptNo)) ;

With constraint names in place, you can reference your constraints individually, as follows:

SET CONSTRAINTS PayEqSumSal DEFERRED;

UPDATE EMPLOYEE

SET Salary = Salary + 100

WHERE EmpNo = '123' ;

UPDATE DEPT D

SET Payroll = Payroll + 100

WHERE D.DeptNo = (SELECT E.DeptNo

FROM EMPLOYEE E

WHERE E.EmpNo = '123') ;

SET CONSTRAINTS PayEqSumSal IMMEDIATE;

Without a constraint name in the CREATE statement, SQL generates one implicitly. That implicit name is in the schema information (catalog) tables, but specifying the names explicitly is more straightforward.

Now suppose that in the second UPDATE statement, you mistakenly specified an increment value of 1000. This value is allowed in the UPDATE statement because the constraint has been deferred. But when you execute SET CONSTRAINTS IMMEDIATE, the specified constraints are checked. If they fail, SET CONSTRAINTS raises an exception. If, rather than a SET CONSTRAINTS IMMEDIATE statement, you execute COMMIT, and the constraints are found to be false, COMMIT instead performs a ROLLBACK.

Bottom line: You can defer the constraints only within a transaction. When the transaction is terminated by a ROLLBACK or a COMMIT, the constraints are both enabled and checked. The SQL capability of deferring constraints is meant to be used within a transaction. If used properly, it doesn’t create any data that violates a constraint available to other transactions.

Getting Familiar with Locking

The gold standard for maintaining database integrity is to operate on it with only serializable transactions. You have two major approaches for providing serializability: locking and timestamps. In this section, I look at locking. I cover timestamps in “Enforcing Serializability with Timestamps,” later in this chapter.

If a transaction is granted a lock on a particular resource, access to that resource by competing transactions is restricted. There are two main kinds of locks:

  • Shared locks: Two or more transactions, each with a shared lock, can concurrently read the contents of a memory location without interfering with one another. As long as none of the transactions attempts to change the data at that location, all the transactions can proceed without delay. The lock manager portion of the DBMS can grant shared locks to all transactions that want to perform only read operations. Shared locks are sometimes called read locks.
  • Exclusive locks: To perform a write operation on a memory location, a transaction must acquire an exclusive lock, which grants to its holder the exclusive right to access the resource being locked. If one transaction holds an exclusive lock on a resource, no competing transaction may acquire either a shared lock or an exclusive lock on that resource until the first transaction releases its lock. Exclusive locks are sometimes called write locks.

Two-phase locking

Two-phase locking is a protocol designed to guarantee serializability. In the first phase, a transaction can acquire shared and exclusive locks, and may also upgrade a shared lock to an exclusive lock. It may not release any locks or downgrade an exclusive lock to a shared lock, however. In the second phase, the transaction may release shared and exclusive locks, as well as downgrade an exclusive lock to a shared lock, but it may not acquire a new shared or exclusive lock, or upgrade a shared lock to an exclusive lock.

In the strictest form of two-phase locking, the second phase, in which locks are released or downgraded, cannot occur until the transaction either commits or rolls back. This restriction protects a competing transaction from acquiring a lock on, and reading a value from, a resource that the original transaction released before aborting. In such a case, the second transaction would potentially read a value that no longer existed in the resource. In fact, after a rollback, it is as though that value never existed.

Granularity

The granularity of a lock determines the size of the resource being locked. Locks that are coarse-grained take rather large resources out of circulation. Fine-grained locks sequester relatively small resources. Course-grained locks deny access to big things, such as tables. Fine-grained locks protect smaller things, such as rows in a table.

This list describes four types of locks and the granularity of each type:

  • Database locks: The database lock is the ultimate in coarse-grained locks. If a transaction puts an exclusive lock on a database, no other transaction can access the database at all until the lock is released. As you might imagine, database locks have a disastrous effect on overall productivity and should be avoided if at all possible. Sometimes, a database administrator must apply a database lock to prevent other transactions from corrupting the database while she is making alterations in the database structure.
  • Table locks: Table locks, by locking an entire database table, are not as restrictive as database locks but are still pretty coarse. Generally, you would impose a table lock only if you were altering the structure of the table or if you were changing data in most or all of the rows in the table.
  • Row locks: Row locks are fine-grained in that they lock only a single row in a table. If you’re changing only a value in a single row, there is no point in locking any rows other than that one target row. The only transactions that are affected by a row lock are those that want to do something to the very same row of the very same table.
  • Page locks: A page lock — which has an intermediate granularity between a table lock and a row lock — locks an entire page in the page buffer. Because information gets transferred between the page buffer and disk a page at a time, some DBMSs provide locks at the page level. As processing proceeds, requiring pages currently residing in the page buffer to be swapped out in favor of pages on disk that are currently needed, the DBMS will resist, if possible, the urge to swap out any page locked by an active transaction. Swapping it out and then swapping it back in again soon would waste a tremendous amount of time.

Deadlock

A deadlock is not a type of lock or an example of granularity, but a problem that can arise in even a well-designed system that uses locking for concurrency control. To illustrate how a deadlock can happen, look again at the example in which you and your friend Calypso share a bank account (refer to “Solving Concurrent-Operation Conflicts,” earlier in this chapter). Now that you are using transactions with two-phase locking, you don’t have to worry about the lost-update problem anymore. A potential problem still exists, however. Once again, you and Calypso arrive at two different ATM machines at about the same time.

  1. You insert your ATM card into your ATM.
  2. The DBMS’s lock manager grants you a shared lock on your account record, enabling you to read your balance of $47.17.
  3. Calypso inserts her ATM card into her ATM.
  4. The DBMS’s lock manager grants Calypso a shared lock on your account record, enabling her to read the balance of $47.17.
  5. You insert $100 in cash into your ATM.
  6. The DBMS’s lock manager attempts to upgrade your shared lock to an exclusive lock but cannot because of Calypso’s shared lock. It goes into a wait loop, waiting for Calypso’s lock to be released.
  7. Calypso inserts $100 in cash into her ATM.
  8. The DBMS’s lock manager attempts to upgrade Calypso’s shared lock to an exclusive lock but cannot because of your shared lock. It goes into a wait loop, waiting for your lock to be released.
  9. The machine is deadlocked. Neither you nor Calypso can complete the transaction.
  10. After some period of time, the DBMS recognizes the deadlock situation and aborts one or both of the deadlocked transactions.

Ideally, instances such as this don’t occur too often. By putting your account update in a transaction, you have traded the lost-update problem for the deadlock problem. This situation is an improvement. At least you don’t end up with incorrect data in your database. It’s a hassle to have to redo your ATM transaction, however. If all goes well, you and Calypso don’t try to redo your transactions at the same time.

Tuning Locks

Locks perform an important function in preserving the integrity of transactions. They prevent database corruption by making sure that changes by one transaction do not affect the results of another transaction that is operating concurrently. They do so at a cost, however. Locks consume memory and also affect performance because it takes time to acquire a lock, and it takes time to release one. Additional performance is lost while transactions wait for resources that have been locked by another. In some cases, such lockouts prevent data corruption; in other cases, locks are needlessly placed when harmful interactions cannot occur.

You can do some things to reduce the overhead burden of locking. Generally, you apply these measures only if performance becomes unsatisfactory. For some of the tuning interventions that I discuss in this section, you may have to trade off some accuracy in exchange for improved performance.

Measuring performance with throughput

What is performance, anyway? There are many ways of thinking about performance and many system parameters that you can measure to glean information about one aspect of performance or another. In this book, when I talk about performance, I am referring to throughput — a measure of the amount of work that gets completed per unit time. It is an overall measure that takes into account all the jobs running on a system. If one job is running really fast, but its locks are slowing all the other jobs running at the same time, throughput may well be lower than it would be if the first job were reconfigured so that it did not run quite so fast and held its locks for a shorter period.

Eliminating unneeded locks

Locks are designed to prevent concurrently running transactions from interfering with one another. They are applied automatically by the DBMS, so the application programmer does not have to worry about whether he should apply a lock. At times, however, such interference is not possible. In those cases, the locking overhead is a burden on the system, but no corresponding benefit in data integrity occurs.

This is true when only one transaction is running at a time, such as when a database is loading. It is also true when all queries are guaranteed to be read-only — when mining archived information, for example. In such cases, it makes sense to take advantage of the option to suppress the acquisition of locks.

Shortening transactions

Long transactions, which do a lot of things, tend to hold locks for a long time. This situation, of course, has a negative effect on the performance of all the other transactions running at the same time. If everyone followed the discipline of making transactions as short as possible, everyone would benefit by being able to acquire needed locks sooner.

Albert Einstein once said, with regard to physics, “Make everything as simple as possible, but not simpler.” The same logic applies here. Make transactions as short as possible, but not shorter. If you chop up transactions too finely, you could lose serializability, which means that you could lose accuracy. To continue the lost-update example (refer to “Solving Concurrent-Operation Conflicts,” earlier in this chapter), if you acquired a shared lock on your bank account, viewed your bank balance, and then dropped your shared lock before acquiring an exclusive lock to make a withdrawal, Calypso could have sneaked in while your locks were down and cleaned out your account. This situation would have been both surprising and disappointing, because you would just have read that there was plenty of money in your account.

Weakening isolation levels (ver-r-ry carefully)

If you weaken your isolation level from SERIALIZABLE to REPEATABLE READ or perhaps READ COMMITTED, you can increase your throughput. A good chance exists that in doing so, however, bad data will creep into your database. In most cases, weakening your isolation level in that way isn’t worthwhile.

In a few scenarios, perfect accuracy in data is not required, and system response time is very important. In such cases, weakening the isolation level may be appropriate.

Consider an airline’s reservation system. Suppose that a traveler goes to an airline’s online website, which is running at the READ COMMITTED isolation level, to look up a particular flight. Checking available seats is a read-only operation that puts a shared lock on the entire airplane. As soon as the cabin image is transmitted, the shared lock is dropped. The traveler decides that she would like to sit in seat 10-C, which the website shows as available. She clicks that seat on her screen image to indicate that she wants to reserve that seat. The database attempts to put an exclusive lock on the record for seat 10-C but fails. In the small interval of time between the database read and the attempted database write, someone else has reserved that seat, and it is no longer available.

This scenario could clearly happen with READ COMMITTED isolation. Is this a problem? Many airlines would think that it is not. Although such a sequence of events is possible, it also tends to be extremely rare. When it does occur, it is not a big deal. The traveler just directs a few choice words at the computer (the universal scapegoat) and successfully selects another seat. In exchange, all the travelers who use the reservation system benefit from faster response to their actions, which might be considered to be a reasonable trade-off.

Warning Be sure to think through the possible problems that could occur if you weaken the isolation level of your database and the consequences that follow from those problems. If you do decide to weaken the isolation level, it should be with full knowledge of the consequences.

Controlling lock granularity

In most systems, row-level locking, which is fine-grained, is the default. This is the best choice for maximizing throughput in an online transaction environment, such as an airline reservation system or a banking system. It’s not necessarily best in a system that runs long transactions that involve most of the rows in a table, however. In that kind of environment, a table lock — one with lower overhead than a large number of row locks — could well deliver better overall throughput. Concurrent transactions could be delayed to a lesser extent than they would be with row-level locking.

Remember Don’t assume that the finest-grained locking setting is the best. Consider the types of jobs that are typically run, and choose lock granularity accordingly.

Scheduling DDL statements correctly

Data Definition Language (DDL) statements such as CREATE TABLE, DROP INDEX, and ALTER TABLE operate on the system catalog. Because these operations are of such a fundamental nature, ordinary traffic comes to a standstill while they are active. In any normal installation, you are going to need to run some DDL statements from time to time. Just keep in mind the way that they monopolize system resources, and schedule them at a time when your normal transaction volume is light.

Partitioning insertions

Sequential insertion of records can be a bottleneck. If multiple transactions are inserting records sequentially into the tail end of a table, all of them will be hitting the same buffer page at about the same time and running into page locks. One way to relieve the congestion is to partition insertions into the table across different pages or even different disks. One way to achieve this result is to set up a clustering index based on something other than the time of insertion. This method spreads out the inserted records to different pages. I define clustered indexes in Book 2, Chapter 3, and will discuss their effect on performance in Book 7, Chapter 1.

Cooling hot spots

Hot spots are those records, pages, or tables that everybody wants access to at the same time. When a lengthy transaction acquires a lock on a hot item, everybody else suffers.

You can do a couple of things to lower the “temperature” of chronic hot spots:

  • Partition transactions as described in the preceding section.
  • Access hot spots as late as possible in a long transaction. Within a transaction, you have some control over the order in which you do things. When possible, place a lock on a hot resource as late as possible. This method makes the overheated resource unavailable for the shortest amount of time.

Tuning the deadlock interval

Earlier in this chapter, in the section titled “Deadlock,” I mention deadlock as being a possible problem, even when you are running with a serializable isolation level. The common solution to the deadlock problem starts when the system senses that two or more transactions have not made any progress for an extended period. To break the deadlock, the system forces the abort and rollback of one or perhaps all of the transactions involved. The deadlock interval is the period that the system waits before allowing an aborted transaction to restart. Clearly, you don’t want to give all the aborted transactions the same deadlock interval. Doing so would just be asking for another deadlock. Even if only one transaction is aborted, restarting it too soon could conflict with a lengthy transaction that is still running.

So how do you choose a good deadlock interval? There is no one good answer, although the interval should have an element of randomness so that you don’t assign the same interval to both participants in a deadlock. Make an educated guess based on the types of transactions that you are running. If excessive deadlocks ensue, try changing the interval or the average difference between the deadlock interval of one aborted transaction and the deadlock interval of the other participant in the deadly embrace that started the whole mess.

Enforcing Serializability with Timestamps

Locks aren’t the only effective mechanisms for keeping concurrent transactions from interfering with one another. Another method involves timestamps. A timestamp is a centrally dispensed number assigned to each transaction in strictly increasing order. A timestamp could be based on the computer’s real-time clock, or it could just be a counter that is continually counting up. This method enables the system to determine which active transaction is the oldest and which is the youngest, as well as the relative positions of all transactions in between. In a conflict situation, the timestamp solution works by designating the younger transaction as the winner.

To demonstrate this method, look again at the bank-account update example (introduced in “Solving Concurrent-Operation Conflicts,” earlier in this chapter):

  1. The system sets the timestamp to 0, which is the timestamp for a creating or updating operation.
  2. You insert your ATM card into your ATM.
  3. Your ATM pulls up your account and notes that you have a balance of $47.17. It sets the timestamp for your transaction to 1 and checks that 1 is greater than or equal to the timestamp of the youngest operation in the system (0). Because 1 is greater than 0, everything is fine.
  4. Calypso inserts her ATM card into her ATM.
  5. Calypso’s ATM pulls up your account and notes that you have a balance of $47.17. It sets the timestamp for her transaction to 2 and checks that 2 is greater than or equal to the youngest operation in the system (1). Because 2 is greater than 1, everything is fine.
  6. You insert $100 in cash into your ATM.
  7. Calypso inserts $100 in cash into her ATM.
  8. Calypso’s ATM checks whether her timestamp (2) is equal to or greater than the most recent timestamp. It is, so everything is fine. Calypso’s transaction commits and the ATM registers a balance of $147.17.
  9. Your ATM checks your timestamp (1) against the youngest timestamp in the system (2). Because 1 is not greater than or equal to 2, you lose. There are no changes in the database to undo, so a new transaction is started for you with timestamp 3.
  10. Your ATM notes that your timestamp (3) is equal to or greater than the timestamp of the most recent create or update operation (2).
  11. Your update is accomplished; your transaction commits, and the account balance goes to $247.17.

There was a little hitch in the proceedings, but you probably didn’t even notice it while you were standing there at the ATM machine. The account updates were performed properly.

The preceding scenario sounds great, but problems could occur if the timing is just a little bit off. Consider the following example:

  1. The system sets the timestamp to 0, which is the timestamp for a creating or updating operation.
  2. You insert your ATM card into your ATM.
  3. Your ATM pulls up your account and notes that you have a balance of $47.17. It sets the timestamp for your transaction to 1 and checks that 1 is greater than or equal to the youngest timestamp in the system (0). It is, so everything is fine.
  4. Calypso inserts her ATM card into her ATM.
  5. Calypso’s ATM pulls up your account and notes that you have a balance of $47.17. It sets the timestamp for her transaction to 2 and checks that 2 is greater than or equal to the youngest timestamp in the system (1). It is, so everything is fine.
  6. You insert $100 in cash into your ATM.
  7. Your ATM checks your timestamp (1) against the timestamp of the youngest read in the system (2). Because 1 is not greater than or equal to 2, you lose. There are no changes in the database to undo, so a new transaction is started for you with timestamp 3.
  8. Your new transaction checks your timestamp (3) against the timestamp of the youngest update in the system (2). Because 3 is equal to or greater than 2, everything is fine. Your transaction reads a balance of $47.17.
  9. Calypso inserts $100 in cash into her ATM.
  10. Calypso’s ATM checks whether her timestamp (2) is equal to or greater than the most recent timestamp (3). It is not, so her transaction is aborted, and a new transaction with timestamp 4 is started for her.
  11. Calypso’s ATM checks whether her timestamp (4) is equal to or greater than the most recent (3). It is, so everything is fine. Her transaction reads a balance of $47.17.
  12. Your ATM checks your timestamp (3) against the timestamp of the youngest create or update operation in the system (4). Because 3 is not greater than or equal to 4, you lose. There are no changes in the database to undo, so a new transaction is started for you with timestamp 5.
  13. And so on, ad infinitum.

Your ATM has eaten $100 of your money and Calypso’s ATM has eaten $100 of her money. This situation is called a livelock as opposed to a deadlock. In a deadlock, two or more transactions are stuck in wait states because they cannot continue without a resource that has been acquired by another one of the deadlocked transactions. A livelock differs in that the participating transactions are continually processing but are moving no closer to completion; they are stuck in a loop.

One way out of this situation is for the DBMS to keep a list of transactions that have been aborted some fixed number of times. When a transaction goes over the threshold, the DBMS can halt the normal flow of execution and execute the livelocked transactions serially. Your account will finally show the correct balance.

Tuning the Recovery System

One thing you can do to maximize performance is to put your database log on a different disk from the disks that contain data. The log is continuously updated with every command performed. It just pours its information onto the log disk sequentially. This minimizes the amount of time that the disk drive spends seeking because data is written sequentially to a single track and then to other tracks on the same cylinder, after which a short seek is made to an adjacent cylinder and the operation continues. By contrast, the data disks are constantly doing random seeks from one track to another. Mixing that operation with the sequential writing of the log would cause a severe hit on performance.

Tuning the recovery system is always a balance between maximizing performance and maintaining system integrity. Although disk failures are rare, they do happen occasionally. Even if your system is protected with a RAID system, you should take a copy of your database offline at intervals. This copy, called a database dump, has an effect on performance because it uses resources while it is being run. The dump gives you a starting point if your hard disk system fails catastrophically. You must decide how often to perform database dumps by weighing the performance hit you take while it is running against the pain you would suffer if you didn’t run it and a disk failure occurred.

Similar considerations apply to checkpoints (refer to “Checkpoints,” earlier in this chapter). At a checkpoint, all committed transactions are flushed to disk. This method is a significant time consumer, because disk operations are orders of magnitude slower than data transfers to solid-state memory. Balanced against this situation is the time that checkpoints save you when that inevitable failure does occur. A checkpoint limits the distance you have to go back in the log to resynchronize the system after a failure.

How frequently should you force a checkpoint? It’s a tradeoff. Compare the overhead cost of a checkpoint with the time it saves you when you have to go to the log to recover consistency after a failure. Set the checkpoint interval at that sweet spot where you save more time in a recovery operation than the overhead of implementing the checkpoints is costing you.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.12.240