# Learn SQL from JS
Table of Contents
What is SQL?Link to heading
It stands for “Server Query Language”:
- Server: abstract word for, in this case, a program that manages a database
- Query: a request/question you can make to that database
- Language: a convenient way to “query” a “server”
In simplified terms, SQL is “a way to ask for data”.
Just like “English” or “JavaScript”, it’s a language with a bunch of dialects (variations), in this case depending on the specific database being used.
What is a database?Link to heading
A database is a collection of structured data.
It’s that generic because it could be anything.
If you, a human (presumably), decided to keep track of what food items you have at home (your inventory), it might look something like this:
item, amount
eggs, 6tomatoes, 2bread slices, 10lettuce heads, 1That’s a database: you have some data, and you store it somewhere, in this case a file.
Just like the database we’ll look at later, it supports certain operations:
- If you buy eggs for the first time, you can Create a new row for them
- If you want to know how many eggs you have, you can Read the amount in the relevant row
- If you eat or buy an egg, you can Update the row to decrease the amount
- If you no longer care about eggs, you can Delete the relevant row
We call these CRUD operations, and typical databases support them.
While databases can be much more complex in order to deal with more data, more efficiently, the basic concept is pretty close to our inventory.txt example.
One of the most loved databases by startups and JavaScript developers for decades is Postgres, so that’s the one we’re going to work with today.
Why use a dedicated database?Link to heading
Say that instead of manually writing to that inventory.txt file, you want to make a web application with buttons so it’s easier to use.
You could write the values directly in your script.js, running in the browser:
// in your browser
const inventory = [ { item: 'eggs', amount: 6 }, { item: 'tomatoes', amount: 2 }, // ...]
useEggButton.onclick = () => { const eggs = inventory.find(row => row.item === 'eggs'); eggs.amount = eggs.amount - 1;
eggsDisplay.textContent = eggs.amount;}This works, as long as you never refresh or leave the page, and only ever use it from one computer.
Because as soon as you re-open the page, inventory will be reset.
So you want this data to come from your server!
// in your express.js server
const inventory = [ { item: 'eggs', amount: 6 }, { item: 'tomatoes', amount: 2 }, // ...]
app.get('/eggs/use', (req, res) => { const eggs = inventory.find(row => row.item === 'eggs'); eggs.amount = eggs.amount - 1;
res.send(eggs.amount);})Great! Now you can refresh the page, or use it from your phone, and the inventory will be shared!
But oh no! You found a bug somewhere in your server, and need to restart it after you fix the code. Your inventory is lost again!
So the server should get the data from a file, so even if you reset it or restart the computer, you don’t lose any data:
// in your express.js server
const inventory = JSON.parse(fs.readFileSync('inventory.json', 'utf-8'))
app.get('/eggs/use', (req, res) => { const eggs = inventory.find(row => row.item === 'eggs'); eggs.amount = eggs.amount - 1;
fs.writeFileSync('inventory.json', JSON.stringify(inventory));
res.send(eggs.amount);})Hooray! For this usecase, that would be perfectly fine, and some simple applications do exactly this.
Say that instead of tracking your home inventory, you’re writing an application for your local supermarket’s inventory.
Our current database helps, but it’s very limited! We want to store not only the amount of each item, but each individual item’s:
date_of_expiryfor health safety reasonssellerfor contract reasonsreturn_addressin case of defectsstatus, e.g. sold, returned, storage, shelves, ordered, …bar_code_idfor scanningimage_urlfor the website
Suddenly that’s a lot of information, for a lot of items! After a few months of business, it might reach gigabytes1!
Node, along with most programs that are not about databases, is not good at keeping gigabytes of data in memory (in a variable). It will slow down and eventually crash.
We can split this into multiple files and load them one at a time, or distribute them among multiple servers and processes, but are you going to write the code to manage all that? While keeping all of them in sync? What about the code to quickly get the item from its bar_code_id so the scanning process is near-instant?
It turns out you’re not the first human (presumably) to have this problem, and you can use existing solutions!
PostgresLink to heading
In postgres, this is valid SQL code for our simple case:
-- equivalent of setting up the inventory.txt file:CREATE TABLE inventory (item TEXT, amount NUMERIC);
-- "creating" an eggs row:INSERT INTO inventory VALUES ('eggs', 6);
-- "reading" how many eggs you have:SELECT amount FROM inventory WHERE item = 'eggs';
-- "updating" the amount of eggs (decreasing by 1):UPDATE inventory SET amount = amount - 1 WHERE item = 'eggs';
-- "deleting" the eggs row:DELETE FROM inventory WHERE item = 'eggs';The best way to learn about this is hands-on, let’s get your own postgres database!
SupabaseLink to heading
There are many ways to play with postgres, but I reccomend you set up a free supabase account, and use the SQL editor in the dashboard.
That’s worth the entire minute it takes to set it up: supabase is a service used by many startups and personal projects to handle data and authentication, so learning how to use it will give you a leg up in the job market.
Create a project. Defaults are fine.
SQL EditorLink to heading
In the dashbord for that project, open the SQL Editor on the left side.
This is where SQL code goes.
This is an example SQL statement, it should never fail:
SELECT 1 + 1 AS result;You can execute it by pressing Ctrl+Enter, you should see something like this:
| result |
|---|
| 2 |
JS RefresherLink to heading
You’ll see the JS equivalent of SQL code at many points in this article.
You may not have encountered some of these JS features before, so here’s a quick overview:
const double3 = double(3); // double3 = 3 * 2 = 6
const tripled = array.map(n => n * 3); // doubled = [3, 12, 18, 27, 30]: for every value, do `n => n * 3`
// all combinedconst objArray = [{ x: 1, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }];const xs = objArray.map(({ x }) => x); // [1, 3, 5]const onlyXs = objArray.map(({ x }) => ({ x })); // [{ x: 1 }, { x: 3 }, { x: 5 }]const withZ = objArray.map((obj) => ({ ...obj, z: 3 })); // [{ x: 1, y: 2, z: 3 }, ...]If you don’t understand them, don’t worry too much, but when you encounter them you can always get back to this section
CreateLink to heading
In postgres, most queries act on [“tables”](LINK NEEDED).
In JS terms, that’s an [array](LINK NEEDED) of [objects](LINK NEEDED)2:
CREATE TABLE inventory (item TEXT, amount NUMERIC)
INSERT INTO inventory VALUES ('eggs', 6);let inventory = [];
inventory.push({ item: 'eggs', amount: 6 }):We have:
- an
inventorytable (array), - with an eggs row (object)
- and
item,amountcolumns (properties)
In a table, all rows have the same columns, meaning all objects in the array have the same property names and types.
In postgres, TEXT is similar to JavaScript’s string, and NUMERIC is similar to JavaScript’s number. The data types in postgres are [much more varied](LINK NEEDED), but you can do plenty with just a few.
Feel free to execute the above SQL, and let’s add a few more rows:
INSERT INTO inventoryVALUES ('tomatoes', 2), ('bread slices', 10), ('lettuce heads', 1);inventory.push( { item: 'tomatoes', amount: 2 }, { item: 'bread slices', amount: 10 }, { item: 'lettuce heads', amount: 1 });To see what our table looks like now, you can go to supabase’s “Table Editor”, just above the SQL Editor, and you should see something like this:
| item | amount |
|---|---|
| eggs | 6 |
| tomatoes | 2 |
| bread slices | 10 |
| lettuce heads | 1 |
You can always get an overview of what your data looks like by going there, but don’t rely on it too much or you won’t learn any SQL!
Anything you can do in that screen, you can also do it via SQL queries: the table above is the result of SELECT * FROM inventory.
ReadLink to heading
This is the most common operation, and what you should practice most.
SELECTLink to heading
To read data from a table, we use SELECT statements.
How many eggs do we have?
SELECT amount FROM inventory WHERE item = 'eggs';inventory .filter(({ item }) => item === 'eggs') .map(({ amount }) => ({ amount }))| amount |
|---|
| 6 |
Each of those lines are called clauses:
- the
SELECTclause defines which column(s) to read out (amount) - the
FROMclause defines which table to look at (inventory) - the
WHEREclause defines which row(s) to look at (ones that haveitem = 'eggs')
A SELECT statement can have many clauses, and each clause has its own syntax.
For a full overview of what’s available, see the documentation. Don’t worry, we’ll go through the important ones, with friendlier descriptions.
You can use the documentation or other tutorials for an in-depth understanding of each individual clause, here we focus on the big picture and learning by example.
I will throw some code at you, play around with it in the SQL editor to try and understand it.
SELECTLink to heading
Decides which column(s) to include in the result:
SELECT item FROM inventoryinventory .map(({ item }) => ({ item }))| item |
|---|
| eggs |
| tomatoes |
| bread slices |
| lettuce heads |
SELECT item, amount FROM inventory;
-- shorthand for all columns SELECT * FROM inventoryinventory .map(({ item, amount }) => ({ item, amount });
inventory .map(row => ({ ...row }));| item | amount |
|---|---|
| eggs | 6 |
| tomatoes | 2 |
| … | … |
You can use AS for simple renaming. We call this “aliasing”:
SELECT item, amount AS count FROM inventoryinventory .map(({ item, amount }) => ({ item, count: amount }));| item | count |
|---|---|
| eggs | 6 |
| tomatoes | 2 |
| … | … |
Or to name a column that is the result of a calculation
SELECT item, amount, amount/2 AS half FROM inventoryinventory .map(({ item, amount }) => ({ item, amount, half: amount/2 }));| item | amount | half |
|---|---|---|
| eggs | 6 | 3 |
| tomatoes | 2 | 1 |
| … | … | … |
WHERELink to heading
Only considers the rows that match the criteria in the result:
SELECT item, amount FROM inventory WHERE item = 'tomatoes'inventory .filter({ item }) => item === 'tomatoes') .map(({ item, amount }) => ({ item, amount }));| item | amount |
|---|---|
| tomatoes | 2 |
The result can be more than one row:
SELECT item, amount FROM inventory WHERE amount < 3inventory .filter({ item }) => amount < 3) .map(({ item, amount }) => ({ item, amount }));| item | amount |
|---|---|
| tomatoes | 2 |
| lettuce heads | 1 |
For multiple conditions, use AND/OR:
SELECT item, amount FROM inventory WHERE amount < 3 -- even amounts AND mod(amount, 2) = 0inventory .filter({ item }) => amount < 3 && (amount % 3 === 0)) .map(({ item, amount }) => ({ item, amount }));| item | amount |
|---|---|
| tomatoes | 2 |
ORDER BYLink to heading
To sort the order of the results:
SELECT item, amount FROM inventory ORDER BY amountinventory .sort((a, b) => a.amount - b.amount) .map(({ item, amount }) => ({ item, amount }));| item | amount |
|---|---|
| lettuce heads | 1 |
| tomatoes | 2 |
| eggs | 6 |
| bread slices | 10 |
By default, it’s in “ascending” order (increasing, a to b). You can specify a descending order instead:
SELECT item, amount FROM inventory ORDER BY amount DESCinventory .sort((a, b) => a.amount - b.amount).reverse() .map(({ item, amount }) => ({ item, amount }));| item | amount |
|---|---|
| bread slices | 10 |
| eggs | 6 |
| tomatoes | 2 |
| lettuce heads | 1 |
You’re not limited to numbers, you can also sort alphabetically:
SELECT item, amount FROM inventory ORDER BY iteminventory .sort((a, b) => a.item.localeCompare(b.item)) .map(({ item, amount }) => ({ item, amount }));| item | amount |
|---|---|
| bread slices | 10 |
| eggs | 6 |
| lettuce heads | 1 |
| tomatoes | 2 |
Pagination: LIMIT + OFFSETLink to heading
For a bit more depth, let’s break down a common feature: pagination.
SELECT item, amount FROM inventory LIMIT 2inventory .map(({ item, amount }) => ({ item, amount })); .slice(0, 2)| item | amount |
|---|---|
| eggs | 6 |
| tomatoes | 2 |
When LIMIT is set, the query returns only the first N results. This is useful so that when you don’t have to all of the relevant rows from the database at once. But how do you get the the next N results?
That’s where OFFSET comes in. We can tell it to skip the first 2 results to get the 2nd page:
SELECT item, amount FROM inventory LIMIT 2 OFFSET 2inventory .map(({ item, amount }) => ({ item, amount })); .slice(2, 2 + 2) // from offset, to offset + limit| item | amount |
|---|---|
| bread slices | 10 |
| lettuce heads | 1 |
In combination with ORDER BY, this is how most results are paginated on websites.
For example, imagine we had thousands of items in a supermarket inventory, and on the website a user clicks a filter for “in stock” (amount >= 1), and sort by “name” in alphabetical order.
They start at page 1, where we show results 1-20, so our application gets the data for those 20 items with this query:
SELECT item FROM inventory WHERE amount >= 1 ORDER BY item LIMIT 20 OFFSET 0inventory .filter(({ amount }) => amount >= 1) .sort((a, b) => a.item.localeCompare(b.item)) .slice(0, 0 + 20) .map(({ item }) => ({ item }))| item |
|---|
| avocado |
| … 18 items |
| bread slices |
Now they click a button to get to page 2, where we have items 21-40, so our query just changes the offset:
SELECT item FROM inventory WHERE amount >= 1 ORDER BY item LIMIT 20 OFFSET 20inventory .filter(({ amount }) => amount >= 1) .sort((a, b) => a.item.localeCompare(b.item)) .slice(20, 20 + 20) .map(({ item }) => ({ item }))| item |
|---|
| brown biscuits |
| … 18 items |
| eggs |
For page 3, we’d just need to change the OFFSET to 40.
In general, if you’re on page page, and each page displays pageSize (20) items, the query is:
...LIMIT <pageSize> OFFSET (<page>-1) * <pageSize>We use page-1 so that page 1 doesn’t skip the first 20 items. Take a moment to convince yourself this works.
CountLink to heading
We’re missing one bit of information to display on the page: how many total items we have.
SELECT COUNT(*) AS total FROM inventory WHERE amount >= 1inventory .filter(({ amount }) => amount >= 1) .length| total |
|---|
| 200 |
If our page size is 20, that means we have 10 pages in total.
Many tablesLink to heading
What if you have multiple tables that are relevant for the data you’re trying to find?
1:many Link to heading
Here’s the scenario: once a month, you want to email your customers letting reminding them to come shop and fill their fridge. But only for items we still have in stock!
You have a purchases table that has a row for every purchase that has happened:
CREATE TABLE purchases (customer_email TEXT, item TEXT);
INSERT INTO purchasesVALUES ('alice@gmail.com', 'avocados'), ('bob@proton.me', 'tomatoes'), ('bob@proton.me', 'eggs'), ('charlie@yahoo.com', 'eggs'), ('charlie@yahoo.com', 'avocados');
-- an out of stock itemINSERT INTO inventoryVALUES ('avocados', 0);So we want that list of emails!
We don’t have any avocados in stock:
- Alice only ever bought avocados, so no point in sending her an email.
- Charlie bought avocados, but also eggs, which we do have, so better drop an email!
- Bob never liked avocados, so an email for him would make sense.
Easy enough to look at our tables and figure it out, but what if we wanted to this with SQL, once we have too much data to process manually?
We can get a full list of emails with this query:
SELECT customer_email FROM purchasespurchases .map(({ customer_email }) => ({ customer_email }));| customer_email |
|---|
| alice@gmail.com |
| bob@proton.me |
| bob@proton.me |
| charlie@yahoo.com |
| charlie@yahoo.com |
Alice is in that list, but she shouldn’t be. How do we only select emails from purchases where the purchased item’s inventory amount is greater than 0?
SELECT customer_email FROM purchases -- not valid! Which inventory row should we look at? WHERE inventory.amount > 0That’s where JOIN comes in. It looks complicated, but bear with it:
SELECT purchases.customer_email FROM purchases JOIN inventory ON inventory.item = purchases.item WHERE intentory.amount > 0purchases // FROM .flatMap((purchases_row) => inventory .filter(({ item }) => item === purchases_row.purchased_item) // ON .map((inventory_row) => ({ purchases_row, inventory_row }))) // JOIN .filter(({ inventory_row }) => inventory_row.amount > 0) // WHERE .map(({ purchases_row }) => ({ customer_email: purchases_row.customer_email }));| customer_email |
|---|
| bob@proton.me |
| bob@proton.me |
| charlie@yahoo.com |
This is where SQL starts looking much simpler than JavaScript.
If you think you can understand the JS, give it a go. Run it through with an example, it will make the intuition for JOIN easier.
Step-by-step JOIN/ON/WHERELink to heading
For the rest of us, here’s a more detailed step-by-step breakdown:
JOINconsiders all combinations of rows frompurchasesandinventory
SELECT purchases.customer_email, purchases.item AS purchased_item, inventory.item AS inventory_item, inventory.amount, FROM purchases JOIN inventorypurchases // FROM .flatMap((purchases_row) => inventory .map((inventory_row) => ({ purchases_row, inventory_row }))) // JOIN .map(({ purchases_row, inventory_row }) => ({ customer_email: purchases_row.customer_email, purchased_item: purchases_row.item, inventory_item: inventory_row.item, amount: inventory_row.amount });| customer_email | purchased_item | inventory_item | amount |
|---|---|---|---|
| alice@gmail.com | ’avocados' | 'eggs’ | 6 |
| alice@gmail.com | ’avocados' | 'tomatoes’ | 2 |
| alice@gmail.com | ’avocados' | 'bread slices’ | 10 |
| alice@gmail.com | ’avocados' | 'lettuce heads’ | 1 |
| alice@gmail.com | ’avocados' | 'avocados’ | 0 |
| - | |||
| bob@proton.me | ’tomatoes' | 'eggs’ | 6 |
| bob@proton.me | ’tomatoes' | 'tomatoes’ | 2 |
| bob@proton.me | ’tomatoes' | 'bread slices’ | 10 |
| bob@proton.me | ’tomatoes' | 'lettuce heads’ | 1 |
| bob@proton.me | ’tomatoes' | 'avocados’ | 0 |
| - | |||
| bob@proton.me | ’eggs' | 'eggs’ | 6 |
| bob@proton.me | ’eggs' | 'tomatoes’ | 2 |
| bob@proton.me | ’eggs' | 'bread slices’ | 10 |
| bob@proton.me | ’eggs' | 'lettuce heads’ | 1 |
| bob@proton.me | ’eggs' | 'avocados’ | 0 |
| - | |||
| charlie@yahoo.com | ’eggs' | 'eggs’ | 6 |
| charlie@yahoo.com | ’eggs' | 'tomatoes’ | 2 |
| charlie@yahoo.com | ’eggs' | 'bread slices’ | 10 |
| charlie@yahoo.com | ’eggs' | 'lettuce heads’ | 1 |
| charlie@yahoo.com | ’eggs' | 'avocados’ | 0 |
| - | |||
| charlie@yahoo.com | ’avocados' | 'eggs’ | 6 |
| charlie@yahoo.com | ’avocados' | 'tomatoes’ | 2 |
| charlie@yahoo.com | ’avocados' | 'bread slices’ | 10 |
| charlie@yahoo.com | ’avocados' | 'lettuce heads’ | 1 |
| charlie@yahoo.com | ’avocados' | 'avocados’ | 0 |
Big table! If purchases has 5 rows, and inventory also has 5 rows, this temporarily table that combines them is 5x5 = 25 rows.
We don’t care about most of these, the only reason we want to combine them is so we can get inventory.amount for the item that was purchased.
So we only want to keep the rows where inventory.item = purchase.item. We do that with ON:
SELECT purchases.customer_email, purchases.item AS purchased_item, inventory.item AS inventory_item, inventory.amount, FROM purchases JOIN inventory ON inventory.item = purchases.itempurchases // FROM .flatMap((purchases_row) => inventory .filter(({ item }) => item === purchases_row.item) // ON .map((inventory_row) => ({ purchases_row, inventory_row }))) // JOIN .map(({ purchases_row, inventory_row }) => ({ customer_email: purchases_row.customer_email, purchased_item: purchases_row.item, inventory_item: inventory_row.item, amount: inventory_row.amount });| customer_email | purchased_item | inventory_item | amount |
|---|---|---|---|
| alice@gmail.com | ’avocados' | 'avocados’ | 0 |
| bob@proton.me | ’tomatoes' | 'tomatoes’ | 2 |
| bob@proton.me | ’eggs' | 'eggs’ | 6 |
| charlie@yahoo.com | ’eggs' | 'eggs’ | 6 |
| charlie@yahoo.com | ’avocados' | 'avocados’ | 0 |
Much nicer! We now only have 1 row per purchase, because each purchases row corresponds to exactly 1 inventory row.
Now we just need to get rid of the ones where the amount is 0!
Adding WHERE inventory.amount > 0 is enough to get rid of the avocado purchases:
| customer_email | purchased_item | inventory_item | amount |
|---|---|---|---|
| bob@proton.me | ’tomatoes' | 'tomatoes’ | 2 |
| bob@proton.me | ’eggs' | 'eggs’ | 6 |
| charlie@yahoo.com | ’eggs' | 'eggs’ | 6 |
See the first JOIN example for the full query.
Note that, technically, omitting the ON clause and using WHERE inventory.item = purchases.item AND inventory.amount > 0 would have lead to the same output.
By convention, if the condition is about picking which rows to join, we put that in the ON clause.
It might affect performance not to do it this way. For example, note that in the JS equivalent, we filter using ON before we filter on the joined rows using WHERE, leading to less comparisons needed overall.
In reality, databases like Postgres are very advanced and perform tons of optimizations behind the scenes, so it might not matter. What’s important for now is to understand the concept of what a JOIN/ON is.
DISTINCTLink to heading
Almost there! The only issue is that our result now includes a duplicate email for Bob.
The way we tell Postgres to only provide unique results is with DISTINCT:
SELECT DISTINCT customer_email FROM purchasespurchases .map(({ customer_email }) => ({ customer_email })) .filter(isUnique) // DISTINCT| customer_email |
|---|
| alice@gmail.com |
| bob@proton.me |
| charlie@yahoo.com |
We haven’t defined isUnique, but a concise, inefficient, implementation could look something like this:
const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);const isUnique = (a, _index, array) => array.filter(b => isEqual(a, b)).length === 1;This uniqueness constraint happens after everything else, and depends on the specific values in the result.
For example, if the result also had to include the purchased item, there would no longer be duplicate rows, so DISTINCT would do nothing.
many:many Link to heading
Alright! You’ve sent those emails: “Dear <name>, you must be starving, come shop again! We have your items in stock”.
You wait for your helpful feature to reach the customers, and for profit to happen, but instead you get a bunch of angry responses: “Unsolicited emails? No thank you, I’m shopping somewhere else from now on!”.
Of course! We focused so much on whether we could, we never stopped to consider whether we should!
It’s still a helpful feature, and some customers might genuinely appreciate knowing that what they typically buy is still in stock after they ran out.
The sensible thing to do is to allow them to “opt in” for this feature. A setting on the website for “I would like to receive monthly information about stock”.
This information goes into a new table:
CREATE TABLE customers (email TEXT, wants_updates BOOLEAN);
INSERT INTO customers VALUES ('alice@gmail.com', true), ('bob@proton.me', false), ('charlie@yahoo.com', true);Ok! We can now write a new query that also checks if the customer wants updates, before sending the automatic email:
SELECT customers.email FROM customers JOIN purchases ON purchases.customer_email = customers.email JOIN inventory ON inventory.item = purchases.item WHERE customers.wants_updates AND inventory.amount > 0customers // FROM .flatMap((customers_row) => purchases .filter(({ customer_email }) => customer_email === customers_row.email) // ON .map((purchases_row) => ({ customers_row, purchases_row }))) // JOIN .flatMap((row) => inventory .filter(({ item }) => item === row.purchases_row.purchased_item) // ON .map((inventory_row) => ({ ...row, inventory_row }))) // JOIN .filter(({ inventory_row, customers_row }) => customers_row.wants_updates && inventory_row.amount > 0) // WHERE .map(({ customers_row }) => ({ email: customers_row.email }));| charlie@yahoo.com |
We need information from all 3 tables, so we need to combine all of them, so we need 2 JOINs!
Conceptually, this temporarily creates a table that has customers.length * purchases.length * inventory.length rows: 3x5x5 = 75.
We can then say which rows we care about with ON, and finally which of the rows with the full information we want with WHERE:
- Alice wanted updates, but we didn’t have any for her, so no email
- Bob didn’t want updates, so even though we had some, no email
- Charlie is the only one that wanted updates, and we had some.
If you could write the above query by yourself in similar contexts, your SQL would be better than most JS developers I’ve met!
So give it a go, invent a scenario, make the database for it, and practice querying it!
UpdateLink to heading
UPDATE + SETLink to heading
The syntax is similar to SELECT’s.
We re-supply our ‘eggs’ , and need to increase the amount by 6.
UPDATE inventory SET amount = amount + 6 WHERE item = 'eggs'inventory .filter(({ item }) => item === 'eggs') .forEach((row) => { row.amount = row.amount + 6 });| item | amount |
|---|---|
| eggs | 12 |
| tomatoes | 2 |
| … |
Our store went up in flames and we all of our inventory burnt up? That’s a problem for our business, but easy to represent in our database:
UPDATE inventory SET amount = 0inventory .forEach((row) => { row.amount = 0 });| item | amount |
|---|---|
| eggs | 0 |
| tomatoes | 0 |
| … |
If you don’t specify WHERE, it will match all of the rows, just as with SELECT.
So watch out, this one is easy to execute accidentally, and there’s no undo button!
ALTERLink to heading
Our database has had a noticeable flaw: we don’t have prices listed for each item!
The online shop experience must have been very confusing.
We don’t need to make a new table for it though, we can add a column to the existing table:
ALTER TABLE inventory ADD COLUMN price NUMERIC DEFAULT 0inventory .forEach((row) => { row.price = 0 });| item | amount | price |
|---|---|---|
| eggs | 0 | 0 |
| tomatoes | 0 | 0 |
| … |
We would then UPDATE the price of our items as needed.
DeleteLink to heading
DELETE FROMLink to heading
Bob sent us a GDPR request to delete his information from our servers.
DELETE FROM customers WHERE email = 'bob@proton.me';
UPDATE purchases SET customer_email = '<redacted>' WHERE customer_email = 'bob@proton.me';customers = customers .filter(({ email }) => email !== 'bob@proton.me');
purchases .filter(({ customer_email }) => customer_email === 'bob@proton.me') .forEach((row) => { row.customer_email = '<redacted>' });| wants_updates | |
|---|---|
| alice@gmail.com | true |
| charlie@yahoo.com | true |
DROP TABLELink to heading
You can also delete an entire table.
Say we decided that we don’t want to track purchases anymore because we care about privacy.
DROP TABLE purchases;delete globalThis.purchasesAgain, no “undo” button in SQL! Act with care!
Nevermind that it would be an accounting nightmare.
ConclusionLink to heading
If you’ve followed this article and practiced, you could now handle the data side of a typical startup.
TODO links
There’s a few more concepts you should learn about before you go all out.
- SQL injection (SQLi): blindly replacing strings in JS to execute SQL may bankrupt you!
- primary keys: efficiently identifying a row by the value of a column, e.g.
customers.email. - foreign keys: efficiently identifying rows in other tables, by the value of a column in this table, e.g.
purchases.customer_email -fk> customers.email. - schemas: a way to organize tables. The
publicschema is like thewindowobject in a browser: you automatically access it.
And some advanced ones that may not be needed right away:
- indices: efficiently finding rows of a table by the value of other columns. Like primary keys, but more flexible.
- data normalization: avoid messy data structures, with duplicate data or unnecessary tables.
More articles are coming about understanding SQL from the perspective of a JS dev, let me know if you’d like to learn about a particular topic!
footnotes
FootnotesLink to heading
-
Probably not, but I challenge you to come up with a more realistic example that is easier to explain to a varied audience! ↩
-
More technically, it’s a “multi-set”: a
Setwhere a value can appear more than once. The main practical difference from a JS array is that there is no guaranteed order:inventory[0]might give you the row for ‘eggs’ at one point in time, but the one for ‘tomatoes’ at another. And rows aren’t exactly objects, each row is closer to an array of its values, e.g.['eggs', 6], where the property name is implied from the table definition:[{ name: 'item', type: 'text' }, { name: 'amount', type: 'numeric' }]. ↩