Relational Databases: What Is A Database?
Relational Databases: What Is A Database?
TOC Previous
Next
Relational Databases
What is a database? Basic facts about sets Operations on sets Sets, tables, relations and databases Codd's 12 rules MySQL and Codd's rules Practical database design rules Normalisation and the normal forms Limits to the relational model
This chapter introduces relational databases and the concepts underlying them. What is a database, a database server, a database client, a table, a row, a column? How do we map problems to these things, and why? What is a join, a query, a transaction? Skim this chapter if you know these fundamentals; if not, take your time to make sure that you grasp this material before venturing further. We need the concepts and the terminology.
What is a database?
Databases are lists of lists. Even if you think you've never designed a database in your life, you already have hundreds of them: lists of people and their addresses and phone numbers, of favourite web addresses, of your insured items and insurance policies, of what to do tomorrow, of bills to pay, of books to read.
If you come to this chapter in a big hurry to start a project, then while reading, start at Step 1 of this how-to outline.
As any obsessive can tell you, the art of listmaking needs serious contemplation. Not just any fact makes it into your lists. What is worth writing down, and what is ephemeral? Will you record the current temperature and humidity in every diary entry, or instead focus on your problems, eliminating extraneous detail? Does it matter that a butterfly flapped her wings in Bangkok a week before your spouse decided to marry you, or leave you? In designing databases, these are the questions you confront. What lists do you need? What levels of importance do they hold? How much do you need to know to survive, or how little can you live with? Since we humans began writing on tablets, we have been storing information. You could say that the collection of printed books in the world is a database of sorts -- a collection of information, but a seriously stupid one, since you cannot ask a book to find all occurrences of a word like "love" in its own text. Even if the book has an index, how confident are we that it is complete? The art of database design lies in elegant storage of information. To get maximum value
Relational Databases
Page 1
from your lists of lists, you must be clever, and on occasion devious. For such talents, we turn to mathematicians.
Relational Databases
Page 2
database model had to account for all possible members in the universe of each of its sets, we could never finish our first database job. Indeed we will expand some of the coloured sets and subsets, but having done so, we will still have that white space representing everything that belongs to none of our categories. All in all, then, Fig. 1 specifies eight sets, for seven of which you might have to deliver membership lists. You have seven lists to maintain, somehow. Are you going to keep seven different lists in seven different files, one labelled 'men', another labelled 'crooks', one called 'crooked men' and so on? Then every time your boss sends you data on someone new, or asks you to delete someone who's moved away, you will find and update the appropriate list or lists? Each deletion will require that you read up to eight lists, and so will each addition (to make sure that the person you are adding is not already filed or misfiled on a list). You could do it that way, yes. Could we call this a neanderthal database? Tomorrow, when your boss gives you three more attributes to track, say, 'owns property' and 'possesses a firearm' and 'finished high school', you will have a lot more than twice as much work to do. With three attributes, a person can be on one of 23-1=7 lists. With six attributes, you have to maintain 26-1=63 lists. Doubling the number of attributes grew your workload ninefold, and it's only day two. You're in trouble. Then you wake up: suppose you make just one list, and define each of these attributes men, crooks, politicians, property owners, gun owners, high school graduates, whatever else is specifiedas an item that can be filled in, yes-or-no, for each person? That is the idea of a record or row. It contains exactly one cell for each attribute. A collection of such rows is a perfectly rectangular table. In such a table, we refer to each attribute as a column. A node where a row and column intersect is a cell containing a value for the corresponding row and column. Rows and columns. In terms of sets, what have we done? We noticed that membership in any one of our sets is more or less logically independent of membership in any other set, and we devised a simple structure, a table, which maps set memberships to columns. Now we can record all possible combinations of membership in these sets for any number of persons, with a minimum of effort. With this concept, you winnow your cumbersome 63 lists down to just one. After you solve one small problem, and one larger problem. The small problem is that, supposing we have a table, a collection of rows with data in them, we have not yet figured out a method for identifying specific rows, for distinguishing one row from another.
Relational Databases
Page 3
There seems to be a simple method at hand: add a column called name, so every row contains a person's name. The name cell in identifies each row, and the list of values in the column delivers a list of all recorded names. Fine, but what if we have three John Smiths? Indeed, and again there is a simple answer. Add one more column, person_id, and assume, for now, a method of assigning, to each row, a unique number that is a person_id value. Each John Smith will then have his own person_id, so we can have as many John Smiths as we like without mixing up their data. So much for the smaller problem. The larger problem is, how do we get information into this table, and retrieve information from it? In a real way, that is the subject of this book for the case where our array of rows and columns is a MySQL table. Suppose though, for now, a generalised mechanism that converts our table design to a data storage process, how do we instruct the mechanism to put rows of data into the table, and to retrieve rows, or summaries of rows, from it? How do you tell it to store
( 'Smith, John', male, crook, non-politician, owns property, owns gun, graduated high school )
in one row? And after you have stored many such rows, how do you ask it to list all the crooked male politicians with firearms? In database lingo, such requests are called queries. We can call our system a database management system (DBMS) only if it supports these two functionalities: 1. creation and maintenance of tables as we have defined them, and 2. queries that update or retrieve information in the tables Obviously if humans are going to use such a system, then table creation, table maintenance and queries need language interfaces. A language for table creation and maintenance is called a Data Definition Language or DDL, and a query language is called a Data Manipulation Language. For the moment, assume we have one of each. With slight touchups, and ignoring for the moment the different kinds of data that will reside in each cell, our table, so far, has these columns:
person_id name gender crook politician owns_property has_firearm hs_diploma
Let us call it the persons table. If the 63 separate lists constituted a neanderthal database, what is our table? We could give it the name JPLDIS, or dBASE II, or Paradox, or many another 1970s-1980s system that implemented simple tables with a simple DDL and DML. Clearly it fits our definition of a database as a set of sets.
Relational Databases
Page 4
holds an electoral office of some sort, or running for electoral office, and belongs to a recognised political party, date of entry into politics amount of political pork available to the politician for redistribution
and there will be more to come. In the course of this conversation it may dawn on you that you will soon be getting similar requests about other columnsproperty ownership, or some other as-yet-undreamt attribute. Your success is about to burden you with an unmanageable explosion of more lists. What to do? Are we to add columns for elected, running, party, entry date and pork dollars to the persons table? If so, what would we put in those cells for persons who are not politicians? Then ask yourself what the persons table will look like when you have done this over and over again for more persons attributes. These questions are about what is called, in database theory, normalisation, which is, roughly, exporting sets of columns to other tables in order to minimise data redundancy. You are going to have to make a new persons table for personal info, and a politicians table that lists the political attributes your boss wishes to track. All references to politicians in the persons table will disappear. Eh? Here is how. One row in the new politicians table is named, say, pol_person_id. A person who is a politician gets a row in the politicians table, and the pol_person_id cell in that row contains that person's person_id from the persons table. And what can we do with these relational databases, these sets of sets of sets?
A set has zero or more members. If it has zero members, it is a null set, otherwise we can form a set of fewer than all members of the set, which is a subset. Given two sets A and B, their intersection is defined by the members that belong to both. In Fig 2, M&P, C&P, C&M and C&M&P are intersection sets. A set that has fewer than an infinite number of members is a finite set. Obviously, all databases are instances of finite sets. If the set of members common to A and B is the null set, A and B are disjoint sets. For example, in Fig 2, no politician is a cat, and no cat is a politician. Politicians and Cats are disjoint. Given two sets A and B, the members that belong to A and/or B constitute the union of A and Bin Fig 2, M-P-C + C-M-P + M&C. The intersection of A and B is always a subset of the union of A and B. A relation R is commutative if a R b = b R a. Intersection and union are commutative. Given two sets A and B, the difference set A-B is the set of elements of A that do not belong to B. If B is a subset of A, A-B is called the complement of B in A. A set has a relation R if, for each pair of elements (x,y) in A, xRy is true. A relation R can have these properties:
reflexiveness: for any element x of A, xRx symmetry: for any x and y in A, if xRy then yRx.
Relational Databases
Page 6
transitiveness: for any x, y and z in A, if xRy and yRz then xRz. equivalence: a relation has equivalence if it is reflexive, symmetric and transitive.
If the idea of equivalence makes your eyes glaze over, don't worry. It will come clear when you need it.
Operations on sets
The most basic set operation is membership: (member) x is in (set) A. Obviously, to speak of adding sets can be misleading if the sets have members in common, and if you expect set addition to work like numeric addition. So instead, we speak of union. In Fig 2, the union of men and crooks comprises the men who are not crooks, the crooks who are not men, and those who are both. For similar reasons it is chancy to try to map subtraction to sets. Instead, we speak of the difference set A-B, the set of As that are not Bs. To speak of multiplying sets can also be misleading, so instead we speak of intersection. And with sets, division is a matter of partitioning into subsets.
Relational Databases
Page 7
We have just dipped our toes, ever so gingerly, into relational algebra, which mathematically defines database relations and the eight operations that can be executed on thempartition, restriction, projection, product, union, intersection, difference, and division. Each relational operation produces a new relation (table). In Chapters 6 and 9, we will see how to perform these operations using the MySQL variant of the SQL language. Before doing that, we need to set down exactly what relational databases are, how they work, and how the MySQL RDBMS works. That is the task of the rest of this chapter, and of chapters 3, 4, 7, and 8. We pick up the thread again, now, with the computer scientist who discoveredor if you prefer, inventedthe relational database.
Relational Databases
Page 8
Relational Databases
Page 9
Since every row in a table is distinct, the system must guarantee that every row may be accessed by means of its unique identifier (primary key). For example, given a table called customers with a unique identifier CustomerID, you must be able to view all the data concerning any given customer solely by referencing its CustomerID.
Relational Databases
Page 10
data definition; view definition; data manipulation (interactive and by program); integrity constraints; and transaction boundaries (begin, commit, and rollback).
The ubiquitous language for such expressions is Structured Query Language or SQL. It is not the only possible language, and in fact SQL has several well-known problems which we explore later.
Relational Databases
Page 11
might wonder how it could be any other way. But there were databases long before Dr. Codd issued his rules, and these older database models required explicit and intimate knowledge of how data was stored. You could not change anything about the storage method without seriously compromising or breaking any existing applications. This explains why it has taken so long, traditionally, to perform even the simplest modifications to legacy mainframe systems. Stating this rule a little more concretely, imagine a customers table whose first three columns are customerID, companyname and streetaddress. This query would retrieve all three columns:
SELECT customerID, companyname, streetaddress FROM customers;
Rule 8 says that you must be able to restructure the table, inserting columns between these three, or rearranging their order, without causing the SELECT to fail.
Entity integrity: No component of a primary key is allowed to have a null value. Referential integrity: For each distinct non-null foreign key value in a relational database, there must exist a matching primary key value from the same domain.
There has been much contention and misunderstanding about the meaning of Rule 10. Some systems allow the first integrity constraint to be broken. Other systems interpret the second constraint to mean that no foreign keys may have a null value. In other cases, some application programmers violate the second constraint using deception: they add a zeroth row to the foreign table, whose purpose is to indicate missing data. We think this approach is very wrong because it distorts the meaning of data.
Relational Databases
Page 12
Relational Databases
Page 13
Physical data independence (Rule 8): The current MySQL version provides some data storage engines which meet this requirement, and some which do not. Logical data independence (Rule 9): In MySQL, different storage engines accept different commands. It may be impossible for any system to strictly obey Rule 9. Integrity independence (Rule 10): MySQL's primary and foreign keys meet Codd's criteria, but it remains possible to create tables which bypass both requirements, and the MySQL 5 implementation of Triggers remains incomplete. Distribution independence (Rule 11): Version 5.0.3 introduced storage engines which violate this requirement. Would such a report card for other major RDBMSs be better or worse than the above? On the whole we think not. Like other major systems, MySQL supports most of Codd's rules.
Relational Databases
Page 14
In a t-shirt company, the first letter of a t-shirt code denotes its quality; the second denotes its color; the third letter denotes its size. All the users know these codes by rote. Why not use these codes as the Primary Key for the t-shirt table? Let us count the reasons: 1. A real-world coding, as opposed to a purely logical one, will inevitably require adjustment. Keys derived from the coding will then have to be modified. Primary keys should never have to be edited. 2. Coding multiple attributes into one column violates Axiom 1 (atomicity). 3. An integer of a given length permits more unique values than a character string occupying the same number of bytes. 4. Searches on subsets are difficult: how will you list the white t-shirts, or all the colors and qualities available in extra-large? 5. At almost every turn, you encounter new problems, all created by the attempt to impose meaning on primary keys. Data modellers object that an externally meaningless PK models nothing in the real world outside the database. Exactly! A PK models the uniqueness of a row, and nothing more. To the extent that you permit the external world to directly determine PK value, you permit external world errors to compromise your database.
2. Doubt NULLs
Seriously question any column that permits nulls. If you can tolerate the absence of data, why bother storing its presence? If its absence is permitted, it is clearly unessential. This is not to say that columns that allow nulls are unacceptable, but rather to point out their expense. For any table that presents a mix of not-null and null columns, consider splitting it into two tables, not-nulls in the critical table, nulls in the associated table.
Relational Databases
Page 15
The columns employees.departmentID and employees.title are NOT NULL. We don't currently know values for this employee.
The solution known as the Zeroth Row is to add a row to the appropriate foreign key tables whose textual value is "Undefined" or something similar. The reason it is called the zeroth row is that typically it is implemented in a tricky way. This row is assigned the PK zero, but the PK column is AUTO_INCREMENT. Typically, the front end to such a system presents the zeroth row as the default value for the relevant picklists. This violation is not occasional. It is often the considered response of experienced people. Those who take this approach typically argue that it simplifies their queries. It does no such thing. It complicates almost every operation that you can perform upon a table, because 99% of the time your application must pretend this row doesn't exist. Almost every SELECT statement will require modification. Suddenly you need special logic to handle these particular "special" non-existent rows. You can never permit the deletion of any row whose PK is zero. And finally, this solution is based on a misunderstanding of basic relational principles. Every instant convenience has an unexpected cost. Never add meaningless rows to foreign tables. Nulls are nulls for a reason.
Other dimensions
Dr. Codd gave us a revolution but over the years it has become apparent that certain problems are not easy to model in relational databases. Two examples are geography and time. They introduce third or fourth dimensions to Relational Flatland. Tables have rows and columns, but have neither depth nor time. MySQL 5&6 implement a subset of the OpenGIS specification; apart from covering that, we leave geographic data modelling to specialists. But change tracking is a universal, non-trivial problem. It has motivated development of post-relational and object-relational databases. Unless you instruct an RDBMS to the contrary, it implements amnesia: when updating, it destroys previous data. That's fine if you just want to know how many frammelgrommets are on the shelf right now, but what if you want to know how many lay there yesterday, or last month, or last year same day? Accountants know the problem of change tracking as the problem of the audit trail. You can implement audit trails in relational databases, but it is by no means easy to do it efficiently with your own code. Some database vendors provide automated audit trailsall you have to do is turn the feature on, at the desired level of granularity. Like many RDBMSs, however, MySQL does not offer this feature. The MySQL implementation of stored procedures and views permits you to port change tracking functionality from client applications into your database. If just a few tables in your application require detailed change tracking and are not too large, you may be able to get by with having your update
Relational Databases
Page 16
procedures copy timestamped row contents into audit tables before changes are written. If the inefficiences of this brute force approach are unacceptable, you will need to make change tracking a central feature of your database design (Chapter 21).
Relational Databases
Page 17
Table 1-1: Normal Forms Definition an unordered table of unordered atomic column values with no repeat rows 1NF + all non-key column values are uniquely determined by the primary key 2NF + all non-key column values are mutually independent Boyce-Codd: 3NF + every determinant is a candidate key BCNF + at most one multivalued dependency 4NF + no cyclic ("join") dependencies
an invoice number, the customer's name and address, the "ship to" address if different, a grid holding the invoice product and cost details (product numbers, descriptions, prices, discounts if any, extended amount) a subtotal of the extended amounts from the grid a list and sum of various taxes shipping charges if applicable the final total list of payments sublist of payment methods (credit card details, cheque number, amount paid) net amount due
Apart from normalisation, there is nothing to stop you from creating a single table containing all these columns. Virtually all commercial and free spreadsheet programs include several examples which do just that. However, such an approach presents huge drawbacks, as you would soon learn if you actually built and used such a "solution":
Relational Databases
Page 18
You will have to enter the customer's name and address information over and over again, for each new sale. Each time, you risk spelling something incorrectly, transposing the street number, and so on. You have no guarantee that the invoice number will be unique. Even if you order preprinted invoice forms, you guarantee only that the invoices included in any particular package are unique. What if the customer wants to buy more than 10 products? Will you load two copies of the spreadsheet, number them both the same, and staple the printouts together? The price for any product could be wrong. Taxes may be applied incorrectly. These problems pale in comparison to the problem of searching your invoices. Each invoice is a separate file in your computer! Spreadsheets typically do not include the ability to search multiple files. There are tools available that can search multiple files, but even simple searches will take an inordinate amount of time. Suppose that you want to know which of your customers purchased a particular product. You will have to search all 10 rows of every invoice. Admittedly, computers can search very quickly, even if they have to search 10 columns per invoice. The problem is compounded by other potential errors. Suppose some of entries are misspelled. How will you find those? This problem grows increasingly difficult as you add items to each spreadsheet. Suppose you want the customers who have purchased both cucumber seeds and lawnmowers? Since either product could be in any one of the ten detail rows, you have to perform ten searches for each product, per invoice! Modern spreadsheets have the ability to store multiple pages in a single file, which might at first seem a solution. You could just add a new page for every new invoice. As you would quickly discover, this approach consumes an astonishing amount of memory for basically no reason. Every time you want to add a new invoice, you have to load the entire group!
In short, spreadsheets and other un-normalised tabular arrangements are the wrong approach to solving this problem. What you need is a database in at least 2NF. Starting with our list of invoice attributes, we take all the customer information out of the invoice and place it in a single row of a new table, called customers. We assign the particular customer a unique number, a primary key, and store only that bit of customer info in the invoice. Using that number we can retrieve any bit of information about the customer that we may need - name, address, phone number and so on. The information about our products should also live in its own table. All we really need in the details table is the product number, since by using it we can look up any other bit of information we need, such as the description and price. We take all the product information out of the details grid and put it in a third table, called products, with its own primary key.
Relational Databases
Page 19
Similarly, we export the grid of invoice line items to a separate table. This has several advantages. Forget being limited to ten line items. We can add as many invoice details (or as few) as we wish. Searching becomes effortless; even compound searches (cucumber seeds and lawnmowers) are easy and quick. Now we have four tables:
Customers - customer number, name, address, phone number, credit card number
and so on.
Invoices - invoice number, date, customer number and so on. Invoice Details - detail number, invoice number, product number, quantity,
Each row in each table is numbered uniquely. This number, its primary key, is the fastest possible way to find the row in the table. Even if there millions of rows, a database engine can find the one of interest in a fraction of second. To normalise invoice data storage to 2NF, we had to take Humpty Dumpty apart. To produce an invoice we have to put him together again. We do this by providing relationship links amongst our tables:
The Customers table has the PK Customer_Id. The Products table has the PK Product_Id. The Invoices table has a column Inv_Customer_Id, and every Invoices row has a Inv_Customer_Id value that points to a unique Customers row. The Invoice Details table has a column InvDet_Invoicer_Id, and every Invoice Details row has a InvDet_Invoice_Id value that points to a unique Invoices row, and a InvDet_Product_Id value that points to a unique row in the Products table.
Relational Databases
Page 20
customer as we wish, identify each by type, and add new address types anytime we need them. Now consider the phone number column. When was the last time you had only one phone number? Almost certainly, you have severaldaytime, evening, cell, work, pager, fax, and whatever new technologies will emerge next week or month or year. As with addresses, this suggests two more tables:
PhoneNumbers - PhoneNumberID, CustomerID, PhoneNo, PhoneTypeID. PhoneTypes - PhoneTypeID, Description.
Our existing design glosses over the problem of suppliers, tucking it into the products table, where it will doubtless blow up in our faces. At the most basic level, we should export the supplier information into its own table, called suppliers. Depending upon the business at hand, there may or may not emerge an additional difficulty. Suppose that you can buy product X from suppliers T, U and V. How will you model that fact? The answer is, a table called productsuppliers, consisting of ProductSupplierNumber, ProductNumber and SupplierNumber.
Relational Databases
Page 21
exports street names to their own table. On the other hand, for a business with customers in many countries, this step might prove to be more a problem than a solution. Suppose you model addresses like this:
AddressID CustomerID AddressTypeID StreetNumber StreetID CityID StateID CountryID
Clearly, StreetID, CityID, StateID and CountryID violate the 4NF rule. Multiple cities may have identical street names, multiple states can have identical city names, and so on. We have many possible foreign keys for StreetID, CityID, StateID and CountryID. Further, we can abstract the notion of address into a hierarchy. It becomes possible to enter nonsensical combinations like New York City, New South Wales, England. The city New York resides in precisely one state, which in turn resides in precisely one country, the USA. On the other hand, a city named Springfield exists in 30 different states in the USA. Clearly, the name of a city, with or without an ID, is insufficient to identify the particular Springfield where Homer Simpson lives. Fourth Normal Form requires that you export stateID to a cities table (cityID, cityname, stateID). StateID then points to exactly one row in the states table (stateID, statename, countryID), and countryID in turn points to exactly one row in a countries table (countryID, countryname). With these 4NF refinements, CityID is enough to distinguish the 30 Springfields in the USA: each is situated in exactly one state, and each state is situated in exactly one country. In this model, given a cityID, we automatically know its state and its country. Further, in terms of data integrity, by limiting the user's choices to lookups in cities and states tables we make it impossible for a user to enter Boston, Ontario and Hungary for city, state and country. All we need is the CityID and we have everything else.
Relational Databases
Page 22
To illustrate 5NF, suppose we buy products to sell in our seed store, not directly from manufacturers, but indirectly through agents. We deal with many agents. Each agent sells many products. We might model this in a single table:
AgentID - foreign key into the agents table ProductID - foreign key into the products table SupplierID - foreign key into the suppliers table PrimaryKey - a unique identifier for each row in this table
The problem with this model is that it attempts to contain two or more many-to-many relationships. Any given agent might represent n companies and p products. If she represents company c, this does not imply that she sells every product c manufactures. She may specialise in perennials, for example, leaving all the annuals to another agent. Or, she might represent companies c, d and e, offering for sale some part of their product lines (only their perennials, for example). In some cases, identical or equivalent products p1, p2 and p3 might be available from suppliers q, r and s. She might offer us cannabis seeds from a dozen suppliers, for example, each slightly different, all equally prohibited. In the simple case, we should create a table for each hidden relationship:
AgentProducts - the list of products sold by the agents. This table will consist of
three columns minimally: AgentProductID - the primary key AgentID - foreign key pointing to the agents table. ProductID - foreign key pointing to the products table.
ProductSuppliers - the list of products made by the suppliers. Minimally this
Having refined the model to 5NF, we can now gracefully handle any combination of agents, products and suppliers. Not every database needs to satisfy 4NF or 5NF. That said, few database designs suffer from the extra effort required to satisfy these two rules.
Relational Databases
Page 23
Relational Databases
Page 24
Summary
Relational databases take their design principles from the branch of mathematics called set theory. They inherit the logical integrity of set theory and thus make it possible to store and retrieve the data that represents a given problem domain. Normalisation is the process of designing an efficient and flexible database. Although often portrayed as rocket science and described in jargon whose first purpose is to support the continued employment of certain experts, normalisation is easy to understand and easy to implement. At the least, your database should follow Codd's three axioms and twelve rules to the extent permitted by your RDBMS, and it should satisfy 3NF. Not every database needs to satisfy 4NF and 5NF. Each of these refinements involves increased complexity as well as decreased performance. Weigh your choices carefully. Benchmark your results in each scenario, and factor in the likelihood that one day you may need to transform your database to 4NF or 5NF. Successful databases change and evolve; failures whimper and die. Even if your design is flawless, the world outside your design will change. Your garden supplies company will come to sell wedding dresses, or (gasp) software. The more flexibility you build in, the less catastrophic such change will be.
References
1. Codd, EF. "A Relational Model of Data for Large Shared Data Banks." CACM 13, 6 (June 1970) pages 377-387. 2. Codd, EF. "Is Your DBMS Really Relational?", "Does Your DBMS Run By the Rules?" ComputerWorld, October 14/21, 1985. 3. Codd, EF. "The Relational Model for Database Management, Version 2." Addison-Wesley 1990. 4. Wikipedia, "The Relational Model", http://en.wikipedia.org/wiki/Relational_model 5. Database Tutorials on the Web, http://www.artfulsoftware.com/dbresources.html. 6. The Birth of SQL, http://www.mcjones.org/System_R/SQL_Reunion_95/sqlr95-The.html 7. Abbott, Edwin A. "Flatland". New York: Dover, 1990.
TOC Previous
Next
Relational Databases
Page 25