Home • Download SQL code • Sample database • Buy the paperback

Contents

Introduction

SQL (pronounced es-kyoo-el) is the standard programming language for creating, updating, and retrieving information that’s stored in databases. With SQL, you can turn your ordinary questions (“Where do our customers live?”) into statements that your database system can understand (SELECT DISTINCT city, state FROM customers;). You might already know how to extract this type of information by using a graphical query or reporting tool, but perhaps you’ve noticed that this tool becomes limiting or cumbersome as your questions grow in complexity—that’s where SQL comes in.

You also can use SQL to add, change, and delete data and database objects. All modern relational database management systems (DBMSs) support SQL, although support varies by product (more about that later in this introduction).

Database vs. DBMS

A database is not the same as the database software that you’re running; it’s incorrect to say, “Oracle is a database.” Database software is called a database management system (DBMS). A database, which is just one component of a DBMS, is the data itself—that is, it’s a container (one or more files) that stores structured information. Besides controlling the organization, integrity, and retrieval of data in databases, DBMSs handle such tasks as physical storage, security, backup, replication, and error recovery.

DBMS also is abbreviated RDBMS, in which the R stands for relational. An RDBMS organizes data according to the relational model (see Chapter 2) rather than, say, a hierarchical or network model. This book covers only relational systems, so the initial R is implied in DBMS.

About SQL

SQL is:

A programming language.SQL is a formal language in which you write programs to create, modify, and query databases. Your database system executes your SQL program, performs the tasks you’ve specified, and then displays the results (or an error message). Programming languages differ from natural (spoken) languages in that programming languages are designed for a specific purpose, have a small vocabulary, and are inflexible and unambiguous. So if you don’t get the results you expect, then it’s because your program contains an error—or bug—and not because the computer misinterpreted your instructions. (Debugging one’s programs is a cardinal programming task.)

SQL, like any formal language, is defined by rules of syntax, which determine the words and symbols you can use and how they can be combined, and semantics, which determine the actual meaning of a syntactically correct statement. Note that you can write a legal SQL statement that expresses the wrong meaning (good syntax, bad semantics). Chapter 3 introduces SQL syntax and semantics.

Easy to learn.Easy compared with other programming languages, that is. If you’ve never written a program before, then you’ll find the transition from natural to formal language to be frustrating. Still, SQL’s statements read like sentences to make things easy on humans. A novice programmer probably would understand the SQL statement SELECT au_fname, au_lname FROM authors ORDER BY au_lname; to mean “List the authors’ first and last names, sorted by last name,” whereas the same person would find the equivalent C or Perl program impenetrable.

Declarative.If you’ve never programmed, then you can skip this point without loss of continuity. If you’ve programmed in a language such as C or JavaScript, then you’ve used a procedural language, in which you specify the explicit steps to follow to produce a result. SQL is a declarative language, in which you describe what you want and not how to do it; your database system’s optimizer will determine the “how.” As such, standard SQL lacks traditional control-flow constructs such as if-then-else, while, for, and goto statements.

To demonstrate this difference, I’ve written programs that perform an equivalent task in Microsoft Access Visual Basic (VB, a procedural language) and SQL. Listing i.1 shows a VB program that extracts author names from a table that contains author information. You needn’t understand the entire program, but note that it uses a Do Until loop to define explicitly how to extract data. Listing i.2 shows how to do the same task with a single SQL statement (as opposed to about 20 lines of VB code). With SQL, you specify only what needs to be accomplished; the DBMS determines and performs internally the actual step-by-step operations needed to get the result.

Listing i.1This Microsoft Access Visual Basic routine extracts the first and last names from a database table containing author information and places the results in an array.

Sub GetAuthorNames()
  Dim db As Database
  Dim rs As Recordset
  Dim i As Integer
  Dim au_names() As String
  Set db = CurrentDb()
  Set rs = db.OpenRecordset("authors")
  rs.MoveLast
  ReDim au_names(rs.RecordCount - 1, 1)
  With rs
    .MoveFirst
    i = 0
    Do Until .EOF
      au_names(i, 0) = ![au_fname]
      au_names(i, 1) = ![au_lname]
      i = i + 1
      .MoveNext
    Loop
  End With
  rs.Close
  db.Close
End Sub

Listing i.2This single SQL statement performs the same query as the Visual Basic routine in Listing i.1. Access’s internal optimizer determines the best way to extract the data.

SELECT au_fname, au_lname
  FROM authors;

Moreover, Listing i.2 is a trivial SQL query. After you add common operations such as sorts, filters, and joins, you might need more than 100 lines of procedural code to accomplish what a single SQL SELECT statement can do.

Interactive or embedded.In interactive SQL, you issue SQL commands directly to your DBMS, which displays the results as soon as they’re produced. DBMS servers come with both graphical and command-line tools that accept typed SQL statements or text files that contain SQL programs (scripts).

If you’re developing database applications, then you can “embed” SQL statements in programs written in a host language, which commonly is a general-purpose language (C++, Java, or COBOL, for example) or a scripting language (Python, R, or PHP). A PHP script can use an SQL statement to query a MySQL database, for example; MySQL will pass the query result back to a PHP variable for further analysis or webpage display. Drawing from the preceding examples, I’ve embedded an SQL statement in an Access Visual Basic program in Listing i.3.

Listing i.3Here, Visual Basic serves as the host language for embedded SQL.

Sub GetAuthorNames2()
  Dim db As Database
  Dim rs As Recordset
  Set db = CurrentDb()
  Set rs = db.OpenRecordset("SELECT au_fname, au_lname FROM authors;")
  '  --Do something with rs here.
  rs.Close
  db.Close
End Sub

This book covers interactive SQL. In general, any SQL statement that can be used interactively also can be used in a host language, although perhaps with slight syntactic differences, depending on your DBMS, host language, and operating environment.

Standardized.SQL isn’t “owned” by any particular firm. It’s an open standard defined by an international standards working group, under the joint leadership of the International Organization for Standardization (ISO) and the International Engineering Consortium (IEC). The American National Standards Institute (ANSI) participates in the working groups and has ratified the standard (Figure i.1). The terms ISO SQL, ANSI SQL, and standard SQL are synonymous. For more information, see “SQL Standards and Conformance” in Chapter 3.

Figure i.1This is the cover of ISO/IEC 9075 (Part 1), which defines the SQL language officially. You can buy it in electronic format at ansi.org or iso.org if you like. Its intended audience is not SQL programmers, however, but people who design DBMS systems, compilers, and optimizers.

Document: Cover of publication ISO/IEC 9075-1, Information technology - Database languages - SQL - Part 1: Framework (SQL/Framework)

All DBMS vendors add proprietary features to standard SQL to enhance the language. These extensions usually are additional commands, keywords, functions, operators, data types, and control-flow constructs such as if-then, while, and goto statements. Microsoft, Oracle, and IBM have added so many features to standard SQL that the resulting languages—Transact-SQL, PL/SQL, and SQL PL, respectively—are actually separate languages in their own right, rather than mere supersets of standard SQL. One vendor’s extensions generally are incompatible with other vendors’ products. I don’t cover proprietary SQL extensions, but I do point out when a vendor’s SQL dialect doesn’t comply with the standard SQL examples in this book; see “Using SQL with a Specific DBMS” later in this introduction.

Used to change data and database objects.SQL statements are divided into three categories:

Not an acronym.It’s a common misconception that SQL stands for structured query language; it stands for S–Q–L and nothing else. Why? Because ISO says so. The official name is Database Language SQL (refer to Figure i.1). Furthermore, referring to it as a structured query language is a disservice to new SQL programmers. It amuses insiders to point out that “structured query language” is the worst possible description because SQL:

About This Book

Chapters 1 through 3 contain expository material about DBMSs, the relational model, and SQL syntax. Later chapters all use a learn-by-example approach to teach you how to use the SQL programming language to maintain and query database information.

You don’t need prior programming experience but you must be familiar with your operating system’s filesystem (the hierarchy of folders and files) and know how to issue commands at a command prompt or shell.

This book isn’t an exhaustive guide to SQL; I’ve limited its scope to the most-used statements and functions (itself a vast topic). For information about other SQL statements, refer to your DBMS’s documentation.

Typographic Conventions

I use the following typographic conventions:

Bold type introduces new terms.

Italic type represents replaceable variables in regular text.

Monospace type denotes SQL code and syntax in listings and in regular text. It also shows command-prompt text.

Highlighted monospace type highlights SQL code fragments and results that are explained in the accompanying text.

Italic monospace type denotes a variable in SQL code that you must replace with a value. You’d replace column with the name of an actual column, for example.

Syntax Conventions

SQL is a free-form language without restrictions on line breaks or the number of words per line, so I use a consistent style in SQL syntax diagrams and code listings to make the code easy to read and maintain:

Using SQL with a Specific DBMS

The DBMS icon flags a vendor-specific departure from the SQL standard. If you see this icon, then it means that a particular vendor’s SQL dialect doesn’t comply with the standard, and you must modify the listed SQL program to run on your DBMS. For example, the standard SQL operator that joins (concatenates) two strings is || (a double pipe), but Microsoft products use + (a plus sign) and MySQL uses the CONCAT() function instead, so you’ll need to change all occurrences of a || b in the example SQL listing to a + b (if you’re using Microsoft Access or Microsoft SQL Server) or to CONCAT(a, b) (if you’re using MySQL). In most cases, the SQL examples will work as is or with minor syntactic changes. Occasionally, SQL code won’t work at all because the DBMS doesn’t support a particular feature.

This book covers the following DBMSs (see Chapter 1 for details):

If you’re using a different DBMS (such as SAP, Teradata, MariaDB, FileMaker, or SAS SQL), and one of the SQL examples doesn’t work, then read the documentation to see how your DBMS’s SQL implementation departs from the SQL standard.

Tip: To compare general and technical information for a number of DBMSs, read the Wikipedia article “Comparison of relational database management systems”.

Server vs. Desktop DBMSs

A server DBMS acts as the server part of a client/server network; it stores databases and responds to SQL requests made by many clients. A client is an application or computer that sends an SQL request to a server and accepts the server’s response. The server does the actual work of executing the SQL against a database; the client merely accepts the answer. If your network uses a client/server architecture, then the client is the computer on your desk, and the server is a powerful, specialized machine in another room, building, or country. Standard database access protocols and interfaces such as ODBC, JDBC, and ADO.NET define how client/server requests and responses are transmitted.

A desktop DBMS is a stand-alone program that can store a database and do all the SQL processing itself or behave as a client of a server. A desktop DBMS can’t accept requests from other clients (that is, it can’t act like a server).

Servers include Microsoft SQL Server, Oracle Database, IBM Db2 Database, MySQL, and PostgreSQL. Desktop systems include Microsoft Access, Claris FileMaker, and LibreOffice Base. By convention, I use the terms client and server to refer to client and server software itself or to the machine on which the software runs, unless the distinction is important.

What You’ll Need

To replicate this book’s examples on your own computer, you’ll need:

A text editor.Typing short or ad-hoc interactive SQL statements at a prompt is convenient, but you’ll want to store nontrivial SQL programs in text files. A text editor is a program that you use to open, create, and edit text files, which contain only printable letters, numbers, and symbols—no formatting, styles, graphics, columns, tables, or any of the clutter usually associated with a word processor. Every operating system includes a free text editor. Windows has Notepad, Unix and Linux have vi and emacs, and macOS has TextEdit, for example. By convention, SQL files have the filename extension .sql, but you can use .txt (or any extension) if you prefer. For programming, professional editors such as Sublime Text (sublimetext.com) and Vim (vim.org) are superior to the basic editors that come with Windows and macOS.

The sample database.The examples in this book use the same database, described in “The Sample Database” in Chapter 2. To build the sample database, follow the instructions in “Creating the Sample Database” in Chapter 2. If you’re working with a production-server DBMS, then you might need permission from your database administrator to run SQL programs that create, update, and delete data and database objects.

A database management system.How do you get SQL? You don’t—you get a DBMS that understands SQL and feed it an SQL program. The DBMS runs your program and displays the results, as described in the next chapter.

1. Running SQL Programs

You need a database management system to run SQL programs. You can have your own private copy of a DBMS running on your personal (local) computer, or you can use a shared DBMS over a network. In the latter case, you use your personal computer to connect to a DBMS server running on another machine. The computer where the DBMS is running is called a host.

Older DBMSs and
Backward Compatibility of SQL

At this writing, the current (“stable”) releases of the DBMSs covered in this book are Microsoft Access 2021, Microsoft SQL Server 2022, Oracle Database 19c, IBM Db2 Database 12, MySQL 8, and PostgreSQL 16. New releases bring new features and fixes, but also bring loss of familiarity, workflow disruptions, new bugs, dropped features, incompatibilities, installation nightmares, endless testing, revised license agreements, new prices, and a chance of data loss. Given these risks and headaches, most DBMS users, from individuals to large organizations, typically are very slow to upgrade their DBMS.

This book favors SQL code that’s backward compatible, meaning it runs on older (“legacy”) systems. Each new release of a DBMS brings additions (whether standard or nonstandard) to its implementation of SQL. Given a choice, opt for tried-and-true legacy SQL code, which runs on legacy and current systems. For basic SQL (SELECT, JOIN, INSERT, CREATE, and so on), the DBMS’s optimizer runs legacy code at the same speed and efficiency as new-style code, so there’s no disadvantage in using legacy SQL code in most cases.

DBMSs and SQL Tools

This chapter describes how to run SQL programs on these DBMSs:

Microsoft Access’s graphical interface lets you run only one SQL statement at a time. The other systems, all DBMS servers, let you run SQL programs in interactive mode or script mode. In interactive mode, you type individual SQL statements at a command prompt and view the results of each statement separately, so input and output are interleaved. In script mode (also called batch mode), you save your entire SQL program in a text file (called a script or a batch file), and a command-line tool takes the file, executes the program, and returns the results without your intervention. I use the sample database and the SQL program shown in Listing 1.1 in all the examples in the rest of this chapter. The examples give the minimal syntax of command-line tools; the complete syntax is given in the DBMS documentation.

Listing 1.1This file, named listing0101.sql, contains a simple SQL SELECT statement, which is used to query the sample database in subsequent DBMS examples.

SELECT au_fname, au_lname
  FROM authors
  ORDER BY au_lname;

The Command Line

Most database professionals prefer to submit commands and SQL scripts through a DBMS’s command-line environment rather than mousing around the menus and windows of a graphical front-end. (Database administrators don’t add 1000 users by pointing and clicking.) If you’re new to DBMSs, then you might find the command line to be cryptic and intimidating, but experience will show you its power, simplicity, and speed. Graphical tools do have a few advantages, though:

You can find free full-featured SQL shells that work across DBMSs by searching the web for sql front end or sql client. Unix lovers stuck with Windows can use Windows Subsystem for Linux (learn.microsoft.com/windows/wsl). Windows PowerShell (learn.microsoft.com/powershell) also provides advanced scripting.

Paths

A path (or pathname) specifies the unique location of a directory (folder) or file in a filesystem hierarchy. An absolute path specifies a location completely, starting at the topmost node of the directory tree, called the root. A relative path specifies a location relative to the current (or working) directory. In Windows, an absolute path starts with a backslash (\) or with a drive letter followed by a colon and a backslash (C:\, for example). In Unix or macOS Terminal, an absolute path starts with a slash (/).

C:\Program Files\Microsoft SQL Server (Windows) and /usr/local/bin/mysql (Unix) are absolute paths, for example. scripts\listing0101.sql (Windows) and doc/readme.txt (Unix) are relative paths. Absolute paths for files and folders on a network also can begin with a double backslash and server name (\\servername, for example). If a path contains spaces, then surround the entire path with double quotes. When you specify the name of an SQL file in script mode, you can include an absolute or relative path.

To run a command-line tool from an arbitrary directory, your PATH environment variable must include the directory that actually contains the tool. This environment variable lists the directories (folders) that the OS searches for programs. For some DBMSs, the installer handles the PATH details; for others, you must add the tool’s directory to PATH yourself.

To view the contents of the PATH variable, type path (Windows) or echo $PATH (Unix or macOS Terminal) at a command prompt. To change the PATH, add the absolute path of the directory in which the tool resides to the PATH environment variable. Search Help for environment variable (Windows), or modify the path command in your login initialization file, usually named .bash_login, .bashrc, .cshrc, .login, .profile, or .shrc (Unix or macOS).

Microsoft Access

Microsoft Access is a personal and commercial desktop DBMS that supports small and medium-size databases. Learn about Access at microsoft.com/microsoft-365/access and download a free trial copy. To determine which version of Access you’re using, choose File tab > Account > About Access.

In Access, you must turn on ANSI-92 SQL syntax to run many of the examples in this book.

To turn on ANSI-92 SQL syntax for a Microsoft Access database:

  1. In Microsoft Access, open the database.
  2. Choose File tab > Options > Object Designers (in the left pane).
  3. Below SQL Server Compatible Syntax (ANSI 92), select “This database”. (Figure 1.1).

    Figure 1.1Select this checkbox to turn on ANSI-92 SQL syntax mode for the open database.

    Screenshot: Selected checkbox for This Database in section SQL Server Compatible Syntax (ANSI 92)

  4. Click OK.

    Access closes, compacts, and then reopens the database before the new setting takes effect. You might see a few warnings, depending on your security settings.

ANSI-89 vs. ANSI-92 SQL

Be careful switching between ANSI-89 and ANSI-92 SQL syntax modes in Microsoft Access. The modes aren’t compatible, so you should pick a mode when you create a database and never change it. The range of data types, reserved words, and wildcard characters differs by mode, so SQL statements created in one mode might not work in the other. The older ANSI-89 standard is limited compared with ANSI-92, so you should choose ANSI-92 syntax for new databases. For more information, see “SQL Standards and Conformance” in Chapter 3.

If you’re using Microsoft Access as a front-end to query a Microsoft SQL Server database, then you must use ANSI-92 syntax, available in Access 2000 or later.

If you’re a casual Access user, then you’ve probably used the query design grid to create a query. When you create a query in Design View, Access builds the equivalent SQL statement behind the scenes for you. You can view, edit, and run the SQL statement in SQL View.

You can run only a single SQL statement through an Access Query object. To run multiple statements, use multiple Query objects or a host language such as Visual Basic or C#.

To run an SQL statement in Microsoft Access:

  1. In Microsoft Access, open a database.
  2. Choose Create tab > Queries group > Query Design (Figure 1.2).

    Figure 1.2Query Design lets you skip the hand-holding wizards.

    Screenshot: Create tab | Queries group | Query Design selected

  3. Without adding tables or queries, click Close in the Show Table dialog box (Figure 1.3).

    Figure 1.3You don’t need to add tables graphically because the SQL statement specifies the tables.

    Screenshot: The Show Table dialog box

  4. To run the SQL statement, choose Design tab > Results group > SQL View (Figure 1.4).

    Figure 1.4SQL View hides the graphical query grid and instead shows a text editor where you can type or paste an SQL statement.

    Screenshot: Design tab | Results group | SQL View selected

  5. Type or paste an SQL statement (Figure 1.5).

    Figure 1.5Enter an SQL statement...

    Screenshot: SQL View text editor with SQL statement SELECT au_fname, au_lname FROM authors ORDER BY au_lname;

  6. Choose Design tab > Results group > Run (Figure 1.6).

    Figure 1.6...and run it.

    Screenshot: Design tab | Results group | Run selected

    Access displays the result of a SELECT statement (Figure 1.7) but blocks or executes other types of SQL statements, with or without warning messages, depending on your settings.

    Figure 1.7Microsoft Access displays the result of a SELECT statement.

    Screenshot: The result of an SQL SELECT statement displayed in a table

Tip: To display a list of existing queries, press F11 to show the Navigation pane (on the left), click the menu at the top of the pane, choose Object Type, click the menu again, and then choose Queries. (The Navigation pane replaced the Database window of early Access versions.)

Microsoft SQL Server

Microsoft SQL Server is a commercial DBMS that supports very large databases and numbers of transactions. It runs on Microsoft Windows and Linux operating systems and is complex enough to require a full-time database administrator (DBA) to run and maintain it.

Learn about SQL Server products at microsoft.com/sql-server and download a free trial copy of SQL Server or a (permanently) free copy of SQL Server Express. SQL Server Express is a free, limited version of SQL Server. To run SQL programs in SQL Server or SQL Server Express, you can use the SQL Server Management Studio graphical tool or the sqlcmd command-line tool.

To determine which version of Microsoft SQL Server you’re using, run the SQL Server command-line command

sqlcmd -S server\instance_name -E -Q "SELECT @@VERSION;"

server\instance_name is the named instance of SQL Server to which to connect. Alternatively, run the query

SELECT SERVERPROPERTY('ProductVersion');

or

SELECT @@VERSION;

Tip: You can use the SET ANSI_DEFAULTS ON option to make SQL Server conform to standard SQL more closely.

To use SQL Server Management Studio:

  1. On the Windows desktop, choose Start > Microsoft SQL Server Tools > Microsoft SQL Server Management Studio.
  2. In the Connect to Server dialog box, select the server and authentication mode, and then click Connect.
  3. In Object Explorer (the left pane), expand the Databases folder of the server that you’re using, and then select a database (Figure 1.8).

    If Object Explorer isn’t visible, then choose View > Object Explorer (or press F8).

    Figure 1.8SQL Server Management Studio uses the selected database to resolve references in your SQL statements.

    Screenshot: The database books selected in the Databases folder of Object Explorer in SQL Server Management Studio

  4. To run SQL interactively, click New Query (on the toolbar) or right-click the database (in Object Explorer) and choose New Query in the context menu. Type or paste an SQL statement in the empty tab that appears in the right pane.

    or

    To run an SQL script, choose File > Open > File (or press Ctrl+O), navigate to and select the script file, and then click Open. The file’s contents appear in a new tab in the right pane.

  5. Click Execute (on the toolbar) or choose Query > Execute (or press F5).

    SQL Server displays the results in the bottom pane (Figure 1.9).

    Figure 1.9The result of a SELECT statement in SQL Server Management Studio.

    Screenshot: The result of an SQL SELECT statement displayed in a table in SQL Server Management Studio

To use the sqlcmd command-line tool interactively:

  1. At an administrator command prompt, type:

    sqlcmd -S server\instance_name -d dbname

    server\instance_name is the named instance of SQL Server to which to connect, and dbname is the name of the database to use.

  2. Type an SQL statement. The statement can span multiple lines. Terminate it with a semicolon (;) and then press Enter.
  3. Type go and then press Enter to display the result. (Figure 1.10).

    Figure 1.10The result of a SELECT statement in sqlcmd interactive mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using sqlcmd in interactive mode at a command prompt

To use the sqlcmd command-line tool in script mode:

  1. At an administrator command prompt, type:

    sqlcmd -S server\instance_name -d dbname -i sql_script

    server\instance_name is the named instance of SQL Server to which to connect, and dbname is the name of the database to use. sql_script is a text file containing SQL statement(s) and can include an absolute or relative path.

  2. Press Enter to display the results (Figure 1.11).

    Figure 1.11The result of a SELECT statement in sqlcmd script mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using sqlcmd in script mode at a command prompt

To exit the sqlcmd command-line tool:

To show sqlcmd command-line options:

sqlcmd tries to use a trusted connection by default. If instead you have to specify a user name and password, then add the option -U login_id. login_id is your user name. sqlcmd will prompt you for your password.

If SQL Server is running on a remote network computer, then the sqlcmd option -S server\instance_name is required to specify the SQL Server instance to connect to. Ask your database administrator for the connection parameters. The -S option also works for local connections, when SQL Server is running on your own personal computer rather than on a server elsewhere.

Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu.

Oracle Database

Oracle Database is a commercial DBMS that supports very large databases and numbers of transactions. It runs on many operating systems and hardware platforms and is complex enough to require a full-time database administrator (DBA) to run and maintain it.

Learn about Oracle products at oracle.com and download Oracle Database Express Edition (XE)—a free, limited version of Oracle Database. Documentation is at docs.oracle.com.

To determine which version of Oracle you’re using, run the query

SELECT banner FROM v$version;

The Oracle version also is displayed in the initial “Connected to” message that appears when you log on to SQL*Plus.

To run SQL programs, use the sqlplus command-line tool.

To use the sqlplus command-line tool interactively:

  1. At an administrator command prompt, type:

    sqlplus user/password@dbname

    user is your Oracle user name, password is your password, and dbname is the name of the database to connect to. For security, you can omit the password and instead type:

    sqlplus user@dbname

    sqlplus will prompt you for your password.

  2. Type an SQL statement. The statement can span multiple lines. Terminate it with a semicolon (;) and then press Enter to display the result (Figure 1.12).

    Figure 1.12The result of a SELECT statement in sqlplus interactive mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using sqlplus in interactive mode at a command prompt

To use the sqlplus command-line tool in script mode:

  1. At an administrator command prompt, type:

    sqlplus user/password@dbname @sql_script

    user is your Oracle user name, password is your password, dbname is the name of the database to connect to, and sql_script is a text file containing SQL statement(s) and can include an absolute or relative path. For security, you can omit the password, and instead type:

    sqlplus user@dbname @sql_script

    sqlplus will prompt you for your password.

  2. Press Enter to display the results (Figure 1.13).

    Figure 1.13The result of a SELECT statement in sqlplus script mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using sqlplus in script mode at a command prompt

To exit the sqlplus command-line tool:

To show sqlplus command-line options:

If you’re running Oracle locally, then you can use the user name system and the password that you specified when you created the database:

sqlplus system@dbname

If you’re connecting to a remote Oracle database, then ask your database administrator for the connection parameters.

An alternative way to start sqlplus in Windows is to choose Start > Oracle > SQL Plus.

Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu.

IBM Db2 Database

IBM Db2 Database is a commercial DBMS that supports very large databases and numbers of transactions. It runs on many operating systems and hardware platforms and is complex enough to require a full-time database administrator (DBA) to run and maintain it.

Learn about Db2 products at ibm.com/products/db2 and download a free trial copy of Db2 or a (permanently) free copy of IBM Db2 Community Edition.

To determine which version of Db2 you’re using, run the Db2 command-line command

db2level

or run the query

SELECT service_level
  FROM SYSIBMADM.ENV_INST_INFO;

To run SQL programs, use the IBM Data Studio graphical tool or the db2 command-line processor (CLP).

To use Data Studio:

  1. Open Data Studio.

    This procedure varies by platform. In Microsoft Windows, for example, choose Start > IBM Data Studio > Data Studio Client.

  2. On the Administration Explorer tab (on the left), expand the All Databases folder of the object tree until you find your instance of Db2, and then select a database (below the Db2 instance).

    If the desired database doesn’t appear, then right-click the All Databases folder, choose “New Connection to a Database” in the context menu, and then connect to the target database.

  3. To run SQL interactively, click New SQL Script The New SQL Script button in IBM Db2 Data Studio on the Administration Explorer toolbar. On the Script tab that opens, type or paste an SQL statement in the box.

    or

    To run an SQL script, choose File > Open File, navigate to and select the script file, and then click Open. On the File tab that opens, click No Connection (if it appears) to connect to the target database.

  4. Choose Script > Run SQL or click The Run SQL button in IBM Db2 Data Studio.

    Data Studio displays the results in the SQL Results tab (at the bottom). Click the Result tab to see the query results or click the Status tab to see query-processing information.

    The result of a SELECT statement in IBM Data Studio. (Click image to enlarge.)

    Screenshot: The result of an SQL SELECT statement displayed in a table in IBM Db2 Data Studio

For technical reasons involving parent and child processes, (only) Microsoft Windows users must start the db2 command-line processor with a special preliminary step.

To start the db2 command-line processor in Windows:

You must use the Db2 CLP window for all db2 commands (described next). If you try to run db2 at a normal Windows command prompt, then Db2 responds with the error “Command line environment not initialized.”

If you launch a new Db2 CLP window via the db2cmd command, then you can close the original command-prompt window.

In the Db2 CLP window, change (cd) your working directory if necessary before you run db2 commands.

To use the db2 command-line processor interactively:

  1. At a command prompt, type:

    db2 -t

    and then press Enter. The -t option tells db2 that a semicolon (;) terminates statements.

    The db2 => prompt appears.

  2. At the db2 prompt, type:

    connect to dbname;

    and then press Enter. dbname is the name of the database to use.

  3. Type an SQL statement. The statement can span multiple lines. Terminate it with a semicolon (;) and then press Enter to display the result (Figure 1.15).

    Figure 1.15The result of a SELECT statement in db2 interactive mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using db2 in interactive mode at a command prompt

Alternatively, you can avoid the db2 => prompt by typing commands and SQL statements right on the command line. For example:

db2 connect to books
db2 SELECT * FROM authors

If you omit the -t option, as here, then don’t terminate commands and SQL statements with a semicolon.

To use the db2 command-line processor in script mode:

  1. At a command prompt, type:

    db2 connect to dbname

    dbname is the name of the database to use.

  2. At a command prompt, type:

    db2 -t -f sql_script

    sql_script is a text file containing SQL statement(s) and can include an absolute or relative path. The -t option tells db2 that a semicolon (;) terminates statements. Add the -v option if you want to echo the contents of sql_script in the output.

  3. Press Enter to display the results (Figure 1.16).

    Figure 1.16The result of a SELECT statement in db2 script mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using db2 in script mode at a command prompt

Tip: An alternative script tool is db2batch.

To exit the db2 command-line tool:

To show db2 command-line options:

MySQL

MySQL (pronounced my-es-kyoo-el) is an open-source DBMS that supports large databases and numbers of transactions. MySQL is known for its speed and ease of use. It’s free for personal use and runs on many operating systems and hardware platforms. You can download it at mysql.com.

To determine which version of MySQL you’re using, run the MySQL command-line command

mysql -V

or run the query

SELECT VERSION();

To run SQL programs, use the mysql command-line tool.

To use the mysql command-line tool interactively:

  1. At an administrator command prompt, type:

    mysql -h host -u user -p dbname

    host is the host name, user is your MySQL user name, and dbname is the name of the database to use. MySQL will prompt you for your password (for a passwordless user, either omit the -p option or press Enter at the password prompt).

  2. Type an SQL statement. The statement can span multiple lines. Terminate it with a semicolon (;) and then press Enter to display the result (Figure 1.18).

    Figure 1.18The result of a SELECT statement in mysql interactive mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using mysql in interactive mode at a command prompt

To use the mysql command-line tool in script mode:

  1. At an administrator command prompt, type:

    mysql -h host -u user -p -t dbname < sql_script

    host is the host name, user is your MySQL user name, and dbname is the name of the database to use. MySQL will prompt you for your password (for a passwordless user, either omit the -p option or press Enter at the password prompt). The -t option formats the results as a table; omit this option if you want tab-delimited output. The redirection operator < reads from the file sql_script, which is a text file containing SQL statement(s) and can include an absolute or relative path.

  2. Press Enter to display the results (Figure 1.19).

    Figure 1.19The result of a SELECT statement in mysql script mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using mysql in script mode at a command prompt

To exit the mysql command-line tool:

To show mysql command-line options:

If MySQL is running on a remote network computer, then ask your database administrator for the connection parameters. If you’re running MySQL locally (that is, on your own computer), then set host to localhost, set user to root, and use the password that you assigned to root when you set up or installed MySQL.

Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu.

PostgreSQL

PostgreSQL (pronounced post-gres-kyoo-el) is an open-source DBMS that supports large databases and numbers of transactions. PostgreSQL is known for its rich feature set and its high conformance with standard SQL. It’s free and runs on many operating systems and hardware platforms. You can download it at postgresql.org.

To determine which version of PostgreSQL you’re using, run the PostgreSQL command-line command

psql -V

or run the query

SELECT VERSION();

To run SQL programs, use the psql command-line tool.

To use the psql command-line tool interactively:

  1. At an administrator command prompt, type:

    psql -h host -U user -W dbname

    host is the host name, user is your PostgreSQL user name, and dbname is the name of the database to use. PostgreSQL will prompt you for your password (for a passwordless user, either omit the -W option or press Enter at the password prompt).

  2. Type an SQL statement. The statement can span multiple lines. Terminate it with a semicolon (;) and then press Enter to display the result (Figure 1.21).

    Figure 1.21The result of a SELECT statement in psql interactive mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using psql in interactive mode at a command prompt

To use the psql command-line tool in script mode:

  1. At an administrator command prompt, type:

    psql -h host -U user -W -f sql_script dbname

    host is the host name, user is your PostgreSQL user name, and dbname is the name of the database to use. PostgreSQL will prompt you for your password (for a passwordless user, either omit the -W option or press Enter at the password prompt). The -f option specifies the name of the SQL file sql_script, which is a text file containing SQL statement(s) and can include an absolute or relative path.

  2. Press Enter to display the results (Figure 1.22).

    Figure 1.22The result of a SELECT statement in psql script mode. (Click image to enlarge.)

    Screenshot: The result of running an SQL SELECT statement by using psql in script mode at a command prompt

To exit the psql command-line tool:

To show psql command-line options:

If PostgreSQL is running on a remote network computer, then ask your database administrator for the connection parameters. If you’re running PostgreSQL locally (that is, on your own computer), then set host to localhost, set user to postgres, and use the password that you assigned to postgres when you set up or installed PostgreSQL.

You can set the environment variables PGHOST, PGDATABASE, and PGUSER to specify the default host, database, and user names used to connect to the database. See “Environment Variables” in the PostgreSQL documentation.

As an alternative to the command prompt, you can use the pgAdmin graphical tool. If the PostgreSQL installer didn’t install pgAdmin automatically, then you can download it for free at pgadmin.org.

Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu.

2. The Relational Model

To program competently in SQL, you must be familiar with the standard data model for organizing and managing data, called the relational model (Figure 2.1).

Figure 2.1Relational databases are based on the data model that Edgar F. Codd defined in A Relational Model of Data for Large Shared Data Banks (Communications of the ACM, Vol. 13, No. 6, June 1970, pp. 377–387).

Document: Abstract of A Relational Model of Data for Large Shared Data Banks by Edgar F. Codd

The foundation of the relational model, set theory, makes you think in terms of sets of data rather than individual items or rows of data. The model describes how to perform common algebraic operations (such as unions and intersections) on database tables in much the same way that they’re performed on mathematical sets.

The Venn diagram in Figure 2.2 expresses the results of operations on sets. The rectangle (U) represents the universe, and the circles (A and B) inside represent sets of objects. The relative position and overlap of the circles indicate relationships between sets. In the relational model, the circles are tables, and the rectangle is all the information in a database.

Tables are analogues of sets: they’re collections of distinct elements having common properties. A mathematical set would contain positive integers, for example, whereas a database table would contain information about, say, students.

Figure 2.2A Venn diagram expresses the results of operations on sets.

Diagram: A Venn diagram showing overlapping circles A and B within the universe U

Tables, Columns, and Rows

If you’re familiar with databases already, then you’ve heard alternative terms for similar concepts. Table 2.1 shows how these terms are related. E. F. Codd’s relational-model terms are in the first column; SQL-standard and DBMS-documentation terms are in the second column; and the third-column terms are holdovers from traditional (nonrelational) file processing. SQL terms are used in this book (although in formal texts the SQL and Model terms never are used interchangeably).

Table 2.1Similar Concepts
Model SQL Files
Relation Table File
Attribute Column Field
Tuple Row Record

Tables

From a user’s point of view, a database is a collection of one or more tables (and nothing but tables). A table:

Columns

Columns in a given table have these characteristics:

Rows

Rows in a given table have these characteristics:

Tips for Tables, Columns, and Rows

Primary Keys

Every value in a database must be accessible. Values are stored at row–column intersections in tables, so a value’s location must refer to a specific table, column, and row. You can identify a table or column by its unique name. Rows are unnamed, however, and need a different identification mechanism called a primary key. A primary key is:

A database designer designates each table’s primary key. This process is crucial because the consequence of a poor key choice is the inability to add data (rows) to a table. I’ll review the essentials here, but read a database-design book if you want to learn more about this topic.

Suppose that you need to choose a primary key for the table in Figure 2.8. The columns au_fname and au_lname separately won’t work because each one violates the uniqueness requirement. Combining au_fname and au_lname into a composite key won’t work because two authors might share a name. Names generally make poor keys because they’re unstable (people divorce, companies merge, spellings change). The correct choice is au_id, which I created to identify authors uniquely. Database designers create unique identifiers when natural or obvious ones (such as names) won’t work.

Figure 2.8The column au_id is the primary key in this table.

au_id au_fname      au_lname
----- ------------- -------------
A01   Sarah         Buchman
A02   Wendy         Heydemark
A03   Hallie        Hull
A04   Klee          Hull

After a primary key is defined, your DBMS will enforce the integrity of table data. You can’t insert the following row because the au_id value A02 already exists in the table:

A02   Christian    Kells

Nor can you insert this row because au_id can’t be null:

NULL  Christian    Kells

This row is legal:

A05   Christian    Kells

Tips for Primary Keys

Learning Database Design

To learn how to design production databases, read an academic text for a grounding in relational algebra, entity–relationship (E–R) modeling, Codd’s relational model, system architecture, nulls, integrity, and other crucial concepts. I like Chris Date’s An Introduction to Database Systems, but alternatives abound—a cheaper (and chattier) option is Date’s Database in Depth. A modern introduction to set theory and logic is Applied Mathematics for Database Professionals by Lex de Haan and Toon Koppelaars. Classical introductions include Robert Stoll’s Set Theory and Logic and the gentler Logic by Wilfrid Hodges. You also can search the web for articles by E. F. Codd, Chris Date, Fabian Pascal, and Hugh Darwen. All this material might seem like overkill, but you’ll be surprised at how complex a database becomes after adding a few tables, constraints, triggers, and stored procedures. As in all fields, a practical grasp of theory lets you predict results and avoid trial-and-error fixes when things go wrong.

Avoid mass-market junk like Database Design for Dummies/Mere Mortals. If you rely on their guidance, then you will create databases where you get answers that you know are wrong, can’t retrieve the information that you want, enter the same data over and over, or type in values only to have them go “missing.” Such books gloss over (or omit) first principles in favor of administrivia like choosing identifier names and interviewing subject-matter experts.

Foreign Keys

Information about different entities is stored in different tables, so you need a way to navigate between tables. The relational model provides a mechanism called a foreign key to associate tables. A foreign key has these characteristics:

Figure 2.9 shows a primary- and foreign-key relationship between two tables.

Figure 2.9The column pub_id is a foreign key of the table titles that references the column pub_id of publishers.

Diagram: The primary-key and foreign-key relationship between the table publishers and the table titles

After a foreign key is defined, your DBMS will enforce referential integrity. You can’t insert the following row into the child table titles because the pub_id value P05 doesn’t exist in the parent table publishers:

T07    I Blame My Mother         P05

You can insert this row only if the foreign key accepts nulls:

T07    I Blame My Mother         NULL

This row is legal:

T07    I Blame My Mother         P03

Tips for Foreign Keys

Relationships

A relationship is an association established between common columns in two tables. A relationship can be:

One-to-One

In a one-to-one relationship, each row in table A can have at most one matching row in the table B, and each row in table B can have at most one matching row in table A. Even though it’s practicable to store all the information from both tables in only one table, one-to-one relationships often are used to segregate confidential information for privacy and security reasons, to speed queries by splitting single monolithic tables, and to avoid inserting nulls into tables (see “Nulls” in Chapter 3).

A one-to-one relationship is established when the primary key of one table also is a foreign key referencing the primary key of another table (Figure 2.10 and Figure 2.11).

Figure 2.10A one-to-one relationship. Each row in titles can have at most one matching row in royalties, and each row in royalties can have at most one matching row in titles. Here, the primary key of royalties also is a foreign key referencing the primary key of titles.

Diagram: The one-to-one relationship between the table titles and the table royalties

Figure 2.11This diagram shows an alternative way to depict the one-to-one relationship in Figure 2.10. The connecting line indicates associated columns. The PK symbol indicates a primary key.

Diagram: An alternative way to show the preceding one-to-one relationship

One-to-Many

In a one-to-many relationship, each row in table A can have many (zero or more) matching rows in table B, but each row in table B has only one matching row in table A. A publisher can publish many books, but each book is published by only one publisher, for example.

One-to-many relationships are established when the primary key of the “one” table appears as a foreign key in the “many” table (Figure 2.12 and Figure 2.13).

Figure 2.12A one-to-many relationship. Each row in publishers can have many matching rows in titles, and each row in titles has only one matching row in publishers. Here, the primary key of publishers (the “one” table) appears as a foreign key in titles (the “many” table).

Diagram: The one-to-many relationship between the table publishers (one) and the table titles (many)

Figure 2.13This diagram shows an alternative way to depict the one-to-many relationship in Figure 2.12. The connecting line’s unadorned end indicates the “one” table, and the arrow indicates the “many” table. The PK symbol indicates a primary key.

Diagram: An alternative way to show the preceding one-to-many relationship

Many-to-Many

In a many-to-many relationship, each row in table A can have many (zero or more) matching rows in table B, and each row in table B can have many matching rows in table A. Each author can write many books, and each book can have many authors, for example.

A many-to-many relationships is established only by creating a third table called a junction table, whose composite primary key is a combination of both tables’ primary keys; each column in the composite key separately is a foreign key. This technique always produces a unique value for each row in the junction table and splits the many-to-many relationship into two separate one-to-many relationships (Figure 2.14 and Figure 2.15).

Figure 2.14A many-to-many relationship. The junction table title_authors splits the many-to-many relationship between titles and authors into two one-to-many relationships. Each row in titles can have many matching rows in title_authors, as can each row in authors. Here, title_id in title_authors is a foreign key that references the primary key of titles, and au_id in title_authors is a foreign key that references the primary key of authors.

Diagram: The many-to-many relationship between the table titles and the table authors, with junction table title_authors

Figure 2.15This diagram shows an alternative way to depict the many-to-many relationship in Figure 2.14. The PK symbol indicates a primary key.

Diagram: An alternative way to show the preceding many-to-many relationship

Tips for Relationships

Normalization

It’s possible to consolidate all information about books (or any entity type) into a single monolithic table, but that table would be loaded with duplicate data; each title (row) would contain redundant author, publisher, and royalty details. Redundancy is the enemy of database users and administrators: it causes databases to grow wildly large, it slows queries, and it’s a maintenance and reporting nightmare. (When someone moves, you want to change his address in one place, not thousands of places.)

Redundancies lead to a variety of update anomalies—that is, difficulties with operations that insert, update, and delete rows. Normalization is the process—a series of steps—of modifying tables to reduce redundancy and inconsistency. After each step, the database is in a particular normal form. The relational model defines three normal forms, named after famous ordinal numbers:

Each normal form is stronger than its predecessors; a database in 3NF also is in 2NF and 1NF. Higher normalization levels tend to increase the number of tables relative to lower levels. Lossless decomposition ensures that table splitting doesn’t cause information loss, and dependency-preserving decomposition ensures that relationships aren’t lost. The matching primary- and foreign-key columns that appear when tables are split are not considered to be redundant data.

Normalization is not systematic; it’s an iterative process that involves repeated table splitting and rejoining and refining until the database designer is (temporarily) happy with the result.

First Normal Form

A table in first normal form:

An atomic value, also called a scalar value, is a single value that can’t be subdivided (Figure 2.16). A repeating group is a set of two or more logically related columns (Figure 2.17). To fix these problems, store the data in two related tables (Figure 2.18).

Figure 2.16In first normal form, each table’s row–column intersection must contain a single value that can’t be subdivided meaningfully. The column authors in this table lists multiple authors and so violates 1NF.

title_id title_name                       authors
-------- -------------------------------- -------------
T01      1977!                            A01
T04      But I Did It Unconsciously       A03, A04
T11      Perhaps It's a Glandular Problem A03, A04, A06

Figure 2.17Redistributing the column authors into a repeating group also violates 1NF. Don’t represent multiple instances of an entity as multiple columns.

title_id title_name                       author1 author2 author3
-------- -------------------------------- ------- ------- -------
T01      1977!                            A01
T04      But I Did It Unconsciously       A03     A04
T11      Perhaps It's a Glandular Problem A03     A04     A06

Figure 2.18The correct design solution is to move the author information to a new child table that contains one row for each author of a title. The primary key in the parent table is title_id, and the composite key in the child table is title_id and au_id.

Diagram: Tables in first normal form

A database that violates 1NF causes problems:

Atomicity

Atomic values are perceived to be indivisible from the point of view of database users. A date, a telephone number, and a character string, for example, aren’t actually intrinsically indivisible because you can decompose the date into a year, month, and day; the telephone number into a country code, area code, prefix, and line number; and the string into its individual characters. What’s important as far as you’re concerned is that the DBMS provide operators and functions that let you extract and manipulate the components of “atomic” values if necessary, such as a substring() function to extract a telephone number’s area code or a year() function to extract a date’s year.

Second Normal Form

A table in second normal form:

A table contains a partial functional dependency if some (but not all) of a composite key’s values determine a nonkey column’s value. A 2NF table is fully functionally dependent, meaning that a nonkey column’s value might need to be updated if any column values in the composite key change.

The composite key in the table in Figure 2.19 is title_id and au_id. The nonkey columns are au_order (the order in which authors are listed on the cover of a book with multiple authors) and au_phone (the author’s telephone number).

Figure 2.19au_phone depends on au_id but not title_id, so this table contains a partial functional dependency and isn’t in 2NF. The PK symbol indicates a primary key.

Diagram: A table not in second normal form

For each nonkey column, ask, “Can I determine a nonkey column value if I know only part of the primary-key value?” A no answer means the nonkey column is fully functionally dependent (good); a yes answer means that it’s partially functionally dependent (bad).

For the column au_order, the questions are:

Good—au_order is fully functionally dependent and can remain in the table. This dependency is denoted by

{title_id, au_id} → {au_order}

and is read “title_id and au_id determine au_order” or “au_order depends on title_id and au_id.” The determinant is the expression to the left of the arrow.

For the column au_phone, the questions are:

Bad—au_phone is partially functionally dependent and must be moved elsewhere (probably to an authors or telephone_numbers table) to satisfy 2NF rules.

Instant 2NF

A 1NF table is in second normal form automatically if:

Third Normal Form

A table in third normal form:

A table contains a transitive dependency if a nonkey column’s value determines another nonkey column’s value. In 3NF tables, nonkey columns are mutually independent and dependent on only primary-key column(s). 3NF is the next logical step after 2NF.

The primary key in the table in Figure 2.20 is title_id. The nonkey columns are price (the book’s price), pub_city (the city where the book is published), and pub_id (the book’s publisher).

Figure 2.20pub_city depends on pub_id, so this table contains a transitive dependency and isn’t in 3NF. The PK symbol indicates a primary key.

Diagram: A table not in third normal form

For each nonkey column, ask, “Can I determine a nonkey column value if I know any other nonkey column value?” A no answer means that the column is not transitively dependent (good); a yes answer means that the column whose value you can determine is transitively dependent on the other column (bad).

For the column price, the questions are:

For the column pub_city, the questions are:

For the column pub_id, the questions are:

Bad—pub_city is transitively dependent on pub_id and must be moved elsewhere (probably to a publishers table) to satisfy 3NF rules.

As you can see, it’s not enough to ask, “Can I determine A if I know B?” to discover a transitive dependency; you also must ask, “Can I determine B if I know A?”

Other Normal Forms

Higher levels of normalization exist, but the relational model doesn’t require (or even mention) them. They’re useful in some cases to avoid redundancy. Briefly, they are:

Denormalization

The increased number of tables that normalization generates might sway you to denormalize your database to speed queries (because having fewer tables reduces computationally expensive joins and storage throughput). This common technique trades off data integrity for performance and presents a few other problems. A denormalized database:

The need for denormalization isn’t a weakness in the relational model but reveals a flawed implementation of the model in DBMSs. A common use for denormalized tables is as permanent logs of data copied from other tables. The logged rows are redundant, but because they’re only INSERTed (never UPDATEd), they serve as an audit trail immune to future changes in the source tables.

The Sample Database

Pick up an SQL or database-design book, and probably you’ll find a students/courses/teachers, customers/orders/products, or authors/books/publishers database. In a bow to convention, most of the SQL examples in this book use an authors/books/publishers sample database named books. Here are some things that you should know about books:

Figure 2.21The sample database books. PK denotes a primary key. (Click image to enlarge.)

Diagram: The sample database, named books, with tables authors, publishers, titles, title_authors, and royalties

The Table authors

The table authors describes the books’ authors. Each author has a unique identifier (au_id) that’s the primary key. Table 2.3 shows the structure of the table authors, and Figure 2.22 shows its contents.

Table 2.3Table Structure of authors
Column Name Description Data Type Nulls? Keys
au_id Unique author identifier CHAR(3) PK
au_fname Author first name VARCHAR(15)
au_lname Author last name VARCHAR(15)
phone Author telephone number VARCHAR(12) Yes
address Author address VARCHAR(20) Yes
city Author city VARCHAR(15) Yes
state Author state CHAR(2) Yes
zip Author zip (postal) code CHAR(5) Yes

Figure 2.22The contents of the table authors.

au_id au_fname  au_lname    phone        address              city           state zip
----- --------- ----------- ------------ -------------------- -------------- ----- -----
A01   Sarah     Buchman     718-496-7223 75 West 205 St       Bronx          NY    10468
A02   Wendy     Heydemark   303-986-7020 2922 Baseline Rd     Boulder        CO    80303
A03   Hallie    Hull        415-549-4278 3800 Waldo Ave, #14F San Francisco  CA    94123
A04   Klee      Hull        415-549-4278 3800 Waldo Ave, #14F San Francisco  CA    94123
A05   Christian Kells       212-771-4680 114 Horatio St       New York       NY    10014
A06             Kellsey     650-836-7128 390 Serra Mall       Palo Alto      CA    94305
A07   Paddy     O'Furniture 941-925-0752 1442 Main St         Sarasota       FL    34236

The Table publishers

The table publishers describes the books’ publishers. Each publisher has a unique identifier (pub_id) that’s the primary key. Table 2.4 shows the structure of the table publishers, and Figure 2.23 shows its contents.

Table 2.4Table Structure of publishers
Column Name Description Data Type Nulls? Keys
pub_id Unique publisher identifier CHAR(3) PK
pub_name Publisher name VARCHAR(20)
city Publisher city VARCHAR(15)
state Publisher state/province/region CHAR(2) Yes
country Publisher country VARCHAR(15)

Figure 2.23The contents of the table publishers.

pub_id pub_name            city          state country
------ ------------------- ------------- ----- -------
P01    Abatis Publishers   New York      NY    USA
P02    Core Dump Books     San Francisco CA    USA
P03    Schadenfreude Press Hamburg       NULL  Germany
P04    Tenterhooks Press   Berkeley      CA    USA

The Table titles

The table titles describes the books. Each book has a unique identifier (title_id) that’s the primary key. titles contains a foreign key, pub_id, that references the table publishers to indicate a book’s publisher. Table 2.5 shows the structure of the table titles, and Figure 2.24 shows its contents.

Table 2.5Table Structure of titles
Column Name Description Data Type Nulls? Keys
title_id Unique title identifier CHAR(3) PK
title_name Book title VARCHAR(40)
type Subject of the book VARCHAR(10) Yes
pub_id Publisher identifier CHAR(3) FK publishers(pub_id)
pages Page count INTEGER Yes
price Cover price DECIMAL(5,2) Yes
sales Lifetime number of copies sold INTEGER Yes
pubdate Date of publication DATE Yes
contract Nonzero if author(s) signed contract SMALLINT

Figure 2.24The contents of the table titles.

title_id title_name                           type       pub_id pages price sales   pubdate    contract
-------- ------------------------------------ ---------- ------ ----- ----- ------- ---------- --------
T01      1977!                                history    P01      107 21.99     566 2000-08-01        1
T02      200 Years of German Humor            history    P03       14 19.95    9566 1998-04-01        1
T03      Ask Your System Administrator        computer   P02     1226 39.95   25667 2000-09-01        1
T04      But I Did It Unconsciously           psychology P04      510 12.99   13001 1999-05-31        1
T05      Exchange of Platitudes               psychology P04      201  6.95  201440 2001-01-01        1
T06      How About Never?                     biography  P01      473 19.95   11320 2000-07-31        1
T07      I Blame My Mother                    biography  P03      333 23.95 1500200 1999-10-01        1
T08      Just Wait Until After School         children   P04       86 10.00    4095 2001-06-01        1
T09      Kiss My Boo-Boo                      children   P04       22 13.95    5000 2002-05-31        1
T10      Not Without My Faberge Egg           biography  P01     NULL  NULL    NULL       NULL        0
T11      Perhaps It's a Glandular Problem     psychology P04      826  7.99   94123 2000-11-30        1
T12      Spontaneous, Not Annoying            biography  P01      507 12.99  100001 2000-08-31        1
T13      What Are The Civilian Applications?  history    P03      802 29.99   10467 1999-05-31        1

The Table title_authors

Authors and books have a many-to-many relationship because an author can write multiple books and a book can have multiple authors. title_authors is the junction table that associates the tables authors and titles; see “Relationships” earlier in this chapter. title_id and au_id together form a composite primary key, and each column separately is a foreign key that references titles and authors, respectively. The nonkey columns indicate the order of the author’s name on the book’s cover (always 1 for a book with a sole author) and the fraction of total royalties that each author receives (always 1.0 for a book with a sole author). Table 2.6 shows the structure of the table title_authors, and Figure 2.25 shows its contents.

Table 2.6Table Structure of title_authors
Column Name Description Data Type Nulls? Keys
title_id Title identifier CHAR(3) PK, FK titles(title_id)
au_id Author identifier CHAR(3) PK, FK authors(au_id)
au_order Author name order on book cover SMALLINT
royalty_share Author fractional royalty share DECIMAL(5,2)

Figure 2.25The contents of the table title_authors.

title_id au_id au_order royalty_share
-------- ----- -------- -------------
T01      A01          1          1.00
T02      A01          1          1.00
T03      A05          1          1.00
T04      A03          1          0.60
T04      A04          2          0.40
T05      A04          1          1.00
T06      A02          1          1.00
T07      A02          1          0.50
T07      A04          2          0.50
T08      A06          1          1.00
T09      A06          1          1.00
T10      A02          1          1.00
T11      A03          2          0.30
T11      A04          3          0.30
T11      A06          1          0.40
T12      A02          1          1.00
T13      A01          1          1.00

The Table royalties

The table royalties specifies the royalty rate paid to all the authors (not each author) of each book, including the total up-front advance against royalties paid to all authors (again, not each author) of a book. The royalties primary key is title_id. The table royalties has a one-to-one relationship with titles, so the royalties primary key also is a foreign key that references the titles primary key. Table 2.7 shows the structure of the table royalties, and Figure 2.26 shows its contents.

Table 2.7Table Structure of royalties
Column Name Description Data Type Nulls? Keys
title_id Unique title identifier CHAR(3) PK, FK titles(title_id)
advance Up-front payment to author(s) DECIMAL(9,2) Yes
royalty_rate Fraction of revenue paid author(s) DECIMAL(5,2) Yes

Figure 2.26The contents of the table royalties.

title_id advance     royalty_rate
-------- ----------- ------------
T01         10000.00         0.05
T02          1000.00         0.06
T03         15000.00         0.07
T04         20000.00         0.08
T05        100000.00         0.09
T06         20000.00         0.08
T07       1000000.00         0.11
T08             0.00         0.04
T09             0.00         0.05
T10             NULL         NULL
T11        100000.00         0.07
T12         50000.00         0.09
T13         20000.00         0.06

Creating the Sample Database

To create (or re-create) the database books on your own DBMS, visit questingvolepress.com, click the Download link for this book, and then follow the onscreen instructions. Creating books is a two-step process:

  1. Use your DBMS’s built-in tools to create a new, blank database named books.
  2. Run an SQL script that creates tables within books and populates them with data.

Listing 2.1 shows a standard (ISO/ANSI) SQL script that creates the sample-database tables and inserts rows into them.

If you’re using Microsoft Access, then you don’t run a script—you simply open an Access database file.

Listing 2.1This standard SQL script, books_standard.sql, creates the tables in the sample database books and populates them with data. The file that you download at the companion website includes versions of this script changed to run on specific DBMSs.

DROP TABLE authors;
CREATE TABLE authors
  (
  au_id    CHAR(3)     NOT NULL,
  au_fname VARCHAR(15) NOT NULL,
  au_lname VARCHAR(15) NOT NULL,
  phone    VARCHAR(12)         ,
  address  VARCHAR(20)         ,
  city     VARCHAR(15)         ,
  state    CHAR(2)             ,
  zip      CHAR(5)             ,
  CONSTRAINT pk_authors
    PRIMARY KEY (au_id)
  );
INSERT INTO authors VALUES('A01','Sarah','Buchman','718-496-7223', '75 West 205 St','Bronx','NY','10468');
INSERT INTO authors VALUES('A02','Wendy','Heydemark','303-986-7020', '2922 Baseline Rd','Boulder','CO','80303');
INSERT INTO authors VALUES('A03','Hallie','Hull','415-549-4278','3800 Waldo Ave, #14F','San Francisco','CA','94123');
INSERT INTO authors VALUES('A04','Klee','Hull','415-549-4278', '3800 Waldo Ave, #14F','San Francisco','CA','94123');
INSERT INTO authors VALUES('A05','Christian','Kells','212-771-4680', '114 Horatio St','New York','NY','10014');
INSERT INTO authors VALUES('A06','','Kellsey','650-836-7128', '390 Serra Mall','Palo Alto','CA','94305');
INSERT INTO authors VALUES('A07','Paddy','O''Furniture','941-925-0752', '1442 Main St','Sarasota','FL','34236');

DROP TABLE publishers;
CREATE TABLE publishers
  (
  pub_id   CHAR(3)     NOT NULL,
  pub_name VARCHAR(20) NOT NULL,
  city     VARCHAR(15) NOT NULL,
  state    CHAR(2)             ,
  country  VARCHAR(15) NOT NULL,
  CONSTRAINT pk_publishers
    PRIMARY KEY (pub_id)
  );
INSERT INTO publishers VALUES('P01','Abatis Publishers','New York','NY','USA');
INSERT INTO publishers VALUES('P02','Core Dump Books','San Francisco','CA','USA');
INSERT INTO publishers VALUES('P03','Schadenfreude Press','Hamburg',NULL,'Germany');
INSERT INTO publishers VALUES('P04','Tenterhooks Press','Berkeley','CA','USA');

DROP TABLE titles;
CREATE TABLE titles
  (
  title_id   CHAR(3)      NOT NULL,
  title_name VARCHAR(40)  NOT NULL,
  type       VARCHAR(10)          ,
  pub_id     CHAR(3)      NOT NULL,
  pages      INTEGER              ,
  price      DECIMAL(5,2)         ,
  sales      INTEGER              ,
  pubdate    DATE                 ,
  contract   SMALLINT     NOT NULL,
  CONSTRAINT pk_titles
    PRIMARY KEY (title_id)
  );
INSERT INTO titles VALUES('T01','1977!','history', 'P01',107,21.99,566,DATE '2000-08-01',1);
INSERT INTO titles VALUES('T02','200 Years of German Humor','history', 'P03',14,19.95,9566,DATE '1998-04-01',1);
INSERT INTO titles VALUES('T03','Ask Your System Administrator','computer', 'P02',1226,39.95,25667,DATE '2000-09-01',1);
INSERT INTO titles VALUES('T04','But I Did It Unconsciously','psychology', 'P04',510,12.99,13001,DATE '1999-05-31',1);
INSERT INTO titles VALUES('T05','Exchange of Platitudes','psychology', 'P04',201,6.95,201440,DATE '2001-01-01',1);
INSERT INTO titles VALUES('T06','How About Never?','biography', 'P01',473,19.95,11320,DATE '2000-07-31',1);
INSERT INTO titles VALUES('T07','I Blame My Mother','biography', 'P03',333,23.95,1500200,DATE '1999-10-01',1);
INSERT INTO titles VALUES('T08','Just Wait Until After School','children', 'P04',86,10.00,4095,DATE '2001-06-01',1);
INSERT INTO titles VALUES('T09','Kiss My Boo-Boo','children', 'P04',22,13.95,5000,DATE '2002-05-31',1);
INSERT INTO titles VALUES('T10','Not Without My Faberge Egg','biography', 'P01',NULL,NULL,NULL,NULL,0);
INSERT INTO titles VALUES('T11','Perhaps It''s a Glandular Problem','psychology', 'P04',826,7.99,94123,DATE '2000-11-30',1);
INSERT INTO titles VALUES('T12','Spontaneous, Not Annoying','biography', 'P01',507,12.99,100001,DATE '2000-08-31',1);
INSERT INTO titles VALUES('T13','What Are The Civilian Applications?','history', 'P03',802,29.99,10467,DATE '1999-05-31',1);

DROP TABLE title_authors;
CREATE TABLE title_authors
  (
  title_id      CHAR(3)      NOT NULL,
  au_id         CHAR(3)      NOT NULL,
  au_order      SMALLINT     NOT NULL,
  royalty_share DECIMAL(5,2) NOT NULL,
  CONSTRAINT pk_title_authors
    PRIMARY KEY (title_id, au_id)
  );
INSERT INTO title_authors VALUES('T01','A01',1,1.0);
INSERT INTO title_authors VALUES('T02','A01',1,1.0);
INSERT INTO title_authors VALUES('T03','A05',1,1.0);
INSERT INTO title_authors VALUES('T04','A03',1,0.6);
INSERT INTO title_authors VALUES('T04','A04',2,0.4);
INSERT INTO title_authors VALUES('T05','A04',1,1.0);
INSERT INTO title_authors VALUES('T06','A02',1,1.0);
INSERT INTO title_authors VALUES('T07','A02',1,0.5);
INSERT INTO title_authors VALUES('T07','A04',2,0.5);
INSERT INTO title_authors VALUES('T08','A06',1,1.0);
INSERT INTO title_authors VALUES('T09','A06',1,1.0);
INSERT INTO title_authors VALUES('T10','A02',1,1.0);
INSERT INTO title_authors VALUES('T11','A03',2,0.3);
INSERT INTO title_authors VALUES('T11','A04',3,0.3);
INSERT INTO title_authors VALUES('T11','A06',1,0.4);
INSERT INTO title_authors VALUES('T12','A02',1,1.0);
INSERT INTO title_authors VALUES('T13','A01',1,1.0);

DROP TABLE royalties;
CREATE TABLE royalties
  (
  title_id     CHAR(3)      NOT NULL,
  advance      DECIMAL(9,2)         ,
  royalty_rate DECIMAL(5,2)         ,
  CONSTRAINT pk_royalties
    PRIMARY KEY (title_id)
  );
INSERT INTO royalties VALUES('T01',10000,0.05);
INSERT INTO royalties VALUES('T02',1000,0.06);
INSERT INTO royalties VALUES('T03',15000,0.07);
INSERT INTO royalties VALUES('T04',20000,0.08);
INSERT INTO royalties VALUES('T05',100000,0.09);
INSERT INTO royalties VALUES('T06',20000,0.08);
INSERT INTO royalties VALUES('T07',1000000,0.11);
INSERT INTO royalties VALUES('T08',0,0.04);
INSERT INTO royalties VALUES('T09',0,0.05);
INSERT INTO royalties VALUES('T10',NULL,NULL);
INSERT INTO royalties VALUES('T11',100000,0.07);
INSERT INTO royalties VALUES('T12',50000,0.09);
INSERT INTO royalties VALUES('T13',20000,0.06);

3. SQL Basics

SQL is based on the relational model but doesn’t implement it faithfully. One departure from the model is that in SQL, primary keys are optional rather than mandatory. Consequently, tables without keys will accept duplicate rows, rendering some data inaccessible. A complete review of the many disparities is beyond the scope of this book (see “Learning Database Design” in Chapter 2). The upshot of these discrepancies is that DBMS users, and not the DBMS itself, are responsible for enforcing a relational structure. Another result is that the Model and SQL terms in Table 2.1 in Chapter 2 aren’t interchangeable.

With that warning, it’s time to learn SQL. An SQL program is a sequence of SQL statements executed in order. To write a program, you must know the rules that govern SQL syntax. This chapter explains how to write valid SQL statements and also covers data types and nulls.

SQL Syntax

Figure 3.1 shows the syntax of an example SQL statement (don’t worry about the actual meaning, or semantics, of the statement).

Figure 3.1An SQL statement, with a comment.

Diagram: An SQL statement and comment with parts labeled sequentially

  1. Comment.A comment is optional text that explains your program. Comments usually describe what a program does and how, or why code was changed. Comments are for humans—the DBMS ignores them. A comment is introduced by two consecutive hyphens and continues until the end of the line.
  2. SQL statement.An SQL statement is a valid combination of tokens introduced by a keyword. Tokens are the basic indivisible particles of the SQL language; they can’t be reduced grammatically. Tokens include keywords, identifiers, operators, literals (constants), and punctuation symbols.
  3. Clauses.An SQL statement has one or more clauses. In general, a clause is a fragment of an SQL statement that’s introduced by a keyword, is required or optional, and must be given in a particular order. SELECT, FROM, WHERE, and ORDER BY introduce the four clauses in this example.
  4. Keywords.A keyword is a word that SQL reserves because it has special meaning in the language. Using a keyword outside its specific context (as an identifier, for example) causes an error. DBMSs use a mix of standard and nonstandard keywords; search your DBMS documentation for keywords or reserved words. The keywords in this example are SELECT, FROM, WHERE, ORDER, and BY.
  5. Identifiers.Identifiers are words that you (or the database designer) use to name database objects such as tables, columns, aliases, indexes, and views. The identifiers in this example are au_fname, au_lname, authors, and state. For more information, see “Identifiers” later in this chapter.
  6. Terminating semicolon.An SQL statement ends with a semicolon.

SQL is a free-form language whose statements can:

Despite this flexibility, you should adopt a consistent style (Figure 3.2). I use uppercase keywords and lowercase identifiers and indent each clause on its own line; see “About This Book” for my typographic and syntax conventions.

Figure 3.2There aren’t many rules about how to format an SQL statement. This statement is equivalent to the one in Figure 3.1.

select au_fname
  ,        AU_LNAME
             FROM
 authors WhErE    state
= 'NY' order
             bY
AU_lnamE
      ;

Tips for SQL Syntax

Common Errors

Some common SQL programming errors are:

These errors usually are easy to catch and correct, even if your DBMS returns an obscure or unhelpful error message. Remember that the real error actually can occur well before the statement the DBMS flags as an error. For example, if you run

CREATE TABLE misspelled_name

then your DBMS will straightaway create a table with the bad name. Your error won’t show up until later, when you try to reference the table with, say,

SELECT * FROM correct_name

A common way to test and fix (debug) SQL programs is to use comments to temporarily stop SQL code from being executed. If you’re working on a long SQL statement and want to test only part of it, then you can comment out some of the code so that the DBMS sees it as comments and ignores it.

SQL Standards and Conformance

The ISO SQL technical committee has been revising the official SQL standard every few years since 1986. Each revision:

The standard is enormous—thousands of pages of dense specifications—and no vendor conforms (or ever will conform) to the entire thing. Instead, vendors try to conform to a subset of the standard called Core SQL. This level of conformance is the minimal category that vendors have to achieve to claim that they conform to standard SQL. The SQL-92 revision introduced levels of conformance, and later standards have them too, so when you read a DBMS’s conformance statement, note which SQL standard it’s referring to and which level. In fact, SQL-92 often is thought of as the standard because it defined many of the most vital and unchanging parts of the language. Except where noted, the SQL elements in this book are part of SQL-92 as well as later standards. The lowest level of SQL-92 conformance is called Entry (not Core).

Your programs should follow the SQL standard as closely as possible. Ideally, you should be able to write portable SQL programs without even knowing which DBMS you’re programming for. Unfortunately, the SQL committee is not made up of language theorists and relational-model purists but is top-heavy with commercial DBMS vendors, all jockeying and maneuvering. The result is that each DBMS vendor devotes resources to approach minimal Entry or Core SQL conformance requirements and then scampers off to add nonstandard features that differentiate their products in the marketplace—meaning that your SQL programs won’t be portable. These vendor-specific lock-ins often force you to modify or rewrite SQL programs to run on different DBMSs.

Of the DBMSs covered in this book, PostgreSQL is the “purest” with respect to the SQL standard. Your DBMS might offer settings that make it better conform to the SQL standard. MySQL has ANSI mode, for example, and Microsoft SQL Server has SET ANSI_DEFAULTS ON.

Tip: For information about using SQL across different versions of the same DBMS, see “Older DBMSs and Backward Compatibility of SQL”.

Identifiers

An identifier is a name that lets you refer to an object unambiguously within the hierarchy of database objects (whether a schema, database, column, key, index, view, constraint, or anything created with a CREATE statement). An identifier must be unique within its scope, which defines where and when it can be referenced. In general:

These rules let you duplicate names for objects whose scopes don’t overlap. You can give the same name to columns in different tables, for example, or to tables in different databases.

Tip: For information about addressing database objects, see Table 2.2 in Chapter 2.

DBMS scopes vary in the extent to which they require identifier names to be unique. Microsoft SQL Server requires an index name to be unique for only its table, for example, whereas Oracle and Db2 require an index name to be unique throughout the database. Search your DBMS documentation for identifiers or names.

Standard SQL has the following identifier rules for names:

Standard SQL distinguishes between reserved and non-reserved keywords. You can’t use reserved keywords as identifiers because they have special meaning in SQL. You can’t name a table “select” or a column “sum”, for example. Non-reserved keywords have a special meaning in only some contexts and can be used as identifiers in other contexts. Most non-reserved keywords actually are the names of built-in tables and functions, so it’s safest never to use them as identifiers either.

You can use a quoted identifier, also called a delimited identifier, to break some of SQL’s identifier rules. A quoted identifier is a name surrounded by double quotes. The name can contain spaces and special characters, is case sensitive, and can be a reserved keyword. Quoted identifiers can annoy other programmers and cause problems with third-party and even a vendor’s own tools, so using them usually is a bad idea.

Here’s some more advice for choosing identifier names:

Although you can’t use (unquoted) reserved words as identifiers, you can embed them in identifiers. group and max are illegal identifiers, for example, but groups and max_price are valid. If you’re worried that your identifier might be a reserved word in some other SQL dialect, then just add an underscore to the end of the name (element_, for example); no reserved keyword ends with an underscore.

You can surround Microsoft SQL Server quoted identifiers with double quotes or brackets ([]); brackets are preferred. In Db2, you can use reserved words as identifiers (but doing so isn’t a good idea because your program won’t be portable). MySQL ANSI_QUOTES mode allows double-quoted identifiers. DBMSs have their own nonstandard keywords; search your DBMS documentation for keywords or reserved words.

In MySQL, the case sensitivity of the underlying operating system determines the case sensitivity of database and table names.

The SQL standard directs DBMSs to convert identifier names to uppercase internally. So in the guts of your SQL compiler, the unquoted identifier myname is equivalent to the quoted identifier "MYNAME" (not "myname"). PostgreSQL doesn’t conform to the standard and converts to lowercase. To write portable programs, always quote a particular name or never quote it (don’t mix them). DBMSs aren’t consistent when it comes to case sensitivity, so the best practice is always to respect case for user-defined identifiers.

Data Types

Recall from “Tables, Columns, and Rows” in Chapter 2 that a domain is the set of valid values allowed in a column. To define a domain, you use a column’s data type (and constraints, described in Chapter 11). A data type, or column type, has these characteristics:

Tips for Data Types

Character String Types

Use character string data types to represent text. A character string, or simply string, has these characteristics:

Table 3.4Character String Types
Type Description
CHARACTER Represents a fixed number of characters. A string stored in a column defined as CHARACTER(length) can have up to length characters, where length is an integer greater than or equal to 1; the maximum length depends on the DBMS. When you store a string with fewer than length characters in a CHARACTER(length) column, the DBMS pads the end of the string with spaces to create a string that has exactly length characters. A CHARACTER(6) string 'Jack' is stored as 'Jack  ', for example. CHARACTER and CHAR are synonyms.
CHARACTER VARYING Represents a variable number of characters. A string stored in a column defined as CHARACTER VARYING(length) can have up to length characters, where length is an integer greater than or equal to 1; the maximum length depends on the DBMS. Unlike CHARACTER, when you store a string with fewer than length characters in a CHARACTER VARYING(length) column, the DBMS stores the string as is and doesn’t pad it with spaces. A CHARACTER VARYING(6) string 'Jack' is stored as 'Jack', for example. CHARACTER VARYING, CHAR VARYING, and VARCHAR are synonyms.
NATIONAL CHARACTER This data type is the same as CHARACTER except that it holds standardized multibyte characters or Unicode characters. In SQL statements, NATIONAL CHARACTER strings are written like CHARACTER strings but have an N in front of the first quote: N'βæþ', for example. NATIONAL CHARACTER, NATIONAL CHAR, and NCHAR are synonyms.
NATIONAL CHARACTER VARYING This data type is the same as CHARACTER VARYING except that it holds standardized multibyte characters or Unicode characters (see NATIONAL CHARACTER). NATIONAL CHARACTER VARYING, NATIONAL CHAR VARYING, and NCHAR VARYING are synonyms.
CLOB The character large object (CLOB) type is intended for use in library databases that hold vast amounts of text. A single CLOB value might hold an entire webpage, book, or genetic sequence, for example. CLOBs can’t be used as keys or in indexes and support fewer functions and operations than do CHAR and VARCHAR. In host languages, CLOBs are referenced with a unique locator (pointer) value, avoiding the overhead of transferring entire CLOBs across a client–server network. CLOB and CHARACTER LARGE OBJECT are synonyms.
NCLOB The national character large object (NCLOB) type is the same as CLOB except that it holds standardized multibyte characters or Unicode characters (see NATIONAL CHARACTER). NCLOB, NCHAR LARGE OBJECT, and NATIONAL CHARACTER LARGE OBJECT are synonyms.

Tips for Character Strings

Table 3.5DBMS Character String Types
DBMS Types
Access short text, long text (in older versions: text, memo)
SQL Server char, varchar, nchar, nvarchar
Oracle char, varchar2, clob, nchar, nvarchar2, nclob
Db2 char, varchar, clob, nchar, nvarchar, nclob, graphic, vargraphic, dbclob
MySQL char, varchar, nchar, nvarchar, tinytext, text, mediumtext, longtext
PostgreSQL char, varchar, text

Unicode

Computers store characters (letters, digits, punctuation, control characters, and other symbols) internally by assigning them unique numeric values. An encoding determines the mapping of characters to numeric values; different languages and computer operating systems use many different native encodings. Standard U.S.-English strings use ASCII encoding, which assigns values to 128 (27) different characters—not much, and not even enough to hold all the Latin characters used in modern European languages, much less all the Chinese ideographs.

Unicode is a single character set that represents the characters of almost all the world’s written languages. Unicode can encode up to about 4.3 billion (232) characters (using UTF-32 encoding). The Unicode Consortium develops and maintains the Unicode standard. The actual Unicode mappings are available in the latest online or printed edition of The Unicode Standard, available at home.unicode.org.

Binary Large Object Type

Use the binary large object (BLOB) data type to store binary data. A BLOB has these characteristics:

Tips for Binary Large Objects

Table 3.6DBMS BLOB Types
DBMS Types
Access ole object, attachment
SQL Server binary, varbinary
Oracle raw, long raw, blob, bfile
Db2 binary, varbinary, blob
MySQL binary, varbinary, tinyblob, blob, mediumblob, longblob
PostgreSQL bytea

Exact Numeric Types

Use exact numeric data types to represent exact numerical values. An exact numerical value has these characteristics:

Table 3.7Exact Numeric Types
Type Description
NUMERIC Represents a decimal number, stored in a column defined as NUMERIC(precision [,scale]). precision is greater than or equal to 1; the maximum precision depends on the DBMS. scale is a value from 0 to precision. If scale is omitted, then it defaults to zero (which makes the number effectively an INTEGER).
DECIMAL This data type is similar to NUMERIC, and some DBMSs define them equivalently. The difference is that the DBMS can choose a precision greater than that specified by DECIMAL(precision [,scale]), so precision specifies the minimum precision, not an exact precision as in NUMERIC. DECIMAL and DEC are synonyms.
INTEGER Represents an integer. The minimum and maximum values that can be stored in an INTEGER column depend on the DBMS. INTEGER takes no arguments. INTEGER and INT are synonyms.
SMALLINT This data type is the same as INTEGER except that it might hold a smaller range of values, depending on the DBMS. SMALLINT takes no arguments.
BIGINT This data type is the same as INTEGER except that it might hold a larger range of values, depending on the DBMS. BIGINT takes no arguments.

Tips for Exact Numeric Types

Table 3.9DBMS Exact Numeric Types
DBMS Types
Access byte, decimal, integer, long integer
SQL Server tinyint, smallint, int, bigint, bit, numeric, decimal, smallmoney, money
Oracle number
Db2 smallint, integer, bigint, decimal, numeric
MySQL tinyint, smallint, mediumint, int, bigint, decimal, numeric
PostgreSQL smallint, integer, bigint, decimal, numeric

Approximate Numeric Types

Use approximate numeric data types to represent approximate numerical values. An approximate numerical value has these characteristics:

Table 3.10Approximate Numeric Types
Type Description
FLOAT Represents a floating-point number, stored in a column defined as FLOAT(precision). precision is greater than or equal to 1 and expressed as the number of bits (not the number of digits); the maximum precision depends on the DBMS.
REAL This data type is the same as FLOAT except that the DBMS defines the precision. REAL numbers usually are called single-precision numbers. REAL takes no arguments.
DOUBLE PRECISION This data type is the same as FLOAT except that the DBMS defines the precision, which must be greater than that of REAL. DOUBLE PRECISION takes no arguments.

Tips for Approximate Numeric Types

Table 3.11DBMS Approximate Numeric Types
DBMS Types
Access single, double
SQL Server float, real
Oracle float, binary_float, binary_double
Db2 real, double, float, decfloat
MySQL float, double
PostgreSQL real, double precision

Boolean Type

Use the boolean data type to store truth values. A boolean value has these characteristics:

Tips for Boolean Values

Table 3.12DBMS Boolean Types
DBMS Types
Access yes/no
SQL Server bit
Oracle number(1)
Db2 decimal(1)
MySQL boolean, bit, tinyint(1)
PostgreSQL boolean

Datetime Types

Use datetime data types to represent the date and time of day. A datetime value has these characteristics:

Table 3.13Datetime Types
Type Description
DATE Represents a date. A date stored in a column defined as DATE has three integer fields—YEAR, MONTH, and DAY—and is formatted yyyy-mm-dd (length 10) (2006-03-17, for example). Table 3.14 lists the valid values for the fields. DATE takes no arguments.
TIME Represents a time of day. A time stored in a column defined as TIME has three fields—HOUR, MINUTE, and SECOND—and is formatted hh:mm:ss (length 8) (22:06:57, for example). You can specify fractional seconds with TIME(precision). precision is the number of fractional digits and is greater than or equal to zero. The maximum precision, which is at least 6, depends on the DBMS. HOUR and MINUTE are integers, and SECOND is a decimal number. The format is hh:mm:ss.ssss... (length 9 plus the number fractional digits) (22:06:57.1333, for example). Table 3.14 lists the valid values for the fields.
TIMESTAMP Represents a combination of DATE and TIME values separated by a space. The TIMESTAMP format is yyyy-mm-dd hh:mm:ss (length 19) (2006-03-17 22:06:57, for example). You can specify fractional seconds with TIMESTAMP(precision). The format is yyyy-mm-dd hh:mm:ss.ssss... (length 20 plus the number fractional digits).
TIME WITH TIMEZONE This data type is the same as TIME except that it adds a field, TIME_ZONE_OFFSET, to indicate the offset in hours from UTC. TIME_ZONE_OFFSET is formatted as INTERVAL HOUR TO MINUTE (see “Interval Types” later in this chapter) and can contain the values listed in Table 3.14. Append AT TIME ZONE time_zone_offset to the TIME to assign a value to the time zone (22:06:57 AT TIME ZONE -08:00, for example). Alternatively, you can append AT LOCAL to indicate that the time zone is the default for the session (22:06:57 AT LOCAL, for example). If the AT clause is omitted, then all times default to AT LOCAL.
TIMESTAMP WITH TIMEZONE This data type is the same as TIMESTAMP except that it adds a field, TIME_ZONE_OFFSET, to indicate the offset in hours from UTC. The syntax rules are the same as those of TIME WITH TIME ZONE except that you must include a date (2006-03-17 22:06:57 AT TIME ZONE -08:00, for example).
Table 3.14Valid Values for Datetime Fields
Type Description
YEAR 0001 to 9999
MONTH 01 to 12
DAY 01 to 31
HOUR 00 to 23
MINUTE 00 to 59
SECOND 00 to 61.999
TIME_ZONE_OFFSET -12:59 to +13:00

Tips for Datetime Values

Table 3.15DBMS Datetime Types
DBMS Types
Access date/time
SQL Server date, datetime, datetime2, datetimeoffset, smalldatetime, time
Oracle date, timestamp
Db2 date, time, timestamp
MySQL date, datetime, timestamp, time, year
PostgreSQL date, time, timestamp

Interval Types

DBMS conformance to standard SQL interval types is spotty or nonexistent, so you might not find this section to be useful in practice. DBMSs have their own extended data types and functions that calculate intervals and perform date and time arithmetic.

Use interval data types to represent sets of time values or spans of time. An interval value has these characteristics:

Table 3.16Interval Types
Type Description
Year-month These intervals contain only a year value, only a month value, or both. The valid column types are INTERVAL YEAR, INTERVAL YEAR(precision), INTERVAL MONTH, INTERVAL MONTH(precision), INTERVAL YEAR TO MONTH, or INTERVAL YEAR(precision) TO MONTH.
Day-time These intervals can contain a day value, hour value, minute value, second value, or some combination thereof. Some examples of the valid column types are INTERVAL MINUTE, INTERVAL DAY(precision), INTERVAL DAY TO HOUR, INTERVAL DAY(precision) TO SECOND, and INTERVAL MINUTE(precision) TO SECOND(frac_precision).

Tips for Interval Types

Table 3.17DBMS Interval Types
DBMS Types
Access Not supported
SQL Server Not supported
Oracle interval year to month, interval day to second
Db2 Not supported
MySQL Not supported
PostgreSQL interval

Unique Identifiers

Unique identifiers are used to generate primary-key values to identify rows (see “Primary Keys” in Chapter 2). An identifier can be unique universally (large random numbers unique in any context) or only within a specific table (simple serial numbers 1, 2, 3,...). Table 3.18 lists unique-identifier types and attributes for the DBMSs. See the DBMS documentation for size limits and usage restrictions. The SQL standard calls columns with auto-incrementing values identity columns. See also “Generating Sequences” in Chapter 15.

Table 3.18Unique Identifiers (Types or Attributes)
DBMS Types or Attributes
Standard SQL IDENTITY
Access autonumber, replication id
SQL Server uniqueidentifier, identity
Oracle rowid, urowid, sequences
Db2 rowid, identity columns and sequences
MySQL auto_increment attribute
PostgreSQL smallserial, serial, bigserial, uuid

UUIDs

A universally unique ID is called a Universally Unique Identifier (UUID) or a Globally Unique Identifier (GUID). When you define a column to have a UUID data type, your DBMS will generate a random UUID automatically in each new row, probably according to ISO/IEC 9834-8 (iso.org) or IETF RFC 4122 (ietf.org).

A UUID in standard form looks like:

a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11

The letters actually are hexadecimal digits (a–f). Your DBMS might use an alternative form with uppercase hex digits, surrounding braces, or omitted hyphens. UUIDs aren’t technically guaranteed to be unique, but the probability of generating a duplicate ID is so tiny that they should be considered singular. For more information, read the Wikipedia article “Universally unique identifier”.

DBMSs provide functions that return UUIDs. Microsoft SQL Server has NEWID(), Oracle has SYS_GUID(), MySQL has UUID(), and PostgreSQL has gen_random_uuid(). Note that the Db2 function GENERATE_UNIQUE() returns a string that is not a UUID.

Other Data Types

The SQL standard defines other data types than the ones covered in the preceding sections, but some of them rarely are implemented or used in practice (ARRAY, MULTISET, REF, and ROW, for example). More useful are the extended (nonstandard) data types that are available in various DBMSs. Depending on your DBMS, you can find data types for:

User-Defined Types

Microsoft SQL Server, Oracle, Db2, and PostgreSQL let you create user-defined types (UDTs). The simplest UDT is a standard or built-in data type (CHARACTER, INTEGER, and so on) with additional check and other constraints. You can define the data type marital_status, for example, as a single-character CHARACTER data type that allows only the values S, M, W, D, or NULL (for single, married, widowed, divorced, or unknown). More-complex UDTs are similar to classes in object-oriented programming languages such as Java or Python. You can define a UDT once and use it in multiple tables, rather than repeat its definition in each table in which it’s used. Search your DBMS documentation for user-defined type. UDTs are created in standard SQL with the statement CREATE TYPE.

Nulls

When your data are incomplete, you can use a null to represent a missing or unknown value. A null has these characteristics:

Tips for Nulls

4. Retrieving Data from a Table

This chapter introduces SQL’s workhorse—the SELECT statement. Most SQL work involves retrieving and manipulating data by using this one (albeit complex) statement. SELECT retrieves rows, columns, and derived values from one or more tables in a database; its syntax is:

SELECT columns
  FROM tables
  [JOIN joins]
  [WHERE search_condition]
  [GROUP BY grouping_columns]
  [HAVING search_condition]
  [ORDER BY sort_columns];

SELECT, FROM, ORDER BY, and WHERE are covered in this chapter, GROUP BY and HAVING in Chapter 6, and JOIN in Chapter 7. By convention, I call only a SELECT statement a query because it returns a result set. DBMS documentation and other books might refer to any SQL statement as a query. Although SELECT is powerful, it’s not dangerous: you can’t use it to add, change, or delete data or database objects. (The dangerous stuff starts in Chapter 10.)

Tip: Recall that italic_type denotes a variable in code that must be replaced with a value, and brackets indicate an optional clause or item; see “About This Book” in the Introduction for typographic and syntax conventions.

Retrieving Columns with SELECT and FROM

In its simplest form, a SELECT statement retrieves columns from a table; you can retrieve one column, multiple columns, or all columns. The SELECT clause lists the columns to display, and the FROM clause specifies the table from which to draw the columns.

To retrieve a column from a table:

Listing 4.1List the cities in which the authors live. See Figure 4.1 for the result.

SELECT city
  FROM authors;

Figure 4.1Result of Listing 4.1.

city
-------------
Bronx
Boulder
San Francisco
San Francisco
New York
Palo Alto
Sarasota

To retrieve multiple columns from a table:

Listing 4.2List each author’s first name, last name, city, and state. See Figure 4.2 for the result.

SELECT au_fname, au_lname, city, state
  FROM authors;

Figure 4.2Result of Listing 4.2.

au_fname  au_lname    city          state
--------- ----------- ------------- -----
Sarah     Buchman     Bronx         NY
Wendy     Heydemark   Boulder       CO
Hallie    Hull        San Francisco CA
Klee      Hull        San Francisco CA
Christian Kells       New York      NY
          Kellsey     Palo Alto     CA
Paddy     O'Furniture Sarasota      FL

To retrieve all columns from a table:

Listing 4.3List all the columns in the table authors. See Figure 4.3 for the result.

SELECT *
  FROM authors;

Figure 4.3Result of Listing 4.3.

au_id au_fname  au_lname    phone        address              city          state zip
----- --------- ----------- ------------ -------------------- ------------- ----- -----
A01   Sarah     Buchman     718-496-7223 75 West 205 St       Bronx         NY    10468
A02   Wendy     Heydemark   303-986-7020 2922 Baseline Rd     Boulder       CO    80303
A03   Hallie    Hull        415-549-4278 3800 Waldo Ave, #14F San Francisco CA    94123
A04   Klee      Hull        415-549-4278 3800 Waldo Ave, #14F San Francisco CA    94123
A05   Christian Kells       212-771-4680 114 Horatio St       New York      NY    10014
A06             Kellsey     650-836-7128 390 Serra Mall       Palo Alto     CA    94305
A07   Paddy     O'Furniture 941-925-0752 1442 Main St         Sarasota      FL    34236

Tips for Retrieving Columns

Results Are Unformatted

All results display raw, unformatted values. Monetary amounts lack currency signs, for example, and numbers might have too many decimal places. Reporting tools—not data-retrieval tools—format data, although DBMSs have nonstandard functions that let you format numbers and datetimes in query results. See Microsoft SQL Server’s datename() function or MySQL’s date_format() function, for example.

Creating Column Aliases with AS

In the query results so far, I’ve allowed the DBMS to use default values for column headings. (A column’s default heading in a result is the source column’s name in the table definition.) You can use the AS clause to create a column alias. A column alias is an alternative name (identifier) that you specify to control how column headings are displayed in a result. Use column aliases if column names are cryptic, hard to type, too long, or too short.

A column alias immediately follows a column name in the SELECT clause of a SELECT statement. Enclose the alias in single or double quotes if it’s a reserved keyword or if it contains spaces, punctuation, or special characters. You can omit the quotes if the alias is a single non-reserved word that contains only letters, digits, or underscores. If you want a particular column to retain its default heading, then omit its AS clause.

To create column aliases:

Listing 4.5 shows the syntactic variations of the AS clause. Figure 4.5 shows the result of Listing 4.5.

Listing 4.5The AS clause specifies a column alias to display in results. This statement shows alternative constructions for AS syntax. In your programs, pick one construction and use it consistently. See Figure 4.5 for the result.

SELECT
    au_fname AS "First name",
    au_lname AS 'Last name',
    city AS City,
    state,
    zip 'Postal code'
  FROM authors;

Figure 4.5Result of Listing 4.5.

First name Last name   City          state Postal code
---------- ----------- ------------- ----- -----------
Sarah      Buchman     Bronx         NY    10468
Wendy      Heydemark   Boulder       CO    80303
Hallie     Hull        San Francisco CA    94123
Klee       Hull        San Francisco CA    94123
Christian  Kells       New York      NY    10014
           Kellsey     Palo Alto     CA    94305
Paddy      O'Furniture Sarasota      FL    34236

In standard SQL and most DBMSs, the keyword AS is optional, but you should include it and surround aliases with double quotes to make your SQL code more portable and readable. With these syntactic conventions, Listing 4.5 is equivalent to:

SELECT
    au_fname AS "First name",
    au_lname AS "Last name",
    city     AS "City",
    state,
    zip      AS "Postal code"
  FROM authors;

Tips for Column Aliases

Eliminating Duplicate Rows with DISTINCT

Columns often contain duplicate values, and it’s common to want a result that lists each duplicate only once. If I type Listing 4.6 to list the states where the authors live, then the result, Figure 4.6, contains unneeded duplicates.

Listing 4.6List the states in which the authors live. See Figure 4.6 for the result.

SELECT state
  FROM authors;

Figure 4.6Result of Listing 4.6. This result contains unneeded duplicates of CA and NY.

state
-----
NY
CO
CA
CA
NY
CA
FL

The DISTINCT keyword eliminates duplicate rows from a result. Note that the columns of a DISTINCT result form a candidate key (unless they contain nulls).

To eliminate duplicate rows:

Listing 4.7List the distinct states in which the authors live. The keyword DISTINCT eliminates duplicate rows in the result. See Figure 4.7 for the result.

SELECT DISTINCT state
  FROM authors;

Figure 4.7Result of Listing 4.7. This result has no CA or NY duplicates.

state
-----
NY
CO
CA
FL

If the SELECT DISTINCT clause contains more than one column, then the values of all the specified columns combined determine the uniqueness of rows. The result of Listing 4.8 is Figure 4.8, which contains a duplicate row that has two columns. The result of Listing 4.9 is Figure 4.9, which eliminates the two-column duplicate.

Listing 4.8List the cities and states in which the authors live. See Figure 4.8 for the result.

SELECT city, state
  FROM authors;

Figure 4.8Result of Listing 4.8. This result contains a duplicate row for San Francisco, California.

city          state
------------- -----
Bronx         NY
Boulder       CO
New York      NY
Palo Alto     CA
San Francisco CA
San Francisco CA
Sarasota      FL

Listing 4.9List the distinct cities and states in which the authors live. See Figure 4.9 for the result.

SELECT DISTINCT city, state
  FROM authors;

Figure 4.9Result of Listing 4.9. It’s the city–state combination that’s considered to be unique, not the value in any single column.

city          state
------------- -----
Bronx         NY
Boulder       CO
New York      NY
Palo Alto     CA
San Francisco CA
Sarasota      FL

Tips for DISTINCT

Sorting Rows with ORDER BY

Rows in a query result are unordered, so you should view the order in which rows appear as being arbitrary. This situation arises because the relational model posits that row order is irrelevant for table operations. You can use the ORDER BY clause to sort rows by a specified column or columns in ascending (lowest to highest) or descending (highest to lowest) order; for details, see “Sort Order” in this section. The ORDER BY clause always is the last clause in a SELECT statement.

To sort by a column:

Listing 4.10List the authors’ first names, last names, cities, and states, sorted by ascending last name. ORDER BY performs ascending sorts by default, so the ASC keyword is optional. See Figure 4.10 for the result.

SELECT au_fname, au_lname, city, state
  FROM authors
  ORDER BY au_lname ASC;

Figure 4.10Result of Listing 4.10. This result is sorted in ascending last-name order.

au_fname  au_lname    city          state
--------- ----------- ------------- -----
Sarah     Buchman     Bronx         NY
Wendy     Heydemark   Boulder       CO
Hallie    Hull        San Francisco CA
Klee      Hull        San Francisco CA
Christian Kells       New York      NY
          Kellsey     Palo Alto     CA
Paddy     O'Furniture Sarasota      FL

Listing 4.11List the authors’ first names, last names, cities, and states, sorted by descending first name. The DESC keyword is required. See Figure 4.11 for the result.

SELECT au_fname, au_lname, city, state
  FROM authors
  ORDER BY au_fname DESC;

Figure 4.11Result of Listing 4.11. This result is sorted in descending first-name order. The first name of the author Kellsey is an empty string ('') and sorts last (or first in ascending order).

au_fname  au_lname    city          state
--------- ----------- ------------- -----
Wendy     Heydemark   Boulder       CO
Sarah     Buchman     Bronx         NY
Paddy     O'Furniture Sarasota      FL
Klee      Hull        San Francisco CA
Hallie    Hull        San Francisco CA
Christian Kells       New York      NY
          Kellsey     Palo Alto     CA

Sort Order

Sorting numeric and datetime values is unambiguous; sorting character strings is complex. A DBMS uses a collating sequence, or collation, to determine the order in which characters are sorted. The collation defines the order of precedence for every character in your character set. Your character set depends on the language that you’re using—European languages (a Latin character set), Hebrew (the Hebrew alphabet), or Chinese (ideographs), for example. The collation also determines case sensitivity (is ‘A’ < ‘a’?), accent sensitivity (is ‘A’ < ‘Á’?), width sensitivity (for multibyte or Unicode characters), and other factors such as linguistic practices. The SQL standard doesn’t define particular collations and character sets, so each DBMS uses its own sorting strategy and default collation. DBMSs provide commands or tools that display the current collation and character set. Run the command exec sp_helpsort in Microsoft SQL Server, for example. Search your DBMS documentation for collation or sort order.

To sort by multiple columns:

Listing 4.12List the authors’ first names, last names, cities, and states, sorted by descending city within ascending state. See Figure 4.12 for the result.

SELECT au_fname, au_lname, city, state
  FROM authors
  ORDER BY
    state ASC,
    city  DESC;

Figure 4.12Result of Listing 4.12.

au_fname  au_lname    city          state
--------- ----------- ------------- -----
Hallie    Hull        San Francisco CA
Klee      Hull        San Francisco CA
          Kellsey     Palo Alto     CA
Wendy     Heydemark   Boulder       CO
Paddy     O'Furniture Sarasota      FL
Christian Kells       New York      NY
Sarah     Buchman     Bronx         NY

Sorting by Substrings

To sort results by specific parts of a string, use the functions described in “Extracting a Substring with SUBSTRING()” in Chapter 5. For example, this query sorts by the last four characters of phone:

SELECT au_id, phone
  FROM authors
  ORDER BY substr(phone, length(phone)-3);

This query works for Oracle, Db2, MySQL, and PostgreSQL. In Microsoft SQL Server, use substring(phone, len(phone)-3, 4). In Microsoft Access, use Mid(phone, len(phone)-3, 4).

You can specify relative column-position numbers instead of column names in ORDER BY. The position numbers refer to the columns in the result, not the original table. Using column positions saves typing, but the resulting code is unclear and invites mistakes if you reorder the columns in the SELECT clause.

To sort by relative column positions:

Listing 4.13List each author’s first name, last name, city, and state, sorted first by ascending state (column 4 in the SELECT clause) and then by descending last name within each state (column 2). See Figure 4.13 for the result.

SELECT au_fname, au_lname, city, state
  FROM authors
  ORDER BY 4 ASC, 2 DESC;

Figure 4.13Result of Listing 4.13.

au_fname  au_lname    city          state
--------- ----------- ------------- -----
          Kellsey     Palo Alto     CA
Hallie    Hull        San Francisco CA
Klee      Hull        San Francisco CA
Wendy     Heydemark   Boulder       CO
Paddy     O'Furniture Sarasota      FL
Christian Kells       New York      NY
Sarah     Buchman     Bronx         NY

Sorting and Nulls

Sorting is one of the situations where SQL departs from the idea that a null isn’t equal to any other value, including another null. (The logical comparison NULL = NULL is unknown, not true.) When nulls are sorted, they all are considered to be equal to one another.

The SQL standard leaves it up to the DBMS to decide whether nulls are either greater than or less than all non-null values. Microsoft Access, Microsoft SQL Server, and MySQL treat nulls as the lowest possible values (Listing 4.14 and Figure 4.14). Oracle, Db2, and PostgreSQL treat nulls as the highest possible values. See also “Nulls” in Chapter 3.

In Oracle and PostgreSQL, use NULLS FIRST or NULLS LAST with ORDER BY to control null-sorting behavior. For other DBMSs, create a derived column (see Chapter 5) that flags nulls—CASE WHEN column IS NULL THEN 0 ELSE 1 END AS is_null, for example—and add it as the first column (with ASC or DESC) in the ORDER BY clause.

Listing 4.14Nulls in a sort column are listed first or last, depending on the DBMS. See Figure 4.14 for the result.

SELECT pub_id, state, country
  FROM publishers
  ORDER BY state ASC;

Figure 4.14Result of Listing 4.14. This result is sorted by ascending state. The DBMS in which I ran this query treats nulls as the lowest possible values, so the row with the null state is listed first. A DBMS that treats nulls as the highest possible values would list the same row last.

pub_id state country
------ ----- -------
P03    NULL  Germany
P04    CA    USA
P02    CA    USA
P01    NY    USA

Tips for ORDER BY

Sorting Speed

The three factors that most affect sorting speed are, in order of importance:

Always restrict a sort to the minimum number of rows needed. Running times of sorting routines don’t scale linearly with the number of rows sorted—so sorting 10n rows takes much more than 10 times longer than sorting n rows. Also try to reduce the number of sorted columns and the columns’ data-type lengths in the table definition, if possible.

Filtering Rows with WHERE

The result of each SELECT statement so far has included every row in the table (for the specified columns). You can use the WHERE clause to filter unwanted rows from the result. This filtering capability gives the SELECT statement its real power. In a WHERE clause, you specify a search condition that has one or more conditions that need to be satisfied by the rows of a table. A condition, or predicate, is a logical expression that evaluates to true, false, or unknown. Rows for which the condition is true are included in the result; rows for which the condition is false or unknown are excluded. (An unknown result, which arises from nulls, is described in the next section.) SQL provides operators that express different types of conditions (Table 4.1). Operators are symbols or keywords that specify actions to perform on values or other elements.

Table 4.1Types of Conditions
Condition SQL Operators
Comparison =, <>, <, <=, >, >=
Pattern matching LIKE
Range filtering BETWEEN
List filtering IN
Null testing IS NULL

SQL’s comparison operators compare two values and evaluate to true, false, or unknown (Table 4.2).

Table 4.2Comparison Operators
Operator Description
= Equal to
<> Not equal to
< Less than
<= Less than or equal to
> Greater than
>= Greater than or equal to

The data type determines how values are compared:

Compare only identical or similar data types. If you try to compare values that have different data types, then your DBMS might:

To filter rows by making a comparison:

Listing 4.18This List the authors whose last name is not Hull. See Figure 4.18 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE au_lname <> 'Hull';

Figure 4.18Result of Listing 4.18.

au_id au_fname  au_lname
----- --------- -----------
A01   Sarah     Buchman
A02   Wendy     Heydemark
A05   Christian Kells
A06             Kellsey
A07   Paddy     O'Furniture

Listing 4.19List the titles for which there is no signed contract. See Figure 4.19 for the result.

SELECT title_name, contract
  FROM titles
  WHERE contract = 0;

Figure 4.19Result of Listing 4.19.

title_name                 contract
-------------------------- --------
Not Without My Faberge Egg        0

Listing 4.20List the titles published in 2001 and later. See Figure 4.20 for the result.

SELECT title_name, pubdate
  FROM titles
  WHERE pubdate >= DATE '2001-01-01';

Figure 4.20Result of Listing 4.20.

title_name                   pubdate
---------------------------- ----------
Exchange of Platitudes       2001-01-01
Just Wait Until After School 2001-06-01
Kiss My Boo-Boo              2002-05-31

The right and left sides of the comparison can also be complex expressions. The general form of a comparison is:

expr1 op expr2

expr1 and expr2 are expressions. An expression is any valid combination of column names, literals, functions, and operators that resolves to a single value (per row). Chapter 5 covers expressions in more detail (Listing 4.21 and Figure 4.21).

Listing 4.21List the titles that generated more than $1 million in revenue. This search condition uses an arithmetic expression. See Figure 4.21 for the result.

SELECT
    title_name,
    price * sales AS "Revenue"
  FROM titles
  WHERE price * sales > 1000000;

Figure 4.21Result of Listing 4.21.

title_name                    Revenue
----------------------------- -----------
Ask Your System Administrator  1025396.65
Exchange of Platitudes         1400008.00
I Blame My Mother             35929790.00
Spontaneous, Not Annoying      1299012.99

Tips for WHERE

Column Aliases and WHERE

If you alias a column in a SELECT clause (see “Creating Column Aliases with AS” earlier in this chapter), then you can’t reference it in the WHERE clause. The following query fails because the WHERE clause is evaluated before the SELECT clause, so the alias copies_sold doesn’t yet exist when the WHERE clause is evaluated:

-- Wrong
SELECT sales AS copies_sold
  FROM titles
  WHERE copies_sold > 100000;

Instead, use a subquery (Chapter 8) in the FROM clause, which is evaluated before the WHERE clause:

-- Correct
SELECT *
  FROM (SELECT sales AS copies_sold
        FROM titles) ta
  WHERE copies_sold > 100000;

This solution works not only for columns aliases but also for aggregate functions, scalar subqueries, and window functions referenced in WHERE clauses. Note that in the latter query, the subquery is aliased ta (a table alias). All DBMSs accept table aliases, but not all require them. See also “Using Subqueries as Column Expressions” in Chapter 8.

Combining and Negating Conditions with AND, OR, and NOT

You can specify multiple conditions in a single WHERE clause to, say, retrieve rows based on the values in multiple columns. You can use the AND and OR operators to combine two or more conditions into a compound condition. AND, OR, and a third operator, NOT, are logical operators. Logical operators, or boolean operators, are operators designed to work with truth values: true, false, and unknown.

If you’ve programmed in other languages (or studied propositional logic), then you’re familiar with the two-value logic (2VL) system. In two-value logic, the result of a logical expression is either true or false. 2VL assumes perfect knowledge, in which all propositions are known to be true or false. Databases model real data, however, and our knowledge of the world is imperfect—that’s why we use nulls to represent unknown values (see “Nulls” in Chapter 3).

2VL is insufficient to represent knowledge gaps, so SQL uses three-value logic (3VL). In three-value logic, the result of a logical expression is true, false, or unknown. If the result of a compound condition is false or unknown, then the row is excluded from the result. (To retrieve rows with nulls, see “Testing for Nulls with IS NULL” later in this chapter.)

The AND Operator

The AND operator’s important characteristics are:

See Listings 4.22 and 4.23, and Figures 4.22 and 4.23, for some AND examples.

Listing 4.22List the biographies that sell for less than $20. See Figure 4.22 for the result.

SELECT title_name, type, price
  FROM titles
  WHERE type = 'biography' AND price < 20;

Figure 4.22Result of Listing 4.22.

title_name                type      price
------------------------- --------- -----
How About Never?          biography 19.95
Spontaneous, Not Annoying biography 12.99

Listing 4.23List the authors whose last names begin with one of the letters H through Z and who don’t live in California. See Figure 4.23 for the result.

SELECT au_fname, au_lname
  FROM authors
  WHERE au_lname >= 'H'
    AND au_lname <= 'Zz'
    AND state <> 'CA';

Figure 4.23Result of Listing 4.23. Remember that the results of string comparisons depend on the DBMS’s collating sequence; see “Sorting Rows with ORDER BY” earlier in this chapter.

au_fname  au_lname
--------- -----------
Wendy     Heydemark
Christian Kells
Paddy     O'Furniture

The OR Operator

The OR operator’s important characteristics are:

See Listings 4.24 and 4.25, and Figures 4.24 and 4.25, for some OR examples.

Listing 4.24List the authors who live in New York State, Colorado, or San Francisco. See Figure 4.24 for the result.

SELECT au_fname, au_lname, city, state
  FROM authors
  WHERE (state = 'NY')
     OR (state = 'CO')
     OR (city  = 'San Francisco');

Figure 4.24Result of Listing 4.24.

au_fname  au_lname  city          state
--------- --------- ------------- -----
Sarah     Buchman   Bronx         NY
Wendy     Heydemark Boulder       CO
Hallie    Hull      San Francisco CA
Klee      Hull      San Francisco CA
Christian Kells     New York      NY

Listing 4.25 shows the effect of nulls in conditions. You might expect the result, Figure 4.25, to display all the rows in the table publishers. But the row for publisher P03 (located in Germany) is missing because it contains a null in the column state. The null causes the result of both of the OR conditions to be unknown, so the row is excluded from the result. To test for nulls, see “Testing for Nulls with IS NULL” later in this chapter.

Listing 4.25List the publishers that are located in California or are not located in California. This example is contrived to show the effect of nulls in conditions. See Figure 4.25 for the result.

SELECT pub_id, pub_name, state, country
  FROM publishers
  WHERE (state =  'CA')
     OR (state <> 'CA');

Figure 4.25Result of Listing 4.25. Publisher P03 is missing because its state is null.

pub_id pub_name          state country
------ ----------------- ----- -------
P01    Abatis Publishers NY    USA
P02    Core Dump Books   CA    USA
P04    Tenterhooks Press CA    USA

The NOT Operator

The NOT operator’s important characteristics are:

See Listings 4.26 and 4.27, and Figures 4.26 and 4.27, for some NOT examples.

Listing 4.26List the authors who don’t live in California. See Figure 4.26 for the result.

SELECT au_fname, au_lname, state
  FROM authors
  WHERE NOT (state = 'CA');

Figure 4.26Result of Listing 4.26.

au_fname  au_lname    state
--------- ----------- -----
Sarah     Buchman     NY
Wendy     Heydemark   CO
Christian Kells       NY
Paddy     O'Furniture FL

Listing 4.27List the titles whose price is not less than $20 and that have sold more than 15000 copies. See Figure 4.27 for the result.

SELECT title_name, sales, price
  FROM titles
  WHERE NOT (price < 20)
    AND (sales > 15000);

Figure 4.27Result of Listing 4.27.

title_name                    sales   price
----------------------------- ------- -----
Ask Your System Administrator   25667 39.95
I Blame My Mother             1500200 23.95

Using AND, OR, and NOT Together

You can combine the three logical operators in a compound condition. Your DBMS uses SQL’s precedence rules to determine which operators to evaluate first. Precedence is covered in “Determining the Order of Evaluation” in Chapter 5, but for now you need know only that when you use multiple logical operators in a compound condition, NOT is evaluated first, then AND, and finally OR. You can override this order with parentheses: everything in parentheses is evaluated first. When parenthesized conditions are nested, the innermost condition is evaluated first. Under the default precedence rules, the condition

p AND NOT q OR r

is equivalent to

(p AND (NOT q)) OR r

It’s wise to use parentheses, rather than rely on the default evaluation order, to make the evaluation order clear.

If I want to list history and biography titles priced less than $20, for example, then Listing 4.28 won’t work. AND is evaluated before OR, so the query is evaluated as follows:

  1. Find all the biography titles less than $20.
  2. Find all the history titles (regardless of price).
  3. List both sets of titles in the result (Figure 4.28).

Listing 4.28This query won’t work if I want to list history and biography titles less than $20 because AND has higher precedence than OR. See Figure 4.28 for the result.

SELECT title_id, type, price
  FROM titles
  WHERE type  = 'history'
     OR type  = 'biography'
    AND price < 20;

Figure 4.28Result of Listing 4.28. This result contains two history titles priced more than $20, which isn’t what I want.

title_id type      price
-------- --------- -----
T01      history   21.99
T02      history   19.95
T06      biography 19.95
T12      biography 12.99
T13      history   29.99

To fix this query, add parentheses to force evaluation of OR first. Listing 4.29 is evaluated as follows:

  1. Find all the biography and history titles.
  2. Of the titles found in step 1, keep the ones priced less than $20.
  3. List the subset of titles in the result (Figure 4.29).

Listing 4.29To fix Listing 4.28, add parentheses to force OR to be evaluated before AND. See Figure 4.29 for the result.

SELECT title_id, type, price
  FROM titles
  WHERE (type  = 'history'
     OR  type  = 'biography')
    AND  price < 20;

Figure 4.29Result of Listing 4.29.

title_id type      price
-------- --------- -----
T02      history   19.95
T06      biography 19.95
T12      biography 12.99

Dissecting WHERE Clauses

If your WHERE clause isn’t working, then you can debug it by displaying the result of each condition individually. To see the result of each comparison in Listing 4.29, for example, put each comparison expression in the SELECT clause’s output column list, along with the values you’re comparing:

SELECT
    type,
    type = 'history' AS "Hist?",
    type = 'biography' AS "Bio?",
    price,
    price < 20 AS "<20?"
  FROM titles;

This query runs on Microsoft Access, MySQL, and PostgreSQL. If your DBMS interprets the = symbol as an assignment operator rather than as a comparison operator, then you must substitute equivalent expressions for the logical comparisons. In Oracle, for example, you can replace type = 'history' with INSTR(type, 'history'). The query’s result is:

type       Hist? Bio?  price  <20?
---------- ----- ----  -----  ----
history        1    0  21.99     0
history        1    0  19.95     1
computer       0    0  39.95     0
psychology     0    0  12.99     1
psychology     0    0   6.95     1
biography      0    1  19.95     1
biography      0    1  23.95     0
children       0    0  10.00     1
children       0    0  13.95     1
biography      0    1   NULL  NULL
psychology     0    0   7.99     1
biography      0    1  12.99     1
history        1    0  29.99     0

The comparison columns display zero if the comparison is false, nonzero if it’s true, or null if it’s unknown.

Tips for AND, OR, and NOT

Re-expressing Conditions

You must master the laws in Table 4.6 to become a competent programmer in SQL (or any language). They’re especially useful when you want to re-express conditions to make queries run faster. For example, the statement

SELECT * FROM mytable
  WHERE col1 = 1
    AND NOT (col1 = col2 OR col3 = 3);

is equivalent to

SELECT * FROM mytable
  WHERE col1 = 1
    AND col2 <> 1
    AND col3 <> 3;

but the latter one will run faster if your DBMS’s optimizer isn’t smart enough to re-express the former internally. (The condition col1 = col2 is more expensive computationally than comparing col1 and col2 to literal values.)

You also can use the laws to change a condition into its opposite. For example, the reverse of the condition

WHERE (col1 = 'A') AND (col2 = 'B')

is

WHERE (col1 <> 'A') OR (col2 <> 'B')

In this case, it would have easier just to negate the entire original expression with NOT:

WHERE NOT ((col1 = 'A') AND (col2 = 'B'))

But this simple approach won’t work with complex conditions involving multiple ANDs, ORs, and NOTs.

Here’s a problem to solve: look at only the first code line below and see whether you can repeatedly apply equivalency rules to push the NOT operators inward until they apply to only the individual expressions p, q, and r:

NOT ((p AND q) OR (NOT p AND r))
= NOT (p AND q) AND NOT (NOT p AND r)
= (NOT p OR NOT q) AND (p OR NOT r)

Matching Patterns with LIKE

The preceding examples retrieved rows based on the exact value of a column or columns. You can use LIKE to retrieve rows based on partial information. LIKE is useful if you don’t know an exact value (“The author’s last name is Kel-something”) or you want to retrieve rows with similar values (“Which authors live in the San Francisco Bay Area?”). The LIKE condition’s important characteristics are:

To filter rows by matching a pattern:

Listing 4.30List the authors whose last names begin with Kel. See Figure 4.30 for the result.

SELECT au_fname, au_lname
  FROM authors
  WHERE au_lname LIKE 'Kel%';

Figure 4.30Result of Listing 4.30.

au_fname  au_lname
--------- --------
Christian Kells
          Kellsey

Listing 4.31List the authors whose last names have ll (el-el) as the third and fourth characters. See Figure 4.31 for the result.

SELECT au_fname, au_lname
  FROM authors
  WHERE au_lname LIKE '__ll%';

Figure 4.31Result of Listing 4.31.

au_fname  au_lname
--------- --------
Hallie    Hull
Klee      Hull
Christian Kells
          Kellsey

Listing 4.32List the authors who live in the San Francisco Bay Area (that is, with postal codes that begin with 94). See Figure 4.32 for the result.

SELECT au_fname, au_lname, city, state, zip
  FROM authors
  WHERE zip LIKE '94___';

Figure 4.32Result of Listing 4.32.

au_fname au_lname city          state zip
-------- -------- ------------- ----- -----
Hallie   Hull     San Francisco CA    94123
Klee     Hull     San Francisco CA    94123
         Kellsey  Palo Alto     CA    94305

Listing 4.33List the authors who live outside the 212, 415, and 303 area codes. This example shows three alternative patterns for excluding telephone numbers. You should favor the first alternative because single-character matches (_) are faster than multiple-character ones (%). See Figure 4.33 for the result.

SELECT au_fname, au_lname, phone
  FROM authors
  WHERE phone NOT LIKE '212-___-____'
    AND phone NOT LIKE '415-___-%'
    AND phone NOT LIKE '303-%';

Figure 4.33Result of Listing 4.33.

au_fname au_lname    phone
-------- ----------- ------------
Sarah    Buchman     718-496-7223
         Kellsey     650-836-7128
Paddy    O'Furniture 941-925-0752

You can search for values that contain the special wildcard characters. Use the ESCAPE keyword to specify an escape character that you can use to search for a percent sign or underscore as a literal character. Immediately precede a wildcard character with an escape character to strip the wildcard of its special meaning. If the escape character is !, for example, then !% in a pattern searches values for a literal %. (Unescaped wildcards still have their special meaning.) The escape character can’t be part of the value that you’re trying to retrieve; if you’re searching for '50% OFF!', then choose an escape character other than !. Table 4.9 shows some examples of escaped and unescaped patterns; the designated escape character is !.

Table 4.9Escaped and Unescaped Patterns
Pattern Matches
'100%' Unescaped. Matches 100 followed by a string of zero or more characters.
'100!%' Escaped. Matches ‘100%’.
'_op' Unescaped. Matches ‘top’, ‘hop’, ‘pop’, and so on.
'!_op' Escaped. Matches ‘_op’.

To match a wildcard character:

Listing 4.34List the titles that contain percent signs. Only the % that follows the escape character ! has its literal meaning; the other two percent signs still act as wildcards. See Figure 4.34 for the result.

SELECT title_name
  FROM titles
  WHERE title_name LIKE '%!%%' ESCAPE '!';

Figure 4.34Result of Listing 4.34. An empty result. No title names contain a % character.

title_name
----------------------------------------

Tips for LIKE

Range Filtering with BETWEEN

Use BETWEEN to determine whether a given value falls within a specified range. The BETWEEN condition’s important characteristics are:

To filter rows by using a range:

Listing 4.35List the authors who live outside the postal range 20000–89999. See Figure 4.35 for the result.

SELECT au_fname, au_lname, zip
  FROM authors
  WHERE zip NOT BETWEEN '20000' AND '89999';

Figure 4.35Result of Listing 4.35.

au_fname  au_lname zip
--------- -------- -----
Sarah     Buchman  10468
Hallie    Hull     94123
Klee      Hull     94123
Christian Kells    10014
          Kellsey  94305

Listing 4.36List the titles priced between $10 and $19.95, inclusive. See Figure 4.36 for the result.

SELECT title_id, price
  FROM titles
  WHERE price BETWEEN 10 AND 19.95;

Figure 4.36Result of Listing 4.36.

title_id price
-------- -----
T02      19.95
T04      12.99
T06      19.95
T08      10.00
T09      13.95
T12      12.99

Listing 4.37List the titles published in 2000. See Figure 4.37 for the result.

SELECT title_id, pubdate
  FROM titles
  WHERE
    pubdate BETWEEN DATE '2000-01-01'
                AND DATE '2000-12-31';

Figure 4.37Result of Listing 4.37.

title_id pubdate
-------- ----------
T01      2000-08-01
T03      2000-09-01
T06      2000-07-31
T11      2000-11-30
T12      2000-08-31

Tips for BETWEEN

List Filtering with IN

Use IN to determine whether a given value matches any value in a specified list. The IN condition’s important characteristics are:

To filter rows by using a list:

Listing 4.39List the authors who don’t live in New York State, New Jersey, or California. See Figure 4.39 for the result.

SELECT au_fname, au_lname, state
  FROM authors
  WHERE state NOT IN ('NY', 'NJ', 'CA');

Figure 4.39Result of Listing 4.39.

au_fname au_lname    state
-------- ----------- -----
Wendy    Heydemark   CO
Paddy    O'Furniture FL

Listing 4.40List the titles for which advances of $0, $1000, or $5000 were paid. See Figure 4.40 for the result.

SELECT title_id, advance
  FROM royalties
  WHERE advance IN (0.00, 1000.00, 5000.00);

Figure 4.40Result of Listing 4.40.

title_id advance
-------- -------
T02      1000.00
T08         0.00
T09         0.00

Listing 4.41List the titles published on the first day of the year 2000, 2001, or 2002. See Figure 4.41 for the result.

SELECT title_id, pubdate
  FROM titles
  WHERE pubdate IN
    (DATE '2000-01-01',
     DATE '2001-01-01',
     DATE '2002-01-01')
;

Figure 4.41Result of Listing 4.41.

title_id pubdate
-------- ----------
T05      2001-01-01

Tips for IN

Testing for Nulls with IS NULL

Recall from “Nulls” in Chapter 3 that nulls represent missing or unknown values. This situation causes a problem: LIKE, BETWEEN, IN, and other WHERE-clause conditions can’t find nulls because unknown values don’t satisfy specific conditions. A null matches no value—not even other nulls. You can’t use = or <> to test whether a value is null.

In the table publishers, for example, note that publisher P03 has a null in the column state because that column doesn’t apply to Germany (Listing 4.42 and Figure 4.42). I can’t use complementary comparisons to select the null because null is neither California nor not-California; it’s undefined (Listings 4.43 and 4.44, Figures 4.43 and 4.44).

Listing 4.42List the locations of all the publishers. See Figure 4.42 for the result.

SELECT pub_id, city, state, country
  FROM publishers;

Figure 4.42Result of Listing 4.42. The column state doesn’t apply to the publisher located in Germany.

pub_id city          state country
------ ------------- ----- -------
P01    New York      NY    USA
P02    San Francisco CA    USA
P03    Hamburg       NULL  Germany
P04    Berkeley      CA    USA

Listing 4.43List the publishers located in California. See Figure 4.43 for the result.

SELECT pub_id, city, state, country
  FROM publishers
  WHERE state = 'CA';

Figure 4.43Result of Listing 4.43. This result doesn’t include publisher P03.

pub_id city          state country
------ ------------- ----- -------
P02    San Francisco CA    USA
P04    Berkeley      CA    USA

Listing 4.44List the publishers located outside California (the wrong way—see Listing 4.45 for the correct way). See Figure 4.44 for the result.

SELECT pub_id, city, state, country
  FROM publishers
  WHERE state <> 'CA';

Figure 4.44Result of Listing 4.44. This result doesn’t include publisher P03 either. The conditions state = 'CA' and state <> 'CA' aren’t complementary after all; nulls don’t match any value and so can’t be selected by using the types of conditions I’ve covered so far.

pub_id city     state country
------ -------- ----- -------
P01    New York NY    USA

To avert disaster, SQL provides IS NULL to determine whether a given value is null. The IS NULL condition’s important characteristics are:

To retrieve rows with nulls or non-null values:

Listing 4.45List the publishers located outside California (the correct way). See Figure 4.45 for the result.

SELECT pub_id, city, state, country
  FROM publishers
  WHERE state <> 'CA'
     OR state IS NULL;

Figure 4.45Result of Listing 4.45. Now publisher P03 is in the result.

pub_id city     state country
------ -------- ----- -------
P01    New York NY    USA
P03    Hamburg  NULL  Germany

Listing 4.46List the biographies whose (past or future) publication dates are known. See Figure 4.46 for the result.

SELECT title_id, type, pubdate
  FROM titles
  WHERE type = 'biography'
    AND pubdate IS NOT NULL;

Figure 4.46Result of Listing 4.46. Without the IS NOT NULL condition, this result would have included title T10.

title_id type      pubdate
-------- --------- ----------
T06      biography 2000-07-31
T07      biography 1999-10-01
T12      biography 2000-08-31

Tips for IS NULL

5. Operators and Functions

Operators and functions let you calculate results derived from column values, system-determined values, constants, and other data. You can perform:

An operator is a symbol or keyword indicating an operation that acts on one or more elements. The elements, called operands, are SQL expressions. Recall from “Tips for SQL Syntax” in Chapter 3 that an expression is any legal combination of symbols and tokens that evaluates to a single value (or null). In the expression price * 2, for example, * is the operator, and price and 2 are its operands.

A function is a built-in, named routine that performs a specialized task. Most functions take parenthesized arguments, which are values you pass to the function that the function then uses to perform its task. Arguments can be column names, literals, nested functions, or more-complex expressions. In UPPER(au_lname), for example, UPPER is the function name, and au_lname is the argument.

Creating Derived Columns

You can use operators and functions to create derived columns. A derived column is the result of a calculation and is created with a SELECT-clause expression that is something other than a simple reference to a column. Derived columns don’t become permanent columns in a table; they’re for display and reporting purposes.

The values in a derived column are often computed from values in existing columns, but you can also create a derived column by using a constant expression (such as a string, number, or date) or system value (such as the system time). Listing 5.1 shows a SELECT statement that yields a trivial arithmetic calculation; it needs no FROM clause because it doesn’t retrieve data from a table. Figure 5.1 shows the result.

Listing 5.1A constant expression in a SELECT clause. No FROM clause is needed because I’m not retrieving data from a table. See Figure 5.1 for the result.

SELECT 2 + 3;

Figure 5.1Result of Listing 5.1. This result is a table with one row and one column.

2 + 3
-----
    5

Recall from “Tables, Columns, and Rows” in Chapter 2 that closure guarantees that every result is a table, so even this simple result is a table: a 1 × 1 table that contains the value 5. If I retrieve a column along with a constant, then the constant appears in every row of the result (Listing 5.2 and Figure 5.2).

Listing 5.2Here, I’ve retrieved a column and a constant expression. See Figure 5.2 for the result.

SELECT au_id, 2 + 3
  FROM authors;

Figure 5.2Result of Listing 5.2. The constant is repeated in each row.

au_id 2 + 3
----- -----
A01       5
A02       5
A03       5
A04       5
A05       5
A06       5
A07       5

Your DBMS will assign the derived column a default name, typically the expression itself as a quoted identifier. You should name derived columns explicitly with an AS clause because system-assigned names can be long, unwieldy, and inconvenient for database applications to refer to; see “Creating Column Aliases with AS” in Chapter 4 (Listing 5.3 and Figure 5.3).

Listing 5.3List the book prices discounted by 10 percent. The derived columns would have DBMS-specific default names if the AS clauses were removed. See Figure 5.3 for the result.

SELECT
    title_id,
    price,
    0.10 AS "Discount",
    price * (1 - 0.10) AS "New price"
  FROM titles;

Figure 5.3Result of Listing 5.3.

title_id price Discount New price
-------- ----- -------- ---------
T01      21.99     0.10     19.79
T02      19.95     0.10     17.95
T03      39.95     0.10     35.96
T04      12.99     0.10     11.69
T05       6.95     0.10      6.25
T06      19.95     0.10     17.95
T07      23.95     0.10     21.56
T08      10.00     0.10      9.00
T09      13.95     0.10     12.56
T10       NULL     0.10      NULL
T11       7.99     0.10      7.19
T12      12.99     0.10     11.69
T13      29.99     0.10     26.99

Tips for Derived Columns

Performing Arithmetic Operations

A monadic (or unary) arithmetic operator performs a mathematical operation on a single numeric operand to produce a result. The – (negation) operator changes the sign of its operand, and the not-very-useful + (identity) operator leaves its operand unchanged. A dyadic (or binary) arithmetic operator performs a mathematical operation on two numeric operands to produce a result. These operators include the usual ones: + (addition), – (subtraction), * (multiplication), and / (division). Table 5.1 lists SQL’s arithmetic operators (expr is a numeric expression).

Table 5.1Arithmetic Operators
Operator What It Does
expr Reverses the sign of expr
+expr Leaves expr unchanged
expr1 + expr2 Sums expr1 and expr2
expr1 – expr2 Subtracts expr2 from expr1
expr1 * expr2 Multiplies expr1 and expr2
expr1 / expr2 Divides expr1 by expr2

To change the sign of a number:

Listing 5.4The negation operator changes the sign of a number. See Figure 5.4 for the result.

SELECT
    title_id,
    -advance AS "Advance"
  FROM royalties;

Figure 5.4Result of Listing 5.4. Note that zero has no sign (is neither positive nor negative).

title_id Advance
-------- -----------
T01        -10000.00
T02         -1000.00
T03        -15000.00
T04        -20000.00
T05       -100000.00
T06        -20000.00
T07      -1000000.00
T08             0.00
T09             0.00
T10             NULL
T11       -100000.00
T12        -50000.00
T13        -20000.00

To add, subtract, multiply, or divide:

Listing 5.5List the biographies by descending revenue (= price × sales). See Figure 5.5 for the result.

SELECT
    title_id,
    price * sales AS "Revenue"
  FROM titles
  WHERE type = 'biography'
  ORDER BY price * sales DESC;

Figure 5.5Result of Listing 5.5.

title_id Revenue
-------- -----------
T07      35929790.00
T12       1299012.99
T06        225834.00
T10             NULL

Tips for Arithmetic Operations

Other Operators and Functions

All DBMSs provide plenty of operators and functions in addition to those defined in the SQL standard (or covered in this book). In fact, the standard is playing catch-up—many of the functions introduced in revised standards have existed for years in DBMSs. The earlier standards were so anemic that they left SQL weaker than a desktop calculator. Search your DBMS documentation for operators and functions to find mathematical, statistical, financial, scientific, trigonometric, conversion, string, datetime, bitwise, system, metadata, security, and other categories.

Determining the Order of Evaluation

Precedence determines the priority of various operators when more than one operator is used in an expression. Operators with higher precedence are evaluated first. Arithmetic operators (+, –, *, and so on) have higher precedence than comparison operators (<, =, >, and so on), which have higher precedence than logical operators (NOT, AND, OR), so the expression

a or b * c >= d

is equivalent to

a or ((b * c) >= d)

Operators with lower precedence are less binding than those with higher precedence. Table 5.2 lists operator precedences from most to least binding. Operators in the same row have equal precedence.

Table 5.2Order of Evaluation (Highest to Lowest)
Operator Description
+, – Monadic identity, monadic negation
*, / Multiplication, division
+, – Addition, subtraction
=, <>, <, <=, >, >= Comparison operators
NOT Logical NOT
AND Logical AND
OR Logical OR

Associativity determines the order of evaluation in an expression when adjacent operators have equal precedence. SQL uses left-to-right associativity.

You don’t need to memorize all this information. You can use parentheses to override precedence and associativity rules (Listing 5.7 and Figure 5.7).

Listing 5.7The first and second columns show how to use parentheses to override precedence rules. The third and fourth columns show how to use parentheses to override associativity rules. See Figure 5.7 for the result.

SELECT
    2 + 3 * 4   AS "2+3*4",
    (2 + 3) * 4 AS "(2+3)*4",
    6 / 2 * 3   AS "6/2*3",
    6 / (2 * 3) AS "6/(2*3)";

Figure 5.7Result of Listing 5.7.

2+3*4 (2+3)*4 6/2*3 6/(2*3)
----- ------- ----- -------
   14      20     9       1

Tips for Order of Evaluation

Concatenating Strings with ||

Use the operator || to combine, or concatenate, strings. The operator’s important characteristics are:

To concatenate strings:

Listing 5.8List the authors’ first and last names, concatenated into a single column and sorted by last name/first name. See Figure 5.8 for the result.

SELECT au_fname || ' ' || au_lname
    AS "Author name"
  FROM authors
  ORDER BY au_lname ASC, au_fname ASC;

Figure 5.8Result of Listing 5.8.

Author name
-----------------
Sarah Buchman
Wendy Heydemark
Hallie Hull
Klee Hull
Christian Kells
 Kellsey
Paddy O'Furniture

Listing 5.9List biography sales by descending sales order. Here, I need to convert sales from an integer to a string. See Figure 5.9 for the result.

SELECT
    CAST(sales AS CHAR(7))
    || ' copies sold of title '
    || title_id

      AS "Biography sales"
  FROM titles
  WHERE type = 'biography'
    AND sales IS NOT NULL
  ORDER BY sales DESC;

Figure 5.9Result of Listing 5.9.

Biography sales
--------------------------------
1500200 copies sold of title T07
100001  copies sold of title T12
11320   copies sold of title T06

Listing 5.10List biographies by descending publication date. Here, I need to convert pubdate from a datetime to a string. See Figure 5.10 for the result.

SELECT
    'Title '
    || title_id
    || ' published on '
    || CAST(pubdate AS CHAR(10))

      AS "Biography publication dates"
  FROM titles
  WHERE type = 'biography'
    AND pubdate IS NOT NULL
  ORDER BY pubdate DESC;

Figure 5.10Result of Listing 5.10.

Biography publication dates
---------------------------------
Title T12 published on 2000-08-31
Title T06 published on 2000-07-31
Title T07 published on 1999-10-01

Listing 5.11List all the authors named Klee Hull. See Figure 5.11 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE au_fname || ' ' || au_lname
        = 'Klee Hull';

Figure 5.11Result of Listing 5.11.

au_id au_fname au_lname
----- -------- --------
A04   Klee     Hull

Tips for Concatenating Strings

Extracting a Substring with SUBSTRING()

Use the function SUBSTRING() to extract part of a string. The function’s important characteristics are:

To extract a substring:

Listing 5.12Split the publisher IDs into alphabetic and numeric parts. The alphabetic part of a publisher ID is the first character, and the remaining characters are the numeric part. See Figure 5.12 for the result.

SELECT
    pub_id,
    SUBSTRING(pub_id FROM 1 FOR 1)
      AS "Alpha part",
    SUBSTRING(pub_id FROM 2)
      AS "Num part"
  FROM publishers;

Figure 5.12Result of Listing 5.12.

pub_id Alpha part Num part
------ ---------- --------
P01    P          01
P02    P          02
P03    P          03
P04    P          04

Listing 5.13List the first initial and last name of the authors from New York State and Colorado. See Figure 5.13 for the result.

SELECT
    SUBSTRING(au_fname FROM 1 FOR 1)
    || '. '
    || au_lname
      AS "Author name",
    state
  FROM authors
  WHERE state IN ('NY', 'CO');

Figure 5.13Result of Listing 5.13.

Author name  state
------------ -----
S. Buchman   NY
W. Heydemark CO
C. Kells     NY

Listing 5.14List the authors whose area code is 415. See Figure 5.14 for the result.

SELECT au_fname, au_lname, phone
  FROM authors
  WHERE SUBSTRING(phone FROM 1 FOR 3) = '415';

Figure 5.14Result of Listing 5.14.

au_fname au_lname phone
-------- -------- ------------
Hallie   Hull     415-549-4278
Klee     Hull     415-549-4278

Tips for Substrings

Changing String Case with UPPER() and LOWER()

Use the function UPPER() to return a string with lowercase letters converted to uppercase, and use the function LOWER() to return a string with uppercase letters converted to lowercase. The functions’ important characteristics are:

To convert a string to uppercase or lowercase:

Listing 5.15List the authors’ first names in lowercase and last names in uppercase. See Figure 5.15 for the result.

SELECT
    LOWER(au_fname) AS "Lower",
    UPPER(au_lname) AS "Upper"
  FROM authors;

Figure 5.15Result of Listing 5.15.

Lower     Upper
--------- -----------
sarah     BUCHMAN
wendy     HEYDEMARK
hallie    HULL
klee      HULL
christian KELLS
          KELLSEY
paddy     O'FURNITURE

Listing 5.16List the titles that contain the characters MO, regardless of case. All the letters in the LIKE pattern must be uppercase for this query to work. See Figure 5.16 for the result.

SELECT title_name
  FROM titles
  WHERE UPPER(title_name) LIKE '%MO%';

Figure 5.16Result of Listing 5.16.

title_name
-------------------------
200 Years of German Humor
I Blame My Mother

Tips for String Case

Case-Insensitive Comparisons

In DBMSs that perform case-sensitive WHERE-clause comparisons by default, UPPER() or LOWER() often is used to make case-insensitive comparisons:

WHERE UPPER(au_fname) = 'JOHN'

If you’re sure that your data are clean, then it’s faster to look for only reasonable letter combinations than to use case functions:

WHERE au_fname = 'JOHN'
   OR au_fname = 'John'

UPPER() and LOWER() affect characters with diacritical marks (such as accents and umlauts): UPPER('ö') is 'Ö', for example. If your data contain such characters and you’re making case-insensitive comparisons such as

WHERE UPPER(au_fname) = 'JOSÉ'

then make sure that your DBMS doesn’t lose the marks on conversion. UPPER('José') should be 'JOSÉ', not 'JOSE'. See also “Filtering Rows with WHERE” in Chapter 4.

Trimming Characters with TRIM()

Use the function TRIM() to remove unwanted characters from the ends of a string. The function’s important characteristics are:

To trim spaces from a string:

Listing 5.17This query strips leading, trailing, and both leading and trailing spaces from the string '  AAA  '. The < and > characters show the extent of the trimmed strings. See Figure 5.17 for the result.

SELECT
  '<' || '  AAA  ' || '>'
    AS "Untrimmed",
  '<' || TRIM(LEADING FROM '  AAA  ') || '>'
    AS "Leading",
  '<' || TRIM(TRAILING FROM '  AAA  ') || '>'
    AS "Trailing",
  '<' || TRIM('  AAA  ') || '>'
     AS "Both";

Figure 5.17Result of Listing 5.17.

Untrimmed Leading   Trailing  Both
--------- --------- --------- -----
<  AAA  > <AAA  >   <  AAA>   <AAA>

To trim characters from a string:

Listing 5.18Strip the leading H from the authors’ last names that begin with H. See Figure 5.18 for the result.

SELECT
    au_lname,
    TRIM(LEADING 'H' FROM au_lname)
      AS "Trimmed name"
  FROM authors;

Figure 5.18Result of Listing 5.18.

au_lname    Trimmed name
----------- ------------
Buchman     Buchman
Heydemark   eydemark
Hull        ull
Hull        ull
Kells       Kells
Kellsey     Kellsey
O'Furniture O'Furniture

Listing 5.19List the three-character title IDs that start with T1, ignoring leading and trailing spaces. See Figure 5.19 for the result.

SELECT title_id
  FROM titles
  WHERE TRIM(title_id) LIKE 'T1_';

Figure 5.19Result of Listing 5.19.

title_id
--------
T10
T11
T12
T13

Tips for Trimming Characters

Finding the Length of a String with CHARACTER_LENGTH()

Use the function CHARACTER_LENGTH() to return the number of characters in a string. The function’s important characteristics are:

To find the length of a string:

Listing 5.20List the lengths of the authors’ first names. See Figure 5.20 for the result.

SELECT
    au_fname,
    CHARACTER_LENGTH(au_fname) AS "Len"
  FROM authors;

Figure 5.20Result of Listing 5.20.

au_fname  Len
--------- ---
Sarah       5
Wendy       5
Hallie      6
Klee        4
Christian   9
            0
Paddy       5

Listing 5.21List the books whose titles contain fewer than 30 characters, sorted by ascending title length. See Figure 5.21 for the result.

SELECT
    title_name,
    CHARACTER_LENGTH(title_name) AS "Len"
  FROM titles
  WHERE CHARACTER_LENGTH(title_name) < 30
  ORDER BY CHARACTER_LENGTH(title_name) ASC;

Figure 5.21Result of Listing 5.21.

title_name                    Len
----------------------------- ---
1977!                           5
Kiss My Boo-Boo                15
How About Never?               16
I Blame My Mother              17
Exchange of Platitudes         22
200 Years of German Humor      25
Spontaneous, Not Annoying      25
But I Did It Unconsciously     26
Not Without My Faberge Egg     26
Just Wait Until After School   28
Ask Your System Administrator  29

Tips for String Lengths

Finding Substrings with POSITION()

Use the function POSITION() to locate a particular substring within a given string. The function’s important characteristics are:

To find a substring:

Listing 5.22List the position of the substring e in the authors’ first names and the position of the substring ma in the authors’ last names. See Figure 5.22 for the result.

SELECT
    au_fname,
    POSITION('e' IN au_fname) AS "Pos e",
    au_lname,
    POSITION('ma' IN au_lname) AS "Pos ma"
  FROM authors;

Figure 5.22Result of Listing 5.22.

au_fname  Pos e au_lname    Pos ma
--------- ----- ----------- ------
Sarah         0 Buchman          5
Wendy         2 Heydemark        6
Hallie        6 Hull             0
Klee          3 Hull             0
Christian     0 Kells            0
              0 Kellsey          0
Paddy         0 O'Furniture      0

Listing 5.23List the books whose titles contain the letter u somewhere within the first 10 characters, sorted by descending position of the u. See Figure 5.23 for the result.

SELECT
    title_name,
    POSITION('u' IN title_name) AS "Pos"
  FROM titles
  WHERE POSITION('u' IN title_name)
        BETWEEN 1 AND 10
  ORDER BY POSITION('u' IN title_name) DESC;

Figure 5.23Result of Listing 5.23.

title_name                    Pos
----------------------------- ---
Not Without My Faberge Egg     10
Spontaneous, Not Annoying      10
How About Never?                8
Ask Your System Administrator   7
But I Did It Unconsciously      2
Just Wait Until After School    2

Tips for Finding Substrings

Performing Datetime and Interval Arithmetic

DBMS compliance with standard SQL datetime and interval operators and functions is spotty because DBMSs usually provide their own extended (nonstandard) operators and functions that perform date and time arithmetic. For information about datetime and interval data types, see “Datetime Types” and “Interval Types” in Chapter 3.

Use the same operators introduced in “Performing Arithmetic Operations” earlier in this chapter to perform datetime and interval arithmetic. The common temporal operations are:

Some operations are undefined; adding two dates makes no sense, for example. Table 5.3 lists the valid SQL operators involving datetimes and intervals. The “Operator Overloading” section explains why you can use the same operator to perform different operations.

Table 5.3Datetime and Interval Operations
Operation Result
Datetime â€“ Datetime Interval
Datetime + Interval Datetime
Datetime â€“ Interval Datetime
Interval + Datetime Datetime
Interval + Interval Interval
Interval â€“ Interval Interval
Interval * Numeric Interval
Interval / Numeric Interval
Numeric * Interval Interval

Operator Overloading

Recall that the +, –, *, and / operators also are used for numeric operations and that Microsoft DBMSs use + for string concatenation as well. Operator overloading is the assignment of more than one function to a particular operator. The operation performed depends on the data types of the operands involved. Here, the +, –, *, and / operators behave differently with numbers than they do with datetimes and intervals (as well as strings, in the case of Microsoft Access and Microsoft SQL Server). Your DBMS might overload other operators and functions as well.

Function overloading is the assignment of more than one behavior to a particular function, depending on the data types of the arguments involved. The MySQL CONCAT() function (see the DBMS tip in “Tips for Concatenating Strings” earlier in this chapter), for example, takes nonstring as well as string arguments. Nonstrings cause CONCAT() to perform additional conversions that it doesn’t need to perform on strings.

The function EXTRACT() isolates a single field of a datetime or interval and returns it as a number. EXTRACT() typically is used in comparison expressions or for formatting results.

To extract part of a datetime or interval:

Listing 5.24List the books published in the first half of the years 2001 and 2002, sorted by descending publication date. See Figure 5.24 for the result.

SELECT
    title_id,
    pubdate
  FROM titles
  WHERE EXTRACT(YEAR FROM pubdate)
        BETWEEN 2001 AND 2002
    AND EXTRACT(MONTH FROM pubdate)
        BETWEEN 1 AND 6
  ORDER BY pubdate DESC;

Figure 5.24Result of Listing 5.24.

title_id pubdate
-------- ----------
T09      2002-05-31
T08      2001-06-01
T05      2001-01-01

Tips for Datetime and Interval Arithmetic

Getting the Current Date and Time

Use the functions CURRENT_DATE, CURRENT_TIME, and CURRENT_TIMESTAMP to get the current date and time from the system clock of the particular computer where the DBMS is running.

To get the current date and time:

Listing 5.25Print the current date, time, and timestamp. See Figure 5.25 for the result.

SELECT
    CURRENT_DATE AS "Date",
    CURRENT_TIME AS "Time",
    CURRENT_TIMESTAMP AS "Timestamp";

Figure 5.25Result of Listing 5.25.

Date       Time     Timestamp
---------- -------- -------------------
2002-03-10 10:09:24 2002-03-10 10:09:24

Listing 5.26List the books whose publication date falls within 90 days of the current date or is unknown, sorted by descending publication date (refer to Figure 5.25 for the “current” date of this query). See Figure 5.26 for the result.

SELECT title_id, pubdate
  FROM titles
  WHERE pubdate
        BETWEEN CURRENT_TIMESTAMP - INTERVAL 90 DAY
            AND CURRENT_TIMESTAMP + INTERVAL 90 DAY
     OR pubdate IS NULL
  ORDER BY pubdate DESC;

Figure 5.26Result of Listing 5.26.

title_id pubdate
-------- ----------
T09      2002-05-31
T10      NULL

Tips for the Current Date and Time

Getting User Information

Use the function CURRENT_USER to identify the active user within the database server.

To get the current user:

Listing 5.27Print the current user. See Figure 5.27 for the result.

SELECT CURRENT_USER AS "User";

Figure 5.27Result of Listing 5.27.

User
------
jsmith

Tips for User Information

Converting Data Types with CAST()

In many situations, your DBMS will convert, or cast, data types automatically. It might allow you to use numbers and dates in character expressions such as concatenation, for example, or it will promote numbers automatically in mixed arithmetic expressions (see “Tips for Arithmetic Operations” earlier in this chapter). Use the function CAST() to convert an expression of one data type to another data type when your DBMS doesn’t perform the conversion automatically. For information about data types, see “Data Types” in Chapter 3. The function’s important characteristics are:

To convert one data type to another:

Listing 5.28Convert the book prices from the DECIMAL data type to INTEGER and CHAR(8) data types. The < and > characters show the extent of the CHAR(8) strings. Your result will be either Figure 5.28a or 5.28b, depending on whether your DBMS truncates or rounds integers.

SELECT
    price
      AS "price(DECIMAL)",
    CAST(price AS INTEGER)
      AS "price(INTEGER)",
    '<' || CAST(price AS CHAR(8)) || '>'
      AS "price(CHAR(8))"
  FROM titles;

Figure 5.28aResult of Listing 5.28. You’ll get this result if your DBMS truncates decimal numbers to convert them to integers.

price(DECIMAL) price(INTEGER) price(CHAR(8))
-------------- -------------- --------------
         21.99             21 <21.99   >
         19.95             19 <19.95   >
         39.95             39 <39.95   >
         12.99             12 <12.99   >
          6.95              6 <6.95    >
         19.95             19 <19.95   >
         23.95             23 <23.95   >
         10.00             10 <10.00   >
         13.95             13 <13.95   >
          NULL           NULL NULL
          7.99              7 <7.99    >
         12.99             12 <12.99   >
         29.99             29 <29.99   >

Figure 5.28bResult of Listing 5.28. You’ll get this result if your DBMS rounds decimal numbers to convert them to integers.

price(DECIMAL) price(INTEGER) price(CHAR(8))
-------------- -------------- --------------
         21.99             22 <21.99   >
         19.95             20 <19.95   >
         39.95             40 <39.95   >
         12.99             13 <12.99   >
          6.95              7 <6.95    >
         19.95             20 <19.95   >
         23.95             24 <23.95   >
         10.00             10 <10.00   >
         13.95             14 <13.95   >
          NULL           NULL NULL
          7.99              8 <7.99    >
         12.99             13 <12.99   >
         29.99             30 <29.99   >

Listing 5.29List history and biography book sales with a portion of the book title, sorted by descending sales. The CHAR(20) conversion shortens the title to make the result more readable. See Figure 5.29 for the result.

SELECT
    CAST(sales AS CHAR(8))
    || ' copies sold of '
    || CAST(title_name AS CHAR(20))
      AS "History and biography sales"
  FROM titles
  WHERE sales IS NOT NULL
    AND type IN ('history', 'biography')
  ORDER BY sales DESC;

Figure 5.29Result of Listing 5.29.

History and biography sales
--------------------------------------------
1500200  copies sold of I Blame My Mother
100001   copies sold of Spontaneous, Not Ann
11320    copies sold of How About Never?
10467    copies sold of What Are The Civilia
9566     copies sold of 200 Years of German
566      copies sold of 1977!

Tips for Converting Data Types

Evaluating Conditional Values with CASE

The CASE expression and its shorthand equivalents, COALESCE() and NULLIF(), let you take actions based on a condition’s truth value (true, false, or unknown). The CASE expression’s important characteristics are:

To use a simple CASE expression:

Listing 5.30Raise the price of history books by 10 percent and psychology books by 20 percent, and leave the prices of other books unchanged. See Figure 5.30 for the result.

SELECT
    title_id,
    type,
    price,
    CASE type
      WHEN 'history'
        THEN price * 1.10
      WHEN 'psychology'
        THEN price * 1.20
      ELSE price
    END
      AS "New price"
  FROM titles
  ORDER BY type ASC, title_id ASC;

Figure 5.30Result of Listing 5.30.

title_id type       price New price
-------- ---------- ----- ---------
T06      biography  19.95     19.95
T07      biography  23.95     23.95
T10      biography   NULL      NULL
T12      biography  12.99     12.99
T08      children   10.00     10.00
T09      children   13.95     13.95
T03      computer   39.95     39.95
T01      history    21.99     24.19
T02      history    19.95     21.95
T13      history    29.99     32.99
T04      psychology 12.99     15.59
T05      psychology  6.95      8.34
T11      psychology  7.99      9.59

To use a searched CASE expression:

Listing 5.31List the books categorized by different sales ranges, sorted by ascending sales. See Figure 5.31 for the result.

SELECT
    title_id,
    CASE
      WHEN sales IS NULL
        THEN 'Unknown'
      WHEN sales <= 1000
        THEN 'Not more than 1,000'
      WHEN sales <= 10000
        THEN 'Between 1,001 and 10,000'
      WHEN sales <= 100000
        THEN 'Between 10,001 and 100,000'
      WHEN sales <= 1000000
        THEN 'Between 100,001 and 1,000,000'
      ELSE 'Over 1,000,000'
    END
      AS "Sales category"
  FROM titles
  ORDER BY sales ASC;

Figure 5.31Result of Listing 5.31.

title_id Sales category
-------- -----------------------------
T10      Unknown
T01      Not more than 1,000
T08      Between 1,001 and 10,000
T09      Between 1,001 and 10,000
T02      Between 1,001 and 10,000
T13      Between 10,001 and 100,000
T06      Between 10,001 and 100,000
T04      Between 10,001 and 100,000
T03      Between 10,001 and 100,000
T11      Between 10,001 and 100,000
T12      Between 100,001 and 1,000,000
T05      Between 100,001 and 1,000,000
T07      Over 1,000,000

Tips for CASE

Checking for Nulls with COALESCE()

The function COALESCE() returns the first non-null expression among its arguments. COALESCE() often is used to display a specific value instead of a null in a result, which is helpful if your users find nulls confusing. COALESCE() is just shorthand for a common form of the searched CASE expression.

COALESCE(expr1, expr2, expr3)

is equivalent to:

CASE
  WHEN expr1 IS NOT NULL THEN expr1
  WHEN expr2 IS NOT NULL THEN expr2
  ELSE expr3
END

To return the first non-null value:

Listing 5.32List the publishers’ locations. If the state is null, then print N/A. See Figure 5.32 for the result.

SELECT
    pub_id,
    city,
    COALESCE(state, 'N/A') AS "state",
    country
  FROM publishers;

Figure 5.32Result of Listing 5.32.

pub_id city          state country
------ ------------- ----- -------
P01    New York      NY    USA
P02    San Francisco CA    USA
P03    Hamburg       N/A   Germany
P04    Berkeley      CA    USA

Tips for COALESCE

Comparing Expressions with NULLIF()

The function NULLIF() compares two expressions and returns null if they are equal or the first expression otherwise. NULLIF() typically is used to convert a user-defined missing, unknown, or inapplicable value to null.

Rather than use a null, some people prefer to represent a missing value with, say, the number −1 or −99, or the string ‘N/A’, ‘Unknown’, or ‘Missing’. DBMSs have clear rules for operations that involve nulls, so it’s sometimes desirable to convert user-defined missing values to nulls. If you want to calculate the average of the values in a column, for example, then you’d get the wrong answer if you had −1 values intermingled with the real, non-missing values. Instead, you can use NULLIF() to convert the −1 values to nulls, which your DBMS will ignore during calculations.

NULLIF() is just shorthand for a common form of the searched CASE expression.

NULLIF(expr1, expr2)

is equivalent to:

CASE
  WHEN expr1 = expr2 THEN NULL
  ELSE expr1
END

To return a null if two expressions are equivalent:

Listing 5.33In the table titles, the column contract contains zero if no book contract exists. This query changes the value zero to null. Nonzero values aren’t affected. See Figure 5.33 for the result.

SELECT
    title_id,
    contract,
    NULLIF(contract, 0) AS "Null contract"
  FROM titles;

Figure 5.33Result of Listing 5.33.

title_id contract Null contract
-------- -------- -------------
T01             1             1
T02             1             1
T03             1             1
T04             1             1
T05             1             1
T06             1             1
T07             1             1
T08             1             1
T09             1             1
T10             0          NULL
T11             1             1
T12             1             1
T13             1             1

Tips for NULLIF

Avoiding Division by Zero

Suppose you want to calculate the male–female ratios for various school clubs, but you discover that the following query fails and issues a divide-by-zero error when it tries to calculate ratio for the Lord of the Rings Club, which has no women:

SELECT
    club_id, males, females,
    males/females AS ratio
  FROM school_clubs;

You can use NULLIF to avoid division by zero. Rewrite the query as:

SELECT
    club_id, males, females,
    males/NULLIF(females,0) AS ratio
  FROM school_clubs;

Any number divided by NULL gives NULL, and no error is generated.

6. Summarizing and Grouping Data

The preceding chapter described scalar functions, which operate on individual row values. This chapter introduces SQL’s aggregate functions, or set functions, which operate on a group of values to produce a single, summarizing value. You apply an aggregate to a set of rows, which can be:

A GROUP BY clause, which groups rows, often is used with a HAVING clause, which filters groups. No matter how many rows the input set contains, an aggregate function returns a single statistic: a sum, minimum, or average, for example.

The main difference between queries with and without aggregate functions is that nonaggregate queries process the rows one by one. Each row is processed independently and put into the result. (ORDER BY and DISTINCT make the DBMS look at all the rows, but they’re essentially postprocessing operations.) Aggregate queries do something completely different: they take a table as a whole and construct new rows from it.

Using Aggregate Functions

Table 6.1 lists SQL’s standard aggregate functions.

Table 6.1Aggregate Functions
Function Returns
MIN(expr) Minimum value in expr
MAX(expr) Maximum value in expr
SUM(expr) Sum of the values in expr
AVG(expr) Average (arithmetic mean) of the values in expr
COUNT(expr) The number of non-null values in expr
COUNT(*) The number of rows in a table or set

The important characteristics of the aggregate functions are:

Tips for Aggregate Functions

Creating Aggregate Expressions

Aggregate functions can be tricky to use. This section explains what’s legal and what’s not.

Tips for Aggregate Expressions

Finding a Minimum with MIN()

Use the aggregate function MIN() to find the minimum of a set of values.

To find the minimum of a set of values:

Listing 6.1 and Figure 6.1 show some queries that involve MIN(). The first query returns the price of the lowest-priced book. The second query returns the earliest publication date. The third query returns the number of pages in the shortest history book.

Listing 6.1Some MIN() queries. See Figure 6.1 for the result.

SELECT MIN(price) AS "Min price"
  FROM titles;

SELECT MIN(pubdate) AS "Earliest pubdate"
  FROM titles;

SELECT MIN(pages) AS "Min history pages"
  FROM titles
  WHERE type = 'history';

Figure 6.1Result of Listing 6.1.

Min price
---------
     6.95

Earliest pubdate
----------------
1998-04-01

Min history pages
-----------------
               14

Tips for MIN

Finding a Maximum with MAX()

Use the aggregate function MAX() to find the maximum of a set of values.

To find the maximum of a set of values:

Listing 6.2 and Figure 6.2 show some queries that involve MAX(). The first query returns the author’s last name that is last alphabetically. The second query returns the prices of the cheapest and most expensive books, as well as the price range. The third query returns the highest revenue (= price × sales) among the history books.

Listing 6.2Some MAX() queries. See Figure 6.2 for the result.

SELECT MAX(au_lname) AS "Max last name"
  FROM authors;

SELECT
    MIN(price) AS "Min price",
    MAX(price) AS "Max price",
    MAX(price) - MIN(price) AS "Range"
  FROM titles;

SELECT MAX(price * sales)
         AS "Max history revenue"
  FROM titles
  WHERE type = 'history';

Figure 6.2Result of Listing 6.2.

Max last name
-------------
O'Furniture

Min price Max price Range
--------- --------- -----
     6.95     39.95 33.00

Max history revenue
-------------------
          313905.33

Tips for MAX

Calculating a Sum with SUM()

Use the aggregate function SUM() to find the sum (total) of a set of values.

To calculate the sum of a set of values:

Listing 6.3 and Figure 6.3 show some queries that involve SUM(). The first query returns the total advances paid to all authors. The second query returns the total sales of books published in 2000. The third query returns the total price, sales, and revenue (= price × sales) of all books. Note a mathematical rule in action here: “The sum of the products doesn’t (necessarily) equal the product of the sums.”

Listing 6.3Some SUM() queries. See Figure 6.3 for the result.

SELECT
    SUM(advance) AS "Total advances"
  FROM royalties;

SELECT
    SUM(sales)
      AS "Total sales (2000 books)"
  FROM titles
  WHERE pubdate
    BETWEEN DATE '2000-01-01'
        AND DATE '2000-12-31';

SELECT
    SUM(price) AS "Total price",
    SUM(sales) AS "Total sales",
    SUM(price * sales) AS "Total revenue"
  FROM titles;

Figure 6.3Result of Listing 6.3.

Total advances
--------------
    1336000.00

Total sales (2000 books)
------------------------
                  231677

Total price Total sales Total revenue
----------- ----------- -------------
     220.65     1975446   41428860.77

Tips for SUM

Calculating an Average with AVG()

Use the aggregate function AVG() to find the average, or arithmetic mean, of a set of values. The arithmetic mean is the sum of a set of quantities divided by the number of quantities in the set.

To calculate the average of a set of values:

Listing 6.4 and Figure 6.4 shows some queries that involve AVG(). The first query returns the average price of all books if prices were doubled. The second query returns the average and total sales for business books; both calculations are null (not zero) because the table contains no business books. The third query uses a subquery (see Chapter 8) to list the books with above-average sales.

Listing 6.4Some AVG() queries. See Figure 6.4 for the result.

SELECT
    AVG(price * 2) AS "AVG(price * 2)"
  FROM titles;

SELECT
    AVG(sales) AS "AVG(sales)",
    SUM(sales) AS "SUM(sales)"
  FROM titles
  WHERE type = 'business';

SELECT title_id, sales
  FROM titles
  WHERE sales >
        (SELECT AVG(sales) FROM titles)
  ORDER BY sales DESC;

Figure 6.4Result of Listing 6.4.

AVG(price * 2)
--------------
     36.775000

AVG(sales) SUM(sales)
---------- ----------
NULL       NULL

title_id sales
-------- -------
T07      1500200
T05       201440

Tips for AVG

Aggregating and Nulls

Aggregate functions (except COUNT(*)) ignore nulls. If an aggregation requires that you account for nulls, then you can replace each null with a specified value by using COALESCE() (see “Checking for Nulls with COALESCE()” in Chapter 5). For example, the following query returns the average sales of biographies by including nulls (replaced by zeroes) in the calculation:

SELECT AVG(COALESCE(sales,0))
    AS AvgSales
  FROM titles
  WHERE type = 'biography';

Statistics in SQL

SQL isn’t a statistical programming language, but you can use built-in functions and a few tricks to calculate simple descriptive statistics such as the sum, mean, and standard deviation. For more-sophisticated analyses you should use your DBMS’s statistics, data science, or machine learning components or export your data to a dedicated statistical environment such as Excel, R, Python, SAS, or SPSS.

What you should not do is write statistical routines yourself in SQL or a host language. Implementing statistical algorithms correctly—even simple ones—means understanding trade-offs in efficiency (the space needed for arithmetic operations), stability (cancellation of significant digits), and accuracy (handling pathologic sets of values). See, for example, Ronald Thisted’s Elements of Statistical Computing or John Monahan’s Numerical Methods of Statistics.

You can get away with using small combinations of built-in SQL functions, such as STDEV()/(SQRT(COUNT()) for the standard error of the mean, but don’t use complex SQL expressions for correlations, regression, ANOVA (analysis of variance), or matrix arithmetic, for example. Check your DBMS’s documentation to see which functions and packages it offers. Built-in functions aren’t portable, but they run far faster and more accurately than equivalent query expressions.

The functions MIN() and MAX() calculate order statistics, which are values derived from a dataset that’s been sorted (ordered) by size. Well-known order statistics include the trimmed mean, rank, range, mode, and median. Chapter 15 covers the trimmed mean, rank, and median. The range is the difference between the largest and smallest values: MAX(expr) – MIN(expr). The mode is the value that appears most frequently. A dataset can have more than one mode. The mode is a weak descriptive statistic because it’s not robust, meaning that it can be affected by adding a small number or unusual or incorrect values to the dataset. This query finds the mode of book prices in the sample database:

SELECT price, COUNT(*) AS frequency
  FROM titles
  GROUP BY price
  HAVING COUNT(*) >=
    ALL(SELECT COUNT(*)
          FROM titles
          GROUP BY price);

price has two modes:

price   frequency
-----   ---------
12.99   2
19.95   2

Counting Rows with COUNT()

Use the aggregate function COUNT() to count the number of rows in a set of values. COUNT() has two forms:

To count non-null rows:

To count all rows, including nulls:

Listing 6.5 and Figure 6.5 show some queries that involve COUNT(expr) and COUNT(*). The three queries count rows in the table titles and are identical except for the WHERE clause. The row counts in the first query differ because the column price contains a null. In the second query, the row counts are identical because the WHERE clause eliminates the row with the null price before the count. The third query shows the row-count differences between the results of the first two queries.

Listing 6.5Some COUNT() queries. See Figure 6.5 for the result.

SELECT
    COUNT(title_id) AS "COUNT(title_id)",
    COUNT(price) AS "COUNT(price)",
    COUNT(*) AS "COUNT(*)"
  FROM titles;

SELECT
    COUNT(title_id) AS "COUNT(title_id)",
    COUNT(price) AS "COUNT(price)",
    COUNT(*) AS "COUNT(*)"
  FROM titles
  WHERE price IS NOT NULL;

SELECT
    COUNT(title_id) AS "COUNT(title_id)",
    COUNT(price) AS "COUNT(price)",
    COUNT(*) AS "COUNT(*)"
  FROM titles
  WHERE price IS NULL;

Figure 6.5Result of Listing 6.5.

COUNT(title_id) COUNT(price) COUNT(*)
--------------- ------------ --------
             13           12       13

COUNT(title_id) COUNT(price) COUNT(*)
--------------- ------------ --------
             12           12       12

COUNT(title_id) COUNT(price) COUNT(*)
--------------- ------------ --------
              1            0        1

Tips for COUNT

Aggregating Distinct Values with DISTINCT

You can use DISTINCT to eliminate duplicate values in aggregate function calculations; see “Eliminating Duplicate Rows with DISTINCT” in Chapter 4. The general syntax of an aggregate function is:

agg_func([ALL | DISTINCT] expr)

agg_func is MIN, MAX, SUM, AVG, or COUNT. expr is a column name, literal, or expression. ALL applies the aggregate function to all values, and DISTINCT specifies that each unique value is considered. ALL is the default and is usually omitted in practice.

With SUM(), AVG(), and COUNT(expr), DISTINCT eliminates duplicate values before the sum, average, or count is calculated. DISTINCT isn’t meaningful with MIN() and MAX(); you can use it, but it won’t change the result. You can’t use DISTINCT with COUNT(*).

To calculate the sum of a set of distinct values:

To calculate the average of a set of distinct values:

To count distinct non-null rows:

The queries in Listing 6.6 return the count, sum, and average of book prices. The non-DISTINCT and DISTINCT results in Figure 6.6 differ because the DISTINCT results eliminate the duplicates of prices $12.99 and $19.95 from calculations.

Listing 6.6Some DISTINCT aggregate queries. See Figure 6.6 for the result.

SELECT
    COUNT(*) AS "COUNT(*)"
  FROM titles;

SELECT
    COUNT(price) AS "COUNT(price)",
    SUM(price)   AS "SUM(price)",
    AVG(price)   AS "AVG(price)"
  FROM titles;

SELECT
    COUNT(DISTINCT price)
      AS "COUNT(DISTINCT)",
    SUM(DISTINCT price)
      AS "SUM(DISTINCT)",
    AVG(DISTINCT price)
      AS "AVG(DISTINCT)"
  FROM titles;

Figure 6.6Result of Listing 6.6.

COUNT(*)
--------
      13

COUNT(price) SUM(price) AVG(price)
------------ ---------- ----------
          12     220.65    18.3875

COUNT(DISTINCT) SUM(DISTINCT) AVG(DISTINCT)
--------------- ------------- -------------
             10        187.71       18.7710

DISTINCT in a SELECT clause (Chapter 4) and DISTINCT in an aggregate function don’t return the same result.

The three queries in Listing 6.7 count the author IDs in the table title_authors. Figure 6.7 shows the results. The first query counts all the author IDs in the table. The second query returns the same result as the first query because COUNT() already has done its work and returned a value in a single row before DISTINCT is applied. In the third query, DISTINCT is applied to the author IDs before COUNT() starts counting.

Listing 6.7DISTINCT in a SELECT clause and DISTINCT in an aggregate function differ in meaning. See Figure 6.7 for the result.

SELECT
    COUNT(au_id)
      AS "COUNT(au_id)"
  FROM title_authors;

SELECT
    DISTINCT COUNT(au_id)
      AS "DISTINCT COUNT(au_id)"
  FROM title_authors;

SELECT
    COUNT(DISTINCT au_id)
      AS "COUNT(DISTINCT au_id)"
  FROM title_authors;

Figure 6.7Result of Listing 6.7.

COUNT(au_id)
------------
          17

DISTINCT COUNT(au_id)
---------------------
                   17

COUNT(DISTINCT au_id)
---------------------
                   6

Mixing non-DISTINCT and DISTINCT aggregates in the same SELECT clause can produce misleading results.

The four queries in Listing 6.8 show the four combinations of non-DISTINCT and DISTINCT sums and counts. Of the four results in Figure 6.8, only the first result (no DISTINCTs) and final result (all DISTINCTs) are consistent mathematically, which you can verify with AVG(price) and AVG(DISTINCT price). In the second and third queries (mixed non-DISTINCTs and DISTINCTs), you can’t calculate a valid average by dividing the sum by the count.

Listing 6.8Combining non-DISTINCT and DISTINCT aggregates gives inconsistent results. See Figure 6.8 for the result.

SELECT
    COUNT(price)
      AS "COUNT(price)",
    SUM(price)
      AS "SUM(price)"
  FROM titles;

SELECT
    COUNT(price)
      AS "COUNT(price)",
    SUM(DISTINCT price)
      AS "SUM(DISTINCT price)"
  FROM titles;

SELECT
    COUNT(DISTINCT price)
      AS "COUNT(DISTINCT price)",
    SUM(price)
      AS "SUM(price)"
  FROM titles;

SELECT
    COUNT(DISTINCT price)
      AS "COUNT(DISTINCT price)",
    SUM(DISTINCT price)
      AS "SUM(DISTINCT price)"
  FROM titles;

Figure 6.8Result of Listing 6.8. The differences in the counts and sums indicate duplicate prices. Averages (sum/count) obtained from the second (187.71/12) or third query (220.65/10) are incorrect. The first (220.65/12) and fourth (187.71/10) queries produce consistent averages.

COUNT(price) SUM(price)
------------ ----------
          12     220.65

COUNT(price) SUM(DISTINCT price)
------------ -------------------
          12              187.71

COUNT(DISTINCT price) SUM(price)
--------------------- ----------
                   10     220.65

COUNT(DISTINCT price) SUM(DISTINCT price)
--------------------- -------------------
                   10              187.71

Tips for DISTINCT

Grouping Rows with GROUP BY

To this point, I’ve used aggregate functions to summarize all the values in a column or just those values that matched a WHERE search condition. You can use the GROUP BY clause to divide a table into logical groups (categories) and calculate aggregate statistics for each group.

An example will clarify the concept. Listing 6.9 uses GROUP BY to count the number of books that each author wrote (or cowrote). In the SELECT clause, the column au_id identifies each author, and the derived column num_books counts each author’s books. The GROUP BY clause causes num_books to be calculated for every unique au_id instead of only once for the entire table. Figure 6.9 shows the result. In this example, au_id is called the grouping column.

Listing 6.9List the number of books each author wrote (or cowrote). See Figure 6.9 for the result.

SELECT
    au_id,
    COUNT(*) AS "num_books"
  FROM title_authors
  GROUP BY au_id;

Figure 6.9Result of Listing 6.9.

au_id num_books
----- ---------
A01           3
A02           4
A03           2
A04           4
A05           1
A06           3

The GROUP BY clause’s important characteristics are:

To group rows:

Listing 6.10 and Figure 6.10 show the difference between COUNT(expr) and COUNT(*) in a query that contains GROUP BY. The table publishers contains one null in the column state (for publisher P03 in Germany). Recall from “Counting Rows with COUNT()” earlier in this chapter that COUNT(expr) counts non-null values and COUNT(*) counts all values, including nulls. In the result, GROUP BY recognizes the null and creates a null group for it. COUNT(*) finds (and counts) the one null in the column state. But COUNT(state) contains a zero for the null group because COUNT(state) finds only a null in the null group, which it excludes from the count—that’s why you have the zero.

Listing 6.10This query illustrates the difference between COUNT(expr) and COUNT(*) in a GROUP BY query. See Figure 6.10 for the result.

SELECT
    state,
    COUNT(state) AS "COUNT(state)",
    COUNT(*)     AS "COUNT(*)"
  FROM publishers
  GROUP BY state;

Figure 6.10Result of Listing 6.10.

state COUNT(state) COUNT(*)
----- ------------ --------
NULL             0        1
CA               2        2
NY               1        1

If a nonaggregate column contains nulls, then using COUNT(*) rather than COUNT(expr) can produce misleading results. Listing 6.11 and Figure 6.11 show summary sales statistics for each type of book. The sales value for one of the biographies is null, so COUNT(sales) and COUNT(*) differ by 1. The average calculation in the fifth column, SUM/COUNT(sales), is consistent mathematically, whereas the sixth-column average, SUM/COUNT(*), is not. I’ve verified the inconsistency with AVG(sales) in the final column. (Recall a similar situation in Listing 6.8 in “Aggregating Distinct Values with DISTINCT” earlier in this chapter.)

Listing 6.11For mathematically consistent results, use COUNT(expr), rather than COUNT(*), if expr contains nulls. See Figure 6.11 for the result.

SELECT
    type,
    SUM(sales)   AS "SUM(sales)",
    COUNT(sales) AS "COUNT(sales)",
    COUNT(*)     AS "COUNT(*)",
    SUM(sales)/COUNT(sales)
      AS "SUM/COUNT(sales)",
    SUM(sales)/COUNT(*)
      AS "SUM/COUNT(*)",
    AVG(sales)   AS "AVG(sales)"
  FROM titles
  GROUP BY type;

Figure 6.11Result of Listing 6.11.

type       SUM(sales) COUNT(sales) COUNT(*) SUM/COUNT(sales) SUM/COUNT(*) AVG(sales)
---------- ---------- ------------ -------- ---------------- ------------ ----------
biography     1611521            3        4        537173.67    402880.25  537173.67
children         9095            2        2          4547.50      4547.50    4547.50
computer        25667            1        1         25667.00     25667.00   25667.00
history         20599            3        3          6866.33      6866.33    6866.33
psychology     308564            3        3        102854.67    102854.67  102854.67

Listing 6.12 and Figure 6.12 show a simple GROUP BY query that calculates the total sales, average sales, and number of titles for each type of book. In Listing 6.13 and Figure 6.13, I’ve added a WHERE clause to eliminate books priced less than $13 before grouping. I’ve also added an ORDER BY clause to sort the result by descending total sales of each book type.

Listing 6.12This simple GROUP BY query calculates a few summary statistics for each type of book. See Figure 6.12 for the result.

SELECT
    type,
    SUM(sales)   AS "SUM(sales)",
    AVG(sales)   AS "AVG(sales)",
    COUNT(sales) AS "COUNT(sales)"
  FROM titles
  GROUP BY type;

Figure 6.12Result of Listing 6.12.

TYPE       SUM(sales) AVG(sales) COUNT(sales)
---------- ---------- ---------- ------------
biography     1611521  537173.67            3
children         9095    4547.50            2
computer        25667   25667.00            1
history         20599    6866.33            3
psychology     308564  102854.67            3

Listing 6.13Here, I’ve added WHERE and ORDER BY clauses to Listing 6.12 to cull books priced less than $13 and sort the result by descending total sales. See Figure 6.13 for the result.

SELECT
    type,
    SUM(sales)   AS "SUM(sales)",
    AVG(sales)   AS "AVG(sales)",
    COUNT(sales) AS "COUNT(sales)"
  FROM titles
  WHERE price >= 13
  GROUP BY type
  ORDER BY "SUM(sales)" DESC;

Figure 6.13Result of Listing 6.13.

type       SUM(sales) AVG(sales) COUNT(sales)
---------- ---------- ---------- ------------
biography     1511520  755760.00            2
computer        25667   25667.00            1
history         20599    6866.33            3
children         5000    5000.00            1

Listing 6.14 and Figure 6.14 use multiple grouping columns to count the number of titles of each type that each publisher publishes.

Listing 6.14List the number of books of each type for each publisher, sorted by descending count within ascending publisher ID. See Figure 6.14 for the result.

SELECT
    pub_id,
    type,
    COUNT(*) AS "COUNT(*)"
  FROM titles
  GROUP BY pub_id, type
  ORDER BY pub_id ASC, "COUNT(*)" DESC;

Figure 6.14Result of Listing 6.14.

pub_id type       COUNT(*)
------ ---------- --------
P01    biography         3
P01    history           1
P02    computer          1
P03    history           2
P03    biography         1
P04    psychology        3
P04    children          2

In Listing 6.15 and Figure 6.15, I revisit Listing 5.31 in “Evaluating Conditional Values with CASE” in Chapter 5. But instead of listing each book categorized by its sales range, I use GROUP BY to list the number of books in each sales range.

Listing 6.15List the number of books in each calculated sales range, sorted by ascending sales. See Figure 6.15 for the result.

SELECT
    CASE
      WHEN sales IS NULL
        THEN 'Unknown'
      WHEN sales <= 1000
        THEN 'Not more than 1,000'
      WHEN sales <= 10000
        THEN 'Between 1,001 and 10,000'
      WHEN sales <= 100000
        THEN 'Between 10,001 and 100,000'
      WHEN sales <= 1000000
        THEN 'Between 100,001 and 1,000,000'
      ELSE 'Over 1,000,000'
    END
      AS "Sales category",
    COUNT(*) AS "Num titles"
  FROM titles
  GROUP BY
    CASE
      WHEN sales IS NULL
        THEN 'Unknown'
      WHEN sales <= 1000
        THEN 'Not more than 1,000'
      WHEN sales <= 10000
        THEN 'Between 1,001 and 10,000'
      WHEN sales <= 100000
        THEN 'Between 10,001 and 100,000'
      WHEN sales <= 1000000
        THEN 'Between 100,001 and 1,000,000'
      ELSE 'Over 1,000,000'
    END
  ORDER BY MIN(sales) ASC;

Figure 6.15Result of Listing 6.15.

Sales category                Num titles
----------------------------- ----------
Unknown                                1
Not more than 1,000                    1
Between 1,001 and 10,000               3
Between 10,001 and 100,000             5
Between 100,001 and 1,000,000          2
Over 1,000,000                         1

When used without an aggregate function, GROUP BY acts like DISTINCT (Listing 6.16 and Figure 6.16). For information about DISTINCT, see “Eliminating Duplicate Rows with DISTINCT” in Chapter 4.

Listing 6.16Both of these queries return the same result. The bottom form is preferred. See Figure 6.16 for the result.

SELECT type
  FROM titles
  GROUP BY type;

SELECT DISTINCT type
  FROM titles;

Figure 6.16Either statement in Listing 6.16 returns this result.

type
----------
biography
children
computer
history
psychology

You can use GROUP BY to look for patterns in your data. In Listing 6.17 and Figure 6.17, I’m looking for a relationship between price categories and average sales.

Listing 6.17List the average sales for each price, sorted by ascending price. See Figure 6.17 for the result.

SELECT price, AVG(sales) AS "AVG(sales)"
  FROM titles
  WHERE price IS NOT NULL
  GROUP BY price
  ORDER BY price ASC;

Figure 6.17Result of Listing 6.17. Ignoring the statistical outlier at $23.95, a weak inverse relationship between price and sales is apparent.

price   AVG(sales)
------- ----------
   6.95   201440.0
   7.99    94123.0
  10.00     4095.0
  12.99    56501.0
  13.95     5000.0
  19.95    10443.0
  21.99      566.0
  23.95  1500200.0
  29.99    10467.0
  39.95    25667.0

Tips for GROUP BY

Categorizing Numeric Values

You can use the function FLOOR(x) to categorize numeric values. FLOOR(x) returns the greatest integer that is lower than x. This query groups books in $10 price intervals:

SELECT
    FLOOR(price/10)*10 AS "Category",
    COUNT(*) AS "Count"
  FROM titles
  GROUP BY FLOOR(price/10)*10;

The result is:

Category Count
-------- -----
0        2
10       6
20       3
30       1
NULL     1

Category 0 counts prices between $0.00 and $9.99; category 10 counts prices between $10.00 and $19.99; and so on. (The analogous function CEILING(x) returns the smallest integer that is higher than x.)

Filtering Groups with HAVING

The HAVING clause sets conditions on the GROUP BY clause similar to the way that WHERE interacts with SELECT. The HAVING clause’s important characteristics are:

The sequence in which the WHERE, GROUP BY, and HAVING clauses are applied is:

  1. The WHERE clause filters the rows that result from the operations specified in the FROM and JOIN clauses.
  2. The GROUP BY clause groups the output of the WHERE clause.
  3. The HAVING clause filters rows from the grouped result.

To filter groups:

In Listing 6.18 and Figure 6.18, I revisit Listing 6.9 earlier in this chapter, but instead of listing the number of books that each author wrote (or cowrote), I use HAVING to list only the authors who have written three or more books.

Listing 6.18List the number of books written (or cowritten) by each author who has written three or more books. See Figure 6.18 for the result.

SELECT
    au_id,
    COUNT(*) AS "num_books"
  FROM title_authors
  GROUP BY au_id
  HAVING COUNT(*) >= 3;

Figure 6.18Result of Listing 6.18.

au_id num_books
----- ---------
A01           3
A02           4
A04           4
A06           3

In Listing 6.19 and Figure 6.19, the HAVING condition also is an aggregate expression in the SELECT clause. This query still works if you remove the AVG() expression from the SELECT list (Listing 6.20 and Figure 6.20).

Listing 6.19List the number of titles and average revenue for the types with average revenue more than $1 million. See Figure 6.19 for the result.

SELECT
    type,
    COUNT(price) AS "COUNT(price)",
    AVG(price * sales) AS "AVG revenue"
  FROM titles
  GROUP BY type
  HAVING AVG(price * sales) > 1000000;

Figure 6.19Result of Listing 6.19.

type       COUNT(price) AVG revenue
---------- ------------ -----------
biography             3 12484878.00
computer              1  1025396.65

Listing 6.20Listing 6.19 still works without AVG(price * sales) in the SELECT list. See Figure 6.20 for the result.

SELECT
    type,
    COUNT(price) AS "COUNT(price)"
  FROM titles
  GROUP BY type
  HAVING AVG(price * sales) > 1000000;

Figure 6.20Result of Listing 6.20.

type       COUNT(price)
---------- ------------
biography             3
computer              1

In Listing 6.21 and Figure 6.21, multiple grouping columns count the number of titles of each type that each publisher publishes. The HAVING condition removes groups in which the publisher has one or fewer titles of a particular type. This query retrieves a subset of the result of Listing 6.14 earlier in this chapter.

Listing 6.21List the number of books of each type for each publisher, for publishers with more than one title of a type. See Figure 6.21 for the result.

SELECT
    pub_id,
    type,
    COUNT(*) AS "COUNT(*)"
  FROM titles
  GROUP BY pub_id, type
  HAVING COUNT(*) > 1
  ORDER BY pub_id ASC, "COUNT(*)" DESC;

Figure 6.21Result of Listing 6.21.

pub_id type       COUNT(*)
------ ---------- --------
P01    biography         3
P03    history           2
P04    psychology        3
P04    children          2

In Listing 6.22 and Figure 6.22, the WHERE clause first removes all rows except for books from only publishers P03 and P04. Then the GROUP BY clause groups the output of the WHERE clause by type. Finally, the HAVING clause filters rows from the grouped result.

Listing 6.22For books from only publishers P03 and P04, list the total sales and average price by type, for types with more than $10000 total sales and less than $20 average price. See Figure 6.22 for the result.

SELECT
    type,
    SUM(sales) AS "SUM(sales)",
    AVG(price) AS "AVG(price)"
  FROM titles
  WHERE pub_id IN ('P03', 'P04')
  GROUP BY type
  HAVING SUM(sales) > 10000
     AND AVG(price) < 20;

Figure 6.22Result of Listing 6.22.

type       SUM(sales) AVG(price)
---------- ---------- ----------
psychology     308564       9.31

Tips for HAVING

7. Joins

All the queries so far have retrieved rows from a single table. This chapter explains how to use joins to retrieve rows from multiple tables simultaneously. Recall from “Relationships” in Chapter 2 that a relationship is an association established between common columns in two tables. A join is a table operation that uses related columns to combine rows from two input tables into one result table. You can chain joins to retrieve rows from an unlimited number of tables.

Why do joins matter? The most important database information isn’t so much stored in the rows of individual tables; rather, it’s the implied relationships between sets of related rows. In the sample database, for example, the individual rows of the tables authors, publishers, and titles contain important values, of course, but it’s the implied relationships that let you understand and analyze your data in its entirety: Who wrote what? Who published what? To whom do we send royalty checks? For how much? And so on.

This chapter explains the different types of joins, why they’re used, and how to create a SELECT statement that uses them.

Qualifying Column Names

Recall from “Tables, Columns, and Rows” in Chapter 2 that column names must be unique within a table but can be reused in other tables. The tables authors and publishers in the sample database both contain a column named city, for example.

To identify an otherwise-ambiguous column uniquely in a query that involves multiple tables, use its qualified name. A qualified name is a table name followed by a dot and the name of the column in the table. Because tables must have different names within a database, a qualified name identifies a single column uniquely within the entire database.

To qualify a column name:

Listing 7.1Here, the qualified names resolve otherwise-ambiguous references to the column city in the tables authors and publishers. See Figure 7.1 for the result.

SELECT au_id, authors.city
  FROM authors
  INNER JOIN publishers
    ON authors.city = publishers.city;

Figure 7.1Result of Listing 7.1. This result lists authors who live in the same city as some publisher; the join syntax is explained later in this chapter.

au_id city
----- -------------
A03   San Francisco
A04   San Francisco
A05   New York

Tips for Qualified Column Names

Creating Table Aliases with AS

You can create table aliases by using AS just as you can create column aliases; see “Creating Column Aliases with AS” in Chapter 4. Table aliases:

To create a table alias:

Listing 7.2Tables aliases make queries shorter and easier to read. Note that you can use an alias in the SELECT clause before it’s actually defined later in the statement. See Figure 7.2 for the result.

SELECT au_fname, au_lname, a.city
  FROM authors a
  INNER JOIN publishers p
    ON a.city = p.city;

Figure 7.2Result of Listing 7.2.

au_fname  au_lname city
--------- -------- -------------
Hallie    Hull     San Francisco
Klee      Hull     San Francisco
Christian Kells    New York

Tips for AS

Using Joins

You can use a join to extract data from more than one table. The rest of this chapter explains the different types of joins (Table 7.1), why they’re used, and how to create SELECT statements that use them.

Table 7.1Types of Joins
Join Description
Cross join Returns all rows from the first table in which each row from the first table is combined with all rows from the second table.
Natural join A join that compares, for equality, all the columns in the first table with corresponding columns that have the same name in the second table.
Inner join A join that uses a comparison operator to match rows from two tables based on the values in common columns from each table. Inner joins are the most common type of join.
Left outer join Returns all the rows from the left table, not just the ones in which the joined columns match. If a row in the left table has no matching rows in the right table, then the associated result row contains nulls for all SELECT-clause columns coming from the right table.
Right outer join The reverse of a left outer join. All rows from the right table are returned. Nulls are returned for the left table if a right-table row has no matching left-table row.
Full outer join Returns all rows in both the left and right tables. If a row has no match in the other table, then the SELECT-clause columns from the other table contain nulls. If there is a match between the tables, then the entire result row contains values from both tables.
Self-join A join of a table to itself.

The important characteristics of joins are:

Domains and Comparisons

The values that you compare in joins and WHERE clauses must be meaningfully comparable—that is, have the same data type and the same meaning. The sample-database columns au_id and pub_id, for example, have the same data type—both are CHAR(3), a letter followed by two digits—but mean different things, so they can’t be compared sensibly.

Recall from “Tables, Columns, and Rows” in Chapter 2 that a domain is the set of permissible values for a column. To prevent meaningless comparisons, the relational model requires that comparable columns draw from domains that have the same meaning. Unfortunately, SQL and DBMSs stray from the model and have no intrinsic mechanism that prevents users from comparing, say, IQ and shoe size. If you’re building a database application, then it’s up to you to stop (or warn) users from making meaningless comparisons that waste processing time or, worse, yield results that might be interpreted as valid.

Creating Joins with JOIN or WHERE

You have two alternative ways of specifying a join: by using JOIN syntax or WHERE syntax. SQL-92 and later standards prescribe JOIN syntax, but older standards prescribe WHERE; hence, both JOIN and WHERE are used widely.

This section explains the general syntax for JOIN and WHERE joins that involve two tables. The actual syntax that you’ll use in real queries will vary by the join type, the number of columns joined, the number of tables joined, and the syntax requirements of your DBMS. The syntax diagrams and examples in the following sections show you how to create specific joins.

To create a join by using JOIN:

To create a join by using WHERE:

Listings 7.3a and 7.3b show equivalent queries that use JOIN and WHERE syntax. See Figure 7.3 for the result.

Listing 7.3aA join that uses JOIN syntax. See Figure 7.3 for the result.

SELECT au_fname, au_lname, a.city
  FROM authors a
  INNER JOIN publishers p
    ON a.city = p.city;

Listing 7.3bThe same join, using WHERE syntax. See Figure 7.3 for the result.

SELECT au_fname, au_lname, a.city
  FROM authors a, publishers p
  WHERE a.city = p.city;

Figure 7.3Result of Listings 7.3a and 7.3b.

au_fname  au_lname city
--------- -------- -------------
Hallie    Hull     San Francisco
Klee      Hull     San Francisco
Christian Kells    New York

Query Execution Sequence

When your DBMS processes joins, it uses a logical sequence to execute the entire query. The DBMS:

  1. Applies the join conditions in the JOIN clause.
  2. Applies the join conditions and search conditions in the WHERE clause.
  3. Groups rows according to the GROUP BY clause.
  4. Applies the search conditions in the HAVING clause to the groups.
  5. Sorts the result according to the ORDER BY clause.

Tips for JOIN and WHERE

The USING Clause

For JOIN syntax, the SQL standard also defines a USING clause that can be used instead of the ON clause if the joined columns have the same name and are compared for equality:

FROM table1 join_type table2
  USING (columns)

columns is a comma-separated list of one or more column names. The parentheses are required. The query performs an equijoin on the named pair(s) of columns. The type of join is called a named columns join. Rewriting Listing 7.3a with USING:

SELECT au_fname, au_lname, city
  FROM authors
  INNER JOIN publishers
    USING (city);

The USING clause acts like a natural join, except that you can use it if you don’t want to join all pairs of columns with the same name in both tables. Note that the preceding USING example joins only on the column city in both tables, whereas a natural join would join on both the columns city and state common to the tables. See “Creating a Natural Join with NATURAL JOIN” later in this chapter.

USING is a syntactic convenience that doesn’t add extra functionality to SQL. A USING clause always can be replicated with an ON clause in JOIN syntax or with a WHERE clause in WHERE syntax.

Microsoft Access, Microsoft SQL Server, and Db2 don’t support USING. MySQL requires the SELECT clause’s common column names to be qualified in USING queries. To run the preceding example, change city to authors.city in the SELECT clause.

Creating a Cross Join with CROSS JOIN

A cross join:

To create a cross join:

Listing 7.4A cross join displays all possible combinations of rows from two tables. See Figure 7.4 for the result.

SELECT
    au_id,
    pub_id,
    a.state AS "au_state",
    p.state AS "pub_state"
  FROM authors a
  CROSS JOIN publishers p;

Figure 7.4Result of Listing 7.4.

au_id pub_id au_state pub_state
----- ------ -------- ---------
A01   P01    NY       NY
A02   P01    CO       NY
A03   P01    CA       NY
A04   P01    CA       NY
A05   P01    NY       NY
A06   P01    CA       NY
A07   P01    FL       NY
A01   P02    NY       CA
A02   P02    CO       CA
A03   P02    CA       CA
A04   P02    CA       CA
A05   P02    NY       CA
A06   P02    CA       CA
A07   P02    FL       CA
A01   P03    NY       NULL
A02   P03    CO       NULL
A03   P03    CA       NULL
A04   P03    CA       NULL
A05   P03    NY       NULL
A06   P03    CA       NULL
A07   P03    FL       NULL
A01   P04    NY       CA
A02   P04    CO       CA
A03   P04    CA       CA
A04   P04    CA       CA
A05   P04    NY       CA
A06   P04    CA       CA
A07   P04    FL       CA

Tips for CROSS JOIN

Creating a Natural Join with NATURAL JOIN

A natural join:

To create a natural join:

When your DBMS runs Listing 7.5, it will join rows in the table publishers with rows in the table titles that have equal values in the columns publishers.pub_id and titles.pub_id—the two columns that have the same name in both tables. See Figure 7.5 for the result.

Listing 7.5List each book’s publisher. See Figure 7.5 for the result.

SELECT
    title_id,
    pub_id,
    pub_name
  FROM publishers
  NATURAL JOIN titles;

Figure 7.5Result of Listing 7.5.

title_id pub_id pub_name
-------- ------ -------------------
T01      P01    Abatis Publishers
T02      P03    Schadenfreude Press
T03      P02    Core Dump Books
T04      P04    Tenterhooks Press
T05      P04    Tenterhooks Press
T06      P01    Abatis Publishers
T07      P03    Schadenfreude Press
T08      P04    Tenterhooks Press
T09      P04    Tenterhooks Press
T10      P01    Abatis Publishers
T11      P04    Tenterhooks Press
T12      P01    Abatis Publishers
T13      P03    Schadenfreude Press

In Listing 7.6, I’ve added another join to Listing 7.5 to retrieve the advance for each book. The WHERE condition retrieves books with advances less than $20000. When your DBMS runs Listing 7.6, it will join the pub_id columns in the tables publishers and titles, and it will join the title_id columns in the tables titles and royalties. See Figure 7.6 for the result.

Listing 7.6List each book’s publisher and advance for books with advances less than $20000. See Figure 7.6 for the result.

SELECT
    title_id,
    pub_id,
    pub_name,
    advance
  FROM publishers
  NATURAL JOIN titles
  NATURAL JOIN royalties
  WHERE advance < 20000;

Figure 7.6Result of Listing 7.6.

title_id pub_id pub_name            advance
-------- ------ ------------------- -------
T01      P01    Abatis Publishers     10000
T02      P03    Schadenfreude Press    1000
T03      P02    Core Dump Books       15000
T08      P04    Tenterhooks Press         0
T09      P04    Tenterhooks Press         0

Tips for NATURAL JOIN

Creating an Inner Join with INNER JOIN

An inner join:

To create an inner join:

Tips for INNER JOIN

Listing 7.7 joins two tables on the column au_id to list the books that each author wrote (or cowrote). Each author’s au_id in the table authors matches zero or more rows in the table title_authors. See Figure 7.7 for the result. Note that author A07 (Paddy O'Furniture) is omitted from the result because he has written no books and so has no matching rows in title_authors.

Listing 7.7List the books that each author wrote (or cowrote). See Figure 7.7 for the result.

SELECT
    a.au_id,
    a.au_fname,
    a.au_lname,
    ta.title_id
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  ORDER BY a.au_id ASC, ta.title_id ASC;

Figure 7.7Result of Listing 7.7.

au_id au_fname  au_lname  title_id
----- --------- --------- --------
A01   Sarah     Buchman   T01
A01   Sarah     Buchman   T02
A01   Sarah     Buchman   T13
A02   Wendy     Heydemark T06
A02   Wendy     Heydemark T07
A02   Wendy     Heydemark T10
A02   Wendy     Heydemark T12
A03   Hallie    Hull      T04
A03   Hallie    Hull      T11
A04   Klee      Hull      T04
A04   Klee      Hull      T05
A04   Klee      Hull      T07
A04   Klee      Hull      T11
A05   Christian Kells     T03
A06             Kellsey   T08
A06             Kellsey   T09
A06             Kellsey   T11

Tips for Listing 7.7

Listing 7.8 joins two tables on the column pub_id to list each book’s title name and ID, and each book’s publisher name and ID. Note that the join is necessary to retrieve only the publisher name (the fourth column in the result); all the other three columns are available in the table titles. See Figure 7.8 for the result.

Listing 7.8List each book’s title name and ID and each book’s publisher name and ID. See Figure 7.8 for the result.

SELECT
    t.title_id,
    t.title_name,
    t.pub_id,
    p.pub_name
  FROM titles t
  INNER JOIN publishers p
    ON p.pub_id = t.pub_id
  ORDER BY t.title_name ASC;

Figure 7.8Result of Listing 7.8.

title_id title_name                          pub_id pub_name
-------- ----------------------------------- ------ -------------------
T01      1977!                               P01    Abatis Publishers
T02      200 Years of German Humor           P03    Schadenfreude Press
T03      Ask Your System Administrator       P02    Core Dump Books
T04      But I Did It Unconsciously          P04    Tenterhooks Press
T05      Exchange of Platitudes              P04    Tenterhooks Press
T06      How About Never?                    P01    Abatis Publishers
T07      I Blame My Mother                   P03    Schadenfreude Press
T08      Just Wait Until After School        P04    Tenterhooks Press
T09      Kiss My Boo-Boo                     P04    Tenterhooks Press
T10      Not Without My Faberge Egg          P01    Abatis Publishers
T11      Perhaps It's a Glandular Problem    P04    Tenterhooks Press
T12      Spontaneous, Not Annoying           P01    Abatis Publishers
T13      What Are The Civilian Applications? P03    Schadenfreude Press

Tips for Listing 7.8

Listing 7.9 uses two join conditions to list the authors who live in the same city and state as some publisher (any publisher). See Figure 7.9 for the result. Note that this query is a natural join on the identically named, nonkey columns city and state in the two tables (see “Creating a Natural Join with NATURAL JOIN” earlier in this chapter). An equivalent query is:

SELECT a.au_id, a.au_fname,
    a.au_lname, a.city, a.state
  FROM authors a
  NATURAL JOIN publishers p
  ORDER BY a.au_id ASC;

Listing 7.9List the authors who live in the same city and state in which a publisher is located. See Figure 7.9 for the result.

SELECT
    a.au_id,
    a.au_fname,
    a.au_lname,
    a.city,
    a.state
  FROM authors a
  INNER JOIN publishers p
    ON a.city = p.city
    AND a.state = p.state
  ORDER BY a.au_id;

Figure 7.9Result of Listing 7.9.

au_id au_fname  au_lname city          state
----- --------- -------- ------------- -----
A03   Hallie    Hull     San Francisco CA
A04   Klee      Hull     San Francisco CA
A05   Christian Kells    New York      NY

Tips for Listing 7.9

Listing 7.10 combines an inner join with WHERE conditions to list books published in California or outside the large North American countries; see “Filtering Rows with WHERE” in Chapter 4. See Figure 7.10 for the result.

Listing 7.10List the books published in California or outside the large North American countries. See Figure 7.10 for the result.

SELECT
    t.title_id,
    t.title_name,
    p.state,
    p.country
  FROM titles t
  INNER JOIN publishers p
    ON t.pub_id = p.pub_id
  WHERE p.state = 'CA'
     OR p.country NOT IN
      ('USA', 'Canada', 'Mexico')
  ORDER BY t.title_id ASC;

Figure 7.10Result of Listing 7.10.

title_id title_name                          state country
-------- ----------------------------------- ----- -------
T02      200 Years of German Humor           NULL  Germany
T03      Ask Your System Administrator       CA    USA
T04      But I Did It Unconsciously          CA    USA
T05      Exchange of Platitudes              CA    USA
T07      I Blame My Mother                   NULL  Germany
T08      Just Wait Until After School        CA    USA
T09      Kiss My Boo-Boo                     CA    USA
T11      Perhaps It's a Glandular Problem    CA    USA
T13      What Are The Civilian Applications? NULL  Germany

Tips for Listing 7.10

Listing 7.11 combines an inner join with the aggregate function COUNT() and a GROUP BY clause to list the number of books that each author wrote (or cowrote). For information about aggregate functions and GROUP BY, see Chapter 6. See Figure 7.11 for the result. Note that, as in Figure 7.7, author A07 (Paddy O'Furniture) is omitted from the result because he has written no books and so has no matching rows in title_authors. See Listing 7.30 in “Creating Outer Joins with OUTER JOIN” later in this chapter for an example that lists authors who have written no books.

Listing 7.11List the number of books that each author wrote (or cowrote). See Figure 7.11 for the result.

SELECT
    a.au_id,
    COUNT(ta.title_id) AS "Num books"
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  GROUP BY a.au_id
  ORDER BY a.au_id ASC;

Figure 7.11Result of Listing 7.11.

au_id Num books
----- ---------
A01           3
A02           4
A03           2
A04           4
A05           1
A06           3

Tips for Listing 7.11

Listing 7.12 uses WHERE conditions to list the advance paid for each biography. See Figure 7.12 for the result.

Listing 7.12List the advance paid for each biography. See Figure 7.12 for the result.

SELECT
    t.title_id,
    t.title_name,
    r.advance
  FROM royalties r
  INNER JOIN titles t
    ON r.title_id = t.title_id
  WHERE t.type = 'biography'
    AND r.advance IS NOT NULL
  ORDER BY r.advance DESC;

Figure 7.12Result of Listing 7.12.

title_id title_name                advance
-------- ------------------------- -----------
T07      I Blame My Mother          1000000.00
T12      Spontaneous, Not Annoying    50000.00
T06      How About Never?             20000.00

Tips for Listing 7.12

Listing 7.13 uses aggregate functions and a GROUP BY clause to list the count and total advance paid for each type of book. See Figure 7.13 for the result.

Listing 7.13List the count and total advance paid for each type of book. See Figure 7.13 for the result.

SELECT
    t.type,
    COUNT(r.advance)
      AS "COUNT(r.advance)",
    SUM(r.advance)
      AS "SUM(r.advance)"
  FROM royalties r
  INNER JOIN titles t
    ON r.title_id = t.title_id
  WHERE r.advance IS NOT NULL
  GROUP BY t.type
  ORDER BY t.type ASC;

Figure 7.13Result of Listing 7.13.

type       COUNT(r.advance) SUM(r.advance)
---------- ---------------- --------------
biography                 3     1070000.00
children                  2           0.00
computer                  1       15000.00
history                   3       31000.00
psychology                3      220000.00

Tips for Listing 7.13

Listing 7.14 is similar to Listing 7.13, except that it uses an additional grouping column to list the count and total advance paid for each type of book by publisher. See Figure 7.14 for the result.

Listing 7.14List the count and total advance paid for each type of book, by publisher. See Figure 7.14 for the result.

SELECT
    t.type,
    t.pub_id,
    COUNT(r.advance) AS "COUNT(r.advance)",
    SUM(r.advance) AS "SUM(r.advance)"
  FROM royalties r
  INNER JOIN titles t
    ON r.title_id = t.title_id
  WHERE r.advance IS NOT NULL
  GROUP BY t.type, t.pub_id
  ORDER BY t.type ASC, t.pub_id ASC;

Figure 7.14Result of Listing 7.14.

type       pub_id COUNT(r.advance) SUM(r.advance)
---------- ------ ---------------- --------------
biography  P01                   2       70000.00
biography  P03                   1     1000000.00
children   P04                   2           0.00
computer   P02                   1       15000.00
history    P01                   1       10000.00
history    P03                   2       21000.00
psychology P04                   3      220000.00

Tips for Listing 7.14

Listing 7.15 uses a HAVING clause to list the number of coauthors of each book written by two or more authors. For information about HAVING, see “Filtering Groups with HAVING” in Chapter 6. See Figure 7.15 for the result.

Listing 7.15List the number of coauthors of each book written by two or more authors. See Figure 7.15 for the result.

SELECT
    ta.title_id,
    COUNT(ta.au_id) AS "Num authors"
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  GROUP BY ta.title_id
  HAVING COUNT(ta.au_id) > 1
  ORDER BY ta.title_id ASC;

Figure 7.15Result of Listing 7.15.

title_id Num authors
-------- -----------
T04                2
T07                2
T11                3

Tips for Listing 7.15

You also can join values in two columns that aren’t equal. Listing 7.16 uses greater-than (>) join to find each book whose revenue (= price × sales) is at least 10 times greater than the advance paid to the author(s). See Figure 7.16 for the result. The use of <, <=, >, and >= joins is common, but not-equal joins (<>) are used rarely. Generally, not-equal joins make sense only when used with a self-join; see “Creating a Self-Join” later in this chapter.

Listing 7.16List each book whose revenue (= price × sales) is at least 10 times greater than its advance. See Figure 7.16 for the result.

SELECT
    t.title_id,
    t.title_name,
    r.advance,
    t.price * t.sales AS "Revenue"
  FROM titles t
  INNER JOIN royalties r
    ON t.price * t.sales > r.advance * 10
    AND t.title_id = r.title_id
  ORDER BY t.price * t.sales DESC;

Figure 7.16Result of Listing 7.16.

title_id title_name                          advance    Revenue
-------- ----------------------------------- ---------- -----------
T07      I Blame My Mother                   1000000.00 35929790.00
T05      Exchange of Platitudes               100000.00  1400008.00
T12      Spontaneous, Not Annoying             50000.00  1299012.99
T03      Ask Your System Administrator         15000.00  1025396.65
T13      What Are The Civilian Applications?   20000.00   313905.33
T06      How About Never?                      20000.00   225834.00
T02      200 Years of German Humor              1000.00   190841.70
T09      Kiss My Boo-Boo                            .00    69750.00
T08      Just Wait Until After School               .00    40950.00

Tips for Listing 7.16

Complicated queries can arise from simple questions. In Listing 7.17, I must join three tables to list the author names and the names of the books that each author wrote (or cowrote). See Figure 7.17 for the result.

Listing 7.17List the author names and the names of the books that each author wrote (or cowrote). See Figure 7.17 for the result.

SELECT
    a.au_fname,
    a.au_lname,
    t.title_name
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  INNER JOIN titles t
    ON t.title_id = ta.title_id
  ORDER BY a.au_lname ASC,
    a.au_fname ASC, t.title_name ASC;

Figure 7.17Result of Listing 7.17.

au_fname  au_lname  title_name
--------- --------- -----------------------------------
Sarah     Buchman   1977!
Sarah     Buchman   200 Years of German Humor
Sarah     Buchman   What Are The Civilian Applications?
Wendy     Heydemark How About Never?
Wendy     Heydemark I Blame My Mother
Wendy     Heydemark Not Without My Faberge Egg
Wendy     Heydemark Spontaneous, Not Annoying
Hallie    Hull      But I Did It Unconsciously
Hallie    Hull      Perhaps It's a Glandular Problem
Klee      Hull      But I Did It Unconsciously
Klee      Hull      Exchange of Platitudes
Klee      Hull      I Blame My Mother
Klee      Hull      Perhaps It's a Glandular Problem
Christian Kells     Ask Your System Administrator
          Kellsey   Just Wait Until After School
          Kellsey   Kiss My Boo-Boo
          Kellsey   Perhaps It's a Glandular Problem

Tips for Listing 7.17

Expanding on Listing 7.17, Listing 7.18 requires a four-table join to list the publisher names along with the names of the authors and books. See Figure 7.18 for the result.

Listing 7.18List the author names, the names of the books that each author wrote (or cowrote), and the publisher names. See Figure 7.18 for the result.

SELECT
    a.au_fname,
    a.au_lname,
    t.title_name,
    p.pub_name
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  INNER JOIN titles t
    ON t.title_id = ta.title_id
  INNER JOIN publishers p
    ON p.pub_id = t.pub_id
  ORDER BY a.au_lname ASC, a.au_fname ASC,
    t.title_name ASC;

Figure 7.18Result of Listing 7.18.

au_fname  au_lname  title_name                          pub_name
--------- --------- ----------------------------------- -------------------
Sarah     Buchman   1977!                               Abatis Publishers
Sarah     Buchman   200 Years of German Humor           Schadenfreude Press
Sarah     Buchman   What Are The Civilian Applications? Schadenfreude Press
Wendy     Heydemark How About Never?                    Abatis Publishers
Wendy     Heydemark I Blame My Mother                   Schadenfreude Press
Wendy     Heydemark Not Without My Faberge Egg          Abatis Publishers
Wendy     Heydemark Spontaneous, Not Annoying           Abatis Publishers
Hallie    Hull      But I Did It Unconsciously          Tenterhooks Press
Hallie    Hull      Perhaps It's a Glandular Problem    Tenterhooks Press
Klee      Hull      But I Did It Unconsciously          Tenterhooks Press
Klee      Hull      Exchange of Platitudes              Tenterhooks Press
Klee      Hull      I Blame My Mother                   Schadenfreude Press
Klee      Hull      Perhaps It's a Glandular Problem    Tenterhooks Press
Christian Kells     Ask Your System Administrator       Core Dump Books
          Kellsey   Just Wait Until After School        Tenterhooks Press
          Kellsey   Kiss My Boo-Boo                     Tenterhooks Press
          Kellsey   Perhaps It's a Glandular Problem    Tenterhooks Press

Tips for Listing 7.18

Listing 7.19 calculates the total royalties for all books. The gross royalty of a book is the book’s revenue (= sales × price) times the royalty rate (the fraction of revenue paid to the author). In most cases, the author receives an initial advance against royalties. The publisher deducts the advance from the gross royalty to get the net royalty. If the net royalty is positive, then the publisher must pay the author; if the net royalty is negative or zero, then the author gets nothing because he or she still hasn’t “earned out” the advance. See Figure 7.19 for the result. Gross royalties are labeled “Total royalties”, gross advances are labeled “Total advances”, and net royalties are labeled “Total due to authors”.

Listing 7.19 calculates total royalties for all books; the subsequent examples in this section will show you how to break down royalties by author, book, publisher, and other groups.

Listing 7.19Calculate the total royalties for all books. See Figure 7.19 for the result.

SELECT
    SUM(t.sales * t.price *
      r.royalty_rate)
      AS "Total royalties",
    SUM(r.advance)
      AS "Total advances",
    SUM((t.sales * t.price *
      r.royalty_rate) - r.advance)
      AS "Total due to authors"
  FROM titles t
  INNER JOIN royalties r
    ON r.title_id = t.title_id
  WHERE t.sales IS NOT NULL;

Figure 7.19Result of Listing 7.19.

Total royalties Total advances Total due to authors
--------------- -------------- --------------------
     4387219.55     1336000.00           3051219.55

Tips for Listing 7.19

Listing 7.20 uses a three-table join to calculate the royalty earned by each author for each book that the author wrote (or cowrote). Because a book can have multiple authors, per-author royalty calculations involve each author’s share of a book’s royalty (and advance). The author’s royalty share for each book is given in the table title_authors in the column royalty_share. For a book with a sole author, royalty_share is 1.0 (100 percent). For a book with multiple authors, the royalty_share of each author is a fractional amount between 0 and 1 (inclusive); all the royalty_share values for a particular book must sum to 1.0 (100 percent). See Figure 7.20 for the result. The sum of the values in each of the last three columns in the result equals the corresponding total in Figure 7.19.

Listing 7.20Calculate the royalty earned by each author for each book that the author wrote (or cowrote). See Figure 7.20 for the result.

SELECT
    ta.au_id,
    t.title_id,
    t.pub_id,
    t.sales * t.price *
      r.royalty_rate * ta.royalty_share
      AS "Royalty share",
    r.advance * ta.royalty_share
      AS "Advance share",
    (t.sales * t.price *
      r.royalty_rate * ta.royalty_share) -
      (r.advance * ta.royalty_share)
      AS "Due to author"
  FROM title_authors ta
  INNER JOIN titles t
    ON t.title_id = ta.title_id
  INNER JOIN royalties r
    ON r.title_id = t.title_id
  WHERE t.sales IS NOT NULL
  ORDER BY ta.au_id ASC, t.title_id ASC;

Figure 7.20Result of Listing 7.20.

au_id title_id pub_id Royalty share Advance share Due to author
----- -------- ------ ------------- ------------- -------------
A01   T01      P01           622.32      10000.00      -9377.68
A01   T02      P03         11450.50       1000.00      10450.50
A01   T13      P03         18834.32      20000.00      -1165.68
A02   T06      P01         18066.72      20000.00      -1933.28
A02   T07      P03       1976138.45     500000.00    1476138.45
A02   T12      P01        116911.17      50000.00      66911.17
A03   T04      P04          8106.38      12000.00      -3893.62
A03   T11      P04         15792.90      30000.00     -14207.10
A04   T04      P04          5404.26       8000.00      -2595.74
A04   T05      P04        126000.72     100000.00      26000.72
A04   T07      P03       1976138.45     500000.00    1476138.45
A04   T11      P04         15792.90      30000.00     -14207.10
A05   T03      P02         71777.77      15000.00      56777.77
A06   T08      P04          1638.00           .00       1638.00
A06   T09      P04          3487.50           .00       3487.50
A06   T11      P04         21057.20      40000.00     -18942.80

Tips for Listing 7.20

Listing 7.21 is similar to Listing 7.20 except that it adds a join to the table authors to print the author names and includes a WHERE condition to retrieve rows with only positive royalties. See Figure 7.21 for the result.

Listing 7.21List only positive royalties earned by each author for each book that the author wrote (or cowrote). See Figure 7.21 for the result.

SELECT
    a.au_id,
    a.au_fname,
    a.au_lname,
    t.title_name,
    (t.sales * t.price *
      r.royalty_rate * ta.royalty_share) -
      (r.advance * ta.royalty_share)
      AS "Due to author"
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  INNER JOIN titles t
    ON t.title_id = ta.title_id
  INNER JOIN royalties r
    ON r.title_id = t.title_id
  WHERE t.sales IS NOT NULL
    AND (t.sales * t.price *
      r.royalty_rate * ta.royalty_share) -
      (r.advance * ta.royalty_share) > 0
  ORDER BY a.au_id ASC, t.title_id ASC;

Figure 7.21Result of Listing 7.21.

au_id au_fname  au_lname  title_name                    Due to author
----- --------- --------- ----------------------------- -------------
A01   Sarah     Buchman   200 Years of German Humor          10450.50
A02   Wendy     Heydemark I Blame My Mother                1476138.45
A02   Wendy     Heydemark Spontaneous, Not Annoying          66911.17
A04   Klee      Hull      Exchange of Platitudes             26000.72
A04   Klee      Hull      I Blame My Mother                1476138.45
A05   Christian Kells     Ask Your System Administrator      56777.77
A06             Kellsey   Just Wait Until After School        1638.00
A06             Kellsey   Kiss My Boo-Boo                     3487.50

Tips for Listing 7.21

Listing 7.22 uses a GROUP BY clause to calculate the total royalties paid by each publisher. The aggregate function COUNT() computes the total number of books for which each publisher pays royalties. Note that each author’s royalty share is unnecessary here because no per-author calculations are involved. See Figure 7.22 for the result. The sum of the values in each of the last three columns in the result equals the corresponding total in Figure 7.19.

Listing 7.22Calculate the total royalties paid by each publisher. See Figure 7.22 for the result.

SELECT
    t.pub_id,
    COUNT(t.sales)
      AS "Num books",
    SUM(t.sales * t.price * r.royalty_rate)
      AS "Total royalties",
    SUM(r.advance)
      AS "Total advances",
    SUM((t.sales * t.price
      * r.royalty_rate) -
      r.advance)
      AS "Total due to authors"
  FROM titles t
  INNER JOIN royalties r
    ON r.title_id = t.title_id
  WHERE t.sales IS NOT NULL
  GROUP BY t.pub_id
  ORDER BY t.pub_id ASC;

Figure 7.22Result of Listing 7.22.

pub_id Num books Total royalties Total advances Total due to authors
------ --------- --------------- -------------- --------------------
P01            3       135600.21       80000.00             55600.21
P02            1        71777.77       15000.00             56777.77
P03            3      3982561.72     1021000.00           2961561.72
P04            5       197279.85      220000.00            -22720.15

Tips for Listing 7.22

Listing 7.23 is similar to Listing 7.22 except that it calculates the total royalties earned by each author for all books written (or cowritten). See Figure 7.23 for the result. The sum of the values in each of the last three columns in the result equals the corresponding total in Figure 7.19.

Listing 7.23Calculate the total royalties earned by each author for all books written (or cowritten). See Figure 7.23 for the result.

SELECT
    ta.au_id,
    COUNT(sales)
      AS "Num books",
    SUM(t.sales * t.price *
      r.royalty_rate * ta.royalty_share)
      AS "Total royalties share",
    SUM(r.advance * ta.royalty_share)
      AS "Total advances share",
    SUM((t.sales * t.price *
      r.royalty_rate * ta.royalty_share) -
      (r.advance * ta.royalty_share))
      AS "Total due to author"
  FROM title_authors ta
  INNER JOIN titles t
    ON t.title_id = ta.title_id
  INNER JOIN royalties r
    ON r.title_id = t.title_id
  WHERE t.sales IS NOT NULL
  GROUP BY ta.au_id
  ORDER BY ta.au_id ASC;

Figure 7.23Result of Listing 7.23.

au_id Num books   Total royalties share Total advances share Total due to author
----- ----------- --------------------- -------------------- -------------------
A01             3              30907.14             31000.00              -92.86
A02             3            2111116.34            570000.00          1541116.34
A03             2              23899.28             42000.00           -18100.72
A04             4            2123336.32            638000.00          1485336.32
A05             1              71777.77             15000.00            56777.77
A06             3              26182.70             40000.00           -13817.30

Tips for Listing 7.23

Listing 7.24 uses two grouping columns to calculate the total royalties to be paid by each U.S. publisher to each author for all books written (or cowritten) by the author. The HAVING condition returns retrieve rows with only positive net royalties, and the WHERE condition retrieves only U.S. publishers. See Figure 7.24 for the result.

Listing 7.24Calculate the positive net royalties to be paid by each U.S. publisher to each author for all books written (or cowritten) by the author. See Figure 7.24 for the result.

SELECT
    t.pub_id,
    ta.au_id,
    COUNT(*)
      AS "Num books",
    SUM(t.sales * t.price *
      r.royalty_rate * ta.royalty_share)
      AS "Total royalties share",
    SUM(r.advance * ta.royalty_share)
      AS "Total advances share",
    SUM((t.sales * t.price *
      r.royalty_rate * ta.royalty_share) -
      (r.advance * ta.royalty_share))
      AS "Total due to author"
  FROM title_authors ta
  INNER JOIN titles t
    ON t.title_id = ta.title_id
  INNER JOIN royalties r
    ON r.title_id = t.title_id
  INNER JOIN publishers p
    ON p.pub_id = t.pub_id
  WHERE t.sales IS NOT NULL
    AND p.country IN ('USA')
  GROUP BY t.pub_id, ta.au_id
  HAVING SUM((t.sales * t.price *
    r.royalty_rate * ta.royalty_share) -
    (r.advance * ta.royalty_share)) > 0
  ORDER BY t.pub_id ASC, ta.au_id ASC;

Figure 7.24Result of Listing 7.24.

pub_id au_id Num books   Total royalties share Total advances share Total due to author
------ ----- ----------- --------------------- -------------------- -------------------
P01    A02             2             134977.89             70000.00            64977.89
P02    A05             1              71777.77             15000.00            56777.77
P04    A04             3             147197.87            138000.00             9197.87

Tips for Listing 7.24

Creating Outer Joins with OUTER JOIN

In the preceding section, you learned that inner joins return rows only if at least one row from both tables satisfies the join condition(s). An inner join eliminates the rows that don’t match with a row from the other table, whereas an outer join returns all rows from at least one of the tables (provided that those rows meet any WHERE or HAVING search conditions).

Outer joins are useful for answering questions that involve missing quantities: authors who have written no books or classes with no enrolled students, for example. Outer joins also are helpful for creating reports in which you want to list all the rows of one table along with matching rows from another table: all authors and any books that sold more than a given number of copies, for example, or all products with order quantities, including products that no one ordered.

Unlike other joins, the order in which you specify the tables in outer joins is important, so the two join operands are called the left table and the right table. Outer joins come in three flavors:

To summarize, all rows are retrieved from the left table referenced in a left outer join, all rows are retrieved from the right table referenced in a right outer join, and all rows from both tables are retrieved in a full outer join. In all cases, unmatched rows are padded with nulls. In the result, you can’t distinguish the nulls (if any) that were in the input tables originally from the nulls inserted by the outer-join operation. Remember that the conditions NULL = NULL and NULL = any_value are unknown and not matches; see “Nulls” in Chapter 3.

To create a left outer join:

To create a right outer join:

To create a full outer join:

Tips for OUTER JOIN

For reference in the following four examples, Listing 7.25 and Figure 7.25 show the city for each author and publisher.

Listing 7.25List the cities of the authors and the cities of the publishers. See Figure 7.25 for the result.

SELECT a.au_fname, a.au_lname, a.city
  FROM authors a;

SELECT p.pub_name, p.city
  FROM publishers p;

Figure 7.25Result of Listing 7.25.

au_fname  au_lname    city
--------- ----------- -------------
Sarah     Buchman     Bronx
Wendy     Heydemark   Boulder
Hallie    Hull        San Francisco
Klee      Hull        San Francisco
Christian Kells       New York
          Kellsey     Palo Alto
Paddy     O'Furniture Sarasota

pub_name            city
------------------- -------------
Abatis Publishers   New York
Core Dump Books     San Francisco
Schadenfreude Press Hamburg
Tenterhooks Press   Berkeley

Listing 7.26 performs an inner join of the tables authors and publishers on their city columns. The result, Figure 7.26, lists only the authors who live in cities in which a publisher is located. You can compare the result of this inner join with the results of the outer joins in the following three examples.

Listing 7.26List the authors who live in cities in which a publisher is located. See Figure 7.26 for the result.

SELECT a.au_fname, a.au_lname, p.pub_name
  FROM authors a
  INNER JOIN publishers p
    ON a.city = p.city;

Figure 7.26Result of Listing 7.26.

au_fname  au_lname pub_name
--------- -------- -----------------
Hallie    Hull     Core Dump Books
Klee      Hull     Core Dump Books
Christian Kells    Abatis Publishers

Tips for Listing 7.26

Listing 7.27 uses a left outer join to include all authors in the result, regardless of whether a publisher is located in the same city. See Figure 7.27 for the result.

Listing 7.27This left outer join includes all rows in the table authors in the result, whether or not there’s a match in the column city in the table publishers. See Figure 7.27 for the result.

SELECT a.au_fname, a.au_lname, p.pub_name
  FROM authors a
  LEFT OUTER JOIN publishers p
    ON a.city = p.city
  ORDER BY p.pub_name ASC,
    a.au_lname ASC, a.au_fname ASC;

Figure 7.27Result of Listing 7.27. Note that there’s no matching data for four of the listed authors, so these rows contain null in the column pub_name.

au_fname  au_lname    pub_name
--------- ----------- -----------------
Sarah     Buchman     NULL
Wendy     Heydemark   NULL
          Kellsey     NULL
Paddy     O'Furniture NULL
Christian Kells       Abatis Publishers
Hallie    Hull        Core Dump Books
Klee      Hull        Core Dump Books

Tips for Listing 7.27

Listing 7.28 uses a right outer join to include all publishers in the result, regardless of whether an author lives in the publisher’s city. See Figure 7.28 for the result.

Listing 7.28This right outer join includes all rows in the table publishers in the result, whether or not there’s a match in the column city in the table authors. See Figure 7.28 for the result.

SELECT a.au_fname, a.au_lname, p.pub_name
  FROM authors a
  RIGHT OUTER JOIN publishers p
    ON a.city = p.city
  ORDER BY p.pub_name ASC,
    a.au_lname ASC, a.au_fname ASC;

Figure 7.28Result of Listing 7.28. Note that there’s no matching data for two of the listed publishers, so these rows contain nulls in the columns au_fname and au_lname.

au_fname  au_lname pub_name
--------- -------- -------------------
Christian Kells    Abatis Publishers
Hallie    Hull     Core Dump Books
Klee      Hull     Core Dump Books
NULL      NULL     Schadenfreude Press
NULL      NULL     Tenterhooks Press

Tips for Listing 7.28

Listing 7.29 uses a full outer join to include all publishers and all authors in the result, regardless of whether a publisher and author are located in the same city. See Figure 7.29 for the result.

Listing 7.29This full outer join includes all rows in the tables authors and publishers in the result, whether or not there’s a match in the city columns. See Figure 7.29 for the result.

SELECT a.au_fname, a.au_lname, p.pub_name
  FROM authors a
  FULL OUTER JOIN publishers p
    ON a.city = p.city
  ORDER BY p.pub_name ASC,
    a.au_lname ASC, a.au_fname ASC;

Figure 7.29Result of Listing 7.29. This result contains nine rows: four rows for authors who have no matching rows in the table publishers, three rows in which the author and publisher coexist in the same city, and two rows for publishers who have no matching city in the table authors.

au_fname  au_lname    pub_name
--------- ----------- -------------------
Sarah     Buchman     NULL
Wendy     Heydemark   NULL
          Kellsey     NULL
Paddy     O'Furniture NULL
Christian Kells       Abatis Publishers
Hallie    Hull        Core Dump Books
Klee      Hull        Core Dump Books
NULL      NULL        Schadenfreude Press
NULL      NULL        Tenterhooks Press

Tips for Listing 7.29

Listing 7.30 uses a left outer join to list the number of books that each author wrote (or cowrote). See Figure 7.30 for the result. Note that in contrast to Listing 7.11 in “Creating an Inner Join with INNER JOIN” earlier in this chapter, the author A07 (Paddy O'Furniture) appears in the result even though he has written no books.

Listing 7.30List the number of books that each author wrote (or cowrote), including authors who have written no books. See Figure 7.30 for the result.

SELECT
    a.au_id,
    COUNT(ta.title_id) AS "Num books"
  FROM authors a
  LEFT OUTER JOIN title_authors ta
    ON a.au_id = ta.au_id
  GROUP BY a.au_id
  ORDER BY a.au_id ASC;

Figure 7.30Result of Listing 7.30.

au_id Num books
----- ---------
A01           3
A02           4
A03           2
A04           4
A05           1
A06           3
A07           0

Tips for Listing 7.30

Listing 7.31 uses a WHERE condition to test for null and list only the authors who haven’t written a book. See Figure 7.31 for the result.

Listing 7.31List the authors who haven’t written (or cowritten) a book. See Figure 7.31 for the result.

SELECT a.au_id, a.au_fname, a.au_lname
  FROM authors a
  LEFT OUTER JOIN title_authors ta
    ON a.au_id = ta.au_id
  WHERE ta.au_id IS NULL;

Figure 7.31Result of Listing 7.31.

au_id au_fname au_lname
----- -------- -----------
A07   Paddy    O'Furniture

Tips for Listing 7.31

Listing 7.32 combines an inner join and a left outer join to list all authors and any books they wrote (or cowrote) that sold more than 100,000 copies. In this example, first I created a filtered INNER JOIN result and then OUTER JOINed it with the table authors, from which I wanted all rows. See Figure 7.32 for the result.

Listing 7.32List all authors and any books written (or cowritten) that sold more than 100,000 copies. See Figure 7.32 for the result.

SELECT a.au_id, a.au_fname, a.au_lname,
    tta.title_id, tta.title_name, tta.sales
  FROM authors a
  LEFT OUTER JOIN
  (SELECT ta.au_id, t.title_id,
      t.title_name, t.sales
    FROM title_authors ta
    INNER JOIN titles t
      ON t.title_id = ta.title_id
    WHERE sales > 100000) tta
    ON a.au_id = tta.au_id
  ORDER BY a.au_id ASC, tta.title_id ASC;

Figure 7.32Result of Listing 7.32.

au_id au_fname  au_lname    title_id title_name                sales
----- --------- ----------- -------- ------------------------- -------
A01   Sarah     Buchman     NULL     NULL                         NULL
A02   Wendy     Heydemark   T07      I Blame My Mother         1500200
A02   Wendy     Heydemark   T12      Spontaneous, Not Annoying  100001
A03   Hallie    Hull        NULL     NULL                         NULL
A04   Klee      Hull        T05      Exchange of Platitudes     201440
A04   Klee      Hull        T07      I Blame My Mother         1500200
A05   Christian Kells       NULL     NULL                         NULL
A06             Kellsey     NULL     NULL                         NULL
A07   Paddy     O'Furniture NULL     NULL                         NULL

Tips for Listing 7.32

Creating a Self-Join

A self-join is a normal SQL join that joins a table to itself and retrieves rows from a table by comparing values in one or more columns in the same table. Self-joins often are used in tables with a reflexive relationship, which is a primary-key/foreign-key relationship from a column or combination of columns in a table to other columns in that same table. For information about keys, see “Primary Keys” and “Foreign Keys” in Chapter 2.

Suppose that you have the following table, named employees:

emp_id emp_name          boss_id
------ ----------------- -------
E01    Lord Copper       NULL
E02    Jocelyn Hitchcock E01
E03    Mr. Salter        E01
E04    William Boot      E03
E05    Mr. Corker        E03

emp_id is a primary key that uniquely identifies the employee, and boss_id is an employee ID that identifies the employee’s manager. Each manager also is an employee, so to ensure that each manager ID that is added to the table matches an existing employee ID, boss_id is defined as a foreign key of emp_id. Listing 7.33 uses this reflexive relationship to compare rows within the table and retrieve the name of the manager of each employee. (You wouldn’t need a join at all to get just the manager’s ID.) See Figure 7.33 for the result.

Listing 7.33List the name of each employee and the name of his or her manager. See Figure 7.33 for the result.

SELECT
    e1.emp_name AS "Employee name",
    e2.emp_name AS "Boss name"
  FROM employees e1
  INNER JOIN employees e2
    ON e1.boss_id = e2.emp_id;

Figure 7.33Result of Listing 7.33. Note that Lord Copper, who has no boss, is excluded from the result because his null boss_id doesn’t satisfy the join condition.

Employee name     Boss name
----------------- -----------
Jocelyn Hitchcock Lord Copper
Mr. Salter        Lord Copper
William Boot      Mr. Salter
Mr. Corker        Mr. Salter

The same table (employees) appears twice in Listing 7.33 with two different aliases (e1 and e2) that are used to qualify column names in the join condition:

e1.boss_id = e2.emp_id

As with any join, a self-join requires two tables, but instead of adding a second table to the join, you add a second instance of the same table. That way, you can compare a column in the first instance of the table to a column in the second instance. As with all joins, the DBMS combines and returns rows of the table that satisfy the join condition. You actually aren’t creating another copy of the table—you’re joining the table to itself—but the effect might be easier to understand if you think about it as being two tables.

To create a self-join:

Tips for Self Joins

Listing 7.34 uses a WHERE search condition and self-join from the column state to itself to find all authors who live in the same state as author A04 (Klee Hull). See Figure 7.34 for the result.

Listing 7.34List the authors who live in the same state as author A04 (Klee Hull). See Figure 7.34 for the result.

SELECT a1.au_id, a1.au_fname,
    a1.au_lname, a1.state
  FROM authors a1
  INNER JOIN authors a2
    ON a1.state = a2.state
  WHERE a2.au_id = 'A04';

Figure 7.34Result of Listing 7.34.

au_id au_fname au_lname state
----- -------- -------- -----
A03   Hallie   Hull     CA
A04   Klee     Hull     CA
A06            Kellsey  CA

Tips for Listing 7.34

For every biography, Listing 7.35 lists the other biographies that outsold it. Note that the WHERE search condition requires type = 'biography' for both tables t1 and t2 because the join condition considers the column type to be two separate columns. See Figure 7.35 for the result.

Listing 7.35For every biography, list the title ID and sales of the other biographies that outsold it. See Figure 7.35 for the result.

SELECT t1.title_id, t1.sales,
    t2.title_id AS "Better seller",
    t2.sales AS "Higher sales"
  FROM titles t1
  INNER JOIN titles t2
    ON t1.sales < t2.sales
  WHERE t1.type = 'biography'
    AND t2.type = 'biography'
  ORDER BY t1.title_id ASC, t2.sales ASC;

Figure 7.35Result of Listing 7.35.

title_id sales  Better seller Higher sales
-------- ------ ------------- ------------
T06       11320 T12                 100001
T06       11320 T07                1500200
T12      100001 T07                1500200

Tips for Listing 7.35

Listing 7.36 is a self-join to find all pairs of authors within New York state. See Figure 7.36 for the result.

Listing 7.36List all pairs of authors who live in New York state. See Figure 7.36 for the result.

SELECT
    a1.au_fname, a1.au_lname,
    a2.au_fname, a2.au_lname
  FROM authors a1
  INNER JOIN authors a2
    ON a1.state = a2.state
  WHERE a1.state = 'NY'
  ORDER BY a1.au_id ASC, a2.au_id ASC;

Figure 7.36Result of Listing 7.36.

au_fname  au_lname au_fname  au_lname
--------- -------- --------- --------
Sarah     Buchman  Sarah     Buchman
Sarah     Buchman  Christian Kells
Christian Kells    Sarah     Buchman
Christian Kells    Christian Kells

Tips for Listing 7.36

The first and fourth rows of Figure 7.36 are unnecessary because they indicate that Sarah Buchman lives in the same state as Sarah Buchman, and likewise for Christian Kells. Adding a join condition retains only those rows in which the two authors differ (Listing 7.37 and Figure 7.37).

Listing 7.37List all different pairs of authors who live in New York state. See Figure 7.37 for the result.

SELECT
    a1.au_fname, a1.au_lname,
    a2.au_fname, a2.au_lname
  FROM authors a1
  INNER JOIN authors a2
    ON a1.state = a2.state
    AND a1.au_id <> a2.au_id
  WHERE a1.state = 'NY'
  ORDER BY a1.au_id ASC, a2.au_id ASC;

Figure 7.37Result of Listing 7.37.

au_fname  au_lname au_fname  au_lname
--------- -------- --------- --------
Sarah     Buchman  Christian Kells
Christian Kells    Sarah     Buchman

Tips for Listing 7.37

Listing 7.37 still isn’t quite what I want because the two result rows are redundant. The first row states that Sarah Buchman lives in the same state as Christian Kells, and the second row gives the same information. To eliminate this redundancy, I’ll change the second join condition’s comparison operator from not-equal to less-than (Listing 7.38 and Figure 7.38).

Listing 7.38List all different pairs of authors who live in New York state, with no redundancies. See Figure 7.38 for the result.

SELECT
    a1.au_fname, a1.au_lname,
    a2.au_fname, a2.au_lname
  FROM authors a1
  INNER JOIN authors a2
    ON a1.state = a2.state
    AND a1.au_id < a2.au_id
  WHERE a1.state = 'NY'
  ORDER BY a1.au_id ASC, a2.au_id ASC;

Figure 7.38Result of Listing 7.38.

au_fname au_lname au_fname  au_lname
-------- -------- --------- --------
Sarah    Buchman  Christian Kells

Tips for Listing 7.38

8. Subqueries

To this point, I’ve used a single SELECT statement to retrieve data from one or more tables. This chapter describes nested queries, which let you retrieve or modify data based on another query’s result.

A subquery, or subselect, is a SELECT statement embedded in another SQL statement. You can nest a subquery in:

In general, you can use a subquery anywhere an expression is allowed, but your DBMS might restrict where they can appear. This chapter covers subqueries nested in a SELECT statement or another subquery; Chapter 10 covers subqueries embedded in INSERT, UPDATE, and DELETE statements.

Understanding Subqueries

This section defines some terms and introduces subqueries by giving an example of a SELECT statement that contains a simple subquery. Subsequent sections explain the types of subqueries and their syntax and semantics.

Suppose that you want to list the names of the publishers of biographies. The naive approach is to write two queries: one query to retrieve the IDs of all the biography publishers (Listing 8.1 and Figure 8.1) and a second query that uses the first query’s result to list the publisher names (Listing 8.2 and Figure 8.2).

Listing 8.1List the biography publishers. See Figure 8.1 for the result.

SELECT pub_id
  FROM titles
  WHERE type = 'biography';

Figure 8.1Result of Listing 8.1. You can add DISTINCT to the SELECT clause of Listing 8.1 to list the publishers only once; see “Eliminating Duplicate Rows with DISTINCT” in Chapter 4.

pub_id
------
P01
P03
P01
P01

Listing 8.2This query uses the result of Listing 8.1 to list the names of the biography publishers. See Figure 8.2 for the result.

SELECT pub_name
  FROM publishers
  WHERE pub_id IN ('P01', 'P03');

Figure 8.2Result of Listing 8.2.

pub_name
-------------------
Abatis Publishers
Schadenfreude Press

A better way is to use an inner join (Listing 8.3 and Figure 8.3); see “Creating an Inner Join with INNER JOIN” in Chapter 7.

Listing 8.3List the names of the biography publishers by using an inner join. See Figure 8.3 for the result.

SELECT DISTINCT pub_name
  FROM publishers p
  INNER JOIN titles t
    ON p.pub_id = t.pub_id
  WHERE t.type = 'biography';

Figure 8.3Result of Listing 8.3.

pub_name
-------------------
Abatis Publishers
Schadenfreude Press

Another alternative is to use a subquery (Listing 8.4 and Figure 8.4). The subquery in Listing 8.4 is highlighted. A subquery also is called an inner query, and the statement containing a subquery is called an outer query. In other words, an enclosed subquery is an inner query of an outer query. Remember that a subquery can be nested in another subquery, so inner and outer are relative terms in statements with multiple nested subqueries.

Listing 8.4List the names of the biography publishers by using a subquery. See Figure 8.4 for the result.

SELECT pub_name
  FROM publishers
  WHERE pub_id IN
    (SELECT pub_id
       FROM titles
       WHERE type = 'biography');

Figure 8.4Result of Listing 8.4.

pub_name
-------------------
Abatis Publishers
Schadenfreude Press

I’ll explain how a DBMS executes subqueries in “Simple and Correlated Subqueries” later in this chapter, but for now, all that you need to know is that in Listing 8.4, the DBMS processes the inner query (highlighted text) first and then uses its interim result to run the outer query (plain text) and get the final result. The IN keyword that introduces the subquery tests for list membership and works like IN in “List Filtering with IN” in Chapter 4. Note that the inner query in Listing 8.4 is the same query as Listing 8.1, and the outer query is the same query as Listing 8.2.

Tips for Subqueries

Subquery Syntax

The syntax of a subquery is the same as that of a normal SELECT statement (see Chapters 4 through 7) except for the following differences:

In practice, a subquery usually appears in a WHERE clause that takes one of these forms:

test_expr is a literal value, a column name, an expression, or a scalar subquery; op is a comparison operator (=, <>, <, <=, >, or >=); and subquery is a simple or correlated subquery. I’ll cover each of these forms later in this chapter. You can use these subquery forms in a HAVING clause, too.

Tips for Subquery Syntax

Subqueries vs. Joins

In “Understanding Subqueries” earlier in this chapter, Listings 8.3 and 8.4 showed two equivalent queries: one used a join, and the other used a subquery. Many subqueries can be formulated alternatively as joins. In fact, a subquery is a way to relate one table to another without actually doing a join.

Because subqueries can be hard to use and debug, you might prefer to use joins, but you can pose some questions only as subqueries. In cases where you can use subqueries and joins interchangeably, you should test queries on your DBMS to see whether a performance difference exists between a statement that uses a subquery and a semantically equivalent statement that uses a join. For example, the query

SELECT MAX(table1.col1)
  FROM table1
  WHERE table1.col1 IN
    (SELECT table2.col1
       FROM table2);

usually will run faster than

SELECT MAX(table1.col1)
  FROM table1
  INNER JOIN table2
    ON table1.col1 = table2.col1;

For more information, see “Comparing Equivalent Queries” later in this chapter.

The following syntax diagrams show some equivalent statements that use subqueries and joins. These two statements are equivalent (IN subquery):

SELECT *
  FROM table1
  WHERE id IN
    (SELECT id FROM table2);

and (inner join):

SELECT DISTINCT table1.*
  FROM table1
  INNER JOIN table2
    ON table1.id = table2.id;

See Listings 8.5a and 8.5b and Figure 8.5 for an example.

Listing 8.5aThis statement uses a subquery to list the authors who live in the same city in which a publisher is located. See Figure 8.5 for the result.

SELECT au_id, city
  FROM authors
  WHERE city IN
    (SELECT city FROM publishers);

Listing 8.5bThis statement is equivalent to Listing 8.5a but uses an inner join instead of a subquery. See Figure 8.5 for the result.

SELECT DISTINCT a.au_id, a.city
  FROM authors a
  INNER JOIN publishers p
    ON a.city = p.city;

Figure 8.5Result of Listings 8.5a and 8.5b.

au_id city
----- -------------
A03   San Francisco
A04   San Francisco
A05   New York

These three statements are equivalent (NOT IN subquery):

SELECT *
  FROM table1
  WHERE id NOT IN
    (SELECT id FROM table2);

and (NOT EXISTS subquery):

SELECT *
  FROM table1
  WHERE NOT EXISTS
    (SELECT *
       FROM table2
       WHERE table1.id = table2.id);

and (left outer join):

SELECT table1.*
  FROM table1
  LEFT OUTER JOIN table2
    ON table1.id = table2.id
  WHERE table2.id IS NULL;

See Listings 8.6a, 8.6b, and 8.6c and Figure 8.6 for an example. IN and EXISTS subqueries are covered later in this chapter.

Listing 8.6aThis statement uses an IN subquery to list the authors who haven’t written (or cowritten) a book. See Figure 8.6 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE au_id NOT IN
    (SELECT au_id FROM title_authors);

Listing 8.6bThis statement is equivalent to Listing 8.6a but uses an EXISTS subquery instead of an IN subquery. See Figure 8.6 for the result.

SELECT au_id, au_fname, au_lname
FROM authors a
  WHERE NOT EXISTS
    (SELECT *
       FROM title_authors ta
       WHERE a.au_id = ta.au_id);

Listing 8.6cThis statement is equivalent to Listings 8.6a and 8.6b but uses a left outer join instead of a subquery. See Figure 8.6 for the result.

SELECT a.au_id, a.au_fname, a.au_lname
  FROM authors a
  LEFT OUTER JOIN title_authors ta
    ON a.au_id = ta.au_id
  WHERE ta.au_id IS NULL;

Figure 8.6Result of Listings 8.6a, 8.6b, and 8.6c.

au_id au_fname au_lname
----- -------- -----------
A07   Paddy    O'Furniture

You also can write a self-join as a subquery (Listings 8.7a and 8.7b and Figure 8.7). For information about self-joins, see “Creating a Self-Join” in Chapter 7.

Listing 8.7aThis statement uses a subquery to list the authors who live in the same state as author A04 (Klee Hull). See Figure 8.7 for the result.

SELECT au_id, au_fname, au_lname, state
  FROM authors
  WHERE state IN
    (SELECT state
       FROM authors
       WHERE au_id = 'A04');

Listing 8.7bThis statement is equivalent to Listing 8.7a but uses an inner join instead of a subquery. See Figure 8.7 for the result.

SELECT a1.au_id, a1.au_fname,
    a1.au_lname, a1.state
  FROM authors a1
  INNER JOIN authors a2
    ON a1.state = a2.state
  WHERE a2.au_id = 'A04';

Figure 8.7Result of Listings 8.7a and 8.7b.

au_id au_fname au_lname state
----- -------- -------- -----
A03   Hallie   Hull     CA
A04   Klee     Hull     CA
A06            Kellsey  CA

Favor subqueries if you’re comparing an aggregate value to other values (Listing 8.8 and Figure 8.8). Without a subquery, you’d need two SELECT statements to list all the books with the highest price: one query to find the highest price and a second query to list all the books selling for that price. For information about aggregate functions, see Chapter 6.

Listing 8.8List all books whose price equals the highest book price. See Figure 8.8 for the result.

SELECT title_id, price
  FROM titles
  WHERE price =
    (SELECT MAX(price)
       FROM titles
);

Figure 8.8Result of Listing 8.8.

title_id price
-------- -----
T03      39.95

Use joins when you include columns from multiple tables in the result. Listing 8.5b uses a join to retrieve authors who live in the same city in which a publisher is located. To include the publisher ID in the result, simply add the column pub_id to the SELECT-clause list (Listing 8.9 and Figure 8.9).

You can’t accomplish this same task with a subquery because it’s illegal to include a column in the outer query’s SELECT-clause list from a table that appears in only the inner query:

SELECT a.au_id, a.city, p.pub_id
  FROM authors a
  WHERE a.city IN
    (SELECT p.city
       FROM publishers p);     --Illegal

Listing 8.9List the authors who live in the same city in which a publisher is located, and include the publisher in the result. See Figure 8.9 for the result.

SELECT a.au_id, a.city, p.pub_id
  FROM authors a
  INNER JOIN publishers p
    ON a.city = p.city;

Figure 8.9Result of Listing 8.9.

au_id city          pub_id
----- ------------- ------
A03   San Francisco P02
A04   San Francisco P02
A05   New York      P01

Tips for Subqueries vs. Joins

Simple and Correlated Subqueries

You can use two types of subqueries:

A simple subquery, or noncorrelated subquery, is a subquery that can be evaluated independently of its outer query and is processed only once for the entire statement. All the subqueries in this chapter’s examples so far have been simple subqueries (except Listing 8.6b).

A correlated subquery can’t be evaluated independently of its outer query; it’s an inner query that depends on data from the outer query. A correlated subquery is used if a statement needs to process a table in the inner query for each row in the outer query.

Correlated subqueries have more-complicated syntax and a knottier execution sequence than simple subqueries, but you can use them to solve problems that you can’t solve with simple subqueries or joins. This section gives an example of a simple subquery and a correlated subquery and then describes how a DBMS executes each one. Subsequent sections in this chapter contain more examples of each type of subquery.

Simple Subqueries

A DBMS evaluates a simple subquery by evaluating the inner query once and substituting its result into the outer query. A simple subquery executes prior to, and independent of, its outer query.

Let’s revisit Listing 8.5a from earlier in this chapter. Listing 8.10 (which is identical to Listing 8.5a) uses a simple subquery to list the authors who live in the same city in which a publisher is located; see Figure 8.10 for the result. Conceptually, a DBMS processes this query in two steps as two separate SELECT statements:

  1. The inner query (a simple subquery) returns the cities of all the publishers (Listing 8.11 and Figure 8.11).
  2. The DBMS substitutes the values returned by the inner query in step 1 into the outer query, which finds the author IDs corresponding to the publishers’ cities (Listing 8.12 and Figure 8.12).

Listing 8.10List the authors who live in the same city in which a publisher is located. See Figure 8.10 for the result.

SELECT au_id, city
  FROM authors
  WHERE city IN
    (SELECT city
       FROM publishers);

Figure 8.10Result of Listing 8.10.

au_id city
----- -------------
A03   San Francisco
A04   San Francisco
A05   New York

Listing 8.11List the cities in which the publishers are located. See Figure 8.11 for the result.

SELECT city
  FROM publishers;

Figure 8.11Result of Listing 8.11.

city
-------------
New York
San Francisco
Hamburg
Berkeley

Listing 8.12List the authors who live in one of the cities returned by Listing 8.11. See Figure 8.12 for the result.

SELECT au_id, city
  FROM authors
  WHERE city IN
    ('New York', 'San Francisco',
     'Hamburg', 'Berkeley');

Figure 8.12Result of Listing 8.12.

au_id city
----- -------------
A03   San Francisco
A04   San Francisco
A05   New York

Correlated Subqueries

Correlated subqueries offer a more powerful data-retrieval mechanism than simple subqueries do. A correlated subquery’s important characteristics are:

Listing 8.13 uses a correlated subquery to list the books that have sales better than the average sales of books of its type; see Figure 8.13 for the result. candidate (following titles in the outer query) and average (following titles in the inner query) are alias table names for the table titles, so that the information can be evaluated as though it comes from two different tables (see “Creating a Self-Join” in Chapter 7).

Listing 8.13List the books that have sales greater than or equal to the average sales of books of its type. The correlation variable candidate.type defines the initial condition to be met by the rows of the inner table average. The outer WHERE condition (sales >=) defines the final test that the rows of the inner table average must satisfy. See Figure 8.13 for the result.

SELECT
    candidate.title_id,
    candidate.type,
    candidate.sales
  FROM titles candidate
  WHERE sales >=
    (SELECT AVG(sales)
       FROM titles average
       WHERE average.type = candidate.type);

Figure 8.13Result of Listing 8.13.

title_id type       sales
-------- ---------- -------
T02      history       9566
T03      computer     25667
T05      psychology  201440
T07      biography  1500200
T09      children      5000
T13      history      10467

In Listing 8.13, the subquery can’t be resolved independently of the outer query. It needs a value for candidate.type, but this value is a correlation variable that changes as the DBMS examines different rows in the table candidate. The column average.type is said to correlate with candidate.type in the outer query. The average sales for a book type are calculated in the subquery by using the type of each book from the table in the outer query (candidate). The subquery computes the average sales for this type and then compares it with a row in the table candidate. If the sales in the table candidate are greater than or equal to average sales for the type, then that book is displayed in the result. A DBMS processes this query as follows:

  1. The book type in the first row of candidate is used in the subquery to compute average sales.

    Take the row for book T01, whose type is history, so the value in the column type in the first row of the table candidate is history. In effect, the subquery becomes:

    SELECT AVG(sales)
      FROM titles average
      WHERE average.type = 'history';

    This pass through the subquery yields a value of 6866—the average sales of history books. In the outer query, book T01’s sales of 566 are compared to the average sales of history books. T01’s sales are lower than average, so T01 isn’t displayed in the result.

  2. Next, book T02’s row in candidate is evaluated.

    T02 also is a history book, so the evaluated subquery is the same as in step 1:

    SELECT AVG(sales)
      FROM titles average
      WHERE average.type = 'history';

    This pass through the subquery again yields 6866 for the average sales of history books. Book T02’s sales of 9566 are higher than average, so T02 is displayed in the result.

  3. Next, book T03’s row in candidate is evaluated.

    T03 is a computer book, so this time, the evaluated subquery is:

    SELECT AVG(sales)
      FROM titles average
      WHERE average.type = 'computer';

    The result of this pass through the subquery is average sales of 25667 for computer books. Because book T03’s sales of 25667 equals the average (it’s the only computer book), T03 is displayed in the result.

  4. The DBMS repeats this process until every row in the outer table candidate has been tested.

If you can get the same result by using a simple subquery or a correlated subquery, then use the simple subquery because it probably will run faster. Listings 8.14a and 8.14b show two equivalent queries that list all authors who earn 100 percent (1.0) of the royalty share on a book. Listing 8.14a, which uses a simple subquery, is more efficient than Listing 8.14b, which uses a correlated subquery. In the simple subquery, the DBMS reads the inner table title_authors once. In the correlated subquery, the DBMS must loop through title_authors five times—once for each qualifying row in the outer table authors. See Figure 8.14 for the result.

Why do I say that a statement that uses a simple subquery probably will run faster than an equivalent statement that uses a correlated subquery when a correlated subquery clearly requires more work? Because your DBMS’s optimizer might be clever enough to recognize and reformulate a correlated subquery as a semantically equivalent simple subquery internally before executing the statement. For more information, see “Comparing Equivalent Queries” later in this chapter.

Listing 8.14bThis statement uses a simple subquery to list all authors who earn 100 percent (1.0) royalty on a book. See Figure 8.14 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE au_id IN
    (SELECT au_id
       FROM title_authors
       WHERE royalty_share = 1.0);

Listing 8.14bThis statement is equivalent to Listing 8.14a but uses a correlated subquery instead of a simple subquery. This query probably will run slower than Listing 8.14a. See Figure 8.14 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE 1.0 IN
    (SELECT royalty_share
       FROM title_authors
       WHERE title_authors.au_id =
             authors.au_id);

Figure 8.14Result of Listings 8.14a and 8.14b.

au_id au_fname  au_lname
----- --------- ---------
A01   Sarah     Buchman
A02   Wendy     Heydemark
A04   Klee      Hull
A05   Christian Kells
A06             Kellsey

Tips for Simple and Correlated Subqueries

Qualifying Column Names in Subqueries

Recall from “Qualifying Column Names” in Chapter 7 that you can qualify a column name explicitly with a table name to identify the column unambiguously. In statements that contain subqueries, column names are qualified implicitly by the table referenced in the FROM clause at the same nesting level.

In Listing 8.15a, which lists the names of biography publishers, the column names are qualified implicitly, meaning:

Listing 8.15b shows Listing 8.15a with explicit qualifiers. See Figure 8.15 for the result.

Listing 8.15aThe tables publishers and titles both contain a column named pub_id, but you don’t have to qualify pub_id in this query because of the implicit assumptions about table names that SQL makes. See Figure 8.15 for the result.

SELECT pub_name
  FROM publishers
  WHERE pub_id IN
    (SELECT pub_id
       FROM titles
       WHERE type = 'biography');

Listing 8.15bThis query is equivalent to Listing 8.15a, but with explicit qualification of pub_id. See Figure 8.15 for the result.

SELECT pub_name
  FROM publishers
  WHERE publishers.pub_id IN
    (SELECT titles.pub_id
       FROM titles
       WHERE type = 'biography');

Figure 8.15Result of Listings 8.15a and 8.15b.

pub_name
-------------------
Abatis Publishers
Schadenfreude Press

Tips for Qualified Column Names in Subqueries

Nulls in Subqueries

Beware of nulls; their presence complicates subqueries. If you don’t eliminate them when they’re present, then you might get an unexpected answer.

A subquery can hide a comparison to a null. Recall from “Nulls” in Chapter 3 that nulls don’t equal each other and that you can’t determine whether a null matches any other value. The following example involves a NOT IN subquery (see “Testing Set Membership with IN” later in this chapter). Consider the following two tables, each with one column. The first table is named table1:

col
----
   1
   2

The second table is named table2:

col
----
   1
   2
   3

If I run Listing 8.16 to list the values in table2 that aren’t in table1, then I get Figure 8.16a, as expected.

Listing 8.16List the values in table2 that aren’t in table1. See Figures 8.16a and 8.16b for the result.

SELECT col
  FROM table2
  WHERE col NOT IN
    (SELECT col
       FROM table1);

Figure 8.16aResult of Listing 8.16 when table1 doesn’t contain a null.

col
----
   3

Now add a null to table1:

col
----
   1
   2
NULL

If I rerun Listing 8.16, then I get Figure 8.16b (an empty table), which is correct logically but not what I expected.

Figure 8.16bResult of Listing 8.16 when table1 contains a null. This result is an empty table, which is correct logically but not what I expected.

col
----

Why is the result empty this time? The solution requires some algebra. I can move the NOT outside the subquery condition without changing the meaning of Listing 8.16:

SELECT col
  FROM table2
  WHERE NOT col IN
    (SELECT col FROM table1);

The IN clause determines whether a value in table2 matches any value in table1, so I can rewrite the subquery as a compound condition:

SELECT col
  FROM table2
  WHERE NOT ((col = 1)
         OR  (col = 2)
         OR  (col = NULL));

If I apply De Morgan’s Laws (refer to Table 4.6 in Chapter 4), then this query becomes:

SELECT col
  FROM table2
  WHERE (col <> 1)
    AND (col <> 2)
    AND (col <> NULL);

The final expression, col <> NULL, always is unknown. Refer to the AND truth table (Table 4.3 in Chapter 4), and you’ll see that the entire WHERE search condition reduces to unknown, which always is rejected by WHERE.

To fix Listing 8.16 so that it doesn’t examine the null in table1, add an IS NOT NULL condition to the subquery (see “Testing for Nulls with IS NULL” in Chapter 4):

SELECT col
  FROM table2
  WHERE col NOT IN
    (SELECT col
       FROM table1
       WHERE col IS NOT NULL);

Tips for Nulls in Subqueries

Using Subqueries as Column Expressions

In Chapters 4, 5, and 6, you learned that the items in a SELECT-clause list can be literals, column names, or more-complex expressions. SQL also lets you to embed a subquery in a SELECT-clause list.

A subquery that’s used as a column expression must be a scalar subquery. Recall from Table 8.1 in “Subquery Syntax” earlier in this chapter that a scalar subquery returns a single value (that is, a one-row, one-column result). In most cases, you’ll have to use an aggregate function or restrictive WHERE conditions in the subquery to guarantee that the subquery returns only one row.

The syntax for the SELECT-clause list is the same as you’ve been using all along, except that you can specify a parenthesized subquery as one of the column expressions in the list, as the following examples show.

Listing 8.17 uses two simple subqueries as column expressions to list each biography, its price, the average price of all books (not just biographies), and the difference between the price of the biography and the average price of all books. The aggregate function AVG() guarantees that each subquery returns a single value. See Figure 8.17 for the result. Remember that AVG() ignores nulls when computing an average; see “Calculating an Average with AVG()” in Chapter 6.

Listing 8.17List each biography, its price, the average price of all books, and the difference between the price of the biography and the average price of all books. See Figure 8.17 for the result.

SELECT title_id, price,
    (SELECT AVG(price) FROM titles)
      AS "AVG(price)",
    price - (SELECT AVG(price) FROM titles)
      AS "Difference"
  FROM titles
  WHERE type='biography';

Figure 8.17Result of Listing 8.17.

title_id price   AVG(price) Difference
-------- ------- ---------- ----------
T06        19.95    18.3875     1.5625
T07        23.95    18.3875     5.5625
T10         NULL    18.3875       NULL
T12        12.99    18.3875    -5.3975

Listing 8.18 uses correlated subqueries to list all the authors of each book in one row, as you’d view them in a report or spreadsheet. See Figure 8.18 for the result. Note that in each WHERE clause, SQL qualifies title_id implicitly with the table alias ta referenced in the subquery’s FROM clause; see “Qualifying Column Names in Subqueries” earlier in this chapter. For a more efficient way to implement this query, see “Tips for Column Expressions” later in this section. See Listing 15.8 in Chapter 15 for the reverse of this query.

Listing 8.18List all the authors of each book in one row. See Figure 8.18 for the result.

SELECT title_id,
    (SELECT au_id
       FROM title_authors ta
       WHERE au_order = 1
         AND title_id = t.title_id)
      AS "Author 1",
    (SELECT au_id
       FROM title_authors ta
       WHERE au_order = 2
         AND title_id = t.title_id)
      AS "Author 2",
    (SELECT au_id
       FROM title_authors ta
       WHERE au_order = 3
         AND title_id = t.title_id)
      AS "Author 3"
  FROM titles t;

Figure 8.18Result of Listing 8.18.

title_id Author 1 Author 2 Author 3
-------- -------- -------- --------
T01      A01      NULL     NULL
T02      A01      NULL     NULL
T03      A05      NULL     NULL
T04      A03      A04      NULL
T05      A04      NULL     NULL
T06      A02      NULL     NULL
T07      A02      A04      NULL
T08      A06      NULL     NULL
T09      A06      NULL     NULL
T10      A02      NULL     NULL
T11      A06      A03      A04
T12      A02      NULL     NULL
T13      A01      NULL     NULL

In Listing 8.19, I revisit Listing 7.30 in “Creating Outer Joins with OUTER JOIN” in Chapter 7, but this time, I’m using a correlated subquery instead of an outer join to list the number of books that each author wrote (or cowrote). See Figure 8.19 for the result.

Listing 8.19List the number of books that each author wrote (or cowrote), including authors who have written no books. See Figure 8.19 for the result.

SELECT au_id,
    (SELECT COUNT(*)
       FROM title_authors ta
       WHERE ta.au_id = a.au_id)
      AS "Num books"
  FROM authors a
  ORDER BY au_id;

Figure 8.19Result of Listing 8.19.

au_id Num books
----- ---------
A01           3
A02           4
A03           2
A04           4
A05           1
A06           3
A07           0

Listing 8.20 uses a correlated subquery to list each author and the latest date on which he or she published a book. You should qualify every column name explicitly in a subquery that contains a join to make it clear which table is referenced (even when qualifiers are unnecessary). See Figure 8.20 for the result.

Listing 8.20List each author and the latest date on which he or she published a book. See Figure 8.20 for the result.

SELECT au_id,
    (SELECT MAX(pubdate)
       FROM titles t
       INNER JOIN title_authors ta
         ON ta.title_id = t.title_id
       WHERE ta.au_id = a.au_id)
      AS "Latest pub date"
  FROM authors a;

Figure 8.20Result of Listing 8.20.

au_id Latest pub date
----- ---------------
A01   2000-08-01
A02   2000-08-31
A03   2000-11-30
A04   2001-01-01
A05   2000-09-01
A06   2002-05-31
A07   NULL

Listing 8.21 uses a correlated subquery to compute the running total of all book sales. A running total, or running sum, is a common calculation: For each book, I want to compute the sum of all sales of the books that precede the book. Here, I’m defining precede to mean those books whose title_id comes before the current book’s title_id alphabetically. Note the use of table aliases to refer to the same table in two contexts. The subquery returns the sum of sales for all books preceding the current book, which is denoted by t1.title_id. See Figure 8.21 for the result. See also “Calculating Running Statistics” in Chapter 15.

Listing 8.21Compute the running sum of all book sales. See Figure 8.21 for the result.

SELECT t1.title_id, t1.sales,
    (SELECT SUM(t2.sales)
       FROM titles t2
       WHERE t2.title_id <= t1.title_id)
      AS "Running total"
  FROM titles t1;

Figure 8.21Result of Listing 8.21.

title_id sales   Running total
-------- ------- -------------
T01          566           566
T02         9566         10132
T03        25667         35799
T04        13001         48800
T05       201440        250240
T06        11320        261560
T07      1500200       1761760
T08         4095       1765855
T09         5000       1770855
T10         NULL       1770855
T11        94123       1864978
T12       100001       1964979
T13        10467       1975446

You also can use a subquery in a FROM clause. In “Tips for DISTINCT” in Chapter 6, I used a FROM subquery to replicate a distinct aggregate function. Listing 8.22 uses a FROM subquery to calculate the greatest number of titles written (or cowritten) by any author. See Figure 8.22 for the result. Note that the outer query uses a table alias (ta) and column alias (count_titles) to reference the inner query’s result. See also “Column aliases and WHERE” in Chapter 4.

Listing 8.22Calculate the greatest number of titles written (or cowritten) by any author. See Figure 8.22 for the result.

SELECT MAX(ta.count_titles) AS "Max titles"
  FROM (SELECT COUNT(*) AS count_titles
          FROM title_authors
          GROUP BY au_id) ta;

Figure 8.22Result of Listing 8.22.

Max titles
----------
         4

Tips for Column Expressions

Comparing a Subquery Value by Using a Comparison Operator

You can use a subquery as a filter in a WHERE clause or HAVING clause by using one of the comparison operators (=, <>, <, <=, >, or >=).

The important characteristics of a subquery comparison test are:

The hard part of writing these statements is getting the subquery to return one value, which you can guarantee several ways:

To compare a subquery value:

Listing 8.23 tests the result of a simple subquery for equality to list the authors who live in the state in which Tenterhooks Press is located. Only one publisher is named Tenterhooks Press, so the inner WHERE condition guarantees that the inner query returns a single-valued result. See Figure 8.23 for the result.

Listing 8.23List the authors who live in the state in which the publisher Tenterhooks Press is located. See Figure 8.23 for the result.

SELECT au_id, au_fname, au_lname, state
  FROM authors
  WHERE state =
    (SELECT state
       FROM publishers
       WHERE pub_name = 'Tenterhooks Press');

Figure 8.23Result of Listing 8.23.

au_id au_fname au_lname state
----- -------- -------- -----
A03   Hallie   Hull     CA
A04   Klee     Hull     CA
A06            Kellsey  CA

Listing 8.24 is the same as Listing 8.23 except for the name of the publisher. No publisher named XXX exists, so the subquery returns an empty table (zero rows). The comparison evaluates to null, so the final result is empty. See Figure 8.24 for the result.

Listing 8.24List the authors who live in the state in which the publisher XXX is located. See Figure 8.24 for the result.

SELECT au_id, au_fname, au_lname, state
  FROM authors
  WHERE state =
    (SELECT state
       FROM publishers
       WHERE pub_name = 'XXX');

Figure 8.24Result of Listing 8.24 (an empty table).

au_id au_fname au_lname state
----- -------- -------- -----

Listing 8.25 lists the books with above-average sales. Subqueries introduced with comparison operators often use aggregate functions to return a single value. See Figure 8.25 for the result.

Listing 8.25List the books with above-average sales. See Figure 8.25 for the result.

SELECT title_id, sales
  FROM titles
  WHERE sales >
    (SELECT AVG(sales)
       FROM titles);

Figure 8.25Result of Listing 8.25.

title_id sales
-------- -------
T05       201440
T07      1500200

To list the authors of the books with above-average sales, I’ve added an inner join to Listing 8.25 (Listing 8.26 and Figure 8.26).

Listing 8.26List the authors of the books with above-average sales by using a join and a subquery. See Figure 8.26 for the result.

SELECT ta.au_id, ta.title_id
  FROM titles t
  INNER JOIN title_authors ta
    ON ta.title_id = t.title_id
  WHERE sales >
    (SELECT AVG(sales)
       FROM titles)
  ORDER BY ta.au_id ASC, ta.title_id ASC;

Figure 8.26Result of Listing 8.26.

au_id title_id
----- --------
A02   T07
A04   T05
A04   T07

Recall from the introduction to this chapter that you can use a subquery almost anywhere an expression is allowed, so this syntax is valid:

WHERE (subquery) op (subquery)

The left subquery must return a single value. Listing 8.27 is equivalent to Listing 8.26, but I’ve removed the inner join and instead placed a correlated subquery to the left of the comparison operator. See Figure 8.27 for the result.

Listing 8.27List the authors of the books with above-average sales by using two subqueries. See Figure 8.27 for the result.

SELECT au_id, title_id
  FROM title_authors ta
  WHERE
    (SELECT AVG(sales)
       FROM titles t
       WHERE ta.title_id = t.title_id)
    >
    (SELECT AVG(sales)
       FROM titles)
  ORDER BY au_id ASC, title_id ASC;

Figure 8.27Result of Listing 8.27.

au_id title_id
----- --------
A02   T07
A04   T05
A04   T07

You can include GROUP BY or HAVING clauses in a subquery if you know that the GROUP BY or HAVING clause itself returns a single value. Listing 8.28 lists the books priced higher than the highest-priced biography. See Figure 8.28 for the result.

Listing 8.28List the books priced higher than the highest-priced biography. See Figure 8.28 for the result.

SELECT title_id, price
  FROM titles
  WHERE price >
    (SELECT MAX(price)
       FROM titles
       GROUP BY type
       HAVING type = 'biography');

Figure 8.28Result of Listing 8.28.

title_id price
-------- -----
T03      39.95
T13      29.99

Listing 8.29 uses a subquery in a HAVING clause to list the publishers whose average sales exceed overall average sales. Again, the subquery returns a single value (the average of all sales). See Figure 8.29 for the result.

Listing 8.29List the publishers whose average sales exceed the overall average sales. See Figure 8.29 for the result.

SELECT pub_id, AVG(sales) AS "AVG(sales)"
  FROM titles
  GROUP BY pub_id
  HAVING AVG(sales) >
    (SELECT AVG(sales)
       FROM titles);

Figure 8.29Result of Listing 8.29.

pub_id AVG(sales)
------ ----------
P03     506744.33

Listing 8.30 uses a correlated subquery to list authors whose royalty share is less than the highest royalty share of any coauthor of a book. The outer query selects the rows of title_authors (that is, of ta1) one by one. The subquery calculates the highest royalty share for each book being considered for selection in the outer query. For each possible value of ta1, the DBMS evaluates the subquery and puts the row being considered in the result if the royalty share is less than the calculated maximum. See Figure 8.30 for the result.

Listing 8.30List authors whose royalty share is less than the highest royalty share of any coauthor of a book. See Figure 8.30 for the result.

SELECT ta1.au_id, ta1.title_id,
    ta1.royalty_share
  FROM title_authors ta1
  WHERE ta1.royalty_share <
    (SELECT MAX(ta2.royalty_share)
       FROM title_authors ta2
       WHERE ta1.title_id = ta2.title_id);

Figure 8.30Result of Listing 8.30.

au_id title_id royalty_share
----- -------- -------------
A04   T04               0.40
A03   T11               0.30
A04   T11               0.30

Listing 8.31 uses a correlated subquery to imitate a GROUP BY clause and list all books that have a price greater than the average for books of its type. For each possible value of t1, the DBMS evaluates the subquery and includes the row in the result if the price value in that row exceeds the calculated average. It’s unnecessary to group by type explicitly because the rows for which the average price is calculated are restricted by the subquery’s WHERE clause. See Figure 8.31 for the result.

Listing 8.31List all books that have a price greater than the average for books of its type. See Figure 8.31 for the result.

SELECT type, title_id, price
  FROM titles t1
  WHERE price >
    (SELECT AVG(t2.price)
       FROM titles t2
       WHERE t1.type = t2.type)
  ORDER BY type ASC, title_id ASC;

Figure 8.31Result of Listing 8.31.

type       title_id price
---------- -------- -----
biography  T06      19.95
biography  T07      23.95
children   T09      13.95
history    T13      29.99
psychology T04      12.99

Listing 8.32 uses the same structure as Listing 8.31 to list all the books whose sales are less than the best-selling books of their types. See Figure 8.32 for the result.

Listing 8.32List all the books whose sales are less than the best-selling books of their types. See Figure 8.32 for the result.

SELECT type, title_id, sales
  FROM titles t1
  WHERE sales <
    (SELECT MAX(sales)
       FROM titles t2
       WHERE t1.type = t2.type
         AND sales IS NOT NULL)
  ORDER BY type ASC, title_id ASC;

Figure 8.32Result of Listing 8.32.

type       title_id sales
---------- -------- ------
biography  T06       11320
biography  T12      100001
children   T08        4095
history    T01         566
history    T02        9566
psychology T04       13001
psychology T11       94123

Tips for Subquery Comparison Operators

Testing Set Membership with IN

“List Filtering with IN” in Chapter 4 describes how to use the IN keyword in a WHERE clause to compare a literal, column value, or more-complex expression to a list of values. You also can use a subquery to generate the list.

The important characteristics of a subquery set membership test are:

To test set membership:

Listing 8.33 lists the names of the publishers that have published biographies. The DBMS evaluates this statement in two steps. First, the inner query returns the IDs of the publishers that have published biographies (P01 and P03). Second, the DBMS substitutes these values into the outer query, which finds the names that go with the IDs in the table publishers. See Figure 8.33 for the result.

Listing 8.33List the names of the publishers that have published biographies. See Figure 8.33 for the result.

SELECT pub_name
  FROM publishers
  WHERE pub_id IN
    (SELECT pub_id
       FROM titles
       WHERE type = 'biography');

Figure 8.33Result of Listing 8.33.

pub_name
-------------------
Abatis Publishers
Schadenfreude Press

Here’s the join version of Listing 8.33:

SELECT DISTINCT pub_name
  FROM publishers p
  INNER JOIN titles t
    ON p.pub_id = t.pub_id
  AND type = 'biography';

Listing 8.34 is the same as Listing 8.33, except that it uses NOT IN to list the names of the publishers that haven’t published biographies. See Figure 8.34 for the result. This statement can’t be converted to a join. The analogous not-equal join has a different meaning: It lists the names of publishers that have published some book that isn’t a biography.

Listing 8.34List the names of the publishers that haven’t published biographies. See Figure 8.34 for the result.

SELECT pub_name
  FROM publishers
  WHERE pub_id NOT IN
    (SELECT pub_id
       FROM titles
       WHERE type = 'biography');

Figure 8.34Result of Listing 8.34.

pub_name
-----------------
Core Dump Books
Tenterhooks Press

Listing 8.35 is equivalent to Listing 7.31 in Chapter 7, except that it uses a subquery instead of an outer join to list the authors who haven’t written (or cowritten) a book. See Figure 8.35 for the result.

Listing 8.35List the authors who haven’t written (or cowritten) a book. See Figure 8.35 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE au_id NOT IN
    (SELECT au_id
       FROM title_authors);

Figure 8.35Result of Listing 8.35.

au_id au_fname au_lname
----- -------- -----------
A07   Paddy    O'Furniture

Listing 8.36 lists the names of the authors who have published a book with publisher P03 (Schadenfreude Press). The join to the table authors is necessary to include the authors’ names (not just their IDs) in the result. See Figure 8.36 for the result.

Listing 8.36List the names of the authors who have published a book with publisher P03. See Figure 8.36 for the result.

SELECT DISTINCT a.au_id, au_fname, au_lname
  FROM title_authors ta
  INNER JOIN authors a
    ON ta.au_id = a.au_id
  WHERE title_id IN
    (SELECT title_id
       FROM titles
       WHERE pub_id = 'P03');

Figure 8.36Result of Listing 8.36.

au_id au_fname au_lname
----- -------- ---------
A01   Sarah    Buchman
A02   Wendy    Heydemark
A04   Klee     Hull

A subquery can itself include one or more subqueries. Listing 8.37 lists the names of authors who have participated in writing at least one biography. The innermost query returns the title IDs T06, T07, T10, and T12. The DBMS evaluates the subquery at the next higher level by using these title IDs and returns the author IDs. Finally, the outermost query uses the author IDs to find the names of the authors. See Figure 8.37 for the result.

Listing 8.37List the names of authors who have participated in writing at least one biography. See Figure 8.37 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE au_id IN
    (SELECT au_id
       FROM title_authors
       WHERE title_id IN
         (SELECT title_id
            FROM titles
            WHERE type = 'biography'));

Figure 8.37Result of Listing 8.37.

au_id au_fname au_lname
----- -------- ---------
A02   Wendy    Heydemark
A04   Klee     Hull

Excessive subquery nesting makes a statement hard to read; often, it’s easier to restate the query as a join. Here’s the join version of Listing 8.37:

SELECT DISTINCT a.au_id, au_fname, au_lname
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  INNER JOIN titles t
    ON t.title_id = ta.title_id
  WHERE type = 'biography';

Listing 8.38 lists the names of all non-lead authors (au_order > 1) who live in California and who receive less than 50 percent of the royalties for a book. See Figure 8.38 for the result.

Listing 8.38List the names of all ancillary authors who live in California and who receive less than 50 percent of the royalties for a book. See Figure 8.38 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors
  WHERE state = 'CA'
    AND au_id IN
      (SELECT au_id
         FROM title_authors
         WHERE royalty_share < 0.5
           AND au_order > 1);

Figure 8.38Result of Listing 8.38.

au_id au_fname au_lname
----- -------- --------
A03   Hallie   Hull
A04   Klee     Hull

Here’s the join version of Listing 8.38:

SELECT DISTINCT a.au_id, au_fname, au_lname
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  WHERE state = 'CA'
    AND royalty_share < 0.5
    AND au_order > 1;

Listing 8.39 lists the names of authors who are coauthors of a book. To determine whether an author is a coauthor or the sole author of a book, examine his or her royalty share for the book. If the royalty share is less than 100 percent (1.0), then the author is a coauthor; otherwise, he or she is the sole author. See Figure 8.39 for the result.

Listing 8.39List the names of authors who are coauthors of a book. See Figure 8.39 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors a
  WHERE au_id IN
    (SELECT au_id
       FROM title_authors
       WHERE royalty_share < 1.0);

Figure 8.39Result of Listing 8.39.

au_id au_fname au_lname
----- -------- ---------
A02   Wendy    Heydemark
A03   Hallie   Hull
A04   Klee     Hull
A06            Kellsey

Listing 8.40 uses a correlated subquery to list the names of authors who are sole authors of a book—that is, authors who earn 100 percent (1.0) of the royalty on a book. See Figure 8.40 for the result. The DBMS considers each row in the outer-query table authors to be a candidate for inclusion in the result. When the DBMS examines the first candidate row in authors, it sets the correlation variable a.au_id equal to A01 (Sarah Buchman), which it substitutes into the inner query:

SELECT royalty_share
  FROM title_authors ta
  WHERE ta.au_id = 'A01';

The inner query returns 1.0, so the outer query evaluates to:

SELECT a.au_id, au_fname, au_lname
  FROM authors a
  WHERE 1.0 IN (1.0)

The WHERE condition is true, so author A01 is included in the result. The DBMS repeats this procedure for every author; see “Simple and Correlated Subqueries” earlier in this chapter.

Listing 8.40List the names of authors who are sole authors of a book. See Figure 8.40 for the result.

SELECT a.au_id, au_fname, au_lname
  FROM authors a
  WHERE 1.0 IN
    (SELECT royalty_share
       FROM title_authors ta
       WHERE ta.au_id = a.au_id);

Figure 8.40Result of Listing 8.40.

au_id au_fname  au_lname
----- --------- ---------
A01   Sarah     Buchman
A02   Wendy     Heydemark
A04   Klee      Hull
A05   Christian Kells
A06             Kellsey

Listing 8.41 lists the names of authors who are both coauthors and sole authors. The inner query returns the author IDs of sole authors, and the outer query compares these IDs with the IDs of the coauthors. See Figure 8.41 for the result.

Listing 8.41List the names of authors who are both coauthors and sole authors. See Figure 8.41 for the result.

SELECT DISTINCT a.au_id, au_fname, au_lname
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  WHERE ta.royalty_share < 1.0
    AND a.au_id IN
      (SELECT a.au_id
         FROM authors a
         INNER JOIN title_authors ta
           ON a.au_id = ta.au_id
         AND ta.royalty_share = 1.0);

Figure 8.41Result of Listing 8.41.

au_id au_fname au_lname
----- -------- ---------
A02   Wendy    Heydemark
A04   Klee     Hull
A06            Kellsey

You can rewrite Listing 8.41 as a join or as an intersection. Here’s the join version of Listing 8.41:

SELECT DISTINCT a.au_id, au_fname, au_lname
  FROM authors a
  INNER JOIN title_authors ta1
    ON a.au_id = ta1.au_id
  INNER JOIN title_authors ta2
    ON a.au_id = ta2.au_id
  WHERE ta1.royalty_share < 1.0
    AND ta2.royalty_share = 1.0;

Here’s the intersection version of Listing 8.41 (see “Finding Common Rows with INTERSECT” in Chapter 9):

SELECT DISTINCT a.au_id, au_fname, au_lname
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  WHERE ta.royalty_share < 1.0
INTERSECT
SELECT DISTINCT a.au_id, au_fname, au_lname
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id
  WHERE ta.royalty_share = 1.0;

Listing 8.42 uses a correlated subquery to list the types of books published by more than one publisher. See Figure 8.42 for the result.

Listing 8.42List the types of books common to more than one publisher. See Figure 8.42 for the result.

SELECT DISTINCT t1.type
  FROM titles t1
  WHERE t1.type IN
    (SELECT t2.type
       FROM titles t2
       WHERE t1.pub_id <> t2.pub_id);

Figure 8.42Result of Listing 8.42.

type
---------
biography
history

Here’s the self-join version of Listing 8.42:

SELECT DISTINCT t1.type
  FROM titles t1
  INNER JOIN titles t2
    ON t1.type = t2.type
    AND t1.pub_id <> t2.pub_id;

Tips for IN

Comparing All Subquery Values with ALL

You can use the ALL keyword to determine whether a value is less than or greater than all the values in a subquery result.

The important characteristics of subquery comparisons that use ALL are:

To compare all subquery values:

Listing 8.43 lists the authors who live in a city in which no publisher is located. The inner query finds all the cities in which publishers are located, and the outer query compares each author’s city to all the publishers’ cities. See Figure 8.43 for the result.

Listing 8.43List the authors who live in a city in which no publisher is located. See Figure 8.43 for the result.

SELECT au_id, au_lname, au_fname, city
  FROM authors
  WHERE city <> ALL
    (SELECT city
       FROM publishers);

Figure 8.43Result of Listing 8.43.

au_id au_lname    au_fname city
----- ----------- -------- ---------
A01   Buchman     Sarah    Bronx
A02   Heydemark   Wendy    Boulder
A06   Kellsey              Palo Alto
A07   O'Furniture Paddy    Sarasota

You can use NOT IN to replicate Listing 8.43:

SELECT au_id, au_lname, au_fname, city
  FROM authors
  WHERE city NOT IN
    (SELECT city FROM publishers);

Listing 8.44 lists the nonbiographies that are priced less than all the biographies. The inner query finds all the biography prices. The outer query inspects the lowest price in the list and determines whether each non-biography is cheaper. See Figure 8.44 for the result. The price IS NOT NULL condition is required because the price of biography T10 is null. Without this condition, the entire query would return zero rows because it’s impossible to determine whether a price is less than null (see “Nulls” in Chapter 3).

Listing 8.44List the nonbiographies that are cheaper than all the biographies. See Figure 8.44 for the result.

SELECT title_id, title_name
  FROM titles
  WHERE type <> 'biography'
    AND price < ALL
    (SELECT price
       FROM titles
       WHERE type = 'biography'
         AND price IS NOT NULL);

Figure 8.44Result of Listing 8.44.

title_id title_name
-------- --------------------------------
T05      Exchange of Platitudes
T08      Just Wait Until After School
T11      Perhaps It's a Glandular Problem

Listing 8.45 lists the books that outsold all the books that author A06 wrote (or cowrote). The inner query uses a join to find the sales of each book by author A06. The outer query inspects the highest sales figure in the list and determines whether each book sold more copies. See Figure 8.45 for the result. Again, the IS NOT NULL condition is needed in case sales is null for a book by author A06.

Listing 8.45List the books that outsold all the books that author A06 wrote (or cowrote). See Figure 8.45 for the result.

SELECT title_id, title_name
  FROM titles
  WHERE sales > ALL
    (SELECT sales
       FROM title_authors ta
       INNER JOIN titles t
         ON t.title_id = ta.title_id
       WHERE ta.au_id = 'A06'
         AND sales IS NOT NULL);

Figure 8.45Result of Listing 8.45.

title_id title_name
-------- -------------------------
T05      Exchange of Platitudes
T07      I Blame My Mother
T12      Spontaneous, Not Annoying

I can replicate Listing 8.45 by using GROUP BY, HAVING, and MAX() (instead of ALL):

SELECT title_id
  FROM titles
  GROUP BY title_id
  HAVING MAX(sales) >
    (SELECT MAX(sales)
       FROM title_authors ta
       INNER JOIN titles t
         ON t.title_id = ta.title_id
       WHERE ta.au_id = 'A06');

Listing 8.46 uses a correlated subquery in the HAVING clause of the outer query to list the types of books for which the highest sales figure is more than twice the average sales for that type. The inner query is evaluated once for each group defined in the outer query (once for each type of book). See Figure 8.46 for the result.

Listing 8.46List the types of books for which the highest sales figure is more than twice the average sales for that type. See Figure 8.46 for the result.

SELECT t1.type
  FROM titles t1
  GROUP BY t1.type
  HAVING MAX(t1.sales) >= ALL
    (SELECT 2.0 * AVG(t2.sales)
       FROM titles t2
       WHERE t1.type = t2.type);

Figure 8.46Result of Listing 8.46.

type
---------
biography

Tips for ALL

Comparing Some Subquery Values with ANY

ANY works like ALL (see the preceding section) but instead determines whether a value is equal to, less than, or greater than any (at least one) of the values in a subquery result.

The important characteristics of subquery comparisons that use ANY are:

To compare some subquery values:

Listing 8.47 lists the authors who live in a city in which a publisher is located. The inner query finds all the cities in which publishers are located, and the outer query compares each author’s city to all the publishers’ cities. See Figure 8.47 for the result.

Listing 8.47List the authors who live in a city in which a publisher is located. See Figure 8.47 for the result.

SELECT au_id, au_lname, au_fname, city
  FROM authors
  WHERE city = ANY
    (SELECT city
       FROM publishers);

Figure 8.47Result of Listing 8.47.

au_id au_lname au_fname  city
----- -------- --------- -------------
A03   Hull     Hallie    San Francisco
A04   Hull     Klee      San Francisco
A05   Kells    Christian New York

You can use IN to replicate Listing 8.47:

SELECT au_id, au_lname, au_fname, city
FROM authors
  WHERE city IN
    (SELECT city FROM publishers);

Listing 8.48 lists the nonbiographies that are priced less than at least one biography. The inner query finds all the biography prices. The outer query inspects the highest price in the list and determines whether each nonbiography is cheaper. See Figure 8.48 for the result.

Unlike the ALL comparison in Listing 8.44 in the preceding section, the price IS NOT NULL condition isn’t required here, even though the price of biography T10 is null. The DBMS doesn’t determine whether all the price comparisons are true—just whether at least one is true—so the null comparison is ignored.

Listing 8.48List the nonbiographies that are cheaper than at least one biography. See Figure 8.48 for the result.

SELECT title_id, title_name
  FROM titles
  WHERE type <> 'biography'
    AND price < ANY
      (SELECT price
         FROM titles
         WHERE type = 'biography');

Figure 8.48Result of Listing 8.48.

title_id title_name
-------- --------------------------------
T01      1977!
T02      200 Years of German Humor
T04      But I Did It Unconsciously
T05      Exchange of Platitudes
T08      Just Wait Until After School
T09      Kiss My Boo-Boo
T11      Perhaps It's a Glandular Problem

Listing 8.49 lists the books that outsold at least one of the books that author A06 wrote (or cowrote). The inner query uses a join to find the sales of each book by author A06. The outer query inspects the lowest sales figure in the list and determines whether each book sold more copies. See Figure 8.49 for the result. Again, unlike the ALL comparison in Listing 8.45 in the preceding section, the IS NOT NULL condition isn’t needed here.

Listing 8.49List the books that outsold at least one of the books that author A06 wrote (or cowrote). See Figure 8.49 for the result.

SELECT title_id, title_name
  FROM titles
  WHERE sales > ANY
    (SELECT sales
       FROM title_authors ta
       INNER JOIN titles t
         ON t.title_id = ta.title_id
       WHERE ta.au_id = 'A06');

Figure 8.49Result of Listing 8.49.

title_id title_name
-------- -----------------------------------
T02      200 Years of German Humor
T03      Ask Your System Administrator
T04      But I Did It Unconsciously
T05      Exchange of Platitudes
T06      How About Never?
T07      I Blame My Mother
T09      Kiss My Boo-Boo
T11      Perhaps It's a Glandular Problem
T12      Spontaneous, Not Annoying
T13      What Are The Civilian Applications?

I can replicate Listing 8.49 by using GROUP BY, HAVING, and MIN() (instead of ANY):

SELECT title_id
  FROM titles
  GROUP BY title_id
  HAVING MIN(sales) >
    (SELECT MIN(sales)
       FROM title_authors ta
       INNER JOIN titles t
         ON t.title_id = ta.title_id
       WHERE ta.au_id = 'A06');

Tips for ANY

Testing Existence with EXISTS

So far in this chapter, I’ve been using the comparison operators IN, ALL, and ANY to compare a specific test value to values in a subquery result. EXISTS and NOT EXISTS don’t compare values; rather, they simply look for the existence or nonexistence of rows in a subquery result.

The important characteristics of an existence test are:

To test existence:

Listing 8.50 lists the names of the publishers that have published biographies. This query considers each publisher’s ID in turn and determines whether it causes the existence test to evaluate to true. Here, the first publisher is P01 (Abatis Publishers). The DBMS ascertains whether any rows exist in the table titles in which pub_id is P01 and type is biography. If so, then Abatis Publishers is included in the final result. The DBMS repeats the same process for each of the other publisher IDs. See Figure 8.50 for the result. If I wanted to list the names of publishers that haven’t published biographies, then I’d change EXISTS to NOT EXISTS. See Listing 8.33 earlier in this chapter for an equivalent query that uses IN.

Listing 8.50List the names of the publishers that have published biographies. See Figure 8.50 for the result.

SELECT pub_name
  FROM publishers p
  WHERE EXISTS
    (SELECT *
       FROM titles t
       WHERE t.pub_id = p.pub_id
         AND type = 'biography');

Figure 8.50Result of Listing 8.50.

pub_name
-------------------
Abatis Publishers
Schadenfreude Press

Listing 8.51 lists the authors who haven’t written (or cowritten) a book. See Figure 8.51 for the result. See Listing 8.35 earlier in this chapter for an equivalent query that uses NOT IN.

Listing 8.51List the authors who haven’t written (or cowritten) a book. See Figure 8.51 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors a
  WHERE NOT EXISTS
    (SELECT *
       FROM title_authors ta
       WHERE ta.au_id = a.au_id);

Figure 8.51Result of Listing 8.51.

au_id au_fname  au_lname
----- --------- -----------
A07   Paddy     O'Furniture

Listing 8.52 lists the authors who live in a city in which a publisher is located. See Figure 8.52 for the result. See Listing 8.47 earlier in this chapter for an equivalent query that uses =ANY.

Listing 8.52List the authors who live in a city in which a publisher is located. See Figure 8.52 for the result.

SELECT au_id, au_lname, au_fname, city
  FROM authors a
  WHERE EXISTS
    (SELECT *
       FROM publishers p
       WHERE p.city = a.city);

Figure 8.52Result of Listing 8.52.

au_id au_lname au_fname  city
----- -------- --------- -------------
A03   Hull     Hallie    San Francisco
A04   Hull     Klee      San Francisco
A05   Kells    Christian New York

“Finding Common Rows with INTERSECT” in Chapter 9 describes how to use INTERSECT to retrieve the rows that two tables have in common. You also can use EXISTS to find an intersection. Listing 8.53 lists the cities in which both an author and publisher are located. See Figure 8.53 for the result. See Listing 9.8 in Chapter 9 for an equivalent query that uses INTERSECT.

You also can replicate this query with an inner join:

SELECT DISTINCT a.city
  FROM authors a
  INNER JOIN publishers p
    ON a.city = p.city;

Listing 8.53List the cities in which both an author and publisher are located. See Figure 8.53 for the result.

SELECT DISTINCT city
  FROM authors a
  WHERE EXISTS
    (SELECT *
       FROM publishers p
       WHERE p.city = a.city);

Figure 8.53Result of Listing 8.53.

city
-------------
New York
San Francisco

“Finding Different Rows with EXCEPT” in Chapter 9 describes how to use EXCEPT to retrieve the rows in one table that aren’t also in another table. You also can use NOT EXISTS to find a difference. Listing 8.54 lists the cities in which an author lives but a publisher isn’t located. See Figure 8.54 for the result. See Listing 9.9 in Chapter 9 for an equivalent query that uses EXCEPT.

You also can replicate this query with NOT IN:

SELECT DISTINCT city
  FROM authors
  WHERE city NOT IN
    (SELECT city
       FROM publishers);

Or with an outer join:

SELECT DISTINCT a.city
  FROM authors a
  LEFT OUTER JOIN publishers p
    ON a.city = p.city
  WHERE p.city IS NULL;

Listing 8.54List the cities in which an author lives but a publisher isn’t located. See Figure 8.54 for the result.

SELECT DISTINCT city
  FROM authors a
  WHERE NOT EXISTS
    (SELECT *
       FROM publishers p
       WHERE p.city = a.city);

Figure 8.54Result of Listing 8.54.

city
---------
Boulder
Bronx
Palo Alto
Sarasota

Listing 8.55 lists the authors who wrote (or cowrote) three or more books. See Figure 8.55 for the result.

Listing 8.55List the authors who wrote (or cowrote) three or more books. See Figure 8.55 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors a
  WHERE EXISTS
    (SELECT *
       FROM title_authors ta
       WHERE ta.au_id = a.au_id
       HAVING COUNT(*) >= 3);

Figure 8.55Result of Listing 8.55.

au_id au_fname au_lname
----- -------- ---------
A01   Sarah    Buchman
A02   Wendy    Heydemark
A04   Klee     Hull
A06            Kellsey

Listing 8.56 uses two existence tests to list the authors who wrote (or cowrote) both children’s and psychology books. See Figure 8.56 for the result.

Listing 8.56List the authors who wrote (or cowrote) a children’s book and also wrote (or cowrote) a psychology book. See Figure 8.56 for the result.

SELECT au_id, au_fname, au_lname
  FROM authors a
  WHERE EXISTS
    (SELECT *
       FROM title_authors ta
       INNER JOIN titles t
         ON t.title_id = ta.title_id
       WHERE ta.au_id = a.au_id
         AND t.type = 'children')
  AND EXISTS
    (SELECT *
       FROM title_authors ta
       INNER JOIN titles t
         ON t.title_id = ta.title_id
       WHERE ta.au_id = a.au_id
         AND t.type = 'psychology');

Figure 8.56Result of Listing 8.56.

au_id au_fname au_lname
----- -------- --------
A06            Kellsey

Listing 8.57 performs a uniqueness test to determine whether duplicates occur in the column au_id in the table authors. The query prints Yes if duplicate values exist in the column au_id; otherwise, it returns an empty result. See Figure 8.57 for the result. au_id is the primary key of authors, so of course it contains no duplicates.

Listing 8.57Does the column au_id in the table authors contain duplicate values? See Figure 8.57 for the result.

SELECT DISTINCT 'Yes' AS "Duplicates?"
  WHERE EXISTS
    (SELECT *
       FROM authors
       GROUP BY au_id
       HAVING COUNT(*) > 1);

Figure 8.57Result of Listing 8.57.

Duplicates?
-----------

Listing 8.58 shows the same query for the table title_authors, which does contain duplicate au_id values. See Figure 8.58 for the result. You can add grouping columns to the GROUP BY clause to determine whether multiple-column duplicates exist.

Listing 8.58Does the column au_id in the table title_authors contain duplicate values? See Figure 8.58 for the result.

SELECT DISTINCT 'Yes' AS "Duplicates?"
  WHERE EXISTS
    (SELECT *
       FROM title_authors
       GROUP BY au_id
       HAVING COUNT(*) > 1);

Figure 8.58Result of Listing 8.58.

Duplicates?
-----------
Yes

Tips for EXISTS

Comparing Equivalent Queries

As you’ve seen in this chapter and the preceding one, you can express the same query in different ways (different syntax, same semantics). To expand on this point, I’ve written the same query six semantically equivalent ways. Each of the statements in Listing 8.60 lists the authors who have written (or cowritten) at least one book. See Figure 8.60 for the result.

Listing 8.60These six queries are equivalent semantically; they all list the authors who have written (or cowritten) at least one book. See Figure 8.60 for the result.

SELECT DISTINCT a.au_id
  FROM authors a
  INNER JOIN title_authors ta
    ON a.au_id = ta.au_id;

SELECT DISTINCT a.au_id
  FROM authors a, title_authors ta
  WHERE a.au_id = ta.au_id;

SELECT au_id
  FROM authors a
  WHERE au_id IN
    (SELECT au_id
       FROM title_authors);

SELECT au_id
  FROM authors a
  WHERE au_id = ANY
    (SELECT au_id
       FROM title_authors);

SELECT au_id
  FROM authors a
  WHERE EXISTS
    (SELECT *
       FROM title_authors ta
       WHERE a.au_id = ta.au_id);

SELECT au_id
  FROM authors a
  WHERE 0 <
    (SELECT COUNT(*)
       FROM title_authors ta
       WHERE a.au_id = ta.au_id);

Figure 8.60Each of the six statements in Listing 8.60 returns this result.

au_id
-----
A01
A02
A03
A04
A05
A06

The first two queries (inner joins) will run at the same speed as one another. Of the third through sixth queries (which use subqueries), the last one probably is the worst performer. The DBMS will stop processing the other subqueries as soon as it encounters a single matching value. But the subquery in the last statement has to count all the matching rows before it returns either true or false. Your DBMS’s optimizer should run the inner joins at about the same speed as the fastest subquery statement.

You might find this programming flexibility to be attractive, but people who design DBMS optimizers don’t, because they’re tasked with considering all the possible ways to express a query, figuring out which one performs best, and reformulating your query internally to its optimal form. (Entire careers are devoted to solving these types of optimization problems.) If your DBMS has a flawless optimizer, then it will run all six of the queries in Listing 8.60 at the same speed. But that situation is unlikely, so you’ll have to experiment with your DBMS to see which version runs fastest.

Tips for Comparing Equivalent Queries

SQL Tuning

After you learn the basics of SQL, your next step is to tune your SQL statements so that they run efficiently, which means learning about your DBMS’s optimizer. Performance tuning involves some platform-independent general principles, but the most effective tuning relies on the idiosyncrasies of the specific DBMS. Tuning is beyond the scope of this book, but the internet has plenty of discussion groups and articles—search for tuning (or performance or optimization) together with the name of your DBMS.

A good book to get started with is Peter Gulutzan and Trudy Pelzer’s SQL Performance Tuning, which covers eight DBMSs, or Dan Tow’s SQL Tuning, which covers Microsoft SQL Server, Oracle, and Db2. If you look up one of these books on Amazon.com, then you can find other tuning books in the “customers also viewed” list.

9. Set Operations

Recall from Chapter 2 that set theory is fundamental to the relational model. But whereas mathematical sets are unchanging, database sets are dynamic—they grow, shrink, and otherwise change over time. This chapter covers the following SQL set operators, which combine the results of two SELECT statements into one result:

These set operations aren’t joins, but you can mix and chain them to combine two or more tables.

Combining Rows with UNION

A UNION operation combines the results of two queries into a single result that has the rows returned by both queries. (This operation differs from a join, which combines columns from two tables.) A UNION expression removes duplicate rows from the result; a UNION ALL expression doesn’t remove duplicates.

Unions are simple, but they have some restrictions:

To combine rows:

Listing 9.1 lists the states where authors and publishers are located. By default, UNION removes duplicate rows from the result. See Figure 9.1 for the result.

Listing 9.1List the states where authors and publishers are located. See Figure 9.1 for the result.

SELECT state FROM authors
UNION
SELECT state FROM publishers;

Figure 9.1Result of Listing 9.1.

state
-----
NULL
CA
CO
FL
NY

Listing 9.2 is the same as Listing 9.1 except that it includes the ALL keyword, so all rows are included in the results, and duplicates aren’t removed. See Figure 9.2 for the result.

Listing 9.2List the states where authors and publishers are located, including duplicates. See Figure 9.2 for the result.

SELECT state FROM authors
UNION ALL
SELECT state FROM publishers;

Figure 9.2Result of Listing 9.2.

state
-----
NY
CO
CA
CA
NY
CA
FL
NY
CA
NULL
CA

Listing 9.3 lists the names of all the authors and publishers. The AS clause in the first query names the column in the result. The ORDER BY clause uses a relative column position instead of a column name to sort the result. See Figure 9.3 for the result.

Listing 9.3List the names of all the authors and publishers. See Figure 9.3 for the result.

SELECT au_fname || ' ' || au_lname AS "Name"
  FROM authors
UNION
SELECT pub_name
  FROM publishers
  ORDER BY 1 ASC;

Figure 9.3Result of Listing 9.3.

Name
-------------------
 Kellsey
Abatis Publishers
Christian Kells
Core Dump Books
Hallie Hull
Klee Hull
Paddy O'Furniture
Sarah Buchman
Schadenfreude Press
Tenterhooks Press
Wendy Heydemark

Listing 9.4 expands on Listing 9.3 and defines the extra column Type to identify which table each row came from. The WHERE conditions retrieve the authors and publishers from New York state only. See Figure 9.4 for the result.

Listing 9.4List the names of all the authors and publishers located in New York state, sorted by type and then by name. See Figure 9.4 for the result.

SELECT
    'author' AS "Type",
    au_fname || ' ' || au_lname AS "Name",
    state
  FROM authors
  WHERE state = 'NY'
UNION
SELECT
    'publisher',
    pub_name,
    state
  FROM publishers
  WHERE state = 'NY'
  ORDER BY 1 ASC, 2 ASC;

Figure 9.4Result of Listing 9.4.

Type      Name              state
--------- ----------------- -----
author    Christian Kells   NY
author    Sarah Buchman     NY
publisher Abatis Publishers NY

Listing 9.5 adds a third query to Listing 9.4 to retrieve the titles of books published in New York state also. See Figure 9.5 for the result.

Listing 9.5List the names of all the authors and publishers located in New York state and the titles of books published in New York state, sorted by type and then by name. See Figure 9.5 for the result.

SELECT
    'author' AS "Type",
    au_fname || ' ' || au_lname AS "Name"
  FROM authors
  WHERE state = 'NY'
UNION
SELECT
    'publisher',
    pub_name
  FROM publishers
  WHERE state = 'NY'
UNION
SELECT
    'title',
    title_name
  FROM titles t
  INNER JOIN publishers p
    ON t.pub_id = p.pub_id
  WHERE p.state = 'NY'
  ORDER BY 1 ASC, 2 ASC;

Figure 9.5Result of Listing 9.5.

Type      Name
--------- --------------------------
author    Christian Kells
author    Sarah Buchman
publisher Abatis Publishers
title     1977!
title     How About Never?
title     Not Without My Faberge Egg
title     Spontaneous, Not Annoying

Listing 9.6 is similar to Listing 9.5 except that it lists the counts of each author, publisher, and book, instead of their names. See Figure 9.6 for the result.

Listing 9.6List the counts of all the authors and publishers located in New York state and the titles of books published in New York state, sorted by type. See Figure 9.6 for the result.

SELECT
    'author' AS "Type",
    COUNT(au_id) AS "Count"
  FROM authors
  WHERE state = 'NY'
UNION
SELECT
    'publisher',
    COUNT(pub_id)
  FROM publishers
  WHERE state = 'NY'
UNION
SELECT
    'title',
    COUNT(title_id)
  FROM titles t
  INNER JOIN publishers p
    ON t.pub_id = p.pub_id
  WHERE p.state = 'NY'
  ORDER BY 1 ASC;

Figure 9.6Result of Listing 9.6.

Type      Count
--------- -----
author        2
publisher     1
title         4

In Listing 9.7, I revisit Listing 5.30 in “Evaluating Conditional Values with CASE” in Chapter 5. But instead of using CASE to change book prices and simulate if-then logic, I use multiple UNION queries. See Figure 9.7 for the result.

Listing 9.7Raise the price of history books by 10 percent and psychology books by 20 percent, and leave the prices of other books unchanged. See Figure 9.7 for the result.

SELECT title_id, type, price,
    price * 1.10 AS "New price"
  FROM titles
  WHERE type = 'history'
UNION
SELECT title_id, type, price, price * 1.20
  FROM titles
  WHERE type = 'psychology'
UNION
SELECT title_id, type, price, price
  FROM titles
  WHERE type NOT IN ('psychology', 'history')
  ORDER BY type ASC, title_id ASC;

Figure 9.7Result of Listing 9.7.

title_id type       price   New price
-------- ---------- ------- ---------
T06      biography    19.95     19.95
T07      biography    23.95     23.95
T10      biography     NULL      NULL
T12      biography    12.99     12.99
T08      children     10.00     10.00
T09      children     13.95     13.95
T03      computer     39.95     39.95
T01      history      21.99     24.19
T02      history      19.95     21.95
T13      history      29.99     32.99
T04      psychology   12.99     15.59
T05      psychology    6.95      8.34
T11      psychology    7.99      9.59

UNION Commutativity

In theory, the order in which the SELECT queries (tables) occur in a union should make no speed difference. But in practice your DBMS might run

small_table1
UNION
small_table2
UNION
big_table;

faster than

small_table1
UNION
big_table
UNION
small_table2;

because of the way the optimizer merges intermediate results and removes duplicate rows. Experiment.

Tips for UNION

Finding Common Rows with INTERSECT

An INTERSECT operation combines the results of two queries into a single result that has all the rows common to both queries. Intersections have the same restrictions as unions; see “Combining Rows with UNION” earlier in this chapter.

To find common rows:

Listing 9.8 uses INTERSECT to list the cities in which both an author and a publisher are located. See Figure 9.8 for the result.

Listing 9.8List the cities in which both an author and a publisher are located. See Figure 9.8 for the result.

SELECT city
  FROM authors
INTERSECT
SELECT city
  FROM publishers;

Figure 9.8Result of Listing 9.8.

city
-------------
New York
San Francisco

Tips for INTERSECT

Finding Different Rows with EXCEPT

An EXCEPT operation, also called a difference, combines the results of two queries into a single result that has the rows that belong to only the first query. To contrast INTERSECT and EXCEPT, A INTERSECT B contains rows from table A that are duplicated in table B, whereas A EXCEPT B contains rows from table A that aren’t duplicated in table B. Differences have the same restrictions as unions; see “Combining Rows with UNION” earlier in this chapter.

To find different rows:

Listing 9.9 uses EXCEPT to list the cities in which an author lives but a publisher isn’t located. See Figure 9.9 for the result.

Listing 9.9List the cities in which an author lives but a publisher isn’t located. See Figure 9.9 for the result.

SELECT city
  FROM authors
EXCEPT
SELECT city
  FROM publishers;

Figure 9.9Result of Listing 9.9.

city
---------
Boulder
Bronx
Palo Alto
Sarasota

Tips for EXCEPT

10. Inserting, Updating, and Deleting Rows

To this point, I’ve explained how to use SELECT to retrieve and analyze the data in tables. In this chapter, I’ll explain how to use SQL statements to modify table data:

These statements don’t return a result, but your DBMS normally will print a message indicating whether the statement ran successfully and, if so, the number of rows affected by the change. To see the actual effect the statement had on a table, use a SELECT statement.

Unlike SELECT, which only accesses data, these statements change data, so your database administrator might need to grant you permission to run them.

Displaying Table Definitions

To use INSERT, UPDATE, or DELETE, you must know about the columns of the table whose data you’re modifying, including:

Table definitions of the sample-database tables are given in “The Sample Database” in Chapter 2, but you can get the same information by using DBMS tools that describe database objects. This section explains how to use those tools to display table definitions for the current database.

To display table definitions in Microsoft Access:

To display table definitions in Microsoft SQL Server:

  1. Start SQL Server Management Studio or the interactive sqlcmd command-line tool (see “Microsoft SQL Server” in Chapter 1).

    The sqlcmd command displays pages that speed by. It’s easier to use the graphical tools and choose Query > Results to Grid.

  2. Type sp_help table.

    table is a table name.

  3. In SQL Server Management Studio, choose Query > Execute or press F5 (Figure 10.2).

    or

    In sqlcmd, press Enter, type go, and then press Enter.

    Figure 10.2Displaying a table definition in Microsoft SQL Server. (Click image to enlarge.)

    Screenshot: A table definition in Microsoft SQL Server

To display table definitions in Oracle Database:

  1. Start the interactive sqlplus command-line tool (see “Oracle Database” in Chapter 1).
  2. Type describe table; and then press Enter (Figure 10.3).

    table is a table name.

    Figure 10.3Displaying a table definition in Oracle Database. (Click image to enlarge.)

    Screenshot: A table definition in Oracle Database

To display table definitions in IBM Db2 Database:

  1. Start the db2 command-line processor (see “IBM Db2 Database” in Chapter 1).
  2. Type describe table table; and then press Enter (Figure 10.4).

    table is a table name.

    Figure 10.4Displaying a table definition in IBM Db2 Database. (Click image to enlarge.)

    Screenshot: A table definition in IBM Db2 Database

To display table definitions in MySQL:

  1. Start the interactive mysql command-line tool (see “MySQL” in Chapter 1).
  2. Type describe table; and then press Enter (Figure 10.5).

    table is a table name.

    Figure 10.5Displaying a table definition in MySQL. (Click image to enlarge.)

    Screenshot: A table definition in MySQL

To display table definitions in PostgreSQL:

  1. Start the interactive psql command-line tool (see “PostgreSQL” in Chapter 1).
  2. Type \d table and then press Enter (Figure 10.6).

    table is a table name. Note that you don’t terminate this command with a semicolon.

    Figure 10.6Displaying a table definition in PostgreSQL. (Click image to enlarge.)

    Screenshot: A table definition in PostgreSQL

Tips for Table Definitions

Inserting Rows with INSERT

The INSERT statement adds new rows to a table. This section explains how to use several variations of INSERT to:

The important characteristics of INSERT are:

To insert a row by using column positions:

Listing 10.1This INSERT statement adds a new row to the table authors by listing values in the same order in which columns are defined in authors. See Figure 10.7 for the result.

INSERT INTO authors
  VALUES(
    'A08',
    'Michael',
    'Polk',
    '512-953-1231',
    '4028 Guadalupe St',
    'Austin',
    'TX',
    '78701');

To insert a row by using column names:

It’s clearer to list column names in the same order as they appear in the table (Listing 10.2), but you can list them in any order (Listing 10.3). In either case, the values in the VALUES clause must match the sequence in which you list the column names.

Listing 10.2This INSERT statement adds a new row to the table authors by listing column names and values in the same order in which columns are defined in authors. See Figure 10.7 for the result.

INSERT INTO authors(
    au_id,
    au_fname,
    au_lname,
    phone,
    address,
    city,
    state,
    zip)
  VALUES(
    'A09',
    'Irene',
    'Bell',
    '415-225-4689',
    '810 Throckmorton Ave',
    'Mill Valley',
    'CA',
    '94941');

Listing 10.3You don’t have to list column names in the same order in which they’re defined in the table. Here, I’ve rearranged the column names and their corresponding values. See Figure 10.7 for the result.

INSERT INTO authors(
    zip,
    phone,
    address,
    au_lname,
    au_fname,
    state,
    au_id,
    city)
  VALUES(
    '60614',
    '312-998-0020',
    '1937 N. Clark St',
    'Weston',
    'Dianne',
    'IL',
    'A10',
    'Chicago');

You can omit column names if you want to provide values for only some columns explicitly (Listing 10.4). If you omit a column, then the DBMS must be able to provide a value based on the column’s definition. The DBMS will insert the column’s default value (if defined) or null (if allowed). If you omit a column that doesn’t have a default value or allow nulls, then the DBMS will display an error message and won’t insert the row. In this case, the VALUES clause is equivalent to VALUES('A11', 'Max', 'Allard', '212-502-0955', NULL, NULL, NULL, NULL). For information about specifying a default value and allowing nulls, see “Specifying a Default Value with DEFAULT” and “Forbidding Nulls with NOT NULL” in Chapter 11.

Listing 10.4Here, I’ve added a row for a new author but omitted column names and values for the author’s address information. The DBMS inserts nulls into the omitted columns automatically. See Figure 10.7 for the result.

INSERT INTO authors(
    au_id,
    au_fname,
    au_lname,
    phone)
  VALUES(
    'A11',
    'Max',
    'Allard',
    '212-502-0955');

Figure 10.7 shows the new rows in table authors after Listings 10.1 through 10.4 have run.

Figure 10.7The table authors has four new rows after I run Listings 10.1 through 10.4.

au_id au_fname  au_lname    phone        address              city          state zip
----- --------- ----------- ------------ -------------------- ------------- ----- -----
A01   Sarah     Buchman     718-496-7223 75 West 205 St       Bronx         NY    10468
A02   Wendy     Heydemark   303-986-7020 2922 Baseline Rd     Boulder       CO    80303
A03   Hallie    Hull        415-549-4278 3800 Waldo Ave, #14F San Francisco CA    94123
A04   Klee      Hull        415-549-4278 3800 Waldo Ave, #14F San Francisco CA    94123
A05   Christian Kells       212-771-4680 114 Horatio St       New York      NY    10014
A06             Kellsey     650-836-7128 390 Serra Mall       Palo Alto     CA    94305
A07   Paddy     O'Furniture 941-925-0752 1442 Main St         Sarasota      FL    34236
A08   Michael   Polk        512-953-1231 4028 Guadalupe St    Austin        TX    78701
A09   Irene     Bell        415-225-4689 810 Throckmorton Ave Mill Valley   CA    94941
A10   Dianne    Weston      312-998-0020 1937 N. Clark St     Chicago       IL    60614
A11   Max       Allard      212-502-0955 NULL                 NULL          NULL  NULL

To insert rows from one table into another table:

The remaining examples in this section use the table new_publishers (Figure 10.8), which I created to show how INSERT SELECT works. new_publishers has the same structure as the table publishers and acts only as the source of new rows; it isn’t itself changed by the INSERT operations.

Figure 10.8This table, named new_publishers, is used in Listings 10.5 through 10.7. new_publishers has the same structure as publishers.

pub_id pub_name             city        state country
------ -------------------- ----------- ----- --------------
P05    This is Pizza? Press New York    NY    USA
P06    This is Beer? Press  Toronto     ON    Canada
P07    This is Irony? Press London      NULL  United Kingdom
P08    This is Fame? Press  Los Angeles CA    USA

Listing 10.5 inserts the rows for Los Angeles-based publishers from new_publishers into publishers. Here, I’ve omitted the column list, so the DBMS uses the column positions in publishers rather than column names to insert values. This statement inserts one row into publishers; see Figure 10.9 for the result.

Listing 10.5Insert the rows for Los Angeles-based publishers from new_publishers into publishers. See Figure 10.9 for the result.

INSERT INTO publishers
  SELECT
    pub_id,
    pub_name,
    city,
    state,
    country
  FROM new_publishers
  WHERE city = 'Los Angeles';

Listing 10.6 inserts the rows for non-U.S. publishers from new_publishers into publishers. Here, the column names are the same in both the INSERT and SELECT clauses, but they don’t have to match because the DBMS disregards the names of the columns returned by SELECT and uses their positions instead. This statement inserts two rows into publishers; see Figure 10.9 for the result.

Listing 10.6Insert the rows for non-U.S. publishers from new_publishers into publishers. See Figure 10.9 for the result.

INSERT INTO publishers(
    pub_id,
    pub_name,
    city,
    state,
    country)
  SELECT
    pub_id,
    pub_name,
    city,
    state,
    country
  FROM new_publishers
  WHERE country <> 'USA';

It’s legal for the SELECT query to return an empty result (zero rows). Listing 10.7 inserts the rows for publishers named XXX from new_publishers into publishers. I can use SELECT * instead of listing column names because new_publishers and publishers have the same structure. This statement inserts no rows into publishers because no publisher is named XXX; see Figure 10.9 for the result.

Listing 10.7Insert the rows for publishers named XXX from new_publishers into publishers. This statement has no effect on the target table. See Figure 10.9 for the result.

INSERT INTO publishers(
    pub_id,
    pub_name,
    city,
    state,
    country)
  SELECT *
    FROM new_publishers
    WHERE pub_name = 'XXX';

Figure 10.9 shows the table publishers after Listings 10.5 through 10.7 are run.

Figure 10.9The table publishers has three new rows after I run Listings 10.5 through 10.7.

pub_id pub_name             city          state country
------ -------------------- ------------- ----- --------------
P01    Abatis Publishers    New York      NY    USA
P02    Core Dump Books      San Francisco CA    USA
P03    Schadenfreude Press  Hamburg       NULL  Germany
P04    Tenterhooks Press    Berkeley      CA    USA
P06    This is Beer? Press  Toronto       ON    Canada
P07    This is Irony? Press London        NULL  United Kingdom
P08    This is Fame? Press  Los Angeles   CA    USA

Tips for INSERT

Updating Rows with UPDATE

The UPDATE statement changes the values in a table’s existing rows. You can use UPDATE to change:

To update rows, you specify:

The important characteristics of UPDATE are:

To update rows:

Listing 10.8 changes the value of contract to zero in every row of titles. The lack of a WHERE clause tells the DBMS to update all the rows in the column contract. This statement updates 13 rows; see Figure 10.10 for the result.

Listing 10.8Change the value of contract to zero in every row. See Figure 10.10 for the result.

UPDATE titles
  SET contract = 0;

Listing 10.9 uses an arithmetic expression and a WHERE condition to double the price of history books. This statement updates three rows; see Figure 10.10 for the result.

Listing 10.9Double the price of history books. See Figure 10.10 for the result.

UPDATE titles
  SET price = price * 2.0
  WHERE type = 'history';

Here’s a tricky way to change prices with CASE:

UPDATE titles
  SET price = price * CASE type
    WHEN 'history'    THEN 1.10
    WHEN 'psychology' THEN 1.20
    ELSE 1
  END;

Listing 10.10 updates the columns type and pages for psychology books. You use only a single SET clause to update multiple columns, with column = expr expressions separated by commas. (Don’t put a comma after the last expression.) This statement updates three rows; see Figure 10.10 for the result.

Listing 10.10For psychology books, set the type to self help and the number of pages to null. See Figure 10.10 for the result.

UPDATE titles
  SET type = 'self help',
      pages = NULL
  WHERE type = 'psychology';

Listing 10.11 uses a subquery and an aggregate function to cut the sales of books with above-average sales in half. This statement updates two rows; see Figure 10.10 for the result.

Listing 10.11Cut the sales of books with above-average sales in half. See Figure 10.10 for the result.

UPDATE titles
  SET sales = sales * 0.5
  WHERE sales >
    (SELECT AVG(sales)
       FROM titles);

You can update values in a given table based on the values stored in another table. Listing 10.12 uses nested subqueries to update the publication date for all the books written (or cowritten) by Sarah Buchman. This statement updates three rows; see Figure 10.10 for the result.

Listing 10.12Change the publication date of all of Sarah Buchman’s books to January 1, 2003. See Figure 10.10 for the result.

UPDATE titles
  SET pubdate = DATE '2003-01-01'
  WHERE title_id IN
    (SELECT title_id
       FROM title_authors
       WHERE au_id IN
         (SELECT au_id
            FROM authors
            WHERE au_fname = 'Sarah'
              AND au_lname = 'Buchman'));

Suppose that Abatis Publishers (publisher P01) swallows Tenterhooks Press (P04) in a merger, so now, all the Tenterhooks Press books are published by Abatis Publishers. Listing 10.13 works in a bottom-up fashion to change the publisher IDs in titles from P04 to P01. The WHERE subquery retrieves the pub_id for Tenterhooks Press. The DBMS uses this pub_id to retrieve the books in the table titles whose publisher is Tenterhooks Press. Finally, the DBMS uses the value returned by the SET subquery to update the appropriate rows in the table titles. Because the subqueries are used with an unmodified comparison operator, they must be scalar subqueries that return a single value (that is, a one-row, one-column result); see “Comparing a Subquery Value by Using a Comparison Operator” in Chapter 8. Listing 10.13 updates five rows; see Figure 10.10 for the result.

Listing 10.13Change the publisher of all of Tenterhooks Press’s books to Abatis Publishers. See Figure 10.10 for the result.

UPDATE titles
  SET pub_id =
    (SELECT pub_id
       FROM publishers
       WHERE pub_name = 'Abatis Publishers')
  WHERE pub_id =
    (SELECT pub_id
       FROM publishers
       WHERE pub_name = 'Tenterhooks Press');

Figure 10.10 shows the table titles after Listings 10.8 through 10.13 are run. Each listing updates values in a different column (or columns) from those in the other listings. The updated values in each column are highlighted.

Figure 10.10The table titles after I run Listings 10.8 through 10.13. The updated values are highlighted.

title_id title_name                          type      pub_id pages price sales  pubdate    contract
-------- ----------------------------------- --------- ------ ----- ----- ------ ---------- --------
T01      1977!                               history   P01    107   43.98    566 2003-01-01        0
T02      200 Years of German Humor           history   P03     14   39.90   9566 2003-01-01        0
T03      Ask Your System Administrator       computer  P02   1226   39.95  25667 2000-09-01        0
T04      But I Did It Unconsciously          self help P01   NULL   12.99  13001 1999-05-31        0
T05      Exchange of Platitudes              self help P01   NULL    6.95 100720 2001-01-01        0
T06      How About Never?                    biography P01    473   19.95  11320 2000-07-31        0
T07      I Blame My Mother                   biography P03    333   23.95 750100 1999-10-01        0
T08      Just Wait Until After School        children  P01     86   10.00   4095 2001-06-01        0
T09      Kiss My Boo-Boo                     children  P01     22   13.95   5000 2002-05-31        0
T10      Not Without My Faberge Egg          biography P01   NULL    NULL   NULL NULL              0
T11      Perhaps It's a Glandular Problem    self help P01   NULL    7.99  94123 2000-11-30        0
T12      Spontaneous, Not Annoying           biography P01    507   12.99 100001 2000-08-31        0
T13      What Are The Civilian Applications? history   P03    802   59.98  10467 2003-01-01        0

Tips for UPDATE

Deleting Rows with DELETE

The DELETE statement removes rows from a table. You can use DELETE to remove:

To delete rows, you specify:

The important characteristics of DELETE are:

To delete rows:

In the following examples, I’m going to ignore referential-integrity constraints—which I wouldn’t do in a production database, of course.

Listing 10.14 deletes every row in royalties. The lack of a WHERE clause tells the DBMS to delete all the rows. This statement deletes 13 rows; see Figure 10.11 for the result.

Listing 10.14Delete all rows from the table royalties. See Figure 10.11 for the result.

DELETE FROM royalties;

Figure 10.11Result of Listing 10.14.

title_id advance royalty_rate
-------- ------- ------------

The WHERE clause in Listing 10.15 tells the DBMS to remove the authors with the last name Hull from authors. This statement deletes two rows; see Figure 10.12 for the result.

Listing 10.15Delete the rows in which the author’s last name is Hull from the table authors. See Figure 10.12 for the result.

DELETE FROM authors
  WHERE au_lname = 'Hull';

Figure 10.12Result of Listing 10.15.

au_id au_fname  au_lname    phone        address          city       state zip
----- --------- ----------- ------------ ---------------- ---------- ----- -----
A01   Sarah     Buchman     718-496-7223 75 West 205 St   Bronx      NY    10468
A02   Wendy     Heydemark   303-986-7020 2922 Baseline Rd Boulder    CO    80303
A05   Christian Kells       212-771-4680 114 Horatio St   New York   NY    10014
A06             Kellsey     650-836-7128 390 Serra Mall   Palo Alto  CA    94305
A07   Paddy     O'Furniture 941-925-0752 1442 Main St     Sarasota   FL    34236

You can delete rows in a given table based on the values stored in another table. Listing 10.16 uses a subquery to remove all the books published by publishers P01 or P04 from title_authors. This statement deletes 12 rows; see Figure 10.13 for the result.

Listing 10.16Delete the rows for books published by publisher P01 or P04 from the table title_authors. See Figure 10.13 for the result.

DELETE FROM title_authors
  WHERE title_id IN
    (SELECT title_id
       FROM titles
       WHERE pub_id IN ('P01', 'P04'));

Figure 10.13Result of Listing 10.16.

title_id au_id au_order royalty_share
-------- ----- -------- -------------
T02      A01          1          1.00
T03      A05          1          1.00
T07      A02          1          0.50
T07      A04          2          0.50
T13      A01          1          1.00

Tips for DELETE

Truncating Tables

If you want to delete all the rows in a table, then the TRUNCATE statement is faster than DELETE. The SQL:2008 standard introduced TRUNCATE, and Microsoft SQL Server, Oracle, Db2, MySQL, and PostgreSQL support it. TRUNCATE works like a DELETE statement with no WHERE clause: Both remove all rows in a table. But TRUNCATE is faster and uses fewer system resources than DELETE because TRUNCATE doesn’t scan the entire table and record changes in the transaction log (see Chapter 14). The trade-off is that with TRUNCATE, you can’t recover (roll back) your changes if you make a mistake. The syntax is:

TRUNCATE TABLE table;

table is the name of the table to be truncated. For information about TRUNCATE, search your DBMS documentation for truncate.

Older versions of Db2 don’t support TRUNCATE; instead, run LOAD with the REPLACE option, using a zero-byte file as input.

11. Creating, Altering, and Dropping Tables

Many DBMSs have interactive, graphical tools that let you create and manage tables and table properties such as column definitions and constraints. This chapter explains how to perform those tasks programmatically by using SQL:

These statements don’t return a result, but your DBMS might print a message indicating whether the statement ran successfully. To see the actual effect the statement had on a table, examine the table’s structure by using one of the commands described in “Displaying Table Definitions” in Chapter 10.

These statements modify database objects and data, so your database administrator might need to grant you permission to run them.

Creating Tables

Database designers spend considerable time normalizing tables and defining relationships and constraints before they write a line of SQL code. If you’re going to create tables for production databases, then study database design and relational-model principles beyond those presented in Chapter 2.

Recall from “Tables, Columns, and Rows” in Chapter 2 that a database is organized around tables. To a user or an SQL programmer, a database appears to be a collection of one or more tables (and nothing but tables). To create a table, you specify the following:

The table name and the column names must conform to the rules for SQL identifiers; see “Identifiers” in Chapter 3. The data type of each column is a character, numeric, datetime, or other data type; see “Data Types” in Chapter 3. A default is the value the column takes if you don’t specify a value explicitly. Constraints define properties such as nullability, keys, and permissible values.

You create a new table by using the CREATE TABLE statement, whose general syntax is:

CREATE TABLE table
  (
  column1 data_type1 [col_constraints1],
  column2 data_type2 [col_constraints2],
  ...
  columnN data_typeN [col_constraintsN]
  [, table_constraint1]
  [, table_constraint2]
  ...
  [, table_constraintM]
  );

Each column definition has a column name, a data type, and an optional list of one or more column constraints. An optional list of table constraints follows the final column definition. By convention, I start each column definition and table constraint on its own line.

Understanding Constraints

Constraints let you define rules for values allowed in columns (Table 11.1). Your DBMS uses these rules to enforce the integrity of information in the database automatically.

Table 11.1Constraints
Constraint Description
NOT NULL Prevents nulls from being inserted into a column
PRIMARY KEY Sets the table’s primary-key column(s)
FOREIGN KEY Sets the table’s foreign-key column(s)
UNIQUE Prevents duplicate values from being inserted into a column
CHECK Limits the values that can be inserted into a column by using logical (boolean) expressions

Constraints come in two flavors:

You can specify some constraints as either column or table constraints, depending on the context in which they’re used. If a primary key contains one column, for example, then you can define it as either a column constraint or a table constraint. If the primary key has two or more columns, then you must use a table constraint.

Assigning names to constraints lets you manage them efficiently; you can change or delete a named constraint by using the ALTER TABLE statement, for example. Constraint names are optional, but many SQL programmers and database designers name all constraints. It’s not uncommon to leave a NOT NULL constraint unnamed, but you always should name other types of constraints (even if I don’t do so in some of the examples).

If you don’t name a constraint explicitly, then your DBMS will generate a name and assign it to the constraint quietly and automatically. System-assigned names often contain strings of random characters and are cumbersome to use, so use the CONSTRAINT clause to assign your own name instead. Constraint names also appear in warnings, error messages, and logs, which is another good reason to name constraints yourself.

To name a constraint:

Creating a New Table with CREATE TABLE

This section describes how to create a new table by using a minimal CREATE TABLE statement. Subsequent sections show you how to add column and table constraints to CREATE TABLE.

To create a new table:

Listing 11.1 creates the sample-database table titles.

Listing 11.1Create the sample-database table titles.

CREATE TABLE titles
  (
  title_id   CHAR(3)     ,
  title_name VARCHAR(40) ,
  type       VARCHAR(10) ,
  pub_id     CHAR(3)     ,
  pages      INTEGER     ,
  price      DECIMAL(5,2),
  sales      INTEGER     ,
  pubdate    DATE        ,
  contract   SMALLINT
  );

Listing 11.2 creates the sample-database table title_authors.

Listing 11.2Create the sample-database table title_authors.

CREATE TABLE title_authors
  (
  title_id      CHAR(3)     ,
  au_id         CHAR(3)     ,
  au_order      SMALLINT    ,
  royalty_share DECIMAL(5,2)
  );

Tips for CREATE TABLE

Forbidding Nulls with NOT NULL

A column’s nullability determines whether its rows can contain nulls—that is, whether values are required or optional in the column. I described nulls and their effects in “Nulls” in Chapter 3, but I’ll review the basics here:

When you’re defining a nullability constraint, some important considerations are:

To specify a column’s nullability:

Listing 11.3 creates the sample-database table authors, forbidding nulls in some columns. Missing addresses and telephone numbers are common, so I’ve allowed nulls in those columns.

Notice that I’ve forbidden nulls in both the first-name and last-name columns. If the author’s name has only a single word (like author A06, Kellsey), then I’ll insert the name into au_lname and insert an empty string ('') into au_fname. Or I could have allowed nulls in au_fname and inserted a null into au_fname for one-named authors. Or I could have allowed nulls in both au_fname and au_lname and added a check constraint that required at least one of the two columns to contain a non-null, non-empty string. The database designer makes these types of decisions before creating a table.

Listing 11.3Create the sample-database table authors. Where omitted, the nullability constraint defaults to allow nulls.

CREATE TABLE authors
  (
  au_id    CHAR(3)     NOT NULL,
  au_fname VARCHAR(15) NOT NULL,
  au_lname VARCHAR(15) NOT NULL,
  phone    VARCHAR(12)         ,
  address  VARCHAR(20)         ,
  city     VARCHAR(15)         ,
  state    CHAR(2)             ,
  zip      CHAR(5)
  );

Most DBMSs let you specify only the NULL keyword (without the NOT) to allow nulls. Listing 11.4 creates the sample-database table titles.

Listing 11.4Create the sample-database table titles and assign nullability constraints to each column explicitly.

CREATE TABLE titles
  (
  title_id   CHAR(3)      NOT NULL,
  title_name VARCHAR(40)  NOT NULL,
  type       VARCHAR(10)  NULL    ,
  pub_id     CHAR(3)      NOT NULL,
  pages      INTEGER      NULL    ,
  price      DECIMAL(5,2) NULL    ,
  sales      INTEGER      NULL    ,
  pubdate    DATE         NULL    ,
  contract   SMALLINT     NOT NULL
  );

Tips for NOT NULL

Specifying a Default Value with DEFAULT

A default specifies a value that your DBMS assigns to a column if you omit a value for the column when inserting a row; see “Inserting Rows with INSERT” in Chapter 10. When you’re defining a default value, some important considerations are:

To specify a column’s default value:

Listing 11.5 assigns defaults to some of the columns in the sample-database table titles. The columns title_id and pub_id are NOT NULL and have no default values, so you must provide explicit values for them in an INSERT statement. The pages clause DEFAULT NULL is equivalent to omitting the DEFAULT. The pubdate and contract defaults show that the defaults can be expressions more complex than plain literals.

Listing 11.5Set default values for some of the columns in the sample-database table.

CREATE TABLE titles
  (
  title_id   CHAR(3)      NOT NULL                     ,
  title_name VARCHAR(40)  NOT NULL DEFAULT ''          ,
  type       VARCHAR(10)           DEFAULT 'undefined' ,
  pub_id     CHAR(3)      NOT NULL                     ,
  pages      INTEGER               DEFAULT NULL        ,
  price      DECIMAL(5,2) NOT NULL DEFAULT 0.00        ,
  sales      INTEGER                                   ,
  pubdate    DATE                  DEFAULT CURRENT_DATE,
  contract   SMALLINT     NOT NULL DEFAULT (3*7)-21
  );

Listing 11.6 shows the minimal INSERT statement that you can use to insert a row into the table titles (as created by Listing 11.5). Figure 11.1 shows the inserted row, with default values highlighted. The title_name default, an empty string (''), is invisible.

Listing 11.6The DBMS inserts default values into columns omitted from this INSERT statement. Where no default is specified, the DBMS inserts a null. See Figure 11.1 for the result.

INSERT INTO titles(title_id, pub_id)
  VALUES('T14','P01');

Figure 11.1Listing 11.6 inserts this row into the table titles.

title_id title_name    type       pub_id pages price sales pubdate    contract
-------- ------------- ---------- ------ ----- ----- ----- ---------- --------
T14                    undefined  P01     NULL  0.00  NULL 2005-02-21        0

Tips for DEFAULT

Listing 11.7In Oracle, the default clause must come before all column constraints.

CREATE TABLE titles
  (
  title_id   CHAR(3)                          NOT NULL,
  title_name VARCHAR(40)  DEFAULT ' '         NOT NULL,
  type       VARCHAR(10)  DEFAULT 'undefined'         ,
  pub_id     CHAR(3)                          NOT NULL,
  pages      INTEGER      DEFAULT NULL                ,
  price      DECIMAL(5,2) DEFAULT 0.00        NOT NULL,
  sales      INTEGER                                  ,
  pubdate    DATE         DEFAULT SYSDATE             ,
  contract   SMALLINT     DEFAULT (3*7)-21    NOT NULL
  );

Specifying a Primary Key with PRIMARY KEY

I described primary keys in “Primary Keys” in Chapter 2, but I’ll review the basics here:

When you’re defining a primary-key constraint, some important considerations are:

To specify a simple primary key:

Listings 11.8a, 11.8b, and 11.8c show three equivalent ways to define a simple primary key for the sample-database table publishers.

Listing 11.8a uses a column constraint to designate the primary-key column. This syntax shows the easiest way to create a simple primary key.

Listing 11.8aDefine a simple primary key for the sample-database table publishers by using a column constraint.

CREATE TABLE publishers
  (
  pub_id   CHAR(3)     PRIMARY KEY,
  pub_name VARCHAR(20) NOT NULL   ,
  city     VARCHAR(15) NOT NULL   ,
  state    CHAR(2)                ,
  country  VARCHAR(15) NOT NULL
  );

Listing 11.8b uses an unnamed table constraint to specify the primary key. I’ve added an explicit NOT NULL column constraint to pub_id, but it’s unnecessary because the DBMS sets this constraint implicitly and silently (except for Db2; see the DBMS tip in “Tips for PRIMARY KEY” in this section).

Listing 11.8bDefine a simple primary key for the sample-database table publishers by using an unnamed table constraint.

CREATE TABLE publishers
  (
  pub_id   CHAR(3)     NOT NULL,
  pub_name VARCHAR(20) NOT NULL,
  city     VARCHAR(15) NOT NULL,
  state    CHAR(2)             ,
  country  VARCHAR(15) NOT NULL,
  PRIMARY KEY (pub_id)
  );

Listing 11.8c uses a named table constraint to specify the primary key. This syntax shows the preferred way to add a primary key; you can use the name publishers_pk if you decide to change or delete the key later. See “Altering a Table with ALTER TABLE” later in this chapter.

Listing 11.8cDefine a simple primary key for the sample-database table publishers by using a named table constraint.

CREATE TABLE publishers
  (
  pub_id   CHAR(3)     NOT NULL,
  pub_name VARCHAR(20) NOT NULL,
  city     VARCHAR(15) NOT NULL,
  state    CHAR(2)             ,
  country  VARCHAR(15) NOT NULL,
  CONSTRAINT publishers_pk
    PRIMARY KEY (pub_id)
  );

To specify a composite primary key:

Listing 11.9 defines a composite primary key for the sample-database table title_authors. The primary-key columns are title_id and au_id, and the key is named title_authors_pk.

Listing 11.9Define a composite primary key for the sample-database table title_authors by using a named table constraint.

CREATE TABLE title_authors
  (
  title_id      CHAR(3)      NOT NULL,
  au_id         CHAR(3)      NOT NULL,
  au_order      SMALLINT     NOT NULL,
  royalty_share DECIMAL(5,2) NOT NULL,
  CONSTRAINT title_authors_pk
    PRIMARY KEY (title_id, au_id)
  );

Tips for PRIMARY KEY

Specifying a Foreign Key with FOREIGN KEY

I described foreign keys in “Foreign Keys” in Chapter 2, but I’ll review the basics here:

When you’re defining a foreign-key constraint, some important considerations are:

To preserve referential integrity, your DBMS won’t let you create orphan rows or make existing rows orphans (rows in a foreign-key table without an associated row in a parent table). When you INSERT, UPDATE, or DELETE a row with a FOREIGN KEY column that references a PRIMARY KEY column in a parent table, your DBMS performs the following referential-integrity checks:

Inserting a row into the foreign-key table.The DBMS checks that the new FOREIGN KEY value matches a PRIMARY KEY value in the parent table. If no match exists, then the DBMS won’t INSERT the row.

Updating a row in the foreign-key table.The DBMS checks that the updated FOREIGN KEY value matches a PRIMARY KEY value in the parent table. If no match exists, then the DBMS won’t UPDATE the row.

Deleting a row in the foreign-key table.A referential-integrity check is unnecessary.

Inserting a row into the parent table.A referential-integrity check is unnecessary.

Updating a row in the parent table.The DBMS checks that none of the FOREIGN KEY values matches the PRIMARY KEY value to be updated. If a match exists, then the DBMS won’t UPDATE the row.

Deleting a row from the parent table.The DBMS checks that none of the FOREIGN KEY values matches the PRIMARY KEY value to be deleted. If a match exists, then the DBMS won’t DELETE the row.

The DBMS skips the referential-integrity check for rows with a null in the FOREIGN KEY column.

To specify a simple foreign key:

Listing 11.10 uses a column constraint to designate a foreign-key column in the table titles. This syntax shows the easiest way to create a simple foreign key. After you run this statement, the DBMS will ensure that values inserted into the column pub_id in titles already exist in the column pub_id in publishers. Note that nulls aren’t allowed in the foreign-key column, so every book must have a publisher.

Listing 11.10Define a simple foreign key for the sample-database table titles by using a column constraint.

CREATE TABLE titles
  (
  title_id   CHAR(3)      NOT NULL
    PRIMARY KEY                   ,
  title_name VARCHAR(40)  NOT NULL,
  type       VARCHAR(10)          ,
  pub_id     CHAR(3)      NOT NULL
    REFERENCES publishers(pub_id) ,
  pages      INTEGER              ,
  price      DECIMAL(5,2)         ,
  sales      INTEGER              ,
  pubdate    DATE                 ,
  contract   SMALLINT     NOT NULL
  );

The table royalties has a one-to-one relationship with the table titles, so Listing 11.11 defines the column title_id to be both the primary key and a foreign key that points to title_id in titles. For information about relationships, see “Relationships” in Chapter 2.

Listing 11.11Define a simple foreign key for the sample-database table royalties by using a named table constraint.

CREATE TABLE royalties
  (
  title_id     CHAR(3)      NOT NULL,
  advance      DECIMAL(9,2)         ,
  royalty_rate DECIMAL(5,2)         ,
  CONSTRAINT royalties_pk
    PRIMARY KEY (title_id),
  CONSTRAINT royalties_title_id_fk
    FOREIGN KEY (title_id)
    REFERENCES titles(title_id)
  );

Listing 11.12 uses named table constraints to create two foreign keys. This syntax shows the preferred way to add foreign keys; you can use the names if you decide to change or delete the keys later. (See “Altering a Table with ALTER TABLE” later in this chapter.) Each foreign-key column is an individual key and not part of a single composite key. Note that foreign keys together, however, comprise the table’s composite primary key.

Listing 11.12Define simple foreign keys for the sample-database table title_authors by using named table constraints.

CREATE TABLE title_authors
  (
  title_id      CHAR(3)      NOT NULL,
  au_id         CHAR(3)      NOT NULL,
  au_order      SMALLINT     NOT NULL,
  royalty_share DECIMAL(5,2) NOT NULL,
  CONSTRAINT title_authors_pk
    PRIMARY KEY (title_id, au_id),
  CONSTRAINT title_authors_fk1
    FOREIGN KEY (title_id)
    REFERENCES titles(title_id),
  CONSTRAINT title_authors_fk2
    FOREIGN KEY (au_id)
    REFERENCES authors(au_id)
  );

To specify a composite foreign key:

The sample database contains no composite foreign keys, but suppose that I create a table named out_of_print to store information about each author’s out-of-print books. The table title_authors has a composite primary key. This constraint shows how to reference this key from the table out_of_print:

CONSTRAINT out_of_print_fk
  FOREIGN KEY (title_id, au_id)
  REFERENCES title_authors(title_id, au_id)

Tips for FOREIGN KEY

Forcing Unique Values with UNIQUE

A unique constraint ensures that a column (or set of columns) contains no duplicate values. A unique constraint is similar to a primary-key constraint, except that a unique column can contain nulls and a table can have multiple unique columns. (For information about primary-key constraints, see “Specifying a Primary Key with PRIMARY KEY” earlier in this chapter.)

Suppose that I add the column isbn to the table titles to hold a book’s ISBN. An ISBN is a unique, standardized identification number that marks a book unmistakably. titles already has a primary key (title_id), so to ensure that each ISBN value is unique, I can define a unique constraint on the column isbn.

When you’re defining a unique constraint, some important considerations are:

To specify a simple unique constraint:

Listings 11.13a and 11.13b show two equivalent ways to define a simple unique constraint for the sample-database table titles.

Listing 11.13a uses a column constraint to designate a unique column. This syntax shows the easiest way to create a simple unique constraint.

Listing 11.13aDefine a simple unique constraint on the column title_name for the sample-database table titles by using a column constraint.

CREATE TABLE titles
  (
  title_id   CHAR(3)      PRIMARY KEY    ,
  title_name VARCHAR(40)  NOT NULL UNIQUE,
  type       VARCHAR(10)                 ,
  pub_id     CHAR(3)      NOT NULL       ,
  pages      INTEGER                     ,
  price      DECIMAL(5,2)                ,
  sales      INTEGER                     ,
  pubdate    DATE                        ,
  contract   SMALLINT     NOT NULL
  );

Listing 11.13b uses a named table constraint to specify a unique column. This syntax shows the preferred way to add a unique constraint; you can use the name if you decide to change or delete the constraint later. See “Altering a Table with ALTER TABLE” later in this chapter.

Listing 11.13bDefine a simple unique constraint on the column title_name for the sample-database table titles by using a named table constraint.

CREATE TABLE titles
  (
  title_id   CHAR(3)      NOT NULL,
  title_name VARCHAR(40)  NOT NULL,
  type       VARCHAR(10)          ,
  pub_id     CHAR(3)      NOT NULL,
  pages      INTEGER              ,
  price      DECIMAL(5,2)         ,
  sales      INTEGER              ,
  pubdate    DATE                 ,
  contract   SMALLINT     NOT NULL,
  CONSTRAINT titles_pk
    PRIMARY KEY (title_id),
  CONSTRAINT titles_unique1
    UNIQUE (title_name)
  );

To specify a composite unique constraint:

Listing 11.14 defines a multicolumn unique constraint for the sample-database table authors. This constraint forces the combination of each author’s first and last name to be unique.

Listing 11.14Define a composite unique constraint on the columns au_fname and au_lname for the sample-database table authors by using a named table constraint.

CREATE TABLE authors
  (
  au_id    CHAR(3)     NOT NULL,
  au_fname VARCHAR(15) NOT NULL,
  au_lname VARCHAR(15) NOT NULL,
  phone    VARCHAR(12)         ,
  address  VARCHAR(20)         ,
  city     VARCHAR(15)         ,
  state    CHAR(2)             ,
  zip      CHAR(5)             ,
  CONSTRAINT authors_pk
    PRIMARY KEY (au_id),
  CONSTRAINT authors_unique1
    UNIQUE (au_fname, au_lname)
  );

Tips for UNIQUE

Adding a Check Constraint with CHECK

So far, the only restrictions on an inserted value are that it have the proper data type, size, and range for its column. You can use check constraints to further limit the values that a column (or set of columns) accepts. Check constraints commonly are used to check the following:

Minimum or maximum values.Prevent sales of fewer than zero items, for example.

Specific values.Allow only 'biology', 'chemistry', or 'physics' in the column science, for example.

A range of values.Make sure that an author’s royalty rate is between 2 percent and 20 percent, for example.

A check constraint resembles a foreign-key constraint in that both restrict the values that can be placed in a column (see “Specifying a Foreign Key with FOREIGN KEY” earlier in this chapter). They differ in how they determine which values are allowed. A foreign-key constraint gets the list of valid values from another table, whereas a check constraint determines the valid values by using a logical (boolean) expression. The following check constraint, for example, ensures that no employee’s salary exceeds $50000:

CHECK (salary <= 50000)

When you’re defining a check constraint, some important considerations are:

To add a check constraint:

Listing 11.15 shows various column and table check constraints for the sample-database table titles. The constraint title_id_chk makes sure the each primary-key value takes the form 'Tnn', in which nn represents an integer between 00 and 99, inclusive.

Listing 11.15Define some check constraints for the sample-database table titles.

CREATE TABLE titles
  (
  title_id   CHAR(3)      NOT NULL,
  title_name VARCHAR(40)  NOT NULL,
  type       VARCHAR(10)
    CONSTRAINT type_chk
      CHECK (type IN ('biography',
        'children', 'computer',
        'history', 'psychology'))
 ,
  pub_id     CHAR(3)      NOT NULL,
  pages      INTEGER
    CHECK (pages > 0)             ,
  price      DECIMAL(5,2)         ,
  sales      INTEGER              ,
  pubdate    DATE                 ,
  contract   SMALLINT     NOT NULL,
  CONSTRAINT titles_pk
    PRIMARY KEY (title_id),
  CONSTRAINT titles_pub_id_fk
    FOREIGN KEY (pub_id)
    REFERENCES publishers(pub_id),
  CONSTRAINT title_id_chk
    CHECK (
    (SUBSTRING(title_id FROM 1 FOR 1) = 'T')
    AND
    (CAST(SUBSTRING(title_id FROM 2 FOR 2)
    AS INTEGER) BETWEEN 0 AND 99)),
  CONSTRAINT price_chk
    CHECK (price >= 0.00
    AND price < 100.00),
  CONSTRAINT sales_chk
    CHECK (sales >= 0),
  CONSTRAINT pubdate_chk
    CHECK (pubdate >= DATE '1950-01-01'),
  CONSTRAINT title_name_chk
    CHECK (title_name <> ''
    AND contract >= 0),
  CONSTRAINT revenue_chk
    CHECK (price * sales >= 0.00)

  );

Tips for CHECK

Creating a Temporary Table with CREATE TEMPORARY TABLE

Every table I’ve created so far has been a permanent table, called a base table, which stores data persistently until you destroy (DROP) the table explicitly. SQL also lets you create temporary tables to use for working storage or intermediate results. Temporary tables commonly are used to:

A temporary table is a table that the DBMS empties automatically at the end of a session or transaction. (The table’s data are destroyed along with the table.) A session is the time during which you’re connected to a DBMS—between logon and logoff—and the DBMS accepts and executes your commands.

When you’re creating a temporary table, some important considerations are:

To create a temporary table:

Listing 11.16A local temporary table is available to only you. It dematerializes when your DBMS session ends.

CREATE LOCAL TEMPORARY TABLE editors
  (
  ed_id    CHAR(3)    ,
  ed_fname VARCHAR(15),
  ed_lname VARCHAR(15),
  phone    VARCHAR(12),
  pub_id   CHAR(3)
  );

Listing 11.17A global temporary table can be accessed by you and other users. It dematerializes when your DBMS session ends and all other tasks have stopped referencing it.

CREATE GLOBAL TEMPORARY TABLE editors
  (
  ed_id    CHAR(3)    ,
  ed_fname VARCHAR(15),
  ed_lname VARCHAR(15),
  phone    VARCHAR(12),
  pub_id   CHAR(3)
  );

Tips for CREATE TEMPORARY TABLE

Creating a New Table from an Existing One with CREATE TABLE AS

The CREATE TABLE AS statement creates a new table and populates it with the result of a SELECT. It’s similar to creating an empty table with CREATE TABLE and then populating the table with INSERT SELECT (see “Inserting Rows with INSERT” in Chapter 10). CREATE TABLE AS commonly is used to:

When you’re using CREATE TABLE AS, some important considerations are:

To create a new table from an existing table:

Listing 11.18 copies the structure and data of the existing table authors to a new table named authors2.

Listing 11.18Copy the structure and data of the existing table authors to a new table named authors2.

CREATE TABLE authors2 AS
  SELECT *
    FROM authors;

Listing 11.19 uses a WHERE condition that’s always false to copy only the structure (but not the data) of the existing table publishers to a new table named publishers2.

Listing 11.19Copy the structure (but not the data) of the existing table publishers to a new table named publishers2.

CREATE TABLE publishers2 AS
  SELECT *
    FROM publishers
    WHERE 1 = 2;

Listing 11.20 creates a global temporary table named titles2 that contains the titles and sales of books published by publisher P01; see “Creating a Temporary Table with CREATE TEMPORARY TABLE” earlier in this chapter.

Listing 11.20Create a global temporary table named titles2 that contains the titles and sales of books published by publisher P01.

CREATE GLOBAL TEMPORARY TABLE titles2 AS
  SELECT title_name, sales
    FROM titles
    WHERE pub_id = 'P01';

Listing 11.21 uses joins to create a new table named author_title_names that contains the names of the authors who aren’t from New York State or California and the titles of their books.

Listing 11.21Create a new table named author_title_names that contains the names of the authors who aren’t from New York state or California and the titles of their books.

CREATE TABLE author_title_names AS
  SELECT a.au_fname, a.au_lname, t.title_name
    FROM authors a
    INNER JOIN title_authors ta
      ON a.au_id = ta.au_id
    INNER JOIN titles t
      ON ta.title_id = t.title_id
    WHERE a.state NOT IN ('CA', 'NY');

Tips for CREATE TABLE AS

Altering a Table with ALTER TABLE

Use the ALTER TABLE statement to modify a table definition by adding, altering, or dropping columns and constraints.

Despite the SQL standard, the implementation of ALTER TABLE varies greatly by DBMS. To determine what you can alter and the conditions under which alterations are allowed, search your DBMS documentation for ALTER TABLE. Depending on your DBMS, some of the modifications that you can make by using ALTER TABLE are:

To alter a table:

Listings 11.22 and 11.23 add and drop the column email_address from the table authors.

Listing 11.22Add the column email_address to the table authors.

ALTER TABLE authors
  ADD email_address CHAR(25);

Listing 11.23Drop the column email_address from the table authors.

ALTER TABLE authors
  DROP COLUMN email_address;

If your DBMS’s ALTER TABLE statement doesn’t support an action that you need (such as, say, dropping or renaming a column or constraint), then check whether your DBMS offers the action in a different SQL statement or as a separate (non-SQL) command via the command line or graphical user interface. As a last resort, you can re-create and repopulate the table in its desired state manually.

To re-create and repopulate a table:

  1. Use CREATE TABLE to create a new table with the new column definitions, column constraints, and table constraints; see “Creating a New Table with CREATE TABLE” and subsequent sections earlier in this chapter.
  2. Use INSERT SELECT to copy rows (from the appropriate columns) from the old table into the new table; see “Inserting Rows with INSERT” in Chapter 10.
  3. Use SELECT * FROM new_table to confirm that the new table has the proper rows; see “Retrieving Columns with SELECT and FROM” in Chapter 4.
  4. Use DROP TABLE to drop the old table; see “Dropping a Table with DROP TABLE” later in this chapter.
  5. Rename the new table to the name of the old table; see the DBMS tip in “Tips for ALTER TABLE”.
  6. Re-create indexes as needed; see “Creating an Index with CREATE INDEX” in Chapter 12.

    You also need to re-create any other properties that were dropped along with the old table, such as permissions and triggers.

Tips for ALTER TABLE

Dropping a Table with DROP TABLE

Use the DROP TABLE statement to remove a table from a database. When you’re dropping a table, some important considerations are:

To drop a table:

Listing 11.24Drop the table royalties.

DROP TABLE royalties;

Tips for DROP TABLE

12. Indexes

Recall from “Tables, Columns, and Rows” in Chapter 2 that rows stored in a table are unordered, as required by the relational model. This lack of order makes it easy for the DBMS to INSERT, UPDATE, and DELETE rows quickly, but its unfortunate side effect is that it makes searching and sorting inefficient. Suppose that you run this query:

SELECT *
  FROM authors
  WHERE au_lname = 'Hull';

To execute this query, the DBMS must search the entire table authors sequentially, comparing the value in each row’s au_lname column to the string Hull. Searching an entire table in a small database is trivial, but production database tables can have millions of rows.

DBMSs provide a mechanism called an index that has the same purpose as its book or library counterpart: speeding data retrieval. At a simplified level, an index is a sorted list in which every distinct value in an indexed column (or set of columns) is stored with the drive address (physical location) of the rows containing that value. Instead of reading an entire table to locate specific rows, the DBMS scans only the index for addresses to access directly. Indexed searches typically are orders of magnitude faster than sequential searches, but some tradeoffs are involved, as explained in this chapter.

Creating an Index with CREATE INDEX

Indexes are complex; their design and effects on performance depend on the idiosyncrasies of your DBMS’s optimizer. I’ll provide guidelines in this section, but search your DBMS documentation for index to learn how your DBMS implements and uses indexes. In general, indexes are appropriate for columns that are frequently:

In general, indexes are inappropriate for columns that:

When you’re creating an index, some important considerations are:

Indexes aren’t part of the SQL standard, so index-related SQL statements vary by DBMS, although the syntax for the minimal CREATE INDEX statement is the same for the DBMSs covered in this book.

To create an index:

Listing 12.1 creates a simple index named pub_id_idx on the column pub_id for the table titles. pub_id is a foreign key and is a good candidate for an index because:

Listing 12.1Create a simple index on the column pub_id for the table titles.

CREATE INDEX pub_id_idx
  ON titles (pub_id);

Listing 12.2 creates a simple unique index named title_name_idx on the column title_name for the table titles. The DBMS will create this index only if no duplicates already exist in the column title_name. This index also prohibits nondistinct title names from being INSERTed or UPDATEd in titles.

Listing 12.2Create a simple unique index on the column title_name for the table titles.

CREATE UNIQUE INDEX title_name_idx
  ON titles (title_name);

Listing 12.3 creates a composite index named state_city_idx on the columns state and city for the table authors. The DBMS uses this index when you sort rows in state plus city order. This index is useless for sorts and searches on state alone, city alone, or city plus state; you must create separate indexes for those purposes.

Listing 12.3Create a composite index on the columns state and city for the table authors.

CREATE INDEX state_city_idx
  ON authors (state, city);

Tips for CREATE INDEX

Dropping an Index with DROP INDEX

Use the DROP INDEX statement to destroy an index. Because an index is logically and physically independent of the data in its associated table, you can drop the index at any time without affecting the table (or other indexes). All SQL programs and other applications will continue to work if you drop an index, but access to previously indexed data will be slower.

The usual reasons for dropping an index are:

The SQL standard omits indexes, so index-related SQL statements vary by DBMS. This section describes how to drop an index for each DBMS covered in this book. If you’re using a different DBMS, then search the documentation for index to learn how to drop an index.

In Oracle, Db2, and PostgreSQL, index names must be unique within a database, so you don’t specify a table name when you drop an index. In Microsoft Access, Microsoft SQL Server, and MySQL, index names must be unique within a table but can be reused in other tables, so you must specify a table along with the index to be dropped. The examples in this section drop the index created by Listing 12.1 in the preceding section.

To drop an index in Microsoft Access or MySQL:

Listing 12.4aDrop the index pub_id_idx (Microsoft Access or MySQL).

DROP INDEX pub_id_idx
  ON titles;

To drop an index in Microsoft SQL Server:

Listing 12.4bDrop the index pub_id_idx (Microsoft SQL Server).

DROP INDEX titles.pub_id_idx;

To drop an index in Oracle Database, IBM Db2 Database, or PostgreSQL:

Listing 12.4cDrop the index pub_id_idx (Oracle, Db2, or PostgreSQL).

DROP INDEX pub_id_idx;

Tips for DROP INDEX

13. Views

A view is a stored SELECT statement that returns a table whose data are derived from one or more other tables (called underlying tables). Some important characteristics of a view are:

Creating a View with CREATE VIEW

Think of a view as being a tailored presentation that provides a tabular window into one or more base tables. The window can display an entire base table, part of a base table, or a combination of base tables (or parts thereof). A view also can reflect the data in base tables through other views—windows into windows. Generally, SQL programmers use views to present data to end-users in database applications. Views offer these advantages:

Simplified data access.Views hide data complexity and simplify statements, so users can perform operations on a view more easily than on the base tables directly. If you create a complex view—one that involves, say, multiple base tables, joins, and subqueries—then users can query this view without having to understand complex relational concepts or even knowing that multiple tables are involved.

Automatic updating.When a base table is updated, all views that reference the table reflect the change automatically. If you insert a row representing a new author into the table authors, for example, then all views defined over authors will reflect the new author automatically. This scheme saves storage space and prevents redundancy because, without views, the DBMS would have to store derived data to keep it synchronized.

Increased security.One of the most common uses of views is to hide data from users by filtering the underlying tables. Suppose that the table employees contains the columns salary and commission. If you create a view on employees that omits these two columns but contains other innocuous columns (such as email_address), then the database administrator can grant users permission to see the view but not see the underlying table, thereby hiding compensation data from the curious.

Logical data independence.Base tables provide a real view of a database. But when you use SQL to build a database application, you want to present end users not the real view, but a virtual view specific to the application. The virtual view hides the parts of the database (entire tables or specific rows or columns) that aren’t relevant to the application. Thus, users interact with the virtual view, which is derived from—although independent of—the real view presented by the base tables.

A virtual view immunizes an application from logical changes in the design of the database. Suppose that many applications access the table titles. Books go out of print over time, so the database designer decides to reduce the system load by segregating out-of-print books. He splits titles into two tables: in_print_titles and out_of_print_titles. Consequently, all the applications break because they expect the now-unavailable table titles.

But if those applications had accessed a view of titles instead of the real table, then that view could be redefined to be the UNION of in_print_titles and out_of_print_titles (see “Combining Rows with UNION” in Chapter 9). The applications transparently would see the two new tables as though they were the one original table and continue to work as though the split never happened. (You can’t use views to immunize an application against all changes, however. Views can’t compensate for dropped tables or columns, for example.)

When you’re creating a view, some important considerations are:

To create a view:

Listing 13.1Create a view that hides the authors’ personal information (telephone numbers and addresses).

CREATE VIEW au_names
  AS
  SELECT au_id, au_fname, au_lname
    FROM authors;

Listing 13.2Create a view that lists the authors who live in a city in which a publisher is located. Note that I use the column names au_city and pub_city in the view. Renaming these columns resolves the ambiguity that would arise if both columns inherited the same column name city from the underlying tables.

CREATE VIEW cities
  (au_id, au_city, pub_id, pub_city)
  AS
  SELECT a.au_id, a.city, p.pub_id, p.city
    FROM authors a
    INNER JOIN publishers p
      ON a.city = p.city;

Listing 13.3Create a view that lists total revenue (= price × sales) grouped by book type within publisher. This view will be easy to query later because I name the result of an arithmetic expression explicitly rather than let the DBMS assign a default name.

CREATE VIEW revenues
  (Publisher, BookType, Revenue)
  AS
  SELECT pub_id, type, SUM(price * sales)
    FROM titles
    GROUP BY pub_id, type;

Listing 13.4Create a view that makes it easy to print mailing labels for authors. Note that I assigned column names in the SELECT clause rather than in the CREATE VIEW clause.

CREATE VIEW mailing_labels
  AS
  SELECT
    TRIM(au_fname || ' ' || au_lname)
      AS "address1",
    TRIM(address)
      AS "address2",
    TRIM(city) || ', ' || TRIM(state) || ' ' || TRIM(zip)
      AS "address3"
    FROM authors;

Listing 13.5Create a view that lists the last names of authors A02 and A05, and the books that each one wrote (or cowrote). Note that this statement uses a nested view: it references the view au_names created by Listing 13.1.

CREATE VIEW au_titles (LastName, Title)
  AS
  SELECT an.au_lname, t.title_name
    FROM title_authors ta
    INNER JOIN au_names an
      ON ta.au_id = an.au_id
    INNER JOIN titles t
      ON t.title_id = ta.title_id
    WHERE an.au_id in ('A02','A05');

Tips for CREATE VIEW

Retrieving Data Through a View

Creating a view displays nothing. All that CREATE VIEW does is cause the DBMS to save the view as a named SELECT statement. To see data through a view, query the view by using SELECT, just as you would query a table. You can:

To retrieve data through a view:

Listing 13.6List all the rows and columns of the view au_titles. See Figure 13.1 for the result.

SELECT *
  FROM au_titles;

Figure 13.1Result of Listing 13.6.

LastName  Title
--------- -----------------------------
Kells     Ask Your System Administrator
Heydemark How About Never?
Heydemark I Blame My Mother
Heydemark Not Without My Faberge Egg
Heydemark Spontaneous, Not Annoying

Listing 13.7List the distinct cities in the view cities. See Figure 13.2 for the result.

SELECT DISTINCT au_city
  FROM cities;

Figure 13.2Result of Listing 13.7.

au_city
-------------
New York
San Francisco

Listing 13.8List the types of books whose average revenue exceeds $1 million. See Figure 13.3 for the result.

SELECT BookType,
    AVG(Revenue) AS "AVG(Revenue)"
  FROM revenues
  GROUP BY BookType
  HAVING AVG(Revenue) > 1000000;

Figure 13.3Result of Listing 13.8.

BookType  AVG(Revenue)
--------- ------------
biography 18727318.50
computer   1025396.65
psychology 2320933.76

Listing 13.9List the third line of the mailing address of each author whose name contains the string Kell. See Figure 13.4 for the result.

SELECT address3
  FROM mailing_labels
  WHERE address1 LIKE '%Kell%';

Figure 13.4Result of Listing 13.9.

address3
-------------------
New York, NY 10014
Palo Alto, CA 94305

Listing 13.10List the name of each author who wasn’t the lead author of at least one book. See Figure 13.5 for the result.

SELECT DISTINCT an.au_fname, an.au_lname
  FROM au_names an
  INNER JOIN title_authors ta
    ON an.au_id = ta.au_id
  WHERE ta.au_order > 1;

Figure 13.5Result of Listing 13.10.

au_fname au_lname
-------- --------
Hallie   Hull
Klee     Hull

Listing 13.11List the names of the authors from California. See Figure 13.6 for the result.

SELECT au_fname, au_lname
  FROM au_names
  WHERE state = 'CA';

Figure 13.6Result of Listing 13.11. The view au_names references authors but hides the column state, so referring to state through the view causes an error.

ERROR: Invalid column name 'state'.

Tips for Retrieving Data Through Views

Updating Data Through a View

An updateable view is a view to which you can apply INSERT, UPDATE, and DELETE operations to modify data in the underlying table(s). Any changes made in an updateable view always pass through to the base table(s) unambiguously. The syntax for the INSERT, UPDATE, and DELETE statements is the same for views as it is for tables; see Chapter 10.

A nonupdateable view (or read-only view) view is one that doesn’t support INSERT, UPDATE, and DELETE operations because changes would be ambiguous. To change the data that appear in a read-only view, you must change the underlying table(s) directly (or through another, nonambiguous view).

Each row in an updateable view is associated with exactly one row in an underlying base table. A view isn’t updateable if its SELECT statement uses GROUP BY, HAVING, DISTINCT, or aggregate functions, for example.

The SQL-92 standard said that an updateable view must be defined over only one table, which is stringent but very safe. The SQL:1999 standard relaxed that restriction because many more types of updateable views exist. By the time that standard was released, the DBMS vendors already offered an expanded set of updateable views. Single-table views always are updateable. DBMSs also examine the underlying tables’ joins and referential-integrity constraints of a multitable view to determine whether the view is updateable. Here are some of the types of queries that can define updateable views:

The examples in this section use updateable views that reference only one underlying table. See your DBMS documentation to find out which multitable views you can update and how those updates affect each base table.

Inserting a Row Through a View

Consider the view ny_authors, which consists of the IDs, names, and states of only those authors from New York State (Listing 13.12 and Figure 13.7). ny_authors references only the base table authors.

Listing 13.12Create and display the view ny_authors, which lists the IDs, names, and states of only those authors from New York state. See Figure 13.7 for the result.

CREATE VIEW ny_authors
  AS
  SELECT au_id, au_fname, au_lname, state
    FROM authors
    WHERE state = 'NY';

SELECT *
  FROM ny_authors;

Figure 13.7Result of Listing 13.12: the view ny_authors.

au_id au_fname  au_lname state
----- --------- -------- -----
A01   Sarah     Buchman  NY
A05   Christian Kells    NY

Listing 13.13 inserts a new row through a view. The DBMS inserts a new row into the table authors. The row contains A08 in the column au_id, Don in au_fname, Dawson in au_lname, and NY in state. The other columns in the row—phone, address, city, and zip—are set to null (or their default values, if DEFAULT constraints exist).

Listing 13.13Insert a new row through the view ny_authors.

INSERT INTO ny_authors
  VALUES('A08','Don','Dawson','NY');

Listing 13.14, like Listing 13.13, inserts a new row through a view. But this time, the new author is from California, not New York, which violates the WHERE condition in the view’s definition. Does the DBMS insert the row or cancel the operation? The answer depends on how the view was created. In this particular example, the insertion is allowed because the CREATE VIEW statement (see Listing 13.12) lacks a WITH CHECK OPTION clause, so the DBMS isn’t forced to maintain consistency with the view’s original definition. For information about WITH CHECK OPTION, see the DBMS tip in “Tips for CREATE VIEW” earlier in this chapter. The DBMS would have canceled the insertion if ny_authors were defined as:

CREATE VIEW ny_authors AS
  SELECT au_id, au_fname, au_lname, state
    FROM authors
    WHERE state = 'NY'
  WITH CHECK OPTION;

Listing 13.14Insert a new row through the view ny_authors. The DBMS would cancel this insertion if WITH CHECK OPTION had been used when ny_authors was created.

INSERT INTO ny_authors
  VALUES('A09','Jill','LeFlore','CA');

Updating a Row Through a View

Listing 13.15 updates an existing row through a view. The DBMS updates the row for author A01 in the table authors by changing the author’s name from Sarah Buchman to Yasmin Howcomely. The values in the other columns in the row—au_id, phone, address, city, state, and zip—don’t change.

Listing 13.15Update an existing row through the view ny_authors.

UPDATE ny_authors
  SET au_fname = 'Yasmin',
      au_lname = 'Howcomely'
  WHERE au_id = 'A01';

But suppose that Listing 13.15 looked like this:

UPDATE ny_authors
  SET au_fname = 'Yasmin',
      au_lname = 'Howcomely',
      state = 'CA'
  WHERE au_id = 'A01';

This statement presents the same problem as Listing 13.14: the desired change would cause Yasmin’s row to no longer meet the conditions for membership in the view. Again, the DBMS will accept or reject the UPDATE depending on whether the WITH CHECK OPTION clause was specified when the view was created. If WITH CHECK OPTION is used, then rows can’t be modified in a way that causes them to disappear from the view.

Deleting a Row Through a View

Listing 13.16 deletes a row through a view. The DBMS deletes the row for author A05 in the table authors. (Every column in the row is deleted, not just those in the view.) In turn, the row disappears from the view ny_authors.

Listing 13.16Delete a row through the view ny_authors.

DELETE FROM ny_authors
  WHERE au_id = 'A05';

View updates can have integrity repercussions, of course. The DBMS will disallow a deletion if removing a row violates a referential-integrity constraint; see “Specifying a Foreign Key with FOREIGN KEY” in Chapter 11. If you delete a row, then all the underlying FOREIGN KEY constraints in related tables must still be satisfied for the deletion to succeed. Some updating can be handled by the CASCADE option (if specified) of a FOREIGN KEY constraint, not by the view definition.

In Listing 13.16, for example, the DBMS will cancel the DELETE if I don’t first change or delete the foreign-key values in the table title_authors that point to author A05 in authors.

Tips for Updating Data Through a View

Dropping a View with DROP VIEW

Use the DROP VIEW statement to destroy a view. Because a view is physically independent of its underlying table(s), you can drop the view at any time without affecting those table(s). All SQL programs, applications, and other views that reference the dropped view will break, however.

To drop a view:

Listing 13.17Drop the view ny_authors.

DROP VIEW ny_authors;

Tips for DROP VIEW

14. Transactions

A transaction is a sequence of one or more SQL statements executed as a single logical unit of work. The DBMS considers a transaction to be an indivisible, all-or-nothing proposition: it executes all the transaction’s statements as a group, or it executes none of them.

For example, suppose that a bank customer transfers $500 from her savings account to her checking account. This operation consists of two separate actions, executed sequentially:

  1. Decrement savings balance by $500.
  2. Increment checking balance by $500.

Figure 14.1 shows the two SQL statements for this transaction. Now imagine that the DBMS fails—power outage, system crash, hardware problem—after it executes the first statement but before the second. The accounts would be out of balance without your knowledge. Accusations of malfeasance and prison time would soon follow.

To avoid legal problems, use a transaction to guarantee that both SQL statements are performed to maintain the accounts in proper balance. When something prevents one of the statements in a transaction from executing, the DBMS undoes (rolls back) the other statements of the transaction. If no error occurs, then the changes are made permanent (committed).

Figure 14.1Two SQL statements are needed when a banking customer transfers money from savings to checking.

UPDATE savings_accounts
  SET balance = balance - 500.00
  WHERE account_number = 208998628;

UPDATE checking_accounts
  SET balance = balance + 500.00
  WHERE account_number = 786783165;

Executing a Transaction

To learn how transactions work, you need to learn a few terms:

Commit.Committing a transaction makes all data modifications performed since the start of the transaction a permanent part of the database. After a transaction is committed, all changes made by the transaction become visible to other users and are guaranteed to be permanent if a crash or other failure occurs.

Roll back.Rolling back a transaction retracts any of the changes resulting from the SQL statements in the transaction. After a transaction is rolled back, the affected data are left unchanged, as though the SQL statements in the transaction were never executed.

Transaction log.The transaction log file, or just log, is a serial record of all modifications that have occurred in a database via transactions. The transaction log records the start of each transaction, the changes to the data, and enough information to undo or redo the changes made by the transaction (if necessary later). The log grows continually as transactions occur in the database.

Although it’s the DBMS’s responsibility to ensure the physical integrity of each transaction, it’s your responsibility to start and end transactions at points that enforce the logical consistency of the data, according to the rules of your organization or business. A transaction should contain only the SQL statements necessary to make a consistent change—no more and no fewer. Data in all referenced tables must be in a consistent state before the transaction begins and after it ends.

When you’re designing and executing transactions, some important considerations are:

Concurrency Control

To humans, computers appear to carry out two or more processes at the same time. In reality, computer operations occur not concurrently, but in sequence. The illusion of simultaneity appears because a microprocessor works with much smaller time slices than people can perceive. In a DBMS, concurrency control is a group of strategies that prevents loss of data integrity caused by interference between two or more users trying to access or change the same data simultaneously.

DBMSs use locking strategies to ensure transactional integrity and database consistency. Locking restricts data access during read and write operations; thus, it prevents users from reading data that are being changed by other users and prevents multiple users from changing the same data at the same time. Without locking, data can become logically incorrect, and statements executed against those data can return unexpected results. Occasionally you’ll end up in a deadlock, where you and another user, each having locked a piece of data needed for the other’s transaction, attempt to get a lock on each other’s piece. Most DBMSs can detect and resolve deadlocks by rolling back one user’s transaction so that the other can proceed (otherwise, you’d both wait forever for the other to release the lock). Locking mechanisms are very sophisticated; search your DBMS documentation for locking.

Concurrency transparency is the appearance from a transaction’s perspective that it’s the only transaction operating on the database. A DBMS isolates a transaction’s changes from changes made by any other concurrent transactions. Consequently, a transaction never sees data in an intermediate state; either it sees data in the state they were in before another concurrent transaction changed them, or it sees the data after the other transaction has completed. Isolated transactions let you reload starting data and replay (roll forward) a series of transactions to end up with the data in the same state they were in after the original transactions were executed.

For a transaction to be executed in all-or-nothing fashion, the transaction’s boundaries (starting and ending points) must be clear. These boundaries let the DBMS execute the statements as one atomic unit of work. A transaction can start implicitly with the first executable SQL statement or explicitly with the START TRANSACTION statement. A transaction ends explicitly with a COMMIT or ROLLBACK statement (it never ends implicitly). You can’t roll back a transaction after you commit it.

Oracle and Db2 transactions always start implicitly, so those DBMSs have no statement that marks the start of a transaction. In Microsoft Access, Microsoft SQL Server, MySQL, and PostgreSQL, you can (or must) start a transaction explicitly by using the BEGIN statement. The SQL:1999 standard introduced the START TRANSACTION statement—long after these DBMSs already were using BEGIN to start transactions, so the extended BEGIN syntax varies by DBMS. MySQL and PostgreSQL support START TRANSACTION (as a synonym for BEGIN).

To start a transaction explicitly:

To commit a transaction:

To roll back a transaction:

The SELECT statements in Listing 14.1 show that the UPDATE operations are performed by the DBMS and then undone by a ROLLBACK statement. See Figure 14.2 for the result.

Listing 14.1Within a transaction block, UPDATE operations (like INSERT and DELETE operations) are never final. See Figure 14.2 for the result.

SELECT SUM(pages), AVG(price) FROM titles;

BEGIN TRANSACTION;
  UPDATE titles SET pages = 0;
  UPDATE titles SET price = price * 2;
  SELECT SUM(pages), AVG(price) FROM titles;
ROLLBACK;

SELECT SUM(pages), AVG(price) FROM titles;

Figure 14.2Result of Listing 14.1. The results of the SELECT statements show that the DBMS cancelled the transaction.

SUM(pages) AVG(price)
---------- ----------
      5107    18.3875

SUM(pages) AVG(price)
---------- ----------
         0    36.7750

SUM(pages) AVG(price)
---------- ----------
      5107    18.3875

Listing 14.2 shows a more practical example of a transaction. I want to delete the publisher P04 from the table publishers without generating a referential-integrity error. Because some of the foreign-key values in titles point to publisher P04 in publishers, I first need to delete the related rows from the tables titles, titles_authors, and royalties. I use a transaction to be certain that all the DELETE statements are executed. If only some of the statements were successful, then the data would be left inconsistent. (For information about referential-integrity checks, see “Specifying a Foreign Key with FOREIGN KEY” in Chapter 11.)

Listing 14.2Use a transaction to delete publisher P04 from the table publishers and delete P04’s related rows in other tables.

BEGIN TRANSACTION;

  DELETE FROM title_authors
    WHERE title_id IN
      (SELECT title_id
         FROM titles
         WHERE pub_id = 'P04');

  DELETE FROM royalties
    WHERE title_id IN
      (SELECT title_id
         FROM titles
         WHERE pub_id = 'P04');

  DELETE FROM titles
    WHERE pub_id = 'P04';

  DELETE FROM publishers
    WHERE pub_id = 'P04';

COMMIT;

ACID

ACID is an acronym that summarizes the properties of a transaction:

Atomicity.Either all of a transaction’s data modifications are performed, or none of them are.

Consistency.A completed transaction leaves all data in a consistent state that maintains all data integrity. A consistent state satisfies all defined database constraints. (Note that consistency isn’t necessarily preserved at any intermediate point within a transaction.)

Isolation.A transaction’s effects are isolated (or concealed) from those of all other transactions. See “Concurrency Control” earlier in this chapter.

Durability.After a transaction completes, its effects are permanent and persist even if the system fails.

Transaction theory is a big topic, separate from the relational model. A good (if sometimes dated) reference is Transaction Processing: Concepts and Techniques by Jim Gray and Andreas Reuter.

Tips for Transactions

15. Advanced SQL

This chapter describes how to solve common problems with SQL programs that

Calculating Running Statistics

A running statistic, or cumulative statistic, is a row-by-row calculation that uses progressively more data values, starting with a single value (the first value), continuing with more values in the order in which they’re supplied, and ending with all the values. The running sum (total) and running average (arithmetic mean) are the most common running statistics.

Listing 15.1 calculates the running sum and running average of book sales, along with a cumulative count of data items. The query cross-joins two instances of the table titles, grouping the result by the first-table (t1) title IDs and limiting the second-table (t2) rows to ID values smaller than or equal to the t1 row to which they’re joined. The intermediate cross-joined table, to which SUM(), AVG(), and COUNT() are applied, looks like this:

t1.id t1.sales t2.id t2.sales
----- -------- ----- --------
T01        566 T01        566
T02       9566 T01        566
T02       9566 T02       9566
T03      25667 T01        566
T03      25667 T02       9566
T03      25667 T03      25667
T04      13001 T01        566
T04      13001 T02       9566
T04      13001 T03      25667
T04      13001 T04      13001
T05     201440 T01        566
...

Note that the running statistics don’t change for title T10 because its sales value is null. The ORDER BY clause is necessary because GROUP BY doesn’t sort the result implicitly. See Figure 15.1 for the result.

Listing 15.1Calculate the running sum, average, and count of book sales. See Figure 15.1 for the result.

SELECT
    t1.title_id,
    SUM(t2.sales) AS RunSum,
    AVG(t2.sales) AS RunAvg,
    COUNT(t2.sales) AS RunCount
  FROM titles t1, titles t2
  WHERE t1.title_id >= t2.title_id
  GROUP BY t1.title_id
  ORDER BY t1.title_id;

Figure 15.1Result of Listing 15.1.

title_id RunSum  RunAvg RunCount
-------- ------- ------ --------
T01          566    566        1
T02        10132   5066        2
T03        35799  11933        3
T04        48800  12200        4
T05       250240  50048        5
T06       261560  43593        6
T07      1761760 251680        7
T08      1765855 220731        8
T09      1770855 196761        9
T10      1770855 196761        9
T11      1864978 186497       10
T12      1964979 178634       11
T13      1975446 164620       12

A moving average is a way of smoothing a time series (such as a list of prices changing over time) by replacing each value by an average of that value and its nearest neighbors. Calculating a moving average is easy if you have a column that contains a sequence of integers or dates, such as in this table, named time_series:

seq price
--- -----
  1  10.0
  2  10.5
  3  11.0
  4  11.0
  5  10.5
  6  11.5
  7  12.0
  8  13.0
  9  15.0
 10  13.5
 11  13.0
 12  12.5
 13  12.0
 14  12.5
 15  11.0

Listing 15.2 calculates the moving average of price. See Figure 15.2 for the result. Each value in the result’s moving-average column is the average of five values: the price in the current row and the prices in the four preceding rows (as ordered by seq). The first four rows are omitted because they don’t have the required number of preceding values. You can adjust the values in the WHERE clause to cover any size averaging window. To make Listing 15.2 calculate a five-point moving average that averages each price with the two prices before it and the two prices after it, for example, change the WHERE clause to:

WHERE t1.seq >= 3
  AND t1.seq <= 13
  AND t1.seq BETWEEN t2.seq - 2 AND t2.seq + 2

Listing 15.2Calculate a moving average with a five-point window. See Figure 15.2 for the result.

SELECT t1.seq, AVG(t2.price) AS MovingAvg
  FROM time_series t1, time_series t2
  WHERE t1.seq >= 5
    AND t1.seq BETWEEN t2.seq AND t2.seq + 4
  GROUP BY t1.seq
  ORDER BY t1.seq;

Figure 15.2Result of Listing 15.2.

seq MovingAvg
--- ---------
  5      10.6
  6      10.9
  7      11.2
  8      11.6
  9      12.4
 10      13.0
 11      13.3
 12      13.4
 13      13.2
 14      12.7
 15      12.2

If you have a table that already has running totals, then you can calculate the differences between pairs of successive rows. Listing 15.3 backs out the intercity distances from the following table, named roadtrip, which contains the cumulative distances for each leg of a trip from Seattle, Washington, to San Diego, California. See Figure 15.3 for the result.

seq city              miles
--- ----------------- -----
  1 Seattle, WA           0
  2 Portland, OR        174
  3 San Francisco, CA   808
  4 Monterey, CA        926
  5 Los Angeles, CA    1251
  6 San Diego, CA      1372

Listing 15.3Calculate intercity distances from cumulative distances. See Figure 15.3 for the result.

SELECT
    t1.seq AS seq1,
    t2.seq AS seq2,
    t1.city AS city1,
    t2.city AS city2,
    t1.miles AS miles1,
    t2.miles AS miles2,
    t2.miles - t1.miles AS dist
  FROM roadtrip t1, roadtrip t2
  WHERE t1.seq + 1 = t2.seq
  ORDER BY t1.seq;

Figure 15.3Result of Listing 15.3.

seq1 seq2 city1             city2             miles1 miles2 dist
---- ---- ----------------- ----------------- ------ ------ ----
   1    2 Seattle, WA       Portland, OR           0    174  174
   2    3 Portland, OR      San Francisco, CA    174    808  634
   3    4 San Francisco, CA Monterey, CA         808    926  118
   4    5 Monterey, CA      Los Angeles, CA      926   1251  325
   5    6 Los Angeles, CA   San Diego, CA       1251   1372  121

Tips for Running Statistics

Generating Sequences

Recall from “Unique Identifiers” in Chapter 3 that you can use sequences of autogenerated integers to create identity columns (typically for primary keys). The SQL standard provides sequence generators to create them.

To define a sequence generator:

Listing 15.4 defines the sequence shown in Figure 15.4.

Listing 15.4Create a sequence generator for the consecutive integers 1 to 10000. See Figure 15.4 for the result.

CREATE SEQUENCE part_seq
  INCREMENT BY 1
  MINVALUE 1
  MAXVALUE 10000
  START WITH 1
  NO CYCLE;

Figure 15.4The sequence that Listing 15.4 generates.

1
2
3
...
9998
9999
10000

You can use a sequence generator in a few ways. Standard SQL provides the built-in function NEXT VALUE FOR to increment a sequence value, as in:

INSERT INTO shipment(part_num, desc, quantity)
  VALUES(NEXT VALUE FOR part_seq, 'motherboard', 5);

If you’re creating a column of unique values, then you can use the keyword IDENTITY to define a sequence right in the CREATE TABLE statement:

CREATE TABLE parts (
  part_num INTEGER AS
    IDENTITY(
      INCREMENT BY 1
      MINVALUE 1
      MAXVALUE 10000
      START WITH 1
      NO CYCLE),
  desc AS VARCHAR(100),
  quantity INTEGER;

This table definition lets you omit NEXT VALUE FOR when you insert a row:

INSERT INTO shipment(
    desc,
    quantity)
  VALUES(
    'motherboard',
    5);

SQL also provides ALTER SEQUENCE and DROP SEQUENCE to change and remove sequence generators.

Microsoft SQL Server, Oracle, Db2, and PostgreSQL support CREATE SEQUENCE, ALTER SEQUENCE, and DROP SEQUENCE. In Oracle, use NOCYCLE instead of NO CYCLE. See your DBMS documentation to see how sequences are used in your system.

Most DBMSs don’t support IDENTITY columns because they have other (pre-standard) ways that define columns with unique values; see “Unique Identifiers” in Chapter 3. PostgreSQL’s generate_series() function offers a quick way to generate numbered rows.

A one-column table containing a sequence of consecutive integers makes it easy to solve problems that would otherwise be difficult with SQL’s limited computational power. Sequence tables aren’t actually part of the data model—they’re auxiliary tables that are adjuncts to queries and other “real” tables.

You can create a sequence table by using one of the methods just described. Alternatively, you can create one by using Listing 15.5, which creates the sequence table seq by cross-joining the intermediate table temp09 with itself. The CAST expression concatenates digit characters into sequential numbers and then casts them as integers. You can drop the table temp09 after seq is created. Figure 15.5 shows the result. The table seq contains the integer sequence 0, 1, 2,..., 9999. You can shrink or grow this sequence by changing the SELECT and FROM expressions in the INSERT INTO seq statement.

Listing 15.5Create a one-column table that contains consecutive integers. See Figure 15.5 for the result.

CREATE TABLE temp09 (
  i CHAR(1) NOT NULL PRIMARY KEY
  );

INSERT INTO temp09 VALUES('0');
INSERT INTO temp09 VALUES('1');
INSERT INTO temp09 VALUES('2');
INSERT INTO temp09 VALUES('3');
INSERT INTO temp09 VALUES('4');
INSERT INTO temp09 VALUES('5');
INSERT INTO temp09 VALUES('6');
INSERT INTO temp09 VALUES('7');
INSERT INTO temp09 VALUES('8');
INSERT INTO temp09 VALUES('9');

CREATE TABLE seq (
  i INTEGER NOT NULL PRIMARY KEY
  );

INSERT INTO seq
  SELECT CAST(t1.i || t2.i ||
      t3.i || t4.i AS INTEGER)
    FROM temp09 t1, temp09 t2,
      temp09 t3, temp09 t4;

DROP TABLE temp09;

Figure 15.5Result of Listing 15.5.

i
-----
0
1
2
3
4
...
9996
9997
9998
9999

A sequence table is especially useful for enumerative and datetime functions. Listing 15.6 lists the 95 printable characters in the ASCII character set (if that’s the character set in use). See Figure 15.6 for the result.

Listing 15.6List the characters associated with a set of character codes. See Figure 15.6 for the result.

SELECT
    i AS CharCode,
    CHR(i) AS Ch
  FROM seq
  WHERE i BETWEEN 32 AND 126;

Figure 15.6Result of Listing 15.6.

CharCode Ch
-------- --
      32
      33  !
      34  "
      35  #
      36  $
      37  %
      38  &
      39  '
      40  (
      41  )
      42  *
      43  +
      44  ,
      45  -
      46  .
      47  /
      48  0
      49  1
      50  2
      51  3
      52  4
...

Listing 15.7 adds monthly intervals to today’s date (7-March-2005) for the next six months. See Figure 15.7 for the result. This example works on Microsoft SQL Server; other DBMSs have similar functions that increment dates.

Listing 15.7Increment today’s date to six months hence, in one-month intervals. See Figure 15.7 for the result.

SELECT
    i AS MonthsAhead,
    DATEADD("m", i, CURRENT_TIMESTAMP)
      AS FutureDate
  FROM seq
  WHERE i BETWEEN 1 AND 6;

Figure 15.7Result of Listing 15.7.

MonthsAhead FutureDate
----------- ----------
          1 2005-04-07
          2 2005-05-07
          3 2005-06-07
          4 2005-07-07
          5 2005-08-07
          6 2005-09-07

Sequence tables are handy for normalizing data that you’ve imported from a nonrelational source such as a spreadsheet or accounting package. Suppose that you have the following non-normalized table, named au_orders, showing the order of the authors’ names on each book’s cover:

title_id author1 author2 author3
-------- ------- ------- -------
T01      A01     NULL    NULL
T02      A01     NULL    NULL
T03      A05     NULL    NULL
T04      A03     A04     NULL
T05      A04     NULL    NULL
T06      A02     NULL    NULL
T07      A02     A04     NULL
T08      A06     NULL    NULL
T09      A06     NULL    NULL
T10      A02     NULL    NULL
T11      A06     A03     A04
T12      A02     NULL    NULL
T13      A01     NULL    NULL

Listing 15.8 cross-joins au_orders with seq to produce Figure 15.8. You can DELETE the result rows with nulls in the column au_id, leaving the result set looking like the table title_authors in the sample database. Note that Listing 15.8 does the reverse of Listing 8.18 in Chapter 8.

Listing 15.8Normalize the table au_orders. See Figure 15.8 for the result.

SELECT title_id,
    (CASE WHEN i=1 THEN '1'
          WHEN i=2 THEN '2'
          WHEN i=3 THEN '3'
    END) AS au_order,
    (CASE WHEN i=1 THEN author1
          WHEN i=2 THEN author2
          WHEN i=3 THEN author3
    END) AS au_id
  FROM au_orders, seq
  WHERE i BETWEEN 1 AND 3
  ORDER BY title_id, i;

Figure 15.8Result of Listing 15.8.

title_id au_order au_id
-------- -------- -----
T01      1        A01
T01      2        NULL
T01      3        NULL
T02      1        A01
T02      2        NULL
T02      3        NULL
T03      1        A05
T03      2        NULL
T03      3        NULL
T04      1        A03
T04      2        A04
T04      3        NULL
T05      1        A04
T05      2        NULL
T05      3        NULL
T06      1        A02
T06      2        NULL
T06      3        NULL
T07      1        A02
T07      2        A04
T07      3        NULL
T08      1        A06
T08      2        NULL
T08      3        NULL
T09      1        A06
T09      2        NULL
T09      3        NULL
T10      1        A02
T10      2        NULL
T10      3        NULL
T11      1        A06
T11      2        A03
T11      3        A04
T12      1        A02
T12      2        NULL
T12      3        NULL
T13      1        A01
T13      2        NULL
T13      3        NULL

Tips for Generating Sequences

Calendar Tables

Another useful auxiliary table is a calendar table. One type of calendar table has a primary-key column that contains a row for each calendar date (past and future) and other columns that indicate the date’s attributes: business day, holiday, international holiday, fiscal-month end, fiscal-year end, Julian date, business-day offsets, and so on. Another type of calendar table stores the starting and ending dates of events (in the columns event_id, start_date, and end_date, for example). Spreadsheets have more date-arithmetic functions than DBMSs, so it might be easier to build a calendar table in a spreadsheet and then import it as a database table.

Even if your DBMS has plenty of date-arithmetic functions, it might be faster to look up data in a calendar table than to call these functions in a query.

Finding Sequences, Runs, and Regions

A sequence is a series of consecutive values without gaps. A run is like a sequence, but the values don’t have to be consecutive, just increasing (that is, gaps are allowed). A region is an unbroken series of values that all are equal.

Finding these series requires a table that has at least two columns: a primary-key column that holds a sequence of consecutive integers and a column that holds the values of interest. The table temps (Listing 15.9 and Figure 15.9) shows a series of high temperatures over 15 days.

As a set-oriented language, SQL isn’t a good choice for finding series of values. The following queries won’t run very fast, so if you have a lot of data to analyze, then consider exporting it to a statistical package or using a procedural host language.

Listing 15.9List all the columns in the table temps. See Figure 15.9 for the result.

SELECT *
  FROM temps;

Figure 15.9Result of Listing 15.9.

id hi_temp
-- -------
 1      49
 2      46
 3      48
 4      50
 5      50
 6      50
 7      51
 8      52
 9      53
10      50
11      50
12      47
13      50
14      51
15      52

Listing 15.10 finds all the sequences in temps and lists each sequence’s start position, end position, and length. See Figure 15.10 for the result.

Listing 15.10List the starting point, ending point, and length of each sequence in the table temps. See Figure 15.10 for the result.

SELECT
    t1.id AS StartSeq,
    t2.id AS EndSeq,
    t2.id - t1.id + 1 AS SeqLen
  FROM temps t1, temps t2
  WHERE (t1.id < t2.id)
    AND NOT EXISTS(
      SELECT *
        FROM temps t3
        WHERE (t3.hi_temp - t3.id <>
               t1.hi_temp - t1.id
               AND t3.id BETWEEN
                   t1.id AND t2.id)
           OR (t3.id = t1.id - 1
               AND t3.hi_temp - t3.id =
                   t1.hi_temp - t1.id)
           OR (t3.id = t2.id + 1
               AND t3.hi_temp - t3.id =
                   t1.hi_temp - t1.id)
    );

Figure 15.10Result of Listing 15.10.

StartSeq EndSeq SeqSize
-------- ------ -------
       6      9       4
      13     15       3

Listing 15.10 is a lot to take in at first glance, but it’s easier to understand it if you look at it piecemeal. Then you’ll be able to understand the rest of the queries in this section.

The subquery’s WHERE clause subtracts id from hi_temp, yielding (internally):

id hi_temp diff
-- ------- ----
 1      49   48
 2      46   44
 3      48   45
 4      50   46
 5      50   45
 6      50   44
 7      51   44
 8      52   44
 9      53   44
10      50   40
11      50   39
12      47   35
13      50   37
14      51   37
15      52   37

In the column diff, note that successive differences are constant for sequences (50 − 6 = 44, 51 − 7 = 44, and so on). To find neighboring rows, the outer query cross-joins two instances of the same table (t1 and t2), as described in “Calculating Running Statistics” earlier in this chapter. The condition

WHERE (t1.id < t2.id)

guarantees that any t1 row represents an element with an index (id) lower than the corresponding t2 row.

The subquery detects sequence breaks with the condition

t3.hi_temp - t3.id <> t1.hi_temp - t1.id

The third instance of temps (t3) in the subquery is used to determine whether any row in a candidate sequence (t3) has the same difference as the sequence’s first row (t1). If so, then it’s a sequence member. If not, then the candidate pair (t1 and t2) is rejected.

The last two OR conditions determine whether the candidate sequence’s borders can expand. A row that satisfies these conditions means the current candidate sequence can be extended and is rejected in favor of a longer one.

To find only sequences larger than n rows, add the WHERE condition

AND (t2.id - t1.id) >= n - 1

To change Listing 15.10 to find all sequences of four or more rows, for example, replace

WHERE (t1.id < t2.id)

with

WHERE (t1.id < t2.id)
  AND (t2.id - t1.id) >= 3

The result is:

StartSeq EndSeq SeqSize
-------- ------ -------
       6      9       4

Listing 15.11 finds all the runs in temps and lists each run’s start position, end position, and length. See Figure 15.11 for the result.

The logic of this query is similar to that of the preceding one but accounts for run values needing only to increase, not (necessarily) be consecutive. The difference between id and hi_temp values doesn’t have to be constant, so a fourth instance of temps (t4) is needed. The subquery cross-joins t3 and t4 to check rows in the middle of a candidate run, whose borders are t1 and t2. For every element between t1 and t2 (limited by BETWEEN), t3 and its predecessor t4 are compared to see whether their values are increasing.

Listing 15.11List the starting point, ending point, and length of each run in the table temps. See Figure 15.11 for the result.

SELECT
    t1.id AS StartRun,
    t2.id AS EndRun,
    t2.id - t1.id + 1 AS RunLen
  FROM temps t1, temps t2
  WHERE (t1.id < t2.id)
    AND NOT EXISTS(
      SELECT *
        FROM temps t3, temps t4
        WHERE (t3.hi_temp <= t4.hi_temp
               AND t4.id = t3.id - 1
               AND t3.id BETWEEN
                   t1.id + 1 AND t2.id)
           OR (t3.id = t1.id - 1
               AND t3.hi_temp < t1.hi_temp)
           OR (t3.id = t2.id + 1
               AND t3.hi_temp > t2.hi_temp)
    );

Figure 15.11Result of Listing 15.11.

StartRun EndRun RunLen
-------- ------ ------
       2      4      3
       6      9      4
      12     15      4

Listing 15.12 finds all regions in temps with a high temperature of 50 and lists each region’s start position, end position, and length. See Figure 15.12 for the result.

Listing 15.12List the starting point, ending point, and length of each region (with value 50) in the table temps. See Figure 15.12 for the result.

SELECT
    t1.id AS StartReg,
    t2.id AS EndReg,
    t2.id - t1.id + 1 AS RegLen
  FROM temps t1, temps t2
  WHERE (t1.id < t2.id)
    AND NOT EXISTS(
      SELECT *
        FROM temps t3
        WHERE (t3.hi_temp <> 50
               AND t3.id BETWEEN
                   t1.id AND t2.id)
           OR (t3.id = t1.id - 1
               AND t3.hi_temp = 50)
           OR (t3.id = t2.id + 1
               AND t3.hi_temp = 50)
    );

Figure 15.12Result of Listing 15.12.

StartReg EndReg RegLen
-------- ------ ------
       4      6      3
      10     11      2

Listing 15.13 is a variation of Listing 15.12 that finds all regions of length 2. See Figure 15.13 for the result. Note that overlapping subregions are listed. To return regions of length n, change the WHERE clause’s second condition to:

AND t2.id - t1.id = n - 1

Listing 15.13List all regions of length 2. See Figure 15.13 for the result.

SELECT
    t1.id AS StartReg,
    t2.id AS EndReg,
    t2.id - t1.id + 1 AS RegLen
  FROM temps t1, temps t2
  WHERE (t1.id < t2.id)
    AND t2.id - t1.id = 1
    AND NOT EXISTS(
      SELECT *
        FROM temps t3
        WHERE (t3.hi_temp <> 50
          AND t3.id BETWEEN t1.id AND t2.id)
    );

Figure 15.13Result of Listing 15.13.

StartReg EndReg RegLen
-------- ------ ------
       4      5      2
       5      6      2
      10     11      2

Tips for Sequences, Runs, and Regions

Limiting the Number of Rows Returned

In practice it’s common to use queries that return a certain number (n) of rows that fall at the top or the bottom of a range specified by an ORDER BY clause. SQL doesn’t require an ORDER BY clause, but if you omit it, then the query will return an unsorted set of rows (because SQL doesn’t promise to deliver query results in any particular order without an ORDER BY clause).

The examples in this section use the table empsales (Listing 15.14 and Figure 15.14), which lists sales figures by employee. Note that some employees have the same sales amounts. A correct query for the top three salespeople in empsales actually will return four rows: employees E09, E02, E10, and E05. Ties shouldn’t force the query to choose arbitrarily between equal values (E10 and E05 in this case). No standard terminology exists, but queries that return at most n rows (regardless of ties) sometimes are called limit queries. Queries that include ties and return possibly more than n rows are top-n queries or quota queries.

You can also use limit and quota queries to limit the number of rows affected by an UPDATE or DELETE statement. Some limit and quota queries might be invalid in some contexts (such as in subqueries or views); see your DBMS documentation.

Listing 15.14List employees by descending sales. See Figure 15.14 for the result.

SELECT emp_id, sales
  FROM empsales
  ORDER BY sales DESC;

Figure 15.14Result of Listing 15.14.

emp_id sales
------ -----
E09      900
E02      800
E10      700
E05      700
E01      600
E04      500
E03      500
E06      500
E08      400
E07      300

The SQL:2003 standard introduced the functions ROW_NUMBER() and RANK() to use in limit and top-n queries. Microsoft SQL Server, Oracle, and Db2 support both functions. Queries that use pre-2003 SQL are complex, unintuitive, and run slowly (see the tips at the end of this section for an SQL-92 example). The SQL standard has lagged DBMSs, which for years have offered nonstandard extensions to create these types of queries. Some DBMSs also let you return a percentage of rows (rather than a fixed n) or return offsets by skipping a specified number of initial rows (returning rows 3–8 instead of 1–5, for example). This section covers the DBMS extensions individually.

Microsoft Access

Listing 15.15 lists the top three salespeople, including ties. See Figure 15.15 for the result. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

The TOP clause always includes ties. Its syntax is:

TOP n [PERCENT]

Listing 15.15List the top three salespeople, with ties. See Figure 15.15 for the result.

SELECT TOP 3 emp_id, sales
  FROM empsales
  ORDER BY sales DESC;

Figure 15.15Result of Listing 15.15.

emp_id sales
------ -----
E09      900
E02      800
E10      700
E05      700

Listing 15.16 lists the bottom 40 percent of salespeople, including ties. See Figure 15.16 for the result. This query orders lowest to highest; to reverse the order, change ASC to DESC in the ORDER BY clause.

Listing 15.16List the bottom 40 percent of salespeople, with ties. See Figure 15.16 for the result.

SELECT TOP 40 PERCENT emp_id, sales
  FROM empsales
  ORDER BY sales ASC;

Figure 15.16Result of Listing 15.16.

emp_id sales
------ -----
E07      300
E08      400
E06      500
E04      500
E03      500

Tips for Microsoft Access

Microsoft SQL Server

Listing 15.17 lists the top three salespeople, not including ties. See Figure 15.17 for the result. Note that this query is inconsistent when ties exist; rerunning it can return either E10 or E05, depending on how ORDER BY sorts the table. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

The TOP clause’s syntax is:

TOP n [PERCENT] [WITH TIES]

Listing 15.17List the top three salespeople, without ties. See Figure 15.17 for the result.

SELECT TOP 3 emp_id, sales
  FROM empsales
  ORDER BY sales DESC;

Figure 15.17Result of Listing 15.17.

emp_id sales
------ -----
E09      900
E02      800
E05      700

Listing 15.18 lists the top three salespeople, including ties. See Figure 15.18 for the result. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

Listing 15.18List the top three salespeople, with ties. See Figure 15.18 for the result.

SELECT TOP 3 WITH TIES emp_id, sales
  FROM empsales
  ORDER BY sales DESC;

Figure 15.18Result of Listing 15.18.

emp_id sales
------ -----
E09      900
E02      800
E05      700
E10      700

Listing 15.19 lists the bottom 40 percent of salespeople, including ties. See Figure 15.19 for the result. This query orders lowest to highest; to reverse the order, change ASC to DESC in the ORDER BY clause.

Listing 15.19List the bottom 40 percent of salespeople, with ties. See Figure 15.19 for the result.

SELECT TOP 40 PERCENT WITH TIES
    emp_id, sales
  FROM empsales
  ORDER BY sales ASC;

Figure 15.19Result of Listing 15.19.

emp_id sales
------ -----
E07      300
E08      400
E06      500
E03      500
E04      500

Tips for Microsoft SQL Server

Oracle Database

Use the built-in ROWNUM pseudocolumn to limit the number of rows returned. The first row selected has a ROWNUM of 1, the second has 2, and so on. Use the window function RANK() to include ties.

Listing 15.20 lists the top three salespeople, not including ties. See Figure 15.20 for the result. Note that this query is inconsistent when ties exist; rerunning it can return either E10 or E05, depending on how ORDER BY sorts the table. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

Listing 15.20List the top three salespeople, without ties. See Figure 15.20 for the result.

SELECT emp_id, sales
  FROM (
    SELECT *
      FROM empsales
      ORDER BY sales DESC)
  WHERE ROWNUM <= 3;

Figure 15.20Result of Listing 15.20.

emp_id sales
------ -----
E09      900
E02      800
E05      700

Listing 15.21 lists the top three salespeople, including ties. See Figure 15.21 for the result. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

Listing 15.21List the top three salespeople, with ties. See Figure 15.21 for the result.

SELECT emp_id, sales
  FROM (
    SELECT
      RANK() OVER
        (ORDER BY sales DESC)
          AS sales_rank,
      emp_id,
      sales
    FROM empsales)
  WHERE sales_rank <= 3;

Figure 15.21Result of Listing 15.21.

emp_id sales
------ -----
E09      900
E02      800
E05      700
E10      700

Tips for Oracle Database

IBM Db2 Database

Listing 15.22 lists the top three salespeople, not including ties. See Figure 15.22 for the result. Note that this query is inconsistent when ties exist; rerunning it can return either E10 or E05, depending on how ORDER BY sorts the table. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

The FETCH clause’s syntax is:

FETCH FIRST n ROW[S] ONLY

Listing 15.22List the top three salespeople, without ties. See Figure 15.22 for the result.

SELECT emp_id, sales
  FROM empsales
  ORDER BY sales DESC
  FETCH FIRST 3 ROWS ONLY;

Figure 15.22Result of Listing 15.22.

emp_id sales
------ -----
E09      900
E02      800
E05      700

Listing 15.23 lists the top three salespeople, including ties. See Figure 15.23 for the result. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

Listing 15.23List the top three salespeople, with ties. See Figure 15.23 for the result.

SELECT emp_id, sales
  FROM (
    SELECT
      RANK() OVER
        (ORDER BY sales DESC)
          AS sales_rank,
      emp_id,
      sales
    FROM empsales)
      AS any_name
  WHERE sales_rank <= 3;

Figure 15.23Result of Listing 15.23.

emp_id sales
------ -----
E09      900
E02      800
E05      700
E10      700

Tips for IBM Db2 Database

MySQL

Listing 15.24 lists the top three salespeople, not including ties. See Figure 15.24 for the result. Note that this query is inconsistent when ties exist; rerunning it can return either E10 or E05, depending on how ORDER BY sorts the table. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

The LIMIT clause’s syntax is:

LIMIT n [OFFSET skip]

or

LIMIT [skip,] n

The offset of the initial row is 0 (not 1).

Listing 15.24List the top three salespeople, without ties. See Figure 15.24 for the result.

SELECT emp_id, sales
  FROM empsales
  ORDER BY sales DESC
  LIMIT 3;

Figure 15.24Result of Listing 15.24.

emp_id sales
------ -----
E09      900
E02      800
E10      700

Listing 15.25 lists the top three salespeople, including ties. The OFFSET value is n − 1 = 2. COALESCE()’s second argument lets the query work in case the table has fewer than n rows; see “Checking for Nulls with COALESCE()” in Chapter 5. See Figure 15.25 for the result. This query orders highest to lowest; to reverse the order, change >= to <= in the comparison, change MIN() to MAX() in the second subquery, and change DESC to ASC in each ORDER BY clause.

Listing 15.25List the top three salespeople, with ties. See Figure 15.25 for the result.

SELECT emp_id, sales
  FROM empsales
  WHERE sales >= COALESCE(
    (SELECT sales
       FROM empsales
       ORDER BY sales DESC
       LIMIT 1 OFFSET 2),
    (SELECT MIN(sales)
       FROM empsales))
  ORDER BY sales DESC;

Figure 15.25Result of Listing 15.25.

emp_id sales
------ -----
E09      900
E02      800
E05      700
E10      700

Listing 15.26 lists the top three salespeople, skipping the initial four rows. See Figure 15.26 for the result. Note that this query is inconsistent when ties exist. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

Listing 15.26List the top three salespeople, skipping the initial four rows. See Figure 15.26 for the result.

SELECT emp_id, sales
  FROM empsales
  ORDER BY sales DESC
  LIMIT 3 OFFSET 4;

Figure 15.26Result of Listing 15.26.

emp_id sales
------ -----
E01      600
E04      500
E03      500

PostgreSQL

Listing 15.27 lists the top three salespeople, not including ties. See Figure 15.27 for the result. Note that this query is inconsistent when ties exist; rerunning it can return either E10 or E05, depending on how ORDER BY sorts the table. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

The LIMIT clause’s syntax is:

LIMIT n [OFFSET skip]

The offset of the initial row is 0 (not 1).

PostgreSQL 13 and later versions support the OFFSET and FETCH clauses:

OFFSET skip [ROW | ROWS]

FETCH [FIRST | NEXT] [n] [ROW | ROWS]
  [ONLY | WITH TIES]

If n is omitted in a FETCH clause, then it defaults to 1. For example,

FETCH FIRST WITH TIES

returns any additional rows that match the last row.

The queries in this section use the LIMIT clause to ensure backward compatibility.

Listing 15.27List the top three salespeople, without ties. See Figure 15.27 for the result.

SELECT emp_id, sales
  FROM empsales
  ORDER BY sales DESC
  LIMIT 3;

Figure 15.27Result of Listing 15.27.

emp_id sales
------ -----
E09      900
E02      800
E05      700

Listing 15.28 lists the top three salespeople, including ties. The OFFSET value is n − 1 = 2. See Figure 15.28 for the result. This query orders highest to lowest; to reverse the order, change >= to <= in the comparison and change DESC to ASC in each ORDER BY clause.

Listing 15.28List the top three salespeople, with ties. See Figure 15.28 for the result.

SELECT emp_id, sales
  FROM empsales
  WHERE (
    sales >= (
      SELECT sales
        FROM empsales
        ORDER BY sales DESC
        LIMIT 1 OFFSET 2)
  ) IS NOT FALSE
  ORDER BY sales DESC;

Figure 15.28Result of Listing 15.28.

emp_id sales
------ -----
E09      900
E02      800
E10      700
E05      700

Listing 15.29 lists the top three salespeople, skipping the initial four rows. See Figure 15.29 for the result. Note that this query is inconsistent when ties exist. This query orders highest to lowest; to reverse the order, change DESC to ASC in the ORDER BY clause.

Listing 15.29List the top three salespeople, skipping the initial four rows. See Figure 15.29 for the result.

SELECT emp_id, sales
  FROM empsales
  ORDER BY sales DESC
  LIMIT 3 OFFSET 4;

Figure 15.29Result of Listing 15.29.

emp_id sales
------ -----
E01      600
E06      500
E03      500

Tips for Limiting the Number of Rows Returned

Assigning Ranks

Ranking, which allocates the numbers 1, 2, 3,... to sorted values, is related to top-n queries and shares the problem of interpreting ties. The following queries calculate ranks for sales values in the table empsales from “Limiting the Number of Rows Returned”.

Listings 15.30a to 15.30e rank employees by sales. The first two queries show the most commonly accepted ways to rank values. The other queries show variations on them. Figure 15.30 shows the result of each ranking method, a to e, combined for brevity and ease of comparison. These queries rank highest to lowest; to reverse the order, change > (or >=) to < (or <=) in the WHERE comparisons. You can add the clause ORDER BY ranking ASC to a query’s outer SELECT to sort the results by rank.

Listing 15.30aRank employees by sales (method a). See Figure 15.30 for the result.

SELECT e1.emp_id, e1.sales,
    (SELECT COUNT(sales)
       FROM empsales e2
       WHERE e2.sales >= e1.sales)
         AS ranking
  FROM empsales e1;

Listing 15.30bRank employees by sales (method b). See Figure 15.30 for the result.

SELECT e1.emp_id, e1.sales,
    (SELECT COUNT(sales)
       FROM empsales e2
       WHERE e2.sales > e1.sales) + 1
         AS ranking
  FROM empsales e1;

Listing 15.30cRank employees by sales (method c). See Figure 15.30 for the result.

SELECT e1.emp_id, e1.sales,
    (SELECT COUNT(sales)
       FROM empsales e2
       WHERE e2.sales > e1.sales)
         AS ranking
  FROM empsales e1;

Listing 15.30dRank employees by sales (method d). See Figure 15.30 for the result.

SELECT e1.emp_id, e1.sales,
    (SELECT COUNT(DISTINCT sales)
       FROM empsales e2
       WHERE e2.sales >= e1.sales)
         AS ranking
  FROM empsales e1;

Listing 15.30eRank employees by sales (method e). See Figure 15.30 for the result.

SELECT e1.emp_id, e1.sales,
    (SELECT COUNT(DISTINCT sales)
       FROM empsales e2
       WHERE e2.sales > e1.sales)
         AS ranking
  FROM empsales e1;

Figure 15.30Compilation of results of Listings 15.30a to 15.30e.

emp_id sales a  b  c  d  e
------ ----- -- -- -- -- --
E09      900  1  1  0  1  0
E02      800  2  2  1  2  1
E10      700  4  3  2  3  2
E05      700  4  3  2  3  2
E01      600  5  5  4  4  3
E04      500  8  6  5  5  4
E03      500  8  6  5  5  4
E06      500  8  6  5  5  4
E08      400  9  9  8  6  5
E07      300 10 10  9  7  6

These ranking queries use correlated subqueries and so run slowly. If you’re ranking a large number of items, then you should use a built-in rank function, if available. The SQL:2003 standard introduced the functions RANK() and DENSE_RANK(), which Microsoft SQL Server, Oracle, Db2, MySQL, and PostgreSQL support. Alternatively, you can use your DBMS’s SQL extensions to calculate ranks efficiently. The following MySQL script, for example, is equivalent to Listing 15.30b:

SET @rownum = 0;
SET @rank = 0;
SET @prev_val = NULL;
SELECT
    @rownum := @rownum + 1 AS row,
    @rank := IF(@prev_val <> sales,
      @rownum, @rank) AS rank,
    @prev_val := sales AS sales
  FROM empsales
  ORDER BY sales DESC;

Microsoft Access doesn’t support COUNT(DISTINCT) and won’t run Listing 15.30d and Listing 15.30e. For a workaround, see “Aggregating Distinct Values with DISTINCT” in Chapter 6.

Calculating a Trimmed Mean

The trimmed mean is a robust order statistic that is the arithmetic mean (average) of the data if the k smallest values and k largest values are discarded. The goal is to avoid the influence of extreme observations.

Listing 15.31 calculates the trimmed mean of book sales in the sample database by omitting the top three and bottom three sales figures. See Figure 15.31 for the result. For reference, the 12 sorted sales values are 566, 4095, 5000, 9566, 10467, 11320, 13001, 25667, 94123, 100001, 201440, and 1500200. This query discards 566, 4095, 5000, 100001, 201440, and 1500200 and then calculates the mean in the usual way by using the remaining six middle values. Nulls are ignored. Duplicate values are either all removed or all retained. (If all sales are the same, for example, then none of them will be trimmed no matter what k is.)

Listing 15.31Calculate the trimmed mean for k = 3. See Figure 15.31 for the result.

SELECT AVG(sales) AS TrimmedMean
  FROM titles t1
  WHERE
    (SELECT COUNT(*)
       FROM titles t2
       WHERE t2.sales <= t1.sales) > 3
    AND
    (SELECT COUNT(*)
       FROM titles t3
       WHERE t3.sales >= t1.sales) > 3;

Figure 15.31Result of Listing 15.31.

TrimmedMean
-----------
 27357.3333

Listing 15.32 is similar to Listing 15.31 but trims a fixed percentage of the extreme values rather than a fixed number. Trimming by 0.25 (25%), for example, discards the sales in the top and bottom quartiles and averages what’s left. See Figure 15.32 for the result.

Listing 15.32Calculate the trimmed mean by discarding the lower and upper 25% of values. See Figure 15.32 for the result.

SELECT AVG(sales) AS TrimmedMean
  FROM titles t1
  WHERE
    (SELECT COUNT(*)
       FROM titles t2
       WHERE t2.sales <= t1.sales) >=
         (SELECT 0.25 * COUNT(*)
           FROM titles)
    AND
    (SELECT COUNT(*)
       FROM titles t3
       WHERE t3.sales >= t1.sales) >=
         (SELECT 0.25 * COUNT(*)
           FROM titles);

Figure 15.32Result of Listing 15.32.

TrimmedMean
-----------
 27357.3333

Microsoft SQL Server and Db2 return an integer for the trimmed mean because the column sales is defined as an INTEGER. To get a floating-point value, change AVG(sales) to AVG(CAST(sales AS FLOAT)). For more information, see “Converting Data Types with CAST()” in Chapter 5.

Picking Random Rows

Some databases are so large, and queries on them so complex, that often it’s impractical (and unnecessary) to retrieve all the data relevant to a query. If you’re interested in finding an overall trend or pattern, for example, then an approximate answer within some margin of error will usually suffice. One way to speed such queries is to select a random sample of rows. An efficient sample can improve performance by orders of magnitude yet still yield accurate results.

Standard SQL’s TABLESAMPLE clause returns a random subset of rows. Microsoft SQL Server, Db2, and PostgreSQL support TABLESAMPLE, and Oracle has a similar feature. For the other DBMSs, use a (nonstandard) function that returns a uniform random number between 0 and 1 (Table 15.1).

Table 15.1Randomization Features
DBMS Clause or Function
Access RND() function
SQL Server TABLESAMPLE clause
Oracle SAMPLE clause or DBMS_RANDOM package
Db2 TABLESAMPLE clause
MySQL RAND() function
PostgreSQL TABLESAMPLE clause

Listing 15.33a randomly picks about 25% (0.25) of the rows from the sample-database table titles. If necessary, change RAND() to the function that appears in Table 15.1 for your DBMS. For Oracle, use Listing 15.33b. For Microsoft SQL Server, Db2, and PostgreSQL, use Listing 15.33c.

Listing 15.33aSelect about 25% percent of the rows in the table titles at random. See Figure 15.33 for a possible result.

SELECT title_id, type, sales
  FROM titles
  WHERE RAND() < 0.25;

Listing 15.33bSelect about 25% percent of the rows in the table titles at random (Oracle only). See Figure 15.33 for a possible result.

SELECT title_id, type, sales
  FROM titles
  SAMPLE (25);

Listing 15.33cSelect about 25% percent of the rows in the table titles at random (Microsoft SQL Server, Db2, and PostgreSQL only). See Figure 15.33 for a possible result.

SELECT title_id, type, sales
  FROM titles
  TABLESAMPLE SYSTEM (25);

Figure 15.33 shows one possible result of a random selection. The rows and the number of rows returned will change every time that you run the query. If you need an exact number of random rows, then increase the sampling percentage and use one of the techniques described in “Limiting the Number of Rows Returned” earlier in this chapter.

Figure 15.33One possible result of Listing 15.33a/b/c.

title_id type       sales
-------- ---------- -----
T03      computer   25667
T04      psychology 13001
T11      psychology 94123

Tips for Picking Random Rows

Selecting Every nth Row

Instead of picking random rows, you can pick every nth row by using a modulo expression:

This expression returns the remainder of m divided by n. For example, MOD(20, 6) is 2 because 20 equals (3 × 6) + 2. MOD(a, 2) is 0 if a is an even number.

The condition MOD(rownumber, n) = 0 picks every nth row, where rownumber is a column of consecutive integers or row identifiers. This Oracle query, for example, picks every third row in a table:

SELECT *
  FROM table
  WHERE (ROWID,0) IN
    (SELECT ROWID, MOD(ROWNUM,3)
       FROM table);

Note that rownumber imposes a row order that doesn’t exist implicitly in a relational-database table.

Handling Duplicates

Normally you use SQL’s PRIMARY KEY or UNIQUE constraints to prevent duplicate rows from appearing in production tables. But you need to know how to handle duplicates that appear when you accidentally import the same data twice or import data from a nonrelational source such as a spreadsheet or accounting package, where redundant information is rampant. This section describes how to detect, count, and remove duplicates.

Suppose that you import rows into a staging table to detect and eliminate any duplicates before inserting the data into a production table (Listing 15.34 and Figure 15.34). The column id is a unique row identifier that lets you identify and select rows that otherwise would be duplicates. If your imported rows don’t already have an identity column, then you can add one yourself; see “Unique Identifiers” and “Generating Sequences”. It’s a good practice to add an identity column to even short-lived working tables, but in this case it also makes deleting duplicates easy. The imported data might include other columns too, but you’ve decided that the combination of only book title, book type, and price determines whether a row is a duplicate, regardless of the values in any other columns. Before you identify or delete duplicates, you must define exactly what it means for two rows to be considered “duplicates” of each other.

Listing 15.34List the imported rows. See Figure 15.34 for the result.

SELECT id, title_name, type, price
  FROM dups;

Figure 15.34Result of Listing 15.34.

id title_name   type      price
-- ------------ --------- -----
 1 Book Title 5 children  15.00
 2 Book Title 3 biography  7.00
 3 Book Title 1 history   10.00
 4 Book Title 2 children  20.00
 5 Book Title 4 history   15.00
 6 Book Title 1 history   10.00
 7 Book Title 3 biography  7.00
 8 Book Title 1 history   10.00

Listing 15.35 lists only the duplicates by counting the number of occurrences of each unique combination of title_name, type, and price. See Figure 15.35 for the result. If this query returns an empty result, then the table contains no duplicates. To list only the nonduplicates, change COUNT(*) > 1 to COUNT(*) = 1.

Listing 15.35List only duplicates. See Figure 15.35 for the result.

SELECT title_name, type, price
  FROM dups
  GROUP BY title_name, type, price
  HAVING COUNT(*) > 1;

Figure 15.35Result of Listing 15.35.

title_name   type      price
------------ --------- -----
Book Title 1 history   10.00
Book Title 3 biography  7.00

Listing 15.36 uses a similar technique to list each row and its duplicate count. See Figure 15.36 for the result. To list only the duplicates, change COUNT(*) >= 1 to COUNT(*) > 1.

Listing 15.36List each row and its number of repetitions. See Figure 15.36 for the result.

SELECT title_name, type, price,
    COUNT(*) AS NumDups
  FROM dups
  GROUP BY title_name, type, price
  HAVING COUNT(*) >= 1
  ORDER BY COUNT(*) DESC;

Figure 15.36Result of Listing 15.36.

title_name   type      price NumDups
------------ --------- ----- -------
Book Title 1 history   10.00       3
Book Title 3 biography  7.00       2
Book Title 4 history   15.00       1
Book Title 2 children  20.00       1
Book Title 5 children  15.00       1

Listing 15.37 deletes duplicate rows from dups in place. This statement uses the column id to leave exactly one occurrence (the one with the highest ID) of each duplicate. Figure 15.37 shows the table dups after running this statement. See also “Deleting Rows with DELETE” in Chapter 10.

Listing 15.37Remove the redundant duplicates in place. See Figure 15.37 for the result.

DELETE FROM dups
  WHERE id < (
    SELECT MAX(d.id)
      FROM dups d
      WHERE dups.title_name = d.title_name
        AND dups.type = d.type
        AND dups.price = d.price);

Figure 15.37Result of Listing 15.37.

id  title_name  type      price
-- ------------ --------- -----
 1 Book Title 5 children  15.00
 4 Book Title 2 children  20.00
 5 Book Title 4 history   15.00
 7 Book Title 3 biography  7.00
 8 Book Title 1 history   10.00

Tips for Handling Duplicates

Messy Data

Deleting duplicates gets harder as data get messier. It’s not unusual to buy a mailing list containing entries that look like this:

name          address1
----------    ------------------
John Smith    123 Main St
John Smith    123 Main St, Apt 1
Jack Smiht    121 Main Rd
John Symthe   123 Main St.
Jon Smith     123 Mian Street

DBMSs offer nonstandard tools such as Soundex (phonetic) functions to suppress spelling variations, but creating an automated deletion program that works over thousands or millions of rows is a major project.

Creating a Telephone List

You can use the function COALESCE() with a left outer join to create a convenient telephone listing from a normalized table of telephone numbers. Suppose that the sample database has an extra table named telephones that stores the authors’ work and home telephone numbers:

au_id tel_type tel_no
----- -------- ------------
A01   H        111-111-1111
A01   W        222-222-2222
A02   W        333-333-3333
A04   H        444-444-4444
A04   W        555-555-5555
A05   H        666-666-6666

The table’s composite primary key is (au_id, tel_type), where tel_type indicates whether tel_no is a work (W) or home (H) number. Listing 15.38 lists the authors’ names and numbers. If an author has only one number, then that number is listed. If an author has both home and work numbers, then only the work number is listed. Authors with no numbers aren’t listed. See Figure 15.38 for the result.

The first left join picks out the work numbers, and the second picks out the home numbers. The WHERE clause filters out authors with no numbers. (You can extend this query to add mobile and other numbers.)

Listing 15.38Lists the authors’ names and telephone numbers, favoring work numbers over home numbers. See Figure 15.38 for the result.

SELECT
    a.au_id AS "ID",
    a.au_fname AS "FirstName",
    a.au_lname AS "LastName",
    COALESCE(twork.tel_no, thome.tel_no)
      AS "TelNo",
    COALESCE(twork.tel_type, thome.tel_type)
      AS "TelType"
  FROM authors a
  LEFT OUTER JOIN telephones twork
    ON a.au_id = twork.au_id
    AND twork.tel_type = 'W'
  LEFT OUTER JOIN telephones thome
    ON a.au_id = thome.au_id
    AND thome.tel_type = 'H'
  WHERE COALESCE(twork.tel_no, thome.tel_no)
    IS NOT NULL
  ORDER BY a.au_fname ASC, a.au_lname ASC;

Figure 15.38Result of Listing 15.38.

ID  FirstName LastName  TelNo        TelType
--- --------- --------- ------------ -------
A05 Christian Kells     666-666-6666 H
A04 Klee      Hull      555-555-5555 W
A01 Sarah     Buchman   222-222-2222 W
A02 Wendy     Heydemark 333-333-3333 W

Microsoft Access won’t run Listing 15.38 because of the restrictions Access puts on join expressions.

Retrieving Metadata

Metadata are data about data. In DBMSs, metadata include information about schemas, databases, users, tables, columns, and so on. When meeting a new database, inspect its metadata: What’s in the database? How big is it? How are the tables organized?

Metadata, like other data, are stored in tables and so can be accessed via SELECT queries. Metadata also can be accessed, often more conveniently, by using command-line and graphical tools. The following listings show DBMS-specific examples for viewing metadata. The DBMS itself maintains metadata—look, but don’t touch.

The SQL standard calls a set of metadata a catalog and specifies that it be accessed through the schema INFORMATION_SCHEMA. Not all DBMSs implement this schema or use the same terms. In Microsoft SQL Server, for example, the equivalent term for a catalog is a database and for a schema, an owner. In Oracle, the repository of metadata is the data dictionary.

Microsoft Access

Microsoft Access metadata are available graphically through the Design View of each database object and programmatically through the Visual Basic for Applications (VBA) or C# language. Access also creates and maintains hidden system tables in each database.

To show system tables in Microsoft Access:

The system tables begin with MSys and are commingled with the database’s other tables. You can open and query them as you would ordinary tables. The most interesting system table is MSysObjects, which catalogs all the objects in the database. Listing 15.39 lists all the tables in the current database. Note that system tables don’t have to be visible to be used in queries.

Listing 15.39List the tables in the current Microsoft Access database. To list queries instead, change Type = 1 to Type = 5.

SELECT Name
  FROM MSysObjects
  WHERE Type = 1;

Microsoft SQL Server

Microsoft SQL Server metadata are available through the schema INFORMATION_SCHEMA and via system stored procedures (Listing 15.40).

Listing 15.40Metadata statements and commands for Microsoft SQL Server.

-- List the databases.
exec sp_helpdb;

-- List the schemas.
SELECT schema_name
  FROM information_schema.schemata;

-- List the tables (Method 1).
SELECT *
  FROM information_schema.tables
  WHERE table_type = 'BASE TABLE'
    AND table_schema = 'schema_name';

-- List the tables (Method 2).
exec sp_tables;

-- Describe a table (Method 1).
SELECT *
  FROM information_schema.columns
  WHERE table_catalog = 'db_name'
    AND table_schema = 'schema_name'
    AND table_name = 'table_name';

-- Describe a table (Method 2).
exec sp_help table_name;

Oracle Database

Oracle metadata are available through data dictionary views and via the sqlplus command-line tool (Listing 15.41). To list data dictionary views, run this query in sqlplus:

SELECT table_name, comments
  FROM dictionary
  ORDER BY table_name;

For a list of Oracle databases (instances) in Unix or Linux, look in the file oratab located in the directory /etc or /var/opt/oracle. In Windows, run this command at a command prompt:

net start | find /i "OracleService"

Or choose Start > Run (Windows Logo Key + R), type services.msc, press Enter, and then inspect the Services list for entries that begin with OracleService.

Listing 15.41Metadata statements and commands for Oracle Database.

-- List the schemas (users).
SELECT *
  FROM all_users;

-- List the tables.
SELECT table_name
  FROM all_tables
  WHERE owner = 'user_name';

-- Describe a table (Method 1).
SELECT *
  FROM all_tab_columns
  WHERE owner = 'user_name'
    AND table_name = 'table_name';

-- Describe a table (Method 2, in sqlplus).
DESCRIBE table_name;

IBM Db2 Database

Db2 metadata are available through the system catalog SYSCAT and via the db2 command-line tool (Listing 15.42).

Listing 15.42Metadata statements and commands for IBM Db2 Database.

-- List the databases (in db2).
LIST DATABASE DIRECTORY;

-- List the schemas.
SELECT schemaname
  FROM syscat.schemata;

-- List the tables (Method 1).
SELECT tabname
  FROM syscat.tables
  WHERE tabschema = 'schema_name';

-- List the tables (Method 2, in db2).
LIST TABLES;

-- List the tables (Method 3, in db2).
LIST TABLES FOR SCHEMA schema_name;

-- Describe a table (Method 1).
SELECT *
  FROM syscat.columns
  WHERE tabname = 'table_name'
    AND tabschema = 'schema_name';

-- Describe a table (Method 2, in db2).
DESCRIBE TABLE table_name SHOW DETAIL;

MySQL

MySQL metadata are available through the schema INFORMATION_SCHEMA and via the mysql command-line tool (Listing 15.43).

Listing 15.43Metadata statements and commands for MySQL.

-- List the databases (Method 1).
SELECT schema_name
  FROM information_schema.schemata;

-- List the databases (Method 2, in mysql).
SHOW DATABASES;

-- List the tables (Method 1).
SELECT table_name
  FROM information_schema.tables
  WHERE table_schema = 'db_name';

-- List the tables (Method 2, in mysql).
SHOW TABLES;

-- Describe a table (Method 1).
SELECT *
  FROM information_schema.columns
  WHERE table_schema = 'db_name'
    AND table_name = 'table_name';

-- Describe a table (Method 2, in mysql).
DESCRIBE table_name;

PostgreSQL

PostgreSQL metadata are available through the schema INFORMATION_SCHEMA and via the psql command-line tool (Listing 15.44).

Listing 15.44Metadata statements and commands for PostgreSQL.

-- List the databases (Method 1).
psql --list

-- List the databases (Method 2, in psql).
\l

-- List the schemas.
SELECT schema_name
  FROM information_schema.schemata;

-- List the tables (Method 1).
SELECT table_name
  FROM information_schema.tables
  WHERE table_schema = 'schema_name';

-- List the tables (Method 2, in psql).
\dt

-- Describe a table (Method 1).
SELECT *
  FROM information_schema.columns
  WHERE table_schema = 'schema_name'
    AND table_name = 'table_name';

-- Describe a table (Method 2, in psql).
\d table_name;

Working with Dates

As pointed out in “Performing Datetime and Interval Arithmetic” and “Getting the Current Date and Time” in Chapter 5, DBMSs provide their own extended (nonstandard) functions for manipulating dates and times. This section explains how to use these built-in functions to do simple date arithmetic. The queries in each listing:

Microsoft Access

The function datepart() extracts the specified part of a datetime. now() returns the current (system) date and time. dateadd() adds a specified time interval to a date. datediff() returns the number of specified time intervals between two dates (Listing 15.45). Alternatives to datepart() are the extraction functions second(), day(), month(), and so on.

Listing 15.45Working with dates in Microsoft Access.

-- Extract parts of the current datetime.
SELECT
  datepart("s",   now()) AS sec_pt,
  datepart("n",   now()) AS min_pt,
  datepart("h",   now()) AS hr_pt,
  datepart("d",   now()) AS day_pt,
  datepart("m",   now()) AS mon_pt,
  datepart("yyyy",now()) AS yr_pt;

-- Add or subtract days, months, and years.
SELECT
    dateadd("d",    2,pubdate) AS p2d,
    dateadd("d",   -2,pubdate) AS m2d,
    dateadd("m",    2,pubdate) AS p2m,
    dateadd("m",   -2,pubdate) AS m2m,
    dateadd("yyyy", 2,pubdate) AS p2y,
    dateadd("yyyy",-2,pubdate) AS m2y
  FROM titles
  WHERE title_id = 'T05';

-- Count the days between two dates.
SELECT datediff("d",date1,date2) AS days
  FROM
    (SELECT pubdate as date1
       FROM titles
       WHERE title_id = 'T05') t1,
    (SELECT pubdate as date2
       FROM titles
       WHERE title_id = 'T06') t2;

-- Count the months between two dates.
SELECT datediff("m",date1,date2) AS months
  FROM
    (SELECT
         MIN(pubdate) AS date1,
         MAX(pubdate) AS date2
       FROM titles) t1;

Microsoft SQL Server

The function datepart() extracts the specified part of a datetime. getdate() returns the current (system) date and time. dateadd() adds a specified time interval to a date. datediff() returns the number of specified time intervals between two dates (Listing 15.46). Alternatives to datepart() are the extraction functions day(), month(), and year().

Listing 15.46Working with dates in Microsoft SQL Server.

-- Extract parts of the current datetime.
SELECT
  datepart("s",   getdate()) AS sec_pt,
  datepart("n",   getdate()) AS min_pt,
  datepart("hh",  getdate()) AS hr_pt,
  datepart("d",   getdate()) AS day_pt,
  datepart("m",   getdate()) AS mon_pt,
  datepart("yyyy",getdate()) AS yr_pt;

-- Add or subtract days, months, and years.
SELECT
    dateadd("d",    2,pubdate) AS p2d,
    dateadd("d",   -2,pubdate) AS m2d,
    dateadd("m",    2,pubdate) AS p2m,
    dateadd("m",   -2,pubdate) AS m2m,
    dateadd("yyyy", 2,pubdate) AS p2y,
    dateadd("yyyy",-2,pubdate) AS m2y
  FROM titles
  WHERE title_id = 'T05';

-- Count the days between two dates.
SELECT datediff("d",date1,date2) AS days
  FROM
    (SELECT pubdate as date1
       FROM titles
       WHERE title_id = 'T05') t1,
    (SELECT pubdate as date2
       FROM titles
       WHERE title_id = 'T06') t2;

-- Count the months between two dates.
SELECT datediff("m",date1,date2) AS months
  FROM
    (SELECT
         MIN(pubdate) AS date1,
         MAX(pubdate) AS date2
       FROM titles) t1;

Oracle Database

The function to_char() converts a datetime to a character value in the given format. to_number() converts its argument to a number. sysdate returns the current (system) date and time. The standard addition and subtraction operators add and subtract days from a date. add_months() adds a specified number of months to a date. Subtracting one date from another yields the number of days between them. months_between() returns the number of months between two dates (Listing 15.47).

Listing 15.47Working with dates in Oracle Database.

-- Extract parts of the current datetime.
SELECT
    to_number(to_char(sysdate,'ss'))   AS sec_pt,
    to_number(to_char(sysdate,'mi'))   AS min_pt,
    to_number(to_char(sysdate,'hh24')) AS hr_pt,
    to_number(to_char(sysdate,'dd'))   AS day_pt,
    to_number(to_char(sysdate,'mm'))   AS mon_pt,
    to_number(to_char(sysdate,'yyyy')) AS yr_pt
  FROM dual;

-- Add or subtract days, months, and years.
SELECT
    pubdate+2               AS p2d,
    pubdate-2               AS m2d,
    add_months(pubdate, +2) AS p2m,
    add_months(pubdate, -2) AS m2m,
    add_months(pubdate,+24) AS p2y,
    add_months(pubdate,-24) AS m2y
  FROM titles
  WHERE title_id = 'T05';

-- Count the days between two dates.
SELECT date2 - date1 AS days
  FROM
    (SELECT pubdate as date1
       FROM titles
       WHERE title_id = 'T05') t1,
    (SELECT pubdate as date2
       FROM titles
       WHERE title_id = 'T06') t2;

-- Count the months between two dates.
SELECT months_between(date2,date1) AS months
  FROM
    (SELECT
         MIN(pubdate) AS date1,
         MAX(pubdate) AS date2
       FROM titles) t1;

IBM Db2 Database

The functions second(), day(), month(), and so on, extract part of a datetime. current_timestamp returns the current (system) date and time. The standard addition and subtraction operators add and subtract time intervals from a date. days() converts a date to an integer serial number (Listing 15.48).

Listing 15.48Working with dates in IBM Db2 Database.

-- Extract parts of the current datetime.
SELECT
    second(current_timestamp) AS sec_pt,
    minute(current_timestamp) AS min_pt,
    hour(current_timestamp)   AS hr_pt,
    day(current_timestamp)    AS day_pt,
    month(current_timestamp)  AS mon_pt,
    year(current_timestamp)   AS yr_pt
  FROM SYSIBM.SYSDUMMY1;

-- Add or subtract days, months, and years.
SELECT
    pubdate + 2 DAY   AS p2d,
    pubdate - 2 DAY   AS m2d,
    pubdate + 2 MONTH AS p2m,
    pubdate - 2 MONTH AS m2m,
    pubdate + 2 YEAR  AS p2y,
    pubdate - 2 YEAR  AS m2y
  FROM titles
  WHERE title_id = 'T05';

-- Count the days between two dates.
SELECT days(date2) - days(date1) AS days
  FROM
    (SELECT pubdate as date1
       FROM titles
       WHERE title_id = 'T05') t1,
    (SELECT pubdate as date2
       FROM titles
       WHERE title_id = 'T06') t2;

-- Count the months between two dates.
SELECT
    (year(date2) * 12 + month(date2)) -
    (year(date1) * 12 + month(date1))
      AS months
  FROM
    (SELECT
         MIN(pubdate) AS date1,
         MAX(pubdate) AS date2
       FROM titles) t1;

MySQL

The function date_format() formats a datetime according to the specified format. current_timestamp returns the current (system) date and time. The standard addition and subtraction operators add and subtract time intervals from a date. datediff() returns the number of days between two dates (Listing 15.49). Alternatives to date_format() are the extraction functions extract(), second(), day(), month(), and so on.

Listing 15.49Working with dates in MySQL.

-- Extract parts of the current datetime.
SELECT
  date_format(current_timestamp,'%s') AS sec_pt,
  date_format(current_timestamp,'%i') AS min_pt,
  date_format(current_timestamp,'%k') AS hr_pt,
  date_format(current_timestamp,'%d') AS day_pt,
  date_format(current_timestamp,'%m') AS mon_pt,
  date_format(current_timestamp,'%Y') AS yr_pt;

-- Add or subtract days, months, and years.
SELECT
    pubdate + INTERVAL 2 DAY   AS p2d,
    pubdate - INTERVAL 2 DAY   AS m2d,
    pubdate + INTERVAL 2 MONTH AS p2m,
    pubdate - INTERVAL 2 MONTH AS m2m,
    pubdate + INTERVAL 2 YEAR  AS p2y,
    pubdate - INTERVAL 2 YEAR  AS m2y
  FROM titles
  WHERE title_id = 'T05';

-- Count the days between two dates.
SELECT datediff(date2,date1) AS days
  FROM
    (SELECT pubdate as date1
       FROM titles
       WHERE title_id = 'T05') t1,
    (SELECT pubdate as date2
       FROM titles
       WHERE title_id = 'T06') t2;

-- Count the months between two dates.
SELECT
    (year(date2) * 12 + month(date2)) -
    (year(date1) * 12 + month(date1))
      AS months
  FROM
    (SELECT
         MIN(pubdate) AS date1,
         MAX(pubdate) AS date2
       FROM titles) t1;

PostgreSQL

The function date_part() extracts the specified part of a datetime. current_timestamp returns the current (system) date and time. The standard addition and subtraction operators add and subtract time intervals from a date. Subtracting one date from another yields the number of days between them (Listing 15.50). An alternative to date_part() is extract().

Listing 15.50Working with dates in PostgreSQL.

-- Extract parts of the current datetime.
SELECT
  date_part('second',current_timestamp) AS sec_pt,
  date_part('minute',current_timestamp) AS min_pt,
  date_part('hour',current_timestamp)   AS hr_pt,
  date_part('day',current_timestamp)    AS day_pt,
  date_part('month',current_timestamp)  AS mon_pt,
  date_part('year',current_timestamp)   AS yr_pt;

-- Add or subtract days, months, and years.
SELECT
    pubdate + INTERVAL '2 DAY'   AS p2d,
    pubdate - INTERVAL '2 DAY'   AS m2d,
    pubdate + INTERVAL '2 MONTH' AS p2m,
    pubdate - INTERVAL '2 MONTH' AS m2m,
    pubdate + INTERVAL '2 YEAR'  AS p2y,
    pubdate - INTERVAL '2 YEAR'  AS m2y
  FROM titles
  WHERE title_id = 'T05';

-- Count the days between two dates.
SELECT date2 - date1 AS days
  FROM
    (SELECT pubdate as date1
       FROM titles
       WHERE title_id = 'T05') t1,
    (SELECT pubdate as date2
       FROM titles
       WHERE title_id = 'T06') t2;

-- Count the months between two dates.
SELECT
    (date_part('year', date2) * 12 +
     date_part('month',date2)) -
    (date_part('year', date1) * 12 +
     date_part('month',date1))
       AS months
  FROM
    (SELECT
         MIN(pubdate) AS date1,
         MAX(pubdate) AS date2
       FROM titles) t1;

Calculating a Median

The median describes the center of the data as the middle point of n (sorted) values. If n is odd, then the median is the observation number (n + 1)/2. If n is even, then the median is the midpoint (average) of observations n/2 and (n/2) + 1.

The examples in this section calculate the median of the column sales in the table empsales (Figure 15.39). The median is 550—the average of the middle two numbers, 500 and 600, in the sorted list.

Figure 15.39The table empsales, sorted by ascending sales.

emp_id sales
------ -----
E07      300
E08      400
E03      500
E04      500
E06      500
E01      600
E05      700
E10      700
E02      800
E09      900

Search the web and you’ll find many standard and DBMS-specific ways to calculate the median. Listing 15.51 shows one way—it uses a self-join and GROUP BY to create a Cartesian product (e1 and e2) without duplicates and then uses HAVING and SUM to find the row (containing the median) where the number of times e1.sales = e2.sales equals (or exceeds) the number of times e1.sales > e2.sales. Like all methods that use standard (or near-standard) SQL, it’s cumbersome, it’s hard to understand, and it runs slowly because it’s difficult to pick the middle value of an ordered set when SQL is about unordered sets.

To run Listing 15.51 in Microsoft Access, change the CASE expression to iif(e1.sales = e2.sales, 1, 0) and change SIGN to SGN.

Listing 15.51Calculate the median of sales in standard SQL.

SELECT AVG(sales) AS median
  FROM
    (SELECT e1.sales
       FROM empsales e1, empsales e2
       GROUP BY e1.sales
       HAVING
         SUM(CASE WHEN e1.sales = e2.sales
             THEN 1 ELSE 0 END) >=
             ABS(SUM(SIGN(e1.sales -
                          e2.sales)))) t1;

It’s faster and more efficient to calculate the median by using DBMS-specific functions. In Microsoft Access, use Visual Basic or C# to call Excel’s MEDIAN() function from within Access. Listing 15.52 calculates the median in Microsoft SQL Server in two ways. Listing 15.53 calculates it in Oracle in two ways. The second query in Listing 15.52 and Listing 15.53 also works in Db2.

Listing 15.52Here are two ways to calculate the median in Microsoft SQL Server. The second way, which also works in Db2, is faster than the first.

-- Method 1.
-- Works in Microsoft SQL Server.
SELECT
  (
    (SELECT MAX(sales) FROM
      (SELECT TOP 50 PERCENT sales
        FROM empsales
        ORDER BY sales ASC) AS t1)
    +
    (SELECT MIN(sales) FROM
      (SELECT TOP 50 PERCENT sales
        FROM empsales
        ORDER BY sales DESC) AS t2)
  )/2 AS median;

-- Method 2.
-- Works in Microsoft SQL Server and Db2.
SELECT AVG(sales) AS median
  FROM
    (SELECT
      sales,
      ROW_NUMBER() OVER (ORDER BY sales)
        AS rownum,
      COUNT(*) OVER () AS cnt
    FROM empsales) t1
  WHERE rownum IN ((cnt+1)/2, (cnt+2)/2);

Listing 15.53Here are two ways to calculate the median in Oracle. The second way, which also works in Db2, is faster than the first.

-- Method 1.
-- Works in Oracle.
SELECT
    percentile_cont(0.5)
      WITHIN GROUP (ORDER BY sales)
      AS median
  FROM empsales;

-- Method 2.
-- Works in Oracle and Db2.
SELECT median(sales) AS median
  FROM empsales;

If you use an alternate method to compute the median, then make sure that it doesn’t eliminate duplicate values during calculations and averages the two middle observations for an even n (rather than just lazily choosing one of them as the median).

See also “Statistics in SQL” in Chapter 6.

Median vs. Mean

The median is a popular statistic because it’s robust, meaning it’s not affected seriously by extreme high or low values, either legitimate or caused by errors. The arithmetic mean (average), on the other hand, is so sensitive that it can swing wildly with the addition or removal of even a single extreme value. That’s why you see the median applied to skewed (lopsided) distributions such as wealth, house prices, military budgets, and gene expression. The median is also known as the 50th percentile or the second quartile. See also “Finding Extreme Values” later in this chapter.

Finding Extreme Values

You can use aggregate functions in a subquery to find the highest and lowest values in a column.

Listing 15.54 finds the rows with the highest and lowest values (ties included) of the column advance in the table royalties. Figure 15.40 shows the result.

You also can use the queries in “Limiting the Number of Rows Returned” to find extremes, although not both highs and lows in the same query.

In Microsoft SQL Server, Oracle, Db2, MySQL, and PostgreSQL you can replicate Listing 15.54 by using the window functions MIN OVER and MAX OVER (Listing 15.55).

Listing 15.54List the books with the highest and lowest advances. See Figure 15.40 for the result.

SELECT title_id, advance
  FROM royalties
  WHERE advance IN (
    (SELECT MIN(advance) FROM royalties),
    (SELECT MAX(advance) FROM royalties));

Figure 15.40Result of Listing 15.54.

title_id advance
-------- ----------
T07      1000000.00
T08            0.00
T09            0.00

Listing 15.55List the books with the highest and lowest advances, using window functions.

SELECT title_id, advance
  FROM
    (SELECT title_id, advance,
         MIN(advance) OVER () min_adv,
         MAX(advance) OVER () max_adv
       FROM royalties) t1
  WHERE advance IN (min_adv, max_adv);

Changing Running Statistics Midstream

You can modify values of an in-progress running statistic depending on values in another column. First, review Listing 15.1 in “Calculating Running Statistics” earlier in this chapter.

Listing 15.56 calculates the running sum of book sales, ignoring biographies. The scalar subquery computes the running sum, and the inner CASE expression identifies biographies and changes their sales value to NULL, which is ignored by the aggregate function SUM(). The outer CASE expression merely creates a label column in the result; it’s not part of the running-sum logic. Figure 15.41 shows the result.

In the inner CASE expression, you can set the value being summed to any number, not only NULL. If you were summing bank transactions, for example, then you could make the deposits positive and withdrawals negative.

Listing 15.56Calculate the running sum of book sales, ignoring biographies. See Figure 15.41 for the result.

SELECT
    t1.title_id,
    CASE WHEN t1.type = 'biography'
      THEN '*IGNORED*'
      ELSE t1.type END
        AS title_type,
    t1.sales,
    (SELECT
         SUM(CASE WHEN t2.type = 'biography'
           THEN NULL
           ELSE t2.sales END)
       FROM titles t2
       WHERE t1.title_id >= t2.title_id)
         AS RunSum
  FROM titles t1;

Figure 15.41Result of Listing 15.56.

title_id title_type sales   RunSum
-------- ---------- ------- ------
T01      history        566    566
T02      history       9566  10132
T03      computer     25667  35799
T04      psychology   13001  48800
T05      psychology  201440 250240
T06      *IGNORED*    11320 250240
T07      *IGNORED*  1500200 250240
T08      children      4095 254335
T09      children      5000 259335
T10      *IGNORED*     NULL 259335
T11      psychology   94123 353458
T12      *IGNORED*   100001 353458
T13      history      10467 363925

To run Listing 15.56 in Microsoft Access, change the two CASE expressions to

iif(t1.type = 'biography', '*IGNORED*', t1.type)

and

iif(t2.type = 'biography', NULL, t2.sales).

In Microsoft SQL Server, Oracle, Db2, MySQL, and PostgreSQL, you can replicate Listing 15.56 by using the window function SUM OVER (Listing 15.57).

Listing 15.57Calculate the running sum of book sales, ignoring biographies and using window functions.

SELECT
    title_id,
    CASE WHEN type = 'biography'
      THEN '*IGNORED*'
      ELSE type END
        AS title_type,
    sales,
    SUM(CASE WHEN type = 'biography'
          THEN NULL
          ELSE sales END)
          OVER (ORDER BY title_id, sales)
        AS RunSum
  FROM titles;

Pivoting Results

Pivoting a table swaps its columns and rows, typically to display data in a compact format on a report.

Listing 15.58 uses SUM functions and CASE expressions to list the number of books each author wrote (or cowrote). But instead of displaying the result in the usual way (see Listing 6.9, for example), like this:

au_id num_books
----- ---------
A01   3
A02   4
A03   2
A04   4
A05   1
A06   3
A07   0

Listing 15.58 produces a pivoted result:

A01 A02 A03 A04 A05 A06 A07
--- --- --- --- --- --- ---
  3   4   2   4   1   3   0

Listing 15.58List the number of books each author wrote (or cowrote), pivoting the result.

SELECT
    SUM(CASE WHEN au_id='A01'
      THEN 1 ELSE 0 END) AS A01,
    SUM(CASE WHEN au_id='A02'
      THEN 1 ELSE 0 END) AS A02,
    SUM(CASE WHEN au_id='A03'
      THEN 1 ELSE 0 END) AS A03,
    SUM(CASE WHEN au_id='A04'
      THEN 1 ELSE 0 END) AS A04,
    SUM(CASE WHEN au_id='A05'
      THEN 1 ELSE 0 END) AS A05,
    SUM(CASE WHEN au_id='A06'
      THEN 1 ELSE 0 END) AS A06,
    SUM(CASE WHEN au_id='A07'
      THEN 1 ELSE 0 END) AS A07
  FROM title_authors;

Listing 15.59 reverses the pivot. The first subquery in the FROM clause returns the unique authors’ IDs. The second subquery reproduces the result of Listing 15.58.

Listing 15.59List the number of books each author wrote (or cowrote), reverse-pivoting the result.

SELECT
    au_ids.au_id,
    CASE au_ids.au_id
      WHEN 'A01' THEN num_books.A01
      WHEN 'A02' THEN num_books.A02
      WHEN 'A03' THEN num_books.A03
      WHEN 'A04' THEN num_books.A04
      WHEN 'A05' THEN num_books.A05
      WHEN 'A06' THEN num_books.A06
      WHEN 'A07' THEN num_books.A07
    END
      AS num_books
  FROM
    (SELECT au_id FROM authors) au_ids,
    (SELECT
      SUM(CASE WHEN au_id='A01'
        THEN 1 ELSE 0 END) AS A01,
      SUM(CASE WHEN au_id='A02'
        THEN 1 ELSE 0 END) AS A02,
      SUM(CASE WHEN au_id='A03'
        THEN 1 ELSE 0 END) AS A03,
      SUM(CASE WHEN au_id='A04'
        THEN 1 ELSE 0 END) AS A04,
      SUM(CASE WHEN au_id='A05'
        THEN 1 ELSE 0 END) AS A05,
      SUM(CASE WHEN au_id='A06'
        THEN 1 ELSE 0 END) AS A06,
      SUM(CASE WHEN au_id='A07'
        THEN 1 ELSE 0 END) AS A07
    FROM title_authors) num_books;

To run Listing 15.58 and Listing 15.59 in Microsoft Access, change the simple CASE expressions to iif functions. For example, change the first CASE expression in Listing 15.58 to

iif(au_id = 'A01', 1, 0)

Also, change the searched CASE expression to a switch() function (see the DBMS tip in “Evaluating Conditional Values with CASE” in Chapter 5).

Working with Hierarchies

A hierarchy ranks and organizes people or things within a system. Each element (except the top one) is a subordinate to a single other element. Figure 15.42 is a tree diagram of a corporate pecking order, with the chief executive officer (CEO) at top, above vice presidents (VP), directors (DIR), and wage slaves (WS).

Figure 15.42An organization chart showing a simple company hierarchy. (Click image to enlarge.)

Diagram: An organization chart showing a company hierarchy with the CEO at the top and wage slaves at the bottom

Hierarchical trees come with their own vocabulary. Each element in the tree is a node. Nodes are connected by branches. Two connected nodes form a parent–child relationship (three connected nodes form a grandparent–parent–child relationship, and so on). At the top of the pyramid is the root node (CEO, in this example). Nodes without children are end nodes or leaf nodes (DIR2 and all the WSs). Branch nodes connect to leaf nodes or other branch nodes (VP1, VP2, DIR1, and DIR3—middle management).

The table hier (Figure 15.43) represents the tree in Figure 15.42. The table hier has the same structure as the table employees in “Creating a Self-Join” in Chapter 7. Review that section for the basics of using self-joins with hierarchies.

Figure 15.43The result of the query SELECT * FROM hier;. The table hier represents the organization chart in Figure 15.42.

emp_id emp_title boss_id
------ --------- -------
E01    CEO       NULL
E02    VP1       E01
E03    VP2       E01
E04    DIR1      E02
E05    DIR2      E02
E06    DIR3      E03
E07    WS1       E04
E08    WS2       E04
E09    WS3       E04
E10    WS4       E06
E11    WS5       E06

Listing 15.60 uses a self-join to list who works for whom. See Figure 15.44 for the result.

Listing 15.60List the parent–child relationships. See Figure 15.44 for the result.

SELECT
    h1.emp_title ||
    ' obeys '    ||
    h2.emp_title
      AS power_structure
  FROM hier h1, hier h2
  WHERE h1.boss_id = h2.emp_id;

Figure 15.44Result of Listing 15.60.

power_structure
---------------
VP1 obeys CEO
VP2 obeys CEO
DIR1 obeys VP1
DIR2 obeys VP1
DIR3 obeys VP2
WS1 obeys DIR1
WS2 obeys DIR1
WS3 obeys DIR1
WS4 obeys DIR3
WS5 obeys DIR3

To run Listing 15.60 in Microsoft Access and Microsoft SQL Server, change each || to +. In MySQL, use CONCAT() to concatenate strings. See “Concatenating Strings with ||” in Chapter 5.

Listing 15.61 traverses the hierarchy by using multiple self-joins to trace the chain of command from employee WS3 to the top of the tree. See Figure 15.45 for the result. Unfortunately, you must know the depth of the hierarchy before you write this query; use one of the alternatives given next, if possible.

Listing 15.61Show the full hierarchical relationship of employee WS3. See Figure 15.45 for the result.

SELECT
    h1.emp_title || ' < ' ||
    h2.emp_title || ' < ' ||
    h3.emp_title || ' < ' ||
    h4.emp_title
      AS chain_of_command
  FROM hier h1, hier h2, hier h3, hier h4
  WHERE h1.emp_title = 'WS3'
    AND h1.boss_id = h2.emp_id
    AND h2.boss_id = h3.emp_id
    AND h3.boss_id = h4.emp_id;

Figure 15.45Result of Listing 15.61.

chain_of_command
----------------------
WS3 < DIR1 < VP1 < CEO

To run Listing 15.61 in Microsoft Access and Microsoft SQL Server, change each || to +. In MySQL, use CONCAT() to concatenate strings. See “Concatenating Strings with ||” in Chapter 5.

In Microsoft SQL Server and Db2, use the (standard) recursive WITH clause to traverse a hierarchy. The following query is equivalent to Listing 15.61 (in Microsoft SQL Server, change each || to +):

WITH recurse (chain, emp_level, boss_id) AS
  (SELECT
      CAST(emp_title AS VARCHAR(50)),
      0,
      boss_id
    FROM hier
    WHERE emp_title = 'WS3'
  UNION ALL
  SELECT
      CAST(recurse.chain || ' < ' ||
        hier.emp_title AS VARCHAR(50)),
      recurse.emp_level + 1,
      hier.boss_id
    FROM hier, recurse
    WHERE recurse.boss_id = hier.emp_id
  )
SELECT chain AS chain_of_command
  FROM recurse
  WHERE emp_level = 3;

In Microsoft SQL Server and Db2, to list everyone who reports to a particular employee (VP1, in this example), either directly or indirectly (through a boss’s boss), use this query:

WITH recurse (emp_title, emp_id) AS
  (SELECT emp_title,emp_id
    FROM hier
    WHERE emp_title = 'VP1'
  UNION ALL
  SELECT hier.emp_title, hier.emp_id
    FROM hier, recurse
    WHERE recurse.emp_id = hier.boss_id
  )
SELECT emp_title AS "Works for VP1"
  FROM recurse
  WHERE emp_title <> 'VP1';

In Oracle, use the (nonstandard) CONNECT BY syntax to traverse a hierarchy. The following query is equivalent to Listing 15.61:

SELECT LTRIM(SYS_CONNECT_BY_PATH(
  emp_title, ' < '), ' < ')
    AS chain_of_command
  FROM hier
  WHERE LEVEL = 4
  START WITH emp_title = 'WS3'
  CONNECT BY PRIOR boss_id = emp_id;

In Oracle, to list everyone who reports to a particular employee (VP1, in this example), either directly or indirectly (through a boss’s boss), use this query:

SELECT emp_title AS "Works for VP1"
  FROM hier
  WHERE emp_title <> 'VP1'
  START WITH emp_title = 'VP1'
  CONNECT BY PRIOR emp_id = boss_id;

Listing 15.62 traverses the hierarchy by using multiple UNIONs and self-joins to trace the chain of command for every employee. See Figure 15.46 for the result. Unfortunately, you must know the maximum depth of the hierarchy before you write this query; use one of the alternatives given next, if possible.

Listing 15.62Show the full hierarchal relationship of every employee. See Figure 15.46 for the result.

SELECT chain AS chains_of_command
  FROM
    (SELECT emp_title as chain
      FROM hier
      WHERE boss_id IS NULL
    UNION
    SELECT
        h1.emp_title || ' > ' ||
        h2.emp_title
      FROM hier h1
      INNER JOIN hier h2
        ON (h1.emp_id = h2.boss_id)
      WHERE h1.boss_id IS NULL
    UNION
    SELECT
        h1.emp_title || ' > ' ||
        h2.emp_title || ' > ' ||
        h3.emp_title
      FROM hier h1
      INNER JOIN hier h2
        ON (h1.emp_id = h2.boss_id)
      LEFT OUTER JOIN hier h3
        ON (h2.emp_id = h3.boss_id)
      WHERE h1.emp_title = 'CEO'
    UNION
    SELECT
        h1.emp_title || ' > ' ||
        h2.emp_title || ' > ' ||
        h3.emp_title || ' > ' ||
        h4.emp_title
      FROM hier h1
      INNER JOIN hier h2
        ON (h1.emp_id = h2.boss_id)
      INNER JOIN hier h3
        ON (h2.emp_id = h3.boss_id)
      LEFT OUTER JOIN hier h4
        ON (h3.emp_id = h4.boss_id)
      WHERE h1.emp_title = 'CEO'
    ) chains
  WHERE chain IS NOT NULL
  ORDER BY chain;

Figure 15.46Result of Listing 15.62.

chains_of_command
----------------------
CEO
CEO > VP1
CEO > VP1 > DIR1
CEO > VP1 > DIR1 > WS1
CEO > VP1 > DIR1 > WS2
CEO > VP1 > DIR1 > WS3
CEO > VP1 > DIR2
CEO > VP2
CEO > VP2 > DIR3
CEO > VP2 > DIR3 > WS4
CEO > VP2 > DIR3 > WS5

Microsoft Access won’t run Listing 15.62 because of the restrictions Access puts on join expressions.

To run Listing 15.62 in Microsoft SQL Server, change each || to +.

To run Listing 15.62 in MySQL, use CONCAT() instead of || to concatenate strings.

In Microsoft SQL Server and Db2, use the (standard) recursive WITH clause to traverse a hierarchy. The following query is equivalent to Listing 15.62 (in Microsoft SQL Server, change each || to +):

WITH recurse (emp_title, emp_id) AS
  (SELECT
      CAST(emp_title AS VARCHAR(50)),
      emp_id
    FROM hier
    WHERE boss_id IS NULL
  UNION ALL
  SELECT
      CAST(recurse.emp_title || ' > ' ||
        h1.emp_title AS VARCHAR(50)),
      h1.emp_id
    FROM hier h1, recurse
    WHERE h1.boss_id = recurse.emp_id
  )
SELECT emp_title emp_tree
  FROM recurse;

In Oracle, use the (nonstandard) CONNECT BY syntax to traverse a hierarchy. The following query is equivalent to Listing 15.62:

SELECT ltrim(SYS_CONNECT_BY_PATH(
    emp_title, ' > '),' > ')
      AS chains_of_command
  FROM hier
  START WITH boss_id IS NULL
  CONNECT BY PRIOR emp_id = boss_id;

Listing 15.63 uses scalar subqueries to determine whether each node in the hierarchy is a root, branch, or leaf node. See Figure 15.47 for the result. A zero in the result denotes True; nonzero, False.

Listing 15.63Determine whether each node is a root, branch, or leaf node. See Figure 15.47 for the result.

SELECT h1.emp_title,
  (SELECT SIGN(COUNT(*))
    FROM hier h2
    WHERE h1.emp_id = h2.emp_id
      AND h2.boss_id IS NULL)
        AS root_node,
  (SELECT SIGN(COUNT(*))
    FROM hier h2
    WHERE h1.emp_id = h2.boss_id
      AND h1.boss_id IS NOT NULL)
        AS branch_node,
  (SELECT SIGN(COUNT(*))
    FROM hier h2
    WHERE 0 =
      (SELECT COUNT(*)
        FROM hier h3
        WHERE h1.emp_id = h3.boss_id))
          AS leaf_node
  FROM hier h1;

Figure 15.47Result of Listing 15.63.

emp_title root_node branch_node leaf_node
--------- --------- ----------- ---------
CEO       1         0           0
VP1       0         1           0
VP2       0         1           0
DIR1      0         1           0
DIR2      0         0           1
DIR3      0         1           0
WS1       0         0           1
WS2       0         0           1
WS3       0         0           1
WS4       0         0           1
WS5       0         0           1

To run Listing 15.63 in Microsoft Access, change each SIGN to SGN.

In Oracle, use the (nonstandard) CONNECT BY syntax to traverse a hierarchy. The following query is equivalent to Listing 15.63:

SELECT
    emp_title,
    (CASE CONNECT_BY_ROOT(emp_title)
    WHEN emp_title THEN 1
    ELSE 0 END)
      AS root_node,
    (SELECT COUNT(*)
      FROM hier h1
      WHERE h1.boss_id = hier.emp_id
        AND hier.boss_id IS NOT NULL
        AND rownum = 1)
          AS branch_node,
      CONNECT_BY_ISLEAF AS leaf_node
  FROM hier
  START WITH boss_id IS NULL
  CONNECT BY PRIOR emp_id = boss_id
  ORDER BY root_node DESC, branch_node DESC;

Download and Create
the Sample Database

The examples in SQL Run use the sample database books, described in “The Sample Database” in Chapter 2. The sample database also includes additional tables that are used in the book’s more-advanced examples.

Tip: This sample database also is compatible with all the author’s earlier SQL books.

  1. Download the zip file and then expand (uncompress) it.

    Download SQL files(279K zip file)

    The text file readme.txt, included in the zip file, lists and describes the files in the distribution.

    Tip: Microsoft Windows also refers to a zip file as a compressed folder.

  2. If you’re running a DBMS locally (that is, on your own computer), then you’re the database administrator (DBA) and have all the privileges that you need to run SQL scripts.

    or

    If you’re connecting to a DBMS on a network server, then ask your DBA for connection parameters and the privileges to run SQL scripts that create, query, update, and drop databases, tables, and other database objects.

  3. Follow the instructions below to create the sample database for your DBMS:

CREATE DATABASE

The following instructions for creating the sample database explain how to use simple tools and settings. As you gain experience, you might want to switch to using the statement CREATE DATABASE to create new databases. CREATE DATABASE is a powerful but nonstandard SQL command, so its syntax and capabilities vary by DBMS; see your DBMS’s documentation. Microsoft Access doesn’t support CREATE DATABASE, but you can use Visual Basic for Applications or Visual C# to create Access databases programmatically.

Opening the Sample Database in Microsoft Access

To open the database books in Microsoft Access:

  1. In File Explorer (or Windows Explorer), navigate to the drive or folder containing the database file books.mdb and then double-click its icon.

    Microsoft Access starts and the database opens.

  2. In Microsoft Access 2007 or later, to inspect the database tables, press F11 to show the Navigation pane. Click the menu at the top of the pane and choose Object Type, and then click the menu again and choose Tables (or All Access Objects).

    In Microsoft Access 2000, 2002, or 2003, to inspect the database tables, click Tables (below Objects) in the Database window.

  3. To run SQL statements against the database, see “Microsoft Access” in Chapter 1.

If you’re running Microsoft Access 97 or earlier, then you won’t be able to open books_rapid.mdb because it’s an Access 2000-format (.mdb) file. To create the sample-database tables, use the Import Text wizard to import the CSV files included in the distribution. A CSV (comma-separated values) file is a text file in which each column value is separated by a comma from the next column’s value and each row starts a new line. The first row contains column names. The CSV files for the various tables are named csv_authors.txt, csv_publishers.txt, and so on.

To import a CSV file as a table in Microsoft Access 97 or earlier:

  1. In Access, open or create a database, or press F11 to switch to the Database window for the open database.
  2. Choose File > Get External Data > Import.
  3. In the Import dialog box, in the Files of Type box, select Text Files.
  4. Navigate to the drive or folder containing the CSV file, and then double-click its icon.
  5. Follow the onscreen instructions in the Import Text wizard. Click Advanced to create or use an import/export specification. (To cancel importing, press Ctrl+Break.)

Creating the Sample Database in Microsoft SQL Server

To create the database books in Microsoft SQL Server:

  1. On the Windows desktop, choose Start > Microsoft SQL Server Tools > Microsoft SQL Server Management Studio.

    Microsoft SQL Server Management Studio opens.

  2. In the Connect to Server dialog box, select the server and authentication mode, and then click Connect.

    Figure 1Microsoft SQL Server > Microsoft SQL Server Management Studio > Connect to Server dialog box. (Click image to enlarge.)

    Screenshot: Microsoft SQL Server - Microsoft SQL Server Management Studio - Connect to Server dialog box

  3. In Object Explorer (the left pane), navigate to the Databases folder of the server that you’re using.

    If Object Explorer isn’t visible, then choose View > Object Explorer (or press F8).

  4. Right-click the Databases folder and then choose New Database.

    The New Database dialog box opens.

    Figure 2Microsoft SQL Server > Microsoft SQL Server Management Studio > New Database command. (Click image to enlarge.)

    Screenshot: Microsoft SQL Server - Microsoft SQL Server Management Studio - New Database command

  5. On the General page, type books in the Database Name field, and then click OK. (The default values for the settings in the General, Options, and Filegroups pages are suitable for the sample database.)

    SQL Server creates the database books and then closes the New Database dialog box.

    Figure 3Microsoft SQL Server > Microsoft SQL Server Management Studio > New Database > General page. (Click image to enlarge.)

    Screenshot: Microsoft SQL Server - Microsoft SQL Server Management Studio - New Database dialog box - General page

  6. In Object Explorer, expand the Databases folder and then select the database books.

    Figure 4Microsoft SQL Server > Microsoft SQL Server Management Studio > Object Explorer > books database. (Click image to enlarge.)

    Screenshot: Microsoft SQL Server - Microsoft SQL Server Management Studio - Object Explorer - books Database

  7. Choose File > Open > File (or press Ctrl+O), navigate to the drive or folder containing the file books_sqlserver.sql, select its icon, and then click Open.

    The file’s contents appear in a new tab in the right pane.

    Figure 5Microsoft SQL Server > Microsoft SQL Server Management Studio > books database > contents of books_sqlserver.sql. (Click image to enlarge.)

    Screenshot: Microsoft SQL Server - Microsoft SQL Server Management Studio - books Database - contents of books_sqlserver.sql

  8. Choose Query > Execute (or press F5).

    SQL Server Management Studio displays the results in the bottom pane. Ignore the messages about nonexistent tables—they’re caused by the script’s DROP TABLE statements, which are needed to rerun books_sqlserver.sql to restore the tables to their original states.

    Figure 6Microsoft SQL Server > Microsoft SQL Server Management Studio > books database > results of executing books_sqlserver.sql. (Click image to enlarge.)

    Screenshot: Microsoft SQL Server - Microsoft SQL Server Management Studio - books database - results of executing books_sqlserver.sql

  9. To run SQL scripts and interactive statements against the database, see “Microsoft SQL Server” in Chapter 1.

    Tip: The script books_sqlserver.sql differs slightly from the standard SQL script books_standard.sql. In the SQL Server script, the data type of the column pubdate in the table titles is DATETIME (rather than DATE). Also, date literals don’t have the DATE keyword. (The standard SQL date value DATE '2000-08-01', for example, is equivalent to the SQL Server date value '2000-08-01'.)

Creating the Sample Database in Oracle Database

To create the database books in Oracle Database:

  1. Start Database Configuration Assistant.

    This procedure varies by platform. In Microsoft Windows, for example, choose Start > Oracle - OraDB18Home1 > Database Configuration Assistant.

    Database Configuration Assistant guides you through the steps needed to create a database.

  2. On the Database Operation page, select “Create a database”, and then click Next.

    Figure 7Oracle Database > Database Configuration Assistant > Database Operation page. (Click image to enlarge.)

    Screenshot: Oracle Database - Database Configuration Assistant - Database Operation page

  3. On the Creation Mode page, select “Typical configuration”, type books in the “Global database name” box, select “File System” for the storage type, type and confirm an administrative password, clear “Create as Container database”, and then click Next.

    Figure 8Oracle Database > Database Configuration Assistant > Creation Mode page. (Click image to enlarge.)

    Screenshot: Oracle Database - Database Configuration Assistant - Creation Mode page

  4. On the Summary page, review the configuration options (and save them to a response file if you like), and then click Finish.

    Figure 9Oracle Database > Database Configuration Assistant > Summary page. (Click image to enlarge.)

    Screenshot: Oracle Database - Database Configuration Assistant - Summary page

  5. On the Progress Page, a progress meter and status list appears while Oracle creates the database.

  6. The Finish page appears when Oracle finishes creating the database. Review the database information and then click Close to exit the Database Configuration Assistant.

  7. Start SQL*Plus (sqlplus) and connect to the books database.

    At an administrator command prompt, type:

    sqlplus user/password@dbname

    user is your Oracle user name, password is your password, and dbname is the name of the database to connect to (books, in this case). For security, you can omit the password and instead type:

    sqlplus user@dbname

    SQL*Plus will prompt you for your password.

    If you’re running Oracle locally, then you can use the user name system and the password that you set in step 3:

    sqlplus system@books

    If you’re connecting to a remote Oracle database, then ask your database administrator (DBA) for the connection parameters.

    Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu.

  8. At the SQL prompt, type:

    @books_oracle.sql

    and then press Enter. You can include an absolute or relative pathname (see “Paths” in Chapter 1).

    sqlplus displays the results. Ignore the messages about nonexistent tables—they’re caused by the script’s DROP TABLE statements, which are needed to rerun books_oracle.sql to restore the tables to their original states.

    Figure 10Oracle Database > SQL*Plus > sqlplus system@books. (Click image to enlarge.)

    Screenshot: Oracle Database - SQL*Plus - sqlplus system@books

  9. To run SQL scripts and interactive statements against the database, see “Oracle Database” in Chapter 1.

    Tip: The script books_oracle.sql differs slightly from the standard SQL script books_standard.sql. In the Oracle script, the value in the column au_fname in the table authors for author A06 is a space character (' '), rather than an empty string (''). This change prevents Oracle from interpreting the first name of author A06 as null; for details, see the DBMS tip in “Nulls” in Chapter 3.

Creating the Sample Database in IBM Db2 Database

To create the database books in IBM Db2 Database:

  1. Open Data Studio.

    This procedure varies by platform. In Microsoft Windows, for example, choose Start > IBM Data Studio > Data Studio Client.

  2. On the Administration Explorer tab (on the left), expand the All Databases folder of the object tree until you find your instance of Db2, right-click the instance, and then click New Database in the context menu.

    Figure 11IBM Db2 > Data Studio > Administration Explorer tab > New Database command. (Click image to enlarge.)

    Screenshot: IBM Db2 - Data Studio - Administration Explorer tab - New Database command

  3. If the “New database” dialog box opens, then type your Db2 user name and password. Click Finish.

    A “New database” tab opens.

    Figure 12IBM Db2 > Data Studio > Administration Explorer tab > New Database command > New Database dialog box. (Click image to enlarge.)

    Screenshot: IBM Db2 - Data Studio - Administration Explorer tab - New Database command - New Database dialog box

  4. In the Details pane on the “New database” tab, type books in the “Database name” box, specify a path in the “Database location” box, and then click Run. (The default values for the settings in the Storage and Locale panes are suitable for the sample database.)

    A progress meter appears while Db2 creates the database.

    Figure 13IBM Db2 > Data Studio > New Database tab > Details pane. (Click image to enlarge.)

    Screenshot: IBM Db2 - Data Studio - New Database tab - Details pane

  5. When Db2 finishes creating the database, the new database books appears below the Db2 instance in the All Databases folder on the Administration Explorer tab.

    Figure 14IBM Db2 > Data Studio > Administration Explorer tab > All Databases folder > books database. (Click image to enlarge.)

    Screenshot: IBM Db2 - Data Studio - Administration Explorer tab - All Databases folder - books database

  6. At an administrator command prompt, type:

    db2batch -d books -f books_db2.sql

    and then press Enter. The -f option specifies the name of the SQL file. You can include an absolute or relative pathname (see “Paths” in Chapter 1). You can add the option -a user[/password] to connect to the database as a specific user.

    db2batch displays the results. Ignore the messages about undefined names (nonexistent tables)—they’re caused by the script’s DROP TABLE statements, which are needed to rerun books_db2.sql to restore the tables to their original states.

    Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu.

    Tip: Instead of db2batch in this step, you can use the db2 command-line processor in script mode (see “IBM Db2 Database” in Chapter 1).

    Figure 15IBM Db2 > db2batch command. (Click image to enlarge.)

    Screenshot: IBM Db2 - db2batch command

  7. To run SQL scripts and interactive statements against the database, see “IBM Db2 Database” in Chapter 1.

    Tip: The script books_db2.sql differs slightly from the standard SQL script books_standard.sql. In the Db2 script, date literals don’t have the DATE keyword. (The standard SQL date value DATE '2000-08-01', for example, is equivalent to the Db2 date value '2000-08-01'.)

Creating the Sample Database in MySQL

To create the database books in MySQL:

  1. At an administrator command prompt, type:

    mysqladmin -h host -u user -p create books

    host is the host name, and user is your MySQL user name. MySQL will prompt you for your password (for a passwordless user, either omit the -p option or press Enter at the password prompt). MySQL creates a new, empty database named books.

    If MySQL is running on a remote network computer, then ask your database administrator (DBA) for the connection parameters. If you’re running MySQL locally (that is, on your own computer), then set host to localhost, set user to root, and use the password that you assigned to the user root when you set up or installed MySQL.

    Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu. As an alternative to the command prompt, you can use the MySQL Workbench graphical tool at mysql.com/products/workbench.

    Tip: You can set the environment variable MYSQL_HOST to specify the default host name used to connect to the database. See “Environment Variables” in MySQL documentation.

  2. At the administrator command prompt, type:

    mysql -h host -u user -p -f books < books_mysql.sql

    The -f option forces mysql to keep running even if an SQL error occurs. The redirection operator < reads from the specified SQL file. You can include an absolute or relative pathname (see “Paths” in Chapter 1).

    mysql displays the results. Ignore the messages about unknown (nonexistent) tables—they’re caused by the script’s DROP TABLE statements, which are needed to rerun books_mysql.sql to restore the tables to their original states.

    Figure 16MySQL > mysqladmin command > mysql command. (Click image to enlarge.)

    Screenshot: MySQL - mysqladmin command - mysql command

  3. To run SQL scripts and interactive statements against the database, see “MySQL” in Chapter 1.

    Tip: The script books_mysql.sql is the same as the standard SQL script books_standard.sql.

Creating the Sample Database in PostgreSQL

To create the database books in PostgreSQL:

  1. At an administrator command prompt, type:

    createdb -h host -U user -W books

    host is the host name, and user is your PostgreSQL user name. PostgreSQL will prompt you for your password (for a passwordless user, either omit the -W option or press Enter at the password prompt). PostgreSQL creates a new, empty database named books.

    If PostgreSQL is running on a remote network computer, then ask your database administrator (DBA) for the connection parameters. If you’re running PostgreSQL locally (that is, on your own computer), then set host to localhost, set user to postgres, and use the password that you assigned to the user postgres when you set up or installed PostgreSQL.

    Tip: To open an administrator command prompt in Microsoft Windows, tap the Windows Logo Key (or click Start), type command, right-click “Command Prompt” in the results list, and then choose “Run as administrator” in the context menu. As an alternative to the command prompt, you can use the pgAdmin graphical tool. If the PostgreSQL installer didn’t install pgAdmin automatically, then you can download it for free at pgadmin.org.

    Tip: You can set the environment variables PGHOST, PGDATABASE, and PGUSER to specify the default host, database, and user names used to connect to the database. See “Environment Variables” in PostgreSQL documentation.

  2. At the administrator command prompt, type:

    psql -h host -U user -W -f books_postgresql.sql books

    The -f option specifies the name of the SQL file. You can include an absolute or relative pathname (see “Paths” in Chapter 1).

    psql displays the results. Ignore the messages about nonexistent tables—they’re caused by the script’s DROP TABLE statements, which are needed to rerun books_postgresql.sql to restore the tables to their original states.

    Figure 17PostgreSQL > createdb command > psql command. (Click image to enlarge.)

    Screenshot: PostgreSQL - createdb command - psql command

  3. To run SQL scripts and interactive statements against the database, see “PostgreSQL” in Chapter 1.

    Tip: The script books_postgresql.sql is the same as the standard SQL script books_standard.sql.

Creating the Sample Database in Other DBMSs

To create the sample database in a DBMS that’s not covered in the book, edit and run one of the books_*.sql scripts included in the distribution. If your DBMS complies (or almost complies) with standard SQL, then you can run books_standard.sql with few or no changes.

If you can’t create the sample database by running an SQL script, then you can create the tables individually by importing the CSV files included in the distribution. A CSV (comma-separated values) file is a text file in which each column value is separated by a comma from the next column’s value and each row starts a new line. The first row contains column names. The CSV files for the various tables are named csv_authors.txt, csv_publishers.txt, and so on. All DBMSs (even non-SQL DBMSs) can import CSV files as tables—look for an Import or Load command.