SQL Basics
What is SQL?
SQL (Structured Query Language) is the declarative language for defining, querying, and modifying relational data. MySQL implements ANSI SQL with extensions (LIMIT, ON DUPLICATE KEY UPDATE, SHOW commands).
SQL categories:
| Category | Commands | Purpose |
|---|---|---|
| DDL | CREATE, ALTER, DROP | Schema definition |
| DML | SELECT, INSERT, UPDATE, DELETE | Data manipulation |
| DCL | GRANT, REVOKE | Access control |
| TCL | START TRANSACTION, COMMIT, ROLLBACK | Transaction control |
Creating a Database and Table
CREATE DATABASE IF NOT EXISTS myapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE myapp;
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
status ENUM('active', 'inactive', 'banned') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_email (email),
KEY idx_status_created (status, created_at)
) ENGINE=InnoDB;
SELECT — Querying Data
-- All columns (avoid in production — specify columns)
SELECT * FROM users;
-- Specific columns
SELECT id, name, email FROM users WHERE status = 'active';
-- Pattern matching
SELECT name FROM users WHERE name LIKE 'A%';
SELECT email FROM users WHERE email LIKE '%@example.com';
-- NULL handling
SELECT name FROM users WHERE deleted_at IS NULL;
SELECT COALESCE(nickname, name) AS display_name FROM users;
-- Sorting and pagination
SELECT id, name FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
-- Distinct values
SELECT DISTINCT status FROM users;
Aggregation and GROUP BY
SELECT COUNT(*) AS total_users FROM users;
SELECT status, COUNT(*) AS cnt FROM users GROUP BY status;
SELECT user_id, COUNT(*) AS order_count, SUM(total) AS revenue
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY user_id
HAVING revenue > 1000
ORDER BY revenue DESC
LIMIT 10;
Rule: columns in SELECT must be in GROUP BY or wrapped in aggregate functions. HAVING filters groups; WHERE filters rows before grouping.
INSERT — Adding Data
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
INSERT INTO users (name, email) VALUES
('Bob', '[email protected]'),
('Carol', '[email protected]');
-- Insert from query
INSERT INTO archived_users (id, name, email, archived_at)
SELECT id, name, email, NOW()
FROM users WHERE status = 'inactive';
-- Upsert
INSERT INTO settings (key_name, value)
VALUES ('theme', 'dark')
ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = NOW();
UPDATE — Modifying Data
UPDATE users SET name = 'Alice Smith' WHERE id = 1;
UPDATE users
SET status = 'inactive'
WHERE last_login_at < '2023-01-01';
-- Always verify scope first
SELECT COUNT(*) FROM users WHERE last_login_at < '2023-01-01';
-- Then UPDATE with same WHERE
Critical: UPDATE without WHERE modifies every row. Always run a SELECT with the same condition first.
DELETE — Removing Data
DELETE FROM users WHERE id = 3;
DELETE FROM sessions WHERE expires_at < NOW();
-- Prefer soft delete for audit trails
UPDATE users SET deleted_at = NOW() WHERE id = 3;
JOINs Overview
SELECT u.name, o.id AS order_id, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.total > 100;
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
| Join Type | Returns |
|---|---|
| INNER JOIN | Matching rows only |
| LEFT JOIN | All left rows + matches (NULL if none) |
| RIGHT JOIN | All right rows + matches |
| CROSS JOIN | Cartesian product |
Useful Clauses Reference
| Clause | Purpose | Example |
|---|---|---|
| WHERE | Filter rows | WHERE status = 'active' |
| GROUP BY | Aggregate groups | GROUP BY user_id |
| HAVING | Filter groups | HAVING COUNT(*) > 5 |
| ORDER BY | Sort | ORDER BY created_at DESC |
| LIMIT | Cap rows | LIMIT 10 OFFSET 20 |
| DISTINCT | Unique values | SELECT DISTINCT country |
ALTER and DROP
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;
ALTER TABLE users ADD INDEX idx_phone (phone);
ALTER TABLE users MODIFY COLUMN name VARCHAR(150) NOT NULL;
DROP TABLE IF EXISTS temp_import;
DROP DATABASE IF EXISTS old_app;
Use migrations tools for production schema changes — never ad-hoc ALTER without backup and review.
EXPLAIN Preview
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
If type is ALL and key is NULL, the query does a full table scan — add an index on email.
Best Practices
- Select only needed columns — reduces I/O and network transfer
- Use parameterized queries in application code — prevents SQL injection
- Index columns in
WHERE,JOIN, andORDER BYclauses - Use transactions for multi-step writes
- Prefer
EXISTSoverINfor large subquery result sets
Common Mistakes
| Mistake | Problem |
|---|---|
SELECT * in production |
Wastes bandwidth; breaks when schema changes |
OR on different columns |
Often prevents index use — use UNION instead |
| Implicit type conversion | WHERE id = '123' may prevent index use |
Comparing NULL with = |
Always use IS NULL / IS NOT NULL |
Troubleshooting
Query returns unexpected empty result:
-- Check for trailing spaces, case sensitivity (utf8mb4_unicode_ci is case-insensitive)
SELECT * FROM users WHERE email = TRIM(' [email protected] ');
-- Check collation
SHOW FULL COLUMNS FROM users;
“Lock wait timeout exceeded” — another transaction holds a row lock. Find blockers:
SELECT * FROM information_schema.innodb_trx;
Production Scenario
An API endpoint lists active users paginated by created_at. The query uses a composite index (status, created_at) and keyset pagination (WHERE created_at < ? ORDER BY created_at DESC LIMIT 20) instead of OFFSET for consistent performance at millions of rows.
SQL fluency is the foundation of every MySQL skill in this track — indexing, transactions, and replication all assume solid query fundamentals.