Today, Structured Query Language is the standard means of manipulating and querying data in relational databases, though with proprietary extensions among the products. The ease and ubiquity of SQL have even led the creators of many “NoSQL” or non-relational data stores, such as Hadoop, to adopt subsets of SQL or come up with their own SQL-like query languages.
But SQL wasn’t always the “universal” language for relational databases. From the beginning (circa 1980), SQL had certain strikes against it. Many researchers and developers at the time, including me, thought that the overhead of SQL would keep it from ever being practical in a production database.
Clearly, we were wrong. But many still believe that, for all of SQL’s ease and accessibility, the price exacted in runtime performance is often too high.
Before there was SQL
Before there was SQL, databases had tight, navigational programming interfaces, and typically were designed around a network schema called the Codasyl data model. Codasyl (Committee on Data Systems Languages) was a consortium that was responsible for the Cobol programming language (starting in 1959) and database language extensions (starting 10 years later).
When you programmed against a Codasyl database, you were navigating to records through sets, which express one-to-many relationships. Older hierarchical databases only allow a record to belong to one set. Network databases allow a record to belong to multiple sets.
Say you wanted to list the students enrolled in CS 101. First you would find
"CS 101" in the
Courses set by name, set that as the owner or parent of the
Enrollees set, find the first member (
ffm) of the
Enrollees set, which is a
Student record, and list it. Then you would go into a loop: Find next member (
fnm) and list it. When
fnm failed, you would exit the loop.
That may seem like a lot of scut work for the database programmer, but it was very efficient at execution time. Experts like Michael Stonebraker of University of California at Berkeley and Ingres pointed out that doing that sort of query in a Codasyl database such as IDMS took roughly half the CPU time and less than half the memory as the same query on a relational database using SQL.
For comparison, the equivalent SQL query to return all of the students in CS 101 would be something like
SELECT student.name FROM courses, enrollees, students WHERE course.name ="CS 101"
That syntax implies a relational inner join (actually two of them), as I’ll explain below, and leaves out some important details, such as the fields used for the joins.
Relational databases and SQL
Why would you give up a doubling of execution speed and memory? There were two big reasons: ease of development and portability. I didn’t think either one mattered much in 1980 compared to performance and memory requirements, but as computer hardware improved and became cheaper people stopped caring about execution speed and memory and worried more about the cost of development.
In other words, Moore’s Law killed Codasyl databases in favor of relational databases. As it happened, the improvement in development time was significant, but SQL portability turned out to be a pipe dream.
Where did the relational model and SQL come from? E.F. “Ted” Codd was a computer scientist at the IBM San Jose Research Laboratory who worked out the theory of the relational model in the 1960s and published it in 1970. IBM was slow to implement a relational database in an effort to protect the revenues of its Codasyl database IMS/DB. When IBM finally started its System R project, the development team (Don Chamberlin and Ray Boyce) wasn’t under Codd, and they ignored Codd’s 1971 Alpha relational language paper to design their own language, Sequel (Structured English Query Language). In 1979, before IBM had even released its product, Larry Ellison incorporated the language in his Oracle Database (using IBM’s pre-launch Sequel publications as his spec). Sequel soon became SQL to avoid an international trademark violation.
The “tom-toms beating for SQL” (as Michael Stonebraker put it) were coming not only from Oracle and IBM, but also from customers. It wasn’t easy to hire or train Codasyl database designers and programmers, so Sequel (and SQL) looked much more attractive. SQL was so attractive in the later 1980s that many database vendors essentially stapled a SQL query processor on top of their Codasyl databases, to the great dismay of Codd, who felt that relational databases had to be designed from scratch to be relational.
A pure relational database, as designed by Codd, is built on tuples grouped into relations, consistent with first-order predicate logic. Real-world relational databases have tables that contain fields, constraints, and triggers, and tables are related through foreign keys. SQL is used to declare the data to be returned, and a SQL query processor and query optimizer turn the SQL declaration into a query plan that is executed by the database engine.
SQL includes a sub-language for defining schemas, the data definition language (DDL), along with a sub-language for modifying data, the data manipulation language (DML). Both of these have roots in early Codasyl specifications. The third sub-language in SQL declares queries, through the
SELECT statement and relational joins.
SELECT statement in SQL
SELECT statement tells the query optimizer what data to return, what tables to look in, what relations to follow, and what order to impose on the returned data. The query optimizer has to figure out by itself what indexes to use to avoid brute force table scans and achieve good query performance, unless the particular database supports index hints.
Part of the art of relational database design hangs on the judicious use of indexes. If you omit an index for a frequent query, the whole database can slow down under heavy read loads. If you have too many indexes, the whole database can slow down under heavy write and update loads.
Another important art is choosing a good, unique primary key for every table. You not only have to consider the impact of the primary key on common queries, but how it will play in joins when it appears as a foreign key in another table, and how it will affect the data’s locality of reference.
In the advanced case of database tables that are split up into different volumes depending on the value of the primary key, called horizontal sharding, you also have to consider how the primary key will affect the sharding. Hint: You want the table distributed evenly across volumes, which suggests that you don’t want to use date stamps or consecutive integers as primary keys.
Discussions of the
SELECT statement may start simple, but can quickly become confusing. Consider:
SELECT * FROM Customers;
Simple, right? It asks for all fields and all rows of the
Customers table. Suppose, however, that the
Customers table has a hundred million rows and a hundred fields, and one of the fields is a large text field for comments. How long will it take to pull down all that data over a 10 megabit per second network connection if each row contains an average of 1 kilobyte of data?
Perhaps you should cut down how much you send over the wire. Consider:
SELECT TOP 100 companyName, lastSaleDate, lastSaleAmount, totalSalesAmount FROM Customers
WHERE state ="Ohio" AND city ="Cleveland"
ORDER BY lastSaleDate DESCENDING;
Now you’re going to pull down a lot less data. You have asked the database to give you only four fields, to only consider the companies in Cleveland, and to give you just the 100 companies with the most recent sales. To do that most efficiently at the database server, however, the
Customers table needs an index on
state+city for the
WHERE clause and an index on
lastSaleDate for the
ORDER BY and
TOP 100 clauses.
By the way,
TOP 100 is valid for SQL Server and SQL Azure, but not MySQL or Oracle. In MySQL, you’d use
LIMIT 100 after the
WHERE clause. In Oracle, you’d use a bound on
ROWNUM as part of the
WHERE clause, i.e.
WHERE... AND ROWNUM <=100. Unfortunately, the ANSI/ISO SQL standards (and there are nine of them to date, stretching from 1986 to 2016) only go so far, beyond which each database introduces its own proprietary clauses and features.
Relational joins in SQL
So far, I’ve described the
SELECT syntax for single tables. Before I can explain
JOIN clauses, you need to understand foreign keys and relations between tables. I’ll explain this by using examples in DDL, using SQL Server syntax.
The short version of this is fairly simple. Every table that you want to use in relations should have a primary key constraint; this can either be a single field or a combination of fields defined by an expression. For example:
CREATE TABLE Persons (
PersonID int NOT NULL PRIMARY KEY,
Every table that needs to relate to
Persons should have a field that corresponds to the
Persons primary key, and to preserve relational integrity that field should have a foreign key constraint. For example:
CREATE TABLE Orders (
OrderID int NOT NULL PRIMARY KEY,
PersonID int FOREIGN KEY REFERENCES Persons(PersonID)
There are longer versions of both statements that use the
CONSTRAINT keyword, which lets you name the constraint. That’s what most database design tools generate.
Primary keys are always indexed and unique (the field values cannot be duplicated). Other fields can optionally be indexed. It’s often useful to create indexes for foreign key fields and for fields that appear in
ORDER BY clauses, although not always because of the potential overhead from writes and updates.
How would you write a query that returns all of the orders placed by John Doe?
SELECT PersonName, OrderID FROM Persons
INNER JOIN Orders ON Persons.PersonID = Orders.PersonID
WHERE PersonName ="John Doe";
In fact, there are four kinds of
INNER JOIN is the default (you can omit the word
INNER), and it’s the one that includes only rows that contain matching values in both tables. If you want to list persons whether or not they have orders, you’d use a
LEFT JOIN, for example:
SELECT PersonName, OrderID FROM Persons
LEFT JOIN Orders ON Persons.PersonID = Orders.PersonID
ORDER BY PersonName;
When you start doing queries that join more than two tables, use expressions, or coerce data types, the syntax can get a little hairy at first. Fortunately, there are database development tools that can generate correct SQL queries for you, often by dragging and dropping tables and fields from the schema diagram into a query diagram.
Sometimes the declarative nature of the
SELECT statement doesn’t get you where you want to go. Most databases have a facility called stored procedures; unfortunately this is an area where the databases pretty much all use proprietary extensions to the ANSI/ISO SQL standards.
In SQL Server, the initial dialect for stored procedures (or stored procs) was Transact-SQL, aka T-SQL; in Oracle, it was PL-SQL. Both databases have added additional languages for stored procedures, such as C#, Java, and R. A simple T-SQL stored procedure might only be a parameterized version of a
SELECT statement. Its advantages are ease of use and efficiency. Stored procedures are optimized when they are saved, not every time they are executed.
A more complicated T-SQL stored procedure might use multiple SQL statements, input and output parameters, local variables,
IF...THEN...ELSE conditions, cursors (row-by-row processing of a set), expressions, temporary tables, and a whole host of other procedural syntax. Obviously if the stored procedure language is C#, Java, or R, you’re going to use the functions and syntax of those procedural languages. In other words, despite the fact that the motivation for SQL was to use standardized declarative queries, in the real world you see lots of database-specific procedural server programming.
That doesn’t quite take us back to the bad old days of Codasyl database programming (although cursors come close), but it does walk back from the ideas that SQL statements should be standardized and that performance concerns should be left to the database query optimizer. In the end, a doubling of performance is often too much to leave on the table.
This story, "What is SQL? The language of databases" was originally published by InfoWorld.