CP4 due Wednesday by 11 pm PST
Install DB Browser for SQLite before section tomorrow
Relational Database: A method of structuring data as tables associated by shared attributes
A table row corresponds to a unit of data called a record; a column corresponds to an attribute of that record
In Excel-speak:
In the above image, the cells highlighted blue are the first "column" and the cells highlighted green are of the first "row" (or "record", or "tuple")
Tables can be visualized just like an Excel sheet, just with different terminology, and more programmatic capabilities.
id | name | platform | release_year | genre | publisher | developer | rating |
---|---|---|---|---|---|---|---|
1 | Pokemon Red/Blue | GB | 1996 | Role-Playing | Nintendo | Nintendo | E |
2 | Spyro Reignited Trilogy | PS4 | 2018 | Platform | Activision | Toys for Bob | E |
3 | Universal Paperclips | PC | 2017 | World Domination | Frank Lantz | Frank Lantz | E |
... | ... | ... | ... | ... | ... | ... | ... |
A "domain-specific language" (HTML is also a DSL) designed specifically for data access and management.
Also like HTML, SQL is a declarative language: describes what data you are seeking, not exactly how to find it.
In SQL, you write statements. The main different types of statements we'll look at:
(There are others, but we won't be talking about them.)
SELECT name FROM menu WHERE qty > 0;
INSERT into menu (name, category, qty, image)
VALUES ("Cookie", "Desserts", 154, "cookie.png");
Structured Query Language (SQL): A language for searching/updating a database.
A standard syntax that is used by all database software (with minor variations). Generally case-insensitive.
These are the basic "building-blocks" of forming "questions" (queries) in SQL
id | name | platform | release_year | genre | publisher | developer | rating |
---|---|---|---|---|---|---|---|
1 | Pokemon Red/Blue | GB | 1996 | Role-Playing | Nintendo | Nintendo | E |
2 | Spyro Reignited Trilogy | PS4 | 2018 | Platform | Activision | Toys for Bob | E |
3 | Universal Paperclips | PC | 2017 | World Domination | Frank Lantz | Frank Lantz | E |
... | ... | ... | ... | ... | ... | ... | ... |
SELECT
StatementSyntax:
SELECT column(s) FROM table;
Example:
SELECT name, release_year FROM Games;
Example output:
name | release_year |
---|---|
Pokemon Red/Blue | 1996 |
Spyro Reignited Trilogy | 2018 |
Universal Paperclips | 2017 |
Super Mario Bros. | 1985 |
... | ... |
The SELECT
statement is used to return data from a database.
It returns the data in a result table containing the row data for column name(s) given. Table and column names are case-sensitive.
DISTINCT
ModifierSyntax:
SELECT DISTINCT column(s) FROM table;
The DISTINCT
modifier eliminates duplicates from the result set.
Example (without DISTINCT
):
SELECT release_year
FROM Games;
release_year |
---|
1996 |
2018 |
2017 |
1985 |
1996 |
2008 |
... |
Example (withDISTINCT
):
SELECT DISTINCT release_year
FROM Games;
release_year |
---|
1996 |
2018 |
2017 |
1985 |
2008 |
... |
WHERE
StatementSyntax:
SELECT column(s) FROM table WHERE condition(s);
Example:
SELECT name, release_year FROM Games WHERE genre = 'puzzle';
Example result:
name | release_year |
---|---|
Tetris | 1989 |
Brain Age 2: More Training in Minutes a Day | 2005 |
Pac-Man | 1982 |
... | ... |
The WHERE
clause filters out rows based on their columns' data values. In large databases, it's critical to use a WHERE
clause to reduce the result set in size.
Suggestion: When trying to write a query, think of the FROM
part first, then the WHERE
part, and lastly the SELECT
part.
WHERE
ClauseSyntax:
WHERE column operator value(s)
Example:
SELECT name, release_year
FROM Games
WHERE release_year < 1990;
Example result:
name | release_year |
---|---|
Super Mario Bros. | 1985 |
Tetris | 1989 |
Duck Hunt | 1984 |
... | ... |
The WHERE
portion of a SELECT
statement can use the
following properties:
BETWEEN
min AND
maxLIKE
patternIN
(value, value, ..., value)Write a SQL query that returns the name
and platform
of all games with a release_year
before 2000.
id | name | platform | release_year | genre | publisher | developer | rating |
---|---|---|---|---|---|---|---|
1 | Pokemon Red/Blue | GB | 1996 | Role-Playing | Nintendo | Nintendo | E |
2 | Spyro Reignited Trilogy | PS4 | 2018 | Platform | Activision | Toys for Bob | E |
3 | Universal Paperclips | PC | 2017 | World Domination | Frank Lantz | Frank Lantz | E |
... | ... | ... | ... | ... | ... | ... | ... |
SELECT name, platform
FROM Games
WHERE release_year < 2000;
WHERE
Clauses: AND, ORExample:
SELECT name, release_year FROM Games
WHERE release_year < 1990 AND genre='puzzle';
Example result:
name | release_year |
---|---|
Tetris | 1989 |
Pac-Man | 1982 |
Dr. Mario | 1989 |
... | ... |
Multiple WHERE
conditions can be combined using AND
or OR
.
LIKE
Syntax:
WHERE column LIKE pattern
Example:
SELECT name, release_year FROM Games
WHERE name LIKE 'Spyro%'
Example results:
name | release_year |
---|---|
Spyro Reignited Trilogy | 2018 |
Spyro the Dragon | 1998 |
Spyro: Year of the Dragon | 2000 |
Spyro 2: Ripto's Rage | 1999 |
... | ... |
LIKE 'text%'
searches for text that starts with a given prefixLIKE '%text'
searches for text that ends with a given suffixLIKE '%text%'
searches for text that contains a given substringNote: In SQLite, the text in the LIKE string is case-insensitive.
ORDER BY
Syntax:
SELECT column(s) FROM table
ORDER BY column(s) ASC|DESC;
Example (ascending order by default):
SELECT name FROM Games
ORDER BY name
name |
---|
'98 Koshien |
007 Racing |
007: Quantum of Solace |
007: The World is not Enough |
... |
Example (descending order):
SELECT name FROM Games
ORDER BY name DESC
name |
---|
Zyuden Sentai Kyoryuger: Game de Gaburincho |
Zwei |
Zumba Fitness: World Party |
Zumba Fitness Rush |
... |
The ORDER BY
keyword is used to sort the result set in ascending or descending order (ascending if not specified)
LIMIT
Syntax:
LIMIT number
Example:
SELECT name FROM Games
WHERE genre='puzzle'
ORDER BY name
LIMIT 3;
Example result:
name |
---|
100 All-Time Favorites |
101-in-1 Explosive Megamix |
3D Lemmings |
LIMIT
can be used to get the top-N of a given category. It can also be useful as a sanity check to make sure you query doesn't return 100000 rows.
Write a SQL query that returns the name
and genre
of all games that have the word 'dragon' in them, ordered by release_year
id | name | platform | release_year | genre | publisher | developer | rating |
---|---|---|---|---|---|---|---|
1 | Pokemon Red/Blue | GB | 1996 | Role-Playing | Nintendo | Nintendo | E |
2 | Spyro Reignited Trilogy | PS4 | 2018 | Platform | Activision | Toys for Bob | E |
3 | Universal Paperclips | PC | 2017 | World Domination | Frank Lantz | Frank Lantz | E |
... | ... | ... | ... | ... | ... | ... | ... |
SELECT name, genre
FROM Games
WHERE name LIKE '%dragon%'
ORDER BY release_year;
SQLZoo has multiple exercises (with built-in databases you don't need to worry about setting up) for practicing SELECT
, WHERE
, ORDER BY
, LIMIT
, LIKE
, etc. We recommend you go through these for additional practice!
CREATE TABLE
: SyntaxCREATE TABLE is used to create a new table.
Syntax:
CREATE TABLE table_name(
column1 datatype PRIMARY KEY,
column2 datatype,
column3 datatype,
.....
columnN datatype
);
Each column must have a defined data type
The following are the most common:
Type | Argument | Description |
---|---|---|
INT | none | An integer number |
VARCHAR(n) | The string can be at most n characters (max of 65,535). | a text string |
DATE | none | Stores dates: YYYY-MM-DD |
DATETIME | none | Stores dates and times for precise time information: YYYY-MM-DD HH:MM:SS |
DECIMAL(d,s) | Has at most d digits, and s digits after the decimal place | A decimal number. Rounds based on the provided precision. |
TEXT | none | A potentially very large text string. |
Prefer VARCHAR to TEXT, and with as small a max limit as you are comfortable setting. This limits the amount of data someone can dump into your table.
The following are very common and useful in CREATE TABLE statements.
These are called constraints - they "constrain" the types of values you can insert in a column.
This reading on constraints is an excellent overview for more details.
Every table should have a column which is used to uniquely identify each row. This improves efficiency and will prove very useful when using multiple tables.
CREATE TABLE students(
id INT PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL
);
Adding PRIMARY KEY
makes it so that the code will error if that
column ever has duplicates. It will use that column to identify each row quickly.
This is usually an integer id, but can also be other types. Conventionally named id
or prefixed with a letter (e.g. sid
for "student id")
AUTOINCREMENT
will make it so that, if you provide no input for that
column, it will pick the next unused value. Perfect for making it so you don't have to
worry about what the next id is.
We introduced the WPL queue to teach Forms and validation on the client-side.
What types of data might we want to store in a database (wpldb
) for the WPL Queue?
Text-based
But what about...
CREATE TABLE
exampleWhat would the CREATE TABLE
SQL command look like to create
a table to hold the queue for the WPL example?
CREATE TABLE queue(
id INT PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255),
email VARCHAR(255),
student_id INT,
length TINYINT, -- e.g. 2 or 10
question VARCHAR(255), -- max of 255 characters per question
creation_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
Can be imported to execute SQL commands, often to create new tables (conventionally starting with setup
in the file name)
-- Author: Melissa Hovik
-- Last updated: 08.13.19
-- Creates a queue table of questions for the WPL database, wpldb.
CREATE TABLE queue(
id INT PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255),
email VARCHAR(255),
student_id INT,
length TINYINT,
question VARCHAR(255),
creation_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
Note: Use --
for comments in SQL (for each line).
CREATE TABLE
exampleThis is an example using a few different DEFAULT constraints to represent a table for blog posts.
For DATETIME columns, it is convenient to use CURRENT_TIMESTAMP as the DEFAULT so that you don't have to calculate the date/time manually
-- Author: Andrew Wolfram
-- Last updated: 05.18.20
-- Sets up a new posts table in a blog database.
CREATE TABLE `posts` (
`id` int(11) unsigned NOT NULL AUTOINCREMENT,
`timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP,
`author` VARCHAR(200) DEFAULT 'Andrew',
`title` VARCHAR(200) DEFAULT NULL,
`body` text,
PRIMARY KEY (`id`)
);
Note how some of this looks different than the previous CREATE TABLE. Both are valid
DROP TABLE table_name;
deletes a table
DROP TABLE IF EXISTS table_name;
deletes a table only
if it exists, which prevents an error if it doesn't
CREATE TABLE IF NOT EXISTS blog_posts( ... )
IF NOT EXISTS
option is useful if you want the table to be created only if it is not already in a database when importing/executing table creation.To insert a new record into a table, we use the INSERT INTO
keyword:
CREATE TABLE cafemenu(
id INT PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
category VARCHAR(100) NOT NULL,
description VARCHAR(256) NOT NULL,
image VARCHAR(100) DEFAULT "food.png",
qty INT DEFAULT 0
);
INSERT INTO cafemenu (name, category, description)
VALUES ("Banana", "Fresh Fruit", "A fresh test banana.");
First provide the table name, then optionally the list of columns you want to set
(by default it sets all columns). Columns left out will be set to NULL, unless they
have AUTOINCREMENT
set.
Then provide the values for each column, which must match the column names specified.
let qry = "SELECT * FROM posts WHERE author = 'Andrew'" +
"ORDER BY timestamp DESC" +
"LIMIT 1";
let [rows, fields] = await db.all(qry);
console.log(JSON.stringify(rows));
// [{"id":5,"timestamp":"2020-05-18T20:04:23.000Z","author":"Andrew",
"title":"Yet another post","body":"OMG Another blog post about dogs. I just
can't anymore. Where are the cats?"}]
A full-stack website consists of three layers:
The web server makes requests of the database server much like our client-side JS used AJAX
The sqlite
module will handle most of this for us.
sqlite
vs. sqlite3
sqlite3
is the most popular module used to connect to a SQLite database in Node.js
Similar to the fs
module functions, sqlite3
functions are callback-last, with callbacks being error-first
We will use sqlite
which is a wrapper for sqlite that adds promises
First install the sqlite3
and sqlite
modules in your project.
npm install sqlite3 sqlite
Then require both modules with the rest of your modules in your Node.js program.
const sqlite3 = require("sqlite3");
const sqlite = require("sqlite");
'use strict';
const sqlite3 = require('sqlite3');
const sqlite = require('sqlite');
* Establishes a database connection to the wpl database and returns the database object.
* Any errors that occur during connection should be caught in the function
* that calls this one.
* @returns {Object} - The database object for the connection.
*/
async function getDBConnection() {
const db = await sqlite.open({
filename: 'data/wpl.db',
driver: sqlite3.Database
});
return db;
}
db
objectOnce you have the db
object, you can now execute SQL queries with db.all
This function takes a SQL query string and an optional array of parameters/placeholder values and returns the resulting rows.
let rows = await db.all(sqlString)
let rows = await db.all("SELECT name, category FROM menu ORDER BY name;");
The query function returns an array of JS objects, which represent information for each row matching the query. In the below example, we limit at most 2 rows in the result.
let qry = "SELECT name, type FROM pokedex LIMIT 2;";
let rows = await db.all(qry);
console.log(rows);
[
{ name: 'Bulbasaur', type: 'grass' },
{ name: 'Ivysaur', type: 'grass' }
]
The column (field) names for each row (record) can be accessed using dot notation (it's just an object!)
...
let qry = "SELECT name, type FROM pokedex LIMIT 2;";
let rows = await db.all(qry);
let firstRow = rows[0];
console.log("Name: " + firstRow.name + "(" + firstRow.type + ")");
Name: Bulbasaur (grass)
Note that only the column names specified in the SELECT statement will be accessible (for this example, name
and type
)
try
/catch
Database connections can have different problems: the database server could be down, the database could be missing or corrupted, the user/password credentials may be incorrect.
You can find a good review of SQLite error codes here - useful when you are debugging and/or want to handle errors
differently (similar to how we can use err.code === "ENOENT"
in our response logic)
try
/catch
helps us catch and
identify when errors occur so we can handle the error correctly.
sqlite
functions
You can try/catch just like you would for fs.readFile()
,
catching any errors that occur in the db.all
function.
try {
let rows = await db.all("SELECT name FROM pokedex"); // error could happen here
// process the result rows somehow
} catch (error) {
res.status(500).send("Something went wrong on the server. Please try again later.");
}