Table of Contents
- What is MySQL? SQL vs MySQL
- Why Use MySQL?
- How MySQL Works: Architecture
- Installation & Setup
- Your First Database & Table
- Data Types Explained
- CRUD — Create, Read, Update, Delete
- Filtering: WHERE, LIKE, IN, BETWEEN
- Sorting & Limiting: ORDER BY, LIMIT
- Constraints: PRIMARY KEY, FOREIGN KEY, UNIQUE
- JOINs: INNER, LEFT, RIGHT
- Aggregations: COUNT, SUM, AVG, GROUP BY
- Indexes & Performance
- Project: E-Commerce Database from Scratch
- Connect MySQL with Node.js
- Best Practices & Cheatsheet
- MySQL vs PostgreSQL vs SQLite
- FAQ
1. What is MySQL? SQL vs MySQL
MySQL is an open-source Relational Database Management System (RDBMS) that stores data in tables (rows and columns) and lets you query it using SQL (Structured Query Language).
Created in 1995 and now owned by Oracle, MySQL powers WordPress, Facebook, Uber, Airbnb, Netflix and millions of websites worldwide. It is the #1 most popular open-source database.
SQL vs MySQL — The Common Confusion
| Term | What It Is | Analogy |
|---|---|---|
| SQL | Language to talk to databases | English language |
| MySQL | Software that understands SQL | Person who speaks English |
| PostgreSQL, SQLite | Other software that also understand SQL | Other people who also speak English |
-- SQL is the same across databases (90% similar):
SELECT * FROM users WHERE age > 18;
-- MySQL is the database that EXECUTES this SQL and returns results.
-- PostgreSQL, SQLite, SQL Server all run similar SQL.
How Data is Stored: Tables
Table: users
+----+----------+-------+---------------------+
| id | name | age | email |
+----+----------+-------+---------------------+
| 1 | Avinash | 28 | avi@example.com |
| 2 | Priya | 26 | priya@example.com |
| 3 | Rohan | 30 | rohan@example.com |
+----+----------+-------+---------------------+
↑ ↑ ↑ ↑
PK VARCHAR INT VARCHAR — columns have types
Rows = records, Columns = fields
In simple words: Think of MySQL as Excel on steroids — tables are like sheets, rows are records, columns are fields. But unlike Excel, MySQL handles millions of rows, enforces rules (constraints), links tables together (JOINs), and serves data to your apps via queries.
2. Why Use MySQL?
Advantages
- Free & open-source — Community Edition costs nothing, enterprise support available
- Beginner friendly — simple installation, readable SQL, huge tutorial community
- Proven at scale — handles millions of queries per second at Facebook / Uber
- Works everywhere — Node.js, PHP, Python, Java, WordPress, shared hosting
- ACID compliant — Atomicity, Consistency, Isolation, Durability — your data stays safe
- Great tooling — MySQL Workbench, phpMyAdmin, CLI, and cloud options (PlanetScale, AWS RDS)
When NOT to Use MySQL
- Unstructured data (JSON-heavy docs) → consider MongoDB
- Need advanced geospatial or full-text search → PostgreSQL may be better
- Tiny local app with no server → SQLite is simpler (single file, zero config)
✅ Use MySQL for: blogs, e-commerce, SaaS, user accounts, inventories
✅ Use MySQL when: data has clear relationships (users → orders → products)
✅ Use MySQL when: you need transactions (payments must not lose money)
3. How MySQL Works: Architecture
Understanding the big picture before writing queries makes everything click faster.
// How your app talks to MySQL:
// ┌──────────┐ SQL Query ┌──────────────────┐ Reads/Writes ┌────────┐
// │ Your App │ ──────────────→ │ MySQL Server │ ───────────────→ │ Disk │
// │ (Node.js)│ ←────────────── │ (mysqld process)│ ←─────────────── │ Files │
// └──────────┘ Result Rows └──────────────────┘ └────────┘
//
// Inside MySQL Server:
// 1. Connection Layer — handles client connections & auth
// 2. SQL Layer — parses SQL, optimizes, executes
// 3. Storage Engine — InnoDB (default) stores actual data on disk
Key Concepts
- Server (mysqld) — the MySQL program running in background that holds all databases
- Client (mysql) — CLI or GUI that connects to the server to run queries
- Database — a container for tables (like a folder). One server can have many databases
- Table — rows + columns inside a database
- Storage Engine — InnoDB is the default (supports transactions, foreign keys, row-level locking)
Mental model: MySQL Server is a building. Inside it are databases (apartments). Inside each apartment are tables (rooms). Inside each room are rows (furniture). SQL is the language you use to ask the building manager to fetch or rearrange things.
4. Installation & Setup
Get MySQL running in under 5 minutes — pick one method.
Option 1: Official Installer (Easiest for Beginners)
# 1. Download MySQL Community Server from https://dev.mysql.com/downloads/mysql/
# 2. Choose your OS (Windows / macOS / Linux)
# 3. Install — set a root password when prompted (remember it!)
# 4. Verify in terminal:
mysql --version
# mysql Ver 8.0.42 for Win64
mysql -u root -p
# Enter password: ****
# mysql> ← you are now inside MySQL shell
# Type EXIT; to leave
Option 2: Docker (Recommended for Developers)
# Run MySQL 8 in one command — no system install needed
docker run --name mysql-dev \
-e MYSQL_ROOT_PASSWORD=mypassword \
-e MYSQL_DATABASE=testdb \
-p 3306:3306 -d mysql:8
# Connect
docker exec -it mysql-dev mysql -u root -p
# or
mysql -h 127.0.0.1 -u root -p
# Stop / start later
docker stop mysql-dev
docker start mysql-dev
Option 3: Online — No Install
Practice instantly without installing: db-fiddle.com, SQL Fiddle, or OneCompiler MySQL.
Install MySQL Workbench (GUI)
Download from dev.mysql.com/downloads/workbench — visual table designer, query editor, and server dashboard. Great for beginners who prefer clicking over typing.
Common mistake: Forgetting the root password. If you do, reset it via mysqld --skip-grant-tables or reinstall via Docker (just delete the container and rerun). For learning, use a simple password like root or password — change it for production.
5. Your First Database & Table
Let's create a database and a table — the foundation of every MySQL project.
-- ============================================
-- Connect and explore
-- ============================================
SHOW DATABASES; -- list all databases
-- Create your own database
CREATE DATABASE shop_db;
USE shop_db; -- switch to it
SELECT DATABASE(); -- confirms: shop_db
-- ============================================
-- Create a table
-- ============================================
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Check it
SHOW TABLES;
DESCRIBE users; -- show columns and types
SHOW CREATE TABLE users; -- full definition
Insert & View Data
-- Insert rows
INSERT INTO users (name, email, age) VALUES
('Avinash', 'avi@example.com', 28),
('Priya', 'priya@example.com', 26),
('Rohan', 'rohan@example.com', 30);
-- View
SELECT * FROM users;
-- Result:
-- +----+---------+---------+------+---------------------+
-- | id | name | email | age | created_at |
-- +----+---------+---------+------+---------------------+
-- | 1 | Avinash | avi@... | 28 | 2026-09-23 10:00:00 |
-- +----+---------+---------+------+---------------------+
Semicolons matter. Every SQL statement ends with ;. In the MySQL CLI you can write multi-line queries — it waits for ; before executing. Press Enter without ; and you get a -> continuation prompt — just add ; and press Enter again.
6. Data Types Explained
Choosing the right type saves space, prevents bugs, and speeds up queries.
| Category | Type | Use For | Example |
|---|---|---|---|
| Numeric | INT |
Whole numbers | age INT → 28 |
DECIMAL(10,2) |
Money (exact) | price DECIMAL(10,2) → 1999.99 |
|
| String | VARCHAR(n) |
Variable-length text | name VARCHAR(100) |
CHAR(n) |
Fixed length (e.g. country code) | code CHAR(2) → 'IN' |
|
TEXT |
Long text (articles, comments) | body TEXT |
|
| Date/Time | DATE |
Date only | 2026-09-23 |
DATETIME |
Date + time | 2026-09-23 10:00:00 |
|
TIMESTAMP |
Auto time zone aware | DEFAULT CURRENT_TIMESTAMP |
|
| Other | BOOLEAN |
True/false (stored as TINYINT) | is_active BOOLEAN |
ENUM('a','b') |
Pick from list | status ENUM('pending','shipped') |
|
JSON |
JSON data (MySQL 5.7+) | meta JSON |
-- Example: products table with varied types
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
stock INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Money tip: Never use FLOAT or DOUBLE for money — they cause rounding errors (0.1 + 0.2 ≠ 0.3). Always use DECIMAL(10,2) for prices, balances, and financial values.
7. CRUD — Create, Read, Update, Delete
CRUD is 90% of what you do with a database. Master these four operations.
-- ============================================
-- C — CREATE (INSERT)
-- ============================================
-- Single row
INSERT INTO users (name, email, age) VALUES ('Neha', 'neha@example.com', 24);
-- Multiple rows at once
INSERT INTO users (name, email, age) VALUES
('Aman', 'aman@example.com', 29),
('Sara', 'sara@example.com', 31);
-- Insert and get the new ID
INSERT INTO users (name, email) VALUES ('Test', 'test@example.com');
SELECT LAST_INSERT_ID(); -- returns the auto-increment id
-- ============================================
-- R — READ (SELECT)
-- ============================================
SELECT * FROM users; -- all rows, all columns
SELECT name, email FROM users; -- only specific columns
SELECT * FROM users WHERE age > 25; -- filtered
SELECT * FROM users WHERE id = 2; -- single row by PK
SELECT COUNT(*) FROM users; -- count rows
-- ============================================
-- U — UPDATE
-- ============================================
-- ⚠️ Always use WHERE — without it, you update EVERY row!
UPDATE users SET age = 29 WHERE id = 1;
UPDATE users SET name = 'Avinash B.', email = 'avi.b@example.com' WHERE id = 1;
-- ============================================
-- D — DELETE
-- ============================================
-- ⚠️ Always use WHERE — without it, you delete EVERY row!
DELETE FROM users WHERE id = 5;
-- Delete with condition
DELETE FROM users WHERE age < 18;
-- Remove all rows but keep table structure
TRUNCATE TABLE users; -- faster than DELETE without WHERE
-- Delete the entire table
DROP TABLE users; -- ⚠️ irreversible!
Danger zone: Running UPDATE users SET age = 0; or DELETE FROM users; without WHERE affects every row. Before any UPDATE/DELETE, first run a SELECT with the same WHERE to preview which rows will be affected.
8. Filtering: WHERE, LIKE, IN, BETWEEN
WHERE is how you ask precise questions of your data.
-- ============================================
-- Comparison operators
-- ============================================
SELECT * FROM users WHERE age = 28; -- equal
SELECT * FROM users WHERE age != 28; -- not equal
SELECT * FROM users WHERE age > 25; -- greater than
SELECT * FROM users WHERE age >= 25; -- greater or equal
SELECT * FROM users WHERE age < 30; -- less than
-- ============================================
-- Logical operators
-- ============================================
SELECT * FROM users WHERE age > 25 AND city = 'Hyderabad';
SELECT * FROM users WHERE age < 20 OR age > 60;
SELECT * FROM users WHERE NOT age = 28;
-- ============================================
-- IN — match any in a list
-- ============================================
SELECT * FROM users WHERE city IN ('Hyderabad', 'Mumbai', 'Delhi');
SELECT * FROM users WHERE id IN (1, 3, 5);
-- ============================================
-- BETWEEN — range inclusive
-- ============================================
SELECT * FROM users WHERE age BETWEEN 25 AND 35;
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
-- ============================================
-- LIKE — pattern matching
-- ============================================
SELECT * FROM users WHERE name LIKE 'A%'; -- starts with A
SELECT * FROM users WHERE name LIKE '%n'; -- ends with n
SELECT * FROM users WHERE email LIKE '%@gmail.com'; -- Gmail users
SELECT * FROM users WHERE name LIKE '%vi%'; -- contains "vi"
-- Case-insensitive by default in MySQL (collation)
-- Use BINARY for case-sensitive:
SELECT * FROM users WHERE BINARY name = 'avinash'; -- exact case
-- ============================================
-- NULL handling
-- ============================================
SELECT * FROM users WHERE phone IS NULL; -- missing phone
SELECT * FROM users WHERE phone IS NOT NULL; -- has phone
-- ⚠️ Never use = NULL — always IS NULL / IS NOT NULL
9. Sorting & Limiting: ORDER BY, LIMIT
-- Sort ascending (default A→Z, 0→9, old→new)
SELECT * FROM users ORDER BY name ASC;
SELECT * FROM users ORDER BY age ASC;
-- Sort descending (Z→A, 9→0, new→old)
SELECT * FROM users ORDER BY age DESC;
SELECT * FROM products ORDER BY price DESC;
-- Multiple sort keys
SELECT * FROM users ORDER BY city ASC, age DESC;
-- sort by city first, then by age within each city
-- Limit results
SELECT * FROM users LIMIT 5; -- first 5 rows
SELECT * FROM users LIMIT 10 OFFSET 20; -- skip 20, take 10 (page 3 if page size 10)
-- Pagination: page 1 → LIMIT 10 OFFSET 0
-- page 2 → LIMIT 10 OFFSET 10
-- page 3 → LIMIT 10 OFFSET 20
-- Combine: get 5 most expensive products
SELECT name, price FROM products ORDER BY price DESC LIMIT 5;
-- Newest users first
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
OFFSET is slow for large tables. For deep pagination (page 1000+), use keyset pagination instead: SELECT * FROM users WHERE id > 1000 ORDER BY id LIMIT 10 — this uses the index and stays fast even at millions of rows.
10. Constraints: PRIMARY KEY, FOREIGN KEY, UNIQUE
Constraints are rules that keep your data clean and consistent — they prevent bugs at the database level.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY, -- unique, never null, auto number
email VARCHAR(150) NOT NULL UNIQUE, -- no duplicates allowed
name VARCHAR(100) NOT NULL, -- must have a value
age INT CHECK (age >= 0 AND age <= 120), -- valid range
city VARCHAR(100) DEFAULT 'Unknown' -- default if not provided
);
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL CHECK (amount > 0),
status ENUM('pending','shipped','delivered') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Foreign key: orders.user_id must exist in users.id
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE -- if user deleted, delete their orders
ON UPDATE CASCADE
);
-- What each constraint does:
-- PRIMARY KEY → unique + not null + indexed (one per table, usually `id`)
-- UNIQUE → no duplicates (e.g. email)
-- NOT NULL → column must have a value
-- CHECK → value must satisfy condition
-- FOREIGN KEY → value must exist in another table
-- DEFAULT → value if none given
Foreign Key Actions
| Action | When parent is deleted | Use when |
|---|---|---|
CASCADE |
Delete child rows too | Orders belong to user — delete together |
SET NULL |
Set child FK to NULL | Optional relationship |
RESTRICT |
Block deletion if children exist | Prevent accidental loss (default) |
11. JOINs: INNER, LEFT, RIGHT
JOINs combine rows from two or more tables based on a related column — the most powerful SQL concept.
-- Setup: two related tables
-- users: id, name, email
-- orders: id, user_id, product, amount
-- Sample data:
-- users: (1, Avinash), (2, Priya), (3, Rohan)
-- orders: (1, user_id=1, Laptop, 60000)
-- (2, user_id=1, Mouse, 800)
-- (3, user_id=2, Keyboard, 3000)
-- Rohan (id=3) has NO orders
INNER JOIN — Only Matching Rows
SELECT users.name, orders.product, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
-- Result (only users who HAVE orders):
-- Avinash | Laptop | 60000
-- Avinash | Mouse | 800
-- Priya | Keyboard | 3000
-- Rohan is EXCLUDED (no orders)
LEFT JOIN — All Left Table Rows + Matching Right
SELECT users.name, orders.product, orders.amount
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
-- Result (ALL users, even without orders):
-- Avinash | Laptop | 60000
-- Avinash | Mouse | 800
-- Priya | Keyboard | 3000
-- Rohan | NULL | NULL ← included with NULLs
-- Find users who never ordered:
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL; -- Rohan
RIGHT JOIN — All Right Table Rows
SELECT users.name, orders.product
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;
-- All orders, even if user was deleted (rare — LEFT JOIN is preferred)
-- Visual cheat sheet:
-- INNER JOIN: A ∩ B (intersection)
-- LEFT JOIN: A + (A ∩ B) (all of A, matches from B)
-- RIGHT JOIN: B + (A ∩ B) (all of B, matches from A)
-- Multiple JOINs:
SELECT users.name, orders.product, products.category
FROM users
JOIN orders ON users.id = orders.user_id
JOIN products ON orders.product_id = products.id
WHERE users.city = 'Hyderabad';
Which JOIN to use? Use INNER JOIN when you want only rows with matches in both tables. Use LEFT JOIN when you want all rows from the left table even if there is no match — e.g., "show all users and their orders, if any." 95% of the time you need one of these two.
12. Aggregations: COUNT, SUM, AVG, GROUP BY
Aggregate functions summarize data — total sales, average age, orders per user.
-- Count rows
SELECT COUNT(*) FROM users; -- total users
SELECT COUNT(*) FROM orders WHERE status = 'shipped';
-- Sum, Avg, Min, Max
SELECT SUM(amount) FROM orders; -- total revenue
SELECT AVG(amount) FROM orders; -- average order value
SELECT MIN(price) FROM products; -- cheapest product
SELECT MAX(price) FROM products; -- most expensive
-- GROUP BY — aggregate per group
-- Orders per user:
SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id;
-- Result:
-- user_id | order_count | total_spent
-- 1 | 2 | 60800
-- 2 | 1 | 3000
-- Revenue per status:
SELECT status, COUNT(*) AS cnt, SUM(amount) AS revenue
FROM orders
GROUP BY status;
-- HAVING — filter AFTER grouping (WHERE filters BEFORE)
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
HAVING total > 5000; -- only users who spent > 5000
-- ⚠️ WHERE vs HAVING:
-- WHERE → filters rows before grouping
-- HAVING → filters groups after grouping
Complete Example: Top Customers
SELECT
users.name,
COUNT(orders.id) AS orders_count,
SUM(orders.amount) AS total_spent
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.id, users.name
ORDER BY total_spent DESC
LIMIT 5;
13. Indexes & Performance
An index is like a book's table of contents — it helps MySQL find rows without scanning every page.
-- Without index: MySQL scans ALL rows (slow for large tables)
SELECT * FROM users WHERE email = 'avi@example.com';
-- → Full table scan: checks every row
-- Add an index:
CREATE INDEX idx_email ON users(email);
-- Now the same query uses the index → instant even at 1M rows
-- Check if index is used:
EXPLAIN SELECT * FROM users WHERE email = 'avi@example.com';
-- Look for: type = ref or const, key = idx_email (good)
-- vs type = ALL (bad — full scan)
-- Auto-indexed columns (no need to create manually):
-- PRIMARY KEY is always indexed
-- UNIQUE columns are indexed
-- FOREIGN KEY columns are indexed
-- Manual indexes — add for columns you filter/sort/join often:
CREATE INDEX idx_city ON users(city);
CREATE INDEX idx_status ON orders(status);
CREATE INDEX idx_created ON orders(created_at);
-- Composite index (multiple columns — order matters!)
CREATE INDEX idx_city_age ON users(city, age);
-- Fast for: WHERE city = 'Hyd' AND age > 25
-- Fast for: WHERE city = 'Hyd'
-- NOT fast for: WHERE age > 25 (city is first in index)
-- Remove index:
DROP INDEX idx_city ON users;
Don't over-index. Each index speeds up reads but slows down writes (INSERT/UPDATE) and uses disk space. Index only columns you actually filter, join, or sort by. Start with no extra indexes, add them when EXPLAIN shows a full table scan on a slow query.
14. Project: E-Commerce Database from Scratch
Let's build a complete mini shop database — users, products, orders — with relationships and real queries.
-- ============================================
-- Step 1: Create database
-- ============================================
DROP DATABASE IF EXISTS shop_db;
CREATE DATABASE shop_db;
USE shop_db;
-- ============================================
-- Step 2: Create tables with relationships
-- ============================================
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
city VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
category VARCHAR(100),
price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
stock INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
amount DECIMAL(10,2) NOT NULL,
status ENUM('pending','shipped','delivered','cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT
);
-- ============================================
-- Step 3: Insert sample data
-- ============================================
INSERT INTO users (name, email, city) VALUES
('Avinash', 'avi@example.com', 'Hyderabad'),
('Priya', 'priya@example.com', 'Mumbai'),
('Rohan', 'rohan@example.com', 'Hyderabad');
INSERT INTO products (name, category, price, stock) VALUES
('Laptop', 'Electronics', 60000.00, 10),
('Mouse', 'Electronics', 800.00, 100),
('Keyboard', 'Electronics', 3000.00, 50),
('Desk Chair', 'Furniture', 12000.00, 20);
INSERT INTO orders (user_id, product_id, quantity, amount, status) VALUES
(1, 1, 1, 60000.00, 'delivered'),
(1, 2, 2, 1600.00, 'shipped'),
(2, 3, 1, 3000.00, 'pending'),
(2, 4, 1, 12000.00, 'delivered');
-- ============================================
-- Step 4: Real queries — try each!
-- ============================================
-- 1. All orders with user name and product name
SELECT users.name, products.name AS product, orders.quantity, orders.amount, orders.status
FROM orders
JOIN users ON orders.user_id = users.id
JOIN products ON orders.product_id = products.id;
-- 2. Total revenue
SELECT SUM(amount) AS total_revenue FROM orders WHERE status != 'cancelled';
-- 3. Revenue per city
SELECT users.city, SUM(orders.amount) AS revenue
FROM orders JOIN users ON orders.user_id = users.id
GROUP BY users.city;
-- 4. Top selling product
SELECT products.name, SUM(orders.quantity) AS total_sold
FROM orders JOIN products ON orders.product_id = products.id
GROUP BY products.id
ORDER BY total_sold DESC LIMIT 1;
-- 5. Users who never ordered
SELECT name FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL;
-- 6. Low stock alert
SELECT name, stock FROM products WHERE stock < 20;
-- 7. Monthly sales
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, SUM(amount) AS sales
FROM orders
GROUP BY month
ORDER BY month;
15. Connect MySQL with Node.js
Most beginners want to use MySQL from JavaScript. Here is the minimal modern setup.
# Install driver
npm install mysql2 dotenv
# .env
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=mypassword
DB_NAME=shop_db
// ============================================
// db.js — connection pool (ESM)
// ============================================
import 'dotenv/config';
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
});
export default pool;
// ============================================
// Usage — query with parameterized SQL (prevents SQL injection!)
// ============================================
import pool from './db.js';
// SELECT
const [users] = await pool.query('SELECT * FROM users WHERE city = ?', ['Hyderabad']);
console.log(users);
// INSERT (safe from injection)
const [result] = await pool.query(
'INSERT INTO users (name, email, city) VALUES (?, ?, ?)',
['Neha', 'neha@example.com', 'Delhi']
);
console.log('Inserted ID:', result.insertId);
// UPDATE
await pool.query('UPDATE users SET city = ? WHERE id = ?', ['Pune', 1]);
// JOIN
const [orders] = await pool.query(`
SELECT users.name, products.name AS product, orders.amount
FROM orders
JOIN users ON orders.user_id = users.id
JOIN products ON orders.product_id = products.id
`);
// Express route example
import express from 'express';
const app = express();
app.use(express.json());
app.get('/api/users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM users');
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000, () => console.log('🚀 API at http://localhost:3000'));
Never concatenate user input into SQL. `SELECT * FROM users WHERE name = '${req.body.name}'` is vulnerable to SQL injection — an attacker can delete your database. Always use ? placeholders with parameter arrays as shown above.
16. Best Practices & Cheatsheet
Essential SQL Cheatsheet
-- Databases
SHOW DATABASES; CREATE DATABASE db; USE db; DROP DATABASE db;
-- Tables
SHOW TABLES; DESCRIBE tbl; DROP TABLE tbl; TRUNCATE TABLE tbl;
-- CRUD
INSERT INTO tbl (col) VALUES ('val');
SELECT col FROM tbl WHERE cond ORDER BY col LIMIT n;
UPDATE tbl SET col='val' WHERE cond;
DELETE FROM tbl WHERE cond;
-- Filtering
WHERE col = val | IN (a,b) | BETWEEN x AND y | LIKE '%pat%' | IS NULL
-- JOINs
SELECT * FROM A INNER JOIN B ON A.id = B.a_id;
SELECT * FROM A LEFT JOIN B ON A.id = B.a_id WHERE B.id IS NULL; -- A without B
-- Aggregation
COUNT(*) SUM(col) AVG(col) MIN(col) MAX(col) GROUP BY col HAVING cond
-- Indexes
CREATE INDEX idx ON tbl(col); DROP INDEX idx ON tbl; EXPLAIN SELECT ...;
Golden Rules
- Always use PRIMARY KEY — every table needs an
INT AUTO_INCREMENT PRIMARY KEY - Use FOREIGN KEY — let the database enforce relationships, not just your app code
- Use
DECIMALfor money — neverFLOAT - Parameterize queries — use
?placeholders to prevent SQL injection - Index wisely — primary keys are auto-indexed; add indexes for WHERE/JOIN columns
- Backup regularly —
mysqldump -u root -p shop_db > backup.sql - Name consistently — tables plural lowercase (
users), columns snake_case (created_at) - Use
NOT NULL+DEFAULT— avoid unexpected NULLs
Backup & restore: mysqldump -u root -p shop_db > backup.sql to export, and mysql -u root -p shop_db < backup.sql to restore. Run this before any major change or migration.
17. MySQL vs PostgreSQL vs SQLite
| Feature | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
| Best for | Web apps, WordPress, SaaS | Complex queries, analytics | Local apps, mobile, prototyping |
| Setup | Server install / Docker | Server install / Docker | Single file, zero config |
| Scale | Millions of rows, replication | Millions, advanced features | Small to medium apps |
| JSON support | JSON column (basic) | JSONB (powerful, indexed) | JSON1 extension |
| Transactions | Yes (InnoDB) | Yes (full ACID) | Yes |
| Learning curve | Low — beginner friendly | Medium | Very low |
| Hosting | Everywhere (cheap) | Render, Supabase, AWS RDS | Embedded only |
| When to pick | Default for most web projects | Need advanced SQL features | No server / offline first |
18. Frequently Asked Questions
Is MySQL the same as SQL?
No. SQL is the language (SELECT * FROM users). MySQL is the database software that understands and executes SQL. Other databases like PostgreSQL and SQLite also use SQL — learning MySQL teaches you SQL you can use anywhere.
Do I need to install MySQL to learn it?
For serious practice, yes — install MySQL Community Server or run it via Docker (docker run -e MYSQL_ROOT_PASSWORD=pass -p 3306:3306 -d mysql:8). For quick experiments, use online tools like db-fiddle.com or OneCompiler without any install.
MySQL vs PostgreSQL — which should a beginner learn?
Start with MySQL. It is simpler to set up, powers more beginner-friendly stacks (WordPress, PHP, Node), and 90% of what you learn transfers to PostgreSQL. Move to PostgreSQL later if you need JSONB, full-text search, or advanced analytics.
Is MySQL still relevant in 2026?
Absolutely. MySQL is the most popular open-source relational database, used by millions of sites and top companies. Knowing it is a standard backend interview requirement and a must-have skill for full-stack developers.
What is the difference between CHAR and VARCHAR?
CHAR(10) always uses 10 characters (pads with spaces) — good for fixed-length codes like CHAR(2) for country codes. VARCHAR(100) uses only as much space as needed — good for names, emails. Use VARCHAR for most text.
How long does it take to learn MySQL?
Basic CRUD and filtering: 2–3 days. JOINs and aggregations: 1–2 weeks with practice. Production-ready skills (indexes, constraints, Node.js integration): 1 month of building real projects like the e-commerce database above.
Your next steps: 1) Build the shop database above → 2) Add authentication tables (users + sessions) → 3) Connect it to an Express API → 4) Learn transactions (START TRANSACTION; COMMIT; ROLLBACK;) for payment flows → 5) Deploy with PlanetScale or AWS RDS. Each step levels up your backend skills.
MySQL is the backbone of data-driven applications. Start with databases and tables, master CRUD, then unlock the real power with JOINs and aggregations. Practice with the e-commerce project, connect it to Node.js, and you will have a portfolio-ready full-stack skill. Open your terminal, run mysql -u root -p, and write your first CREATE TABLE today.