CREATE, INSERT, SELECT, UPDATE, DELETE — the five magic words that let you talk to any database.
SQL stands for Structured Query Language. It is the universal language every relational database understands — PostgreSQL, MySQL, SQL Server, Oracle, SQLite — all of them speak SQL. If databases were countries, SQL would be English: the one language everyone agrees to communicate in.
Imagine you walk into a huge library with millions of books. You can't just grab random shelves — you need to ask the librarian. SQL is the language you use to talk to the librarian (the database). You say things like "Show me all the science fiction books published after 2020" and the librarian fetches exactly what you asked for.
In the early 1970s, two researchers at IBM — Edgar F. Codd, Donald Chamberlin, and Raymond Boyce — developed a way to store and retrieve data using simple English-like commands. They called the first version "SEQUEL" (Structured English Query Language). Over time the name was shortened to SQL.
Today, SQL is the most in-demand technical skill in data jobs worldwide. Every data analyst, data engineer, backend developer, and data scientist uses it daily.
Think of SQL as the remote control for your TV. The TV (database) holds thousands of channels (data), but without the remote (SQL), you'd have to walk up and press buttons on the TV itself. SQL lets you sit back and command the database from a distance: "Show me channel 42", "Record this show", "Delete last week's recordings."
SQL commands are grouped into families. In this lesson we focus on the two you'll use most:
| Category | Stands For | What It Does | Example Commands |
|---|---|---|---|
| DDL | Data Definition Language | Defines the structure of your data | CREATE, ALTER, DROP |
| DML | Data Manipulation Language | Works with the data itself | INSERT, SELECT, UPDATE, DELETE |
| DCL | Data Control Language | Controls access & permissions | GRANT, REVOKE |
| TCL | Transaction Control Language | Manages transactions | BEGIN, COMMIT, ROLLBACK |
CRUD is an acronym that describes the four fundamental things you can do with data:
Open the Contacts app on your phone. Everything you do there maps to CRUD:
That's it! Every app — Instagram, Gmail, Amazon, Uber — does these four things under the hood with SQL.
| CRUD Operation | SQL Command | English Translation |
|---|---|---|
| Create | INSERT INTO | "Add this new row to the table" |
| Read | SELECT | "Show me the data that matches..." |
| Update | UPDATE | "Change this value to something new" |
| Delete | DELETE | "Remove these rows forever" |
We also need CREATE TABLE (to build the table in the first place) and ALTER/DROP TABLE (to change or destroy it). This lesson covers all of them.
Before you can store any data, you need a table. A table is like a spreadsheet: it has columns (the headers) and rows (the actual data). The CREATE TABLE command defines the columns, what type of data each column holds, and any rules the data must follow.
Imagine opening a fresh Excel spreadsheet. Before typing any numbers, you first set up the column headers: "Name", "Department", "Salary", "Hire Date". That's exactly what CREATE TABLE does — it defines the column headers and their rules, but the table starts completely empty.
CREATE TABLE table_name ( column1 DATA_TYPE constraints, column2 DATA_TYPE constraints, ... );
Throughout this lesson, we'll work with an employees table. Here's how we create it:
CREATE TABLE employees ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, department VARCHAR(50), salary DECIMAL(10,2), hire_date DATE DEFAULT CURRENT_DATE );
Let's break every piece of this down:
This is a PostgreSQL shortcut that means "auto-incrementing integer." Every time you add a new row, PostgreSQL automatically assigns the next number (1, 2, 3, ...). You never have to type the id yourself.
This marks the column as the unique identifier for each row. No two rows can have the same id. Think of it like a Social Security Number — every person (row) gets a unique one.
A text column that can hold up to 100 characters. "VARCHAR" stands for "VARiable CHARacters." If you type "Alice" (5 characters), it only stores 5, not 100.
This means "this column must always have a value." You can't leave it blank. An employee must have a name — it would be weird to have a nameless person on payroll!
A number with up to 10 digits total and 2 decimal places. Perfect for money: 99999999.99 is the max. You don't want salary stored as "around 50k" — you want the exact number.
If you don't provide a hire_date, PostgreSQL automatically fills in today's date. Defaults save you time and prevent empty fields.
After running the command above, you get a table with the structure defined but zero rows:
| id | name | department | salary | hire_date |
|---|---|---|---|---|
| (0 rows — the table is empty) | ||||
Use lowercase letters and underscores for table and column names. Avoid spaces, special characters, and reserved words. Good: hire_date. Bad: Hire Date, date (reserved word).
Now that we have our empty table, let's fill it with data. The INSERT INTO command adds new rows (records) to a table.
Imagine you have a brand new diary with pre-printed columns: Date, Mood, What Happened. Every evening, you write a new line. That's INSERT — you're adding a brand-new entry to your diary (table) every time.
INSERT INTO employees (name, department, salary, hire_date) VALUES ('Alice Johnson', 'Engineering', 85000.00, '2023-01-15');
Notice we did not provide a value for id. Because it's SERIAL, PostgreSQL auto-assigns 1.
You can insert many rows at once by separating each set of values with a comma:
INSERT INTO employees (name, department, salary, hire_date) VALUES ('Bob Smith', 'Marketing', 62000.00, '2023-03-20'), ('Carol Davis', 'Engineering', 91000.00, '2022-11-01'), ('David Lee', 'Sales', 55000.00, '2024-01-10'), ('Eva Martinez', 'Engineering', 97000.00, '2021-07-22'), ('Frank Wilson', 'HR', 58000.00, '2023-06-05'), ('Grace Chen', 'Marketing', 67000.00, '2022-09-14'), ('Hank Brown', 'Sales', 52000.00, '2024-02-28'), ('Irene Taylor', 'Engineering', 89000.00, '2023-04-18'), ('Jake Nguyen', 'HR', 61000.00, '2023-08-30');
After both INSERTs, the table now has 10 employees (Alice + 9 others).
| id | name | department | salary | hire_date |
|---|---|---|---|---|
| 1 | Alice Johnson | Engineering | 85000.00 | 2023-01-15 |
| 2 | Bob Smith | Marketing | 62000.00 | 2023-03-20 |
| 3 | Carol Davis | Engineering | 91000.00 | 2022-11-01 |
| 4 | David Lee | Sales | 55000.00 | 2024-01-10 |
| 5 | Eva Martinez | Engineering | 97000.00 | 2021-07-22 |
| 6 | Frank Wilson | HR | 58000.00 | 2023-06-05 |
| 7 | Grace Chen | Marketing | 67000.00 | 2022-09-14 |
| 8 | Hank Brown | Sales | 52000.00 | 2024-02-28 |
| 9 | Irene Taylor | Engineering | 89000.00 | 2023-04-18 |
| 10 | Jake Nguyen | HR | 61000.00 | 2023-08-30 |
Try inserting a row without the name column (which is NOT NULL):
INSERT INTO employees (department, salary) VALUES ('Marketing', 70000); -- ERROR: null value in column "name" violates not-null constraint
When you define a column as NOT NULL, the database will reject any INSERT that tries to leave it empty. This protects your data quality. Always think about which columns truly need a value.
If you skip hire_date, the DEFAULT CURRENT_DATE kicks in:
INSERT INTO employees (name, department, salary) VALUES ('Karen White', 'Sales', 59000); -- hire_date will automatically be today's date
SELECT is the most important SQL command. You'll use it far more than any other. It retrieves (reads) data from a table. You can fetch everything, fetch specific columns, filter rows, sort results, and much more.
Imagine walking into a library with a million books. You don't rummage through every shelf — you go to the librarian and say: "Show me all mystery novels by Agatha Christie published after 1950." The librarian disappears into the stacks and comes back with exactly the books you asked for. That's SELECT — you describe what you want, and the database fetches it.
The asterisk * means "all columns." This fetches every column and every row:
SELECT * FROM employees;
Result: the full 10-row table we saw above. While SELECT * is great for quick exploration, in production code you should always name the columns you need (we'll see why in a moment).
Instead of all columns, pick only the ones you need:
SELECT name, department, salary FROM employees;
| name | department | salary |
|---|---|---|
| Alice Johnson | Engineering | 85000.00 |
| Bob Smith | Marketing | 62000.00 |
| Carol Davis | Engineering | 91000.00 |
| David Lee | Sales | 55000.00 |
| Eva Martinez | Engineering | 97000.00 |
| Frank Wilson | HR | 58000.00 |
| Grace Chen | Marketing | 67000.00 |
| Hank Brown | Sales | 52000.00 |
| Irene Taylor | Engineering | 89000.00 |
| Jake Nguyen | HR | 61000.00 |
SELECT * is convenient for quick checks, but in real applications you should always list the columns you need. Why? (1) It's faster — the database doesn't fetch unnecessary data. (2) It's clearer — anyone reading your code knows exactly which columns are used. (3) It's safer — if someone adds a column later, your query won't break.
You can rename columns in the output using AS:
SELECT name AS employee_name, department AS dept, salary AS annual_pay FROM employees;
| employee_name | dept | annual_pay |
|---|---|---|
| Alice Johnson | Engineering | 85000.00 |
| Bob Smith | Marketing | 62000.00 |
| ... (8 more rows) | ||
Aliases don't change the actual table — they only change the column names in your query output. They're great for making reports more readable.
Want to see the highest-paid employees first?
SELECT name, salary FROM employees ORDER BY salary DESC;
| name | salary |
|---|---|
| Eva Martinez | 97000.00 |
| Carol Davis | 91000.00 |
| Irene Taylor | 89000.00 |
| Alice Johnson | 85000.00 |
| Grace Chen | 67000.00 |
| Bob Smith | 62000.00 |
| Jake Nguyen | 61000.00 |
| Frank Wilson | 58000.00 |
| David Lee | 55000.00 |
| Hank Brown | 52000.00 |
DESC = descending (biggest first). ASC = ascending (smallest first, which is the default).
When you have millions of rows, you don't want to fetch them all. LIMIT restricts the number of rows returned:
SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 3;
| name | salary |
|---|---|
| Eva Martinez | 97000.00 |
| Carol Davis | 91000.00 |
| Irene Taylor | 89000.00 |
The WHERE clause filters rows based on conditions. Only rows that match the condition are returned. Without WHERE, you get everything. With WHERE, you get exactly what you need.
Imagine a buffet with 50 dishes. A picky eater doesn't grab everything — they say "Only give me dishes that are vegetarian AND spicy." The WHERE clause is the picky eater. It looks at each row and asks: "Do you meet my criteria?" If yes, you're in. If no, you're skipped.
| Operator | Meaning | Example |
|---|---|---|
= | Equal to | department = 'Sales' |
!= or <> | Not equal to | department != 'HR' |
> | Greater than | salary > 70000 |
< | Less than | salary < 60000 |
>= | Greater than or equal | salary >= 85000 |
<= | Less than or equal | salary <= 60000 |
SELECT name, department, salary FROM employees WHERE department = 'Engineering';
| name | department | salary |
|---|---|---|
| Alice Johnson | Engineering | 85000.00 |
| Carol Davis | Engineering | 91000.00 |
| Eva Martinez | Engineering | 97000.00 |
| Irene Taylor | Engineering | 89000.00 |
SELECT name, salary FROM employees WHERE salary > 80000;
| name | salary |
|---|---|
| Alice Johnson | 85000.00 |
| Carol Davis | 91000.00 |
| Eva Martinez | 97000.00 |
| Irene Taylor | 89000.00 |
AND means both conditions must be true. OR means at least one must be true.
-- Engineers earning more than 90k SELECT name, department, salary FROM employees WHERE department = 'Engineering' AND salary > 90000;
| name | department | salary |
|---|---|---|
| Carol Davis | Engineering | 91000.00 |
| Eva Martinez | Engineering | 97000.00 |
-- People in Sales OR HR SELECT name, department FROM employees WHERE department = 'Sales' OR department = 'HR';
| name | department |
|---|---|
| David Lee | Sales |
| Frank Wilson | HR |
| Hank Brown | Sales |
| Jake Nguyen | HR |
PostgreSQL gives you handy shortcuts for common filtering patterns:
-- Salaries between 60k and 80k (inclusive) SELECT name, salary FROM employees WHERE salary BETWEEN 60000 AND 80000; -- Same as: WHERE salary >= 60000 AND salary <= 80000
-- People in specific departments (cleaner than multiple ORs) SELECT name, department FROM employees WHERE department IN ('Engineering', 'Marketing');
-- Names starting with 'A' (pattern matching) SELECT name FROM employees WHERE name LIKE 'A%'; -- % means "any characters after". 'A%' matches Alice, Andrew, Angela...
BETWEEN is like telling the librarian: "Show me books published between 2010 and 2020." IN is like saying: "I only want books by Stephen King, J.K. Rowling, or Dan Brown." LIKE is like saying: "Show me any book whose title starts with 'The'."
When data changes in the real world, you need to reflect that in the database. Got a raise? New department? Changed your name? The UPDATE command modifies existing rows.
Remember using white-out (correction fluid) on handwritten notes? You'd cover the old text and write the new text on top. UPDATE is the digital version of white-out — it replaces old values with new ones, right where they are. The row stays in the same place; only the data changes.
UPDATE table_name SET column1 = new_value1, column2 = new_value2 WHERE condition;
UPDATE employees SET salary = 92000.00 WHERE name = 'Alice Johnson';
Before:
| id | name | salary |
|---|---|---|
| 1 | Alice Johnson | 85000.00 |
After:
| id | name | salary |
|---|---|---|
| 1 | Alice Johnson | 92000.00 |
UPDATE employees SET department = 'Sales', salary = 65000.00 WHERE name = 'Bob Smith';
You can update multiple columns at once by separating them with commas in the SET clause.
UPDATE employees SET salary = salary * 1.10 WHERE department = 'Engineering';
This multiplies the current salary by 1.10 (a 10% increase) for every engineer. The salary on the right side of = refers to the current value.
If you forget the WHERE clause, every single row in the table gets updated:
-- THIS IS DANGEROUS! Every employee's salary becomes 50000! UPDATE employees SET salary = 50000;
Always double-check your WHERE clause before running an UPDATE. A good habit: write the SELECT first to see which rows match, then convert it to an UPDATE.
Before running a risky UPDATE, run a SELECT with the same WHERE clause to see which rows will be affected:
-- Step 1: Preview (safe) SELECT * FROM employees WHERE department = 'Engineering'; -- Step 2: If the rows look right, update UPDATE employees SET salary = salary * 1.10 WHERE department = 'Engineering';
DELETE removes rows from a table. This is the "scary" command because once data is deleted, it's gone (unless you have backups or use transactions).
Imagine you have a filing cabinet full of folders. DELETE is like pulling out one specific folder and feeding it into a paper shredder. It's gone forever. You can't un-shred paper, and you can't un-delete data (without backups). That's why we're extra careful with DELETE.
DELETE FROM table_name WHERE condition;
DELETE FROM employees WHERE name = 'Hank Brown';
This removes only Hank Brown's row. Everyone else stays untouched.
DELETE FROM employees WHERE department = 'Sales';
This deletes every row where department is 'Sales'. David Lee and Hank Brown (if he hadn't been deleted already) would be removed.
Just like UPDATE, forgetting WHERE in a DELETE statement is catastrophic:
-- THIS DELETES EVERY SINGLE ROW in the table! DELETE FROM employees;
The table structure survives (columns, constraints remain), but all your data is erased. Always include WHERE.
If you genuinely want to wipe all rows, PostgreSQL offers TRUNCATE which is faster:
| Feature | DELETE FROM table | TRUNCATE TABLE table |
|---|---|---|
| Removes rows | One by one (can use WHERE) | All at once (no WHERE) |
| Speed | Slower for large tables | Much faster |
| Can be rolled back? | Yes (inside a transaction) | Yes in PostgreSQL (not all DBs) |
| Resets auto-increment? | No | Yes (with RESTART IDENTITY) |
| Fires triggers? | Yes | No |
DELETE with WHERE is like shredding one specific document. DELETE without WHERE is like shredding every document in the cabinet one by one. TRUNCATE is like tipping the entire filing cabinet into a dumpster — same result (all gone) but way faster.
After a table exists, you might realize you forgot a column, or need to rename something, or want to destroy the entire table. That's where ALTER TABLE and DROP TABLE come in.
Oops, we forgot to track email addresses!
ALTER TABLE employees ADD COLUMN email VARCHAR(255);
This adds a new email column. Existing rows will have NULL for this column until you update them.
Don't need a column anymore? Remove it:
ALTER TABLE employees DROP COLUMN email;
When you drop a column, all the data in that column vanishes immediately and permanently. This is not recoverable unless you have a backup. Always double-check before dropping columns on production databases.
ALTER TABLE employees RENAME COLUMN name TO full_name;
ALTER TABLE employees RENAME TO staff;
ALTER TABLE employees ALTER COLUMN salary TYPE NUMERIC(12,2);
DROP TABLE doesn't just delete the data — it destroys the entire table structure. The table ceases to exist. Gone. Columns, rows, constraints, indexes — everything.
-- Destroys the table completely DROP TABLE employees; -- Safer version: only drops if the table exists (no error if it doesn't) DROP TABLE IF EXISTS employees;
Think of it this way: DELETE erases the entries you wrote in the notebook but keeps the notebook. DROP TABLE throws the notebook in the fire. The notebook (table) is gone — not just the entries. You'd have to buy a whole new notebook (CREATE TABLE again) to start over.
| Action | SQL Command |
|---|---|
| Add a column | ALTER TABLE t ADD COLUMN col TYPE; |
| Drop a column | ALTER TABLE t DROP COLUMN col; |
| Rename a column | ALTER TABLE t RENAME COLUMN old TO new; |
| Rename table | ALTER TABLE t RENAME TO new_name; |
| Change column type | ALTER TABLE t ALTER COLUMN col TYPE new_type; |
| Set default | ALTER TABLE t ALTER COLUMN col SET DEFAULT val; |
| Remove default | ALTER TABLE t ALTER COLUMN col DROP DEFAULT; |
| Add NOT NULL | ALTER TABLE t ALTER COLUMN col SET NOT NULL; |
| Remove NOT NULL | ALTER TABLE t ALTER COLUMN col DROP NOT NULL; |
Use DROP TABLE IF EXISTS instead of plain DROP TABLE. If the table doesn't exist, PostgreSQL will silently do nothing instead of throwing an error. Similarly, use ADD COLUMN IF NOT EXISTS (PostgreSQL 9.6+) to avoid duplicate column errors in migration scripts.
Let's put it all together. Here's every command from this lesson in one place:
| Operation | SQL Command | What It Does |
|---|---|---|
| Create Table | CREATE TABLE t (...) | Builds a new empty table with defined columns |
| Create (Insert) | INSERT INTO t VALUES (...) | Adds one or more new rows |
| Read (Select) | SELECT ... FROM t WHERE ... | Fetches rows matching your criteria |
| Update | UPDATE t SET col=val WHERE ... | Changes values in existing rows |
| Delete | DELETE FROM t WHERE ... | Removes rows permanently |
| Alter Table | ALTER TABLE t ... | Modifies the table structure |
| Drop Table | DROP TABLE t | Destroys the table entirely |
Think of a database as a restaurant kitchen:
CREATE TABLE = setting up a new station with labeled containers.
INSERT = stocking ingredients into the containers.
SELECT = the chef checking what ingredients are available.
UPDATE = replacing a stale ingredient with a fresh one.
DELETE = tossing out expired items.
ALTER TABLE = adding a new container to the station.
DROP TABLE = demolishing the entire station.
Time to practice! Try these exercises on your own PostgreSQL database. We recommend opening psql or pgAdmin and typing these out manually — don't just copy-paste. Muscle memory matters.
Create a table called products with these columns:
product_id — auto-incrementing primary keyproduct_name — text up to 150 characters, requiredcategory — text up to 50 charactersprice — decimal with 2 decimal placesin_stock — boolean, default trueUsing a single multi-row INSERT, add these products: Laptop ($999.99, Electronics), Notebook ($4.50, Stationery), Coffee Mug ($12.99, Kitchen), USB Cable ($8.75, Electronics), Desk Lamp ($34.00, Furniture).
Write a SELECT statement that shows only the product_name and price of products in the "Electronics" category, sorted by price from highest to lowest.
The Laptop's price dropped to $899.99. Write an UPDATE statement to change it. Make sure you only update the Laptop!
First, update the USB Cable to in_stock = false. Then, write a DELETE that removes all products where in_stock is false.
Add a new column created_at of type TIMESTAMP with a default of NOW(). Then rename the category column to product_category.
Try each exercise on your own first! If you're stuck, here are the first two solutions:
-- Exercise 1: Create products table CREATE TABLE products ( product_id SERIAL PRIMARY KEY, product_name VARCHAR(150) NOT NULL, category VARCHAR(50), price DECIMAL(10,2), in_stock BOOLEAN DEFAULT true );
-- Exercise 2: Insert 5 products INSERT INTO products (product_name, category, price) VALUES ('Laptop', 'Electronics', 999.99), ('Notebook', 'Stationery', 4.50), ('Coffee Mug', 'Kitchen', 12.99), ('USB Cable', 'Electronics', 8.75), ('Desk Lamp', 'Furniture', 34.00);
Test what you've learned! Click the answer you think is correct.
UPDATE employees SET salary = 0; without a WHERE clause?email to an existing table. Which command do you use?You now know the fundamental building blocks of SQL. Every application — from tiny personal projects to billion-dollar platforms like Netflix and Amazon — uses these exact same commands under the hood. The difference is just scale.
In the next module, we'll dive deeper into Data Types & Constraints. You'll learn about INTEGER vs BIGINT vs SMALLINT, TEXT vs VARCHAR vs CHAR, BOOLEAN, TIMESTAMP, UUID, and the powerful constraint system (UNIQUE, CHECK, FOREIGN KEY) that keeps your data clean and consistent. If this lesson was about learning to speak Database Language, the next one is about expanding your vocabulary!
The best way to learn SQL is by doing. Open psql, create a few tables about something you care about (video games, recipes, your music collection), and practice INSERT, SELECT, UPDATE, and DELETE until they feel second nature. Don't just read — type the commands yourself.