SQL is how you talk to a database. Every instruction you write is an SQL command  a declarative sentence telling the database what you want, not how to get it.
SQL commands are instructions used to communicate with a database (which contains multiple tables). You tell the database what you want  it does the work (creates objects, inserts data, fetches rows, controls access, manages transactions).
| Category | Full Form | What It Does | Common Commands | Real Meaning |
|---|---|---|---|---|
| DDL | Data Definition Language | Creates / changes structure | CREATE, ALTER, DROP, TRUNCATE, RENAME |
Build / modify database & tables |
| DML | Data Manipulation Language | Changes actual data | INSERT, UPDATE, DELETE |
Add / edit / remove rows |
| DQL | Data Query Language | Reads data | SELECT |
Fetch rows / columns |
| DCL | Data Control Language | Permissions / security | GRANT, REVOKE |
Who can do what? |
| TCL | Transaction Control Language | Commit / undo operations | COMMIT, ROLLBACK, SAVEPOINT |
All-or-nothing safety |
Note: SQL keywords are typically case-insensitive (CREATE = create), but object names (database/table names) can be case-sensitive depending on settings/OS.
Where does SELECT belong?
CREATE DATABASE tuf_sql_2;
CREATE DATABASE tuf_sql_2;
Database is created and appears in DB list.
SHOW DATABASES;
\l
-- or
SELECT datname FROM pg_database;
| Database |
|---|
| information_schema |
| mysql |
| performance_schema |
| sys |
| tuf_sql |
| tuf_sql_2 |
| tuf_sql_3 |
USE tuf_sql;
\c tuf_sql
-- or
SET search_path TO tuf_sql;
After USE, all your queries run inside that selected database.
DROP DATABASE tuf_sql_2;
DROP DATABASE tuf_sql_2;
Note: DROP DATABASE is irreversible in most cases. It removes the whole database.
CREATE DATABASE IF NOT EXISTS tuf_sql;
CREATE DATABASE tuf_sql; -- no IF NOT EXISTS in older PG; handle error in app
Prevents errors if the database already exists.
Data types define what kind of data a column can store.
| Data Type | Stores | Common Use | Example |
|---|---|---|---|
INT | Whole numbers | IDs, counts, ages | age INT |
BIGINT | Very large whole numbers | Population, large counters | views BIGINT |
VARCHAR(n) | Variable-length text up to n chars | Names, emails | email VARCHAR(50) |
TEXT | Long text | Descriptions, blogs | description TEXT |
DATE | Date only | Birthdate, signup | dob DATE |
DATETIME / TIMESTAMP | Date + time | Logins, payment times | created_at DATETIME |
BOOLEAN (often TINYINT) | true/false (1/0) | Flags: is_active | is_active BOOLEAN |
DECIMAL(p,s) | Exact decimal numbers | Money, balances | balance DECIMAL(10,2) |
CREATE TABLE IF NOT EXISTS users (
user_id INT,
name VARCHAR(30),
email VARCHAR(50),
is_active BOOLEAN,
account_balance DECIMAL(10,2),
signup_datetime DATETIME,
last_seen DATETIME
);
CREATE TABLE IF NOT EXISTS users (
user_id INT,
name VARCHAR(30),
email VARCHAR(50),
is_active BOOLEAN,
account_balance DECIMAL(10,2),
signup_datetime DATETIME,
last_seen DATETIME
);
SHOW TABLES;
\dt
-- or
SELECT tablename FROM pg_tables WHERE schemaname = 'public';
The database tuf_sql contains a single table named users, as shown under Tables_in_tuf_sql.
DROP TABLE users;
-- safe variant
DROP TABLE IF EXISTS users;
DROP TABLE users;
-- safe variant
DROP TABLE IF EXISTS users;
IF EXISTS prevents errors when the table is already missing.
INSERT INTO users (user_id, name, email)
VALUES (1, 'Raj', 'raj@gmail.com');
INSERT INTO users (user_id, name, email)
VALUES (1, 'Raj', 'raj@gmail.com');
INSERT INTO users (user_id, name, email)
VALUES
(2, 'Mark', 'mark@gmail.com'),
(3, 'Albert', 'albert@gmail.com'),
(4, 'Peter', 'peter@gmail.com');
INSERT INTO users (user_id, name, email)
VALUES
(2, 'Mark', 'mark@gmail.com'),
(3, 'Albert', 'albert@gmail.com'),
(4, 'Peter', 'peter@gmail.com');
SELECT * FROM users;
SELECT * FROM users;
Note: Avoid SELECT * on huge tables in production. Prefer selecting only required columns.
SELECT
name,
email AS mail_id
FROM users;
SELECT
name,
email AS mail_id
FROM users;
A Primary Key uniquely identifies each row. It must be unique and NOT NULL.
-- Way 1 (inline)
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(30),
email VARCHAR(50)
);
-- Way 2 (at the end)
CREATE TABLE users (
user_id INT,
name VARCHAR(30),
email VARCHAR(50), PRIMARY KEY (user_id)
);
-- Way 1 (inline)
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(30),
email VARCHAR(50)
);
-- Way 2 (at the end)
CREATE TABLE users (
user_id INT,
name VARCHAR(30),
email VARCHAR(50), PRIMARY KEY (user_id)
);
A Foreign Key links one table to another table's primary key (creates a relationship).
CREATE TABLE orders (
order_id INT PRIMARY KEY, user_id INT,
amount DECIMAL(10,2),
order_date DATE, FOREIGN KEY (user_id) REFERENCES users(user_id)
);
CREATE TABLE orders (
order_id INT PRIMARY KEY, user_id INT,
amount DECIMAL(10,2),
order_date DATE, FOREIGN KEY (user_id) REFERENCES users(user_id)
);
Note: If a table is referenced by a foreign key, you usually must drop the child table first (e.g. drop orders before users).
Multiple columns together form the unique identifier.
CREATE TABLE enrollments (
email VARCHAR(50), batch_id INT,
enrolled_at DATETIME, PRIMARY KEY (email, batch_id)
);
CREATE TABLE enrollments (
email VARCHAR(50), batch_id INT,
enrolled_at DATETIME, PRIMARY KEY (email, batch_id)
);
Here, email alone can repeat across batches, but the pair (email, batch_id) must be unique.
Note: More columns in a composite key increases index size and may impact performance. Use only when needed.
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
email VARCHAR(50)
);
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
-- or INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
name VARCHAR(30),
email VARCHAR(50)
);
INSERT INTO users (name, email)
VALUES ('Raj', 'raj@gmail.com');
INSERT INTO users (name, email)
VALUES ('Raj', 'raj@gmail.com');
Rule: In MySQL, AUTO_INCREMENT must be on a key column (typically PRIMARY KEY or UNIQUE).
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
email VARCHAR(50) NOT NULL
);
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name VARCHAR(30),
email VARCHAR(50) NOT NULL
);
Now email is mandatory on insert.
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
email VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name VARCHAR(30),
email VARCHAR(50) NOT NULL UNIQUE
);
Duplicate emails are blocked.
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
email VARCHAR(50) NOT NULL UNIQUE, age INT,
CONSTRAINT age_check CHECK (age >= 15 AND age <= 30)
);
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name VARCHAR(30),
email VARCHAR(50) NOT NULL UNIQUE, age INT,
CONSTRAINT age_check CHECK (age >= 15 AND age <= 30)
);
Insert with age = 12 fails due to the check constraint.
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
email VARCHAR(50) NOT NULL UNIQUE, age INT,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name VARCHAR(30),
email VARCHAR(50) NOT NULL UNIQUE, age INT,
is_active BOOLEAN DEFAULT TRUE
);
If you don't pass is_active, DB stores TRUE automatically.
ALTER TABLE is DDL  it changes the shape of an existing table without dropping it. You use it to add, remove, or modify columns and constraints as requirements evolve.
Add phone to users after the table already exists.
ALTER TABLE users ADD COLUMN phone VARCHAR(15);
ALTER TABLE users ADD COLUMN phone VARCHAR(15);
With a default for existing rows:
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
ALTER TABLE users DROP COLUMN phone;
ALTER TABLE users DROP COLUMN phone;
Increase name from VARCHAR(30) to VARCHAR(100). Syntax differs slightly:
-- MySQL / SQL Server style
ALTER TABLE users MODIFY COLUMN name VARCHAR(100);
-- or
ALTER TABLE users CHANGE COLUMN name name VARCHAR(100);
ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(100);
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users RENAME TO app_users;
ALTER TABLE users RENAME TO app_users;
Add a UNIQUE or CHECK after creation, and drop it if needed:
-- add
ALTER TABLE users ADD CONSTRAINT email_unique UNIQUE (email);
ALTER TABLE users ADD CONSTRAINT age_check CHECK (age >= 15 AND age <= 30);
-- drop
ALTER TABLE users DROP CONSTRAINT email_unique;
ALTER TABLE users DROP CHECK age_check; -- MySQL 8.0.16+
-- add
ALTER TABLE users ADD CONSTRAINT email_unique UNIQUE (email);
ALTER TABLE users ADD CONSTRAINT age_check CHECK (age >= 15 AND age <= 30);
-- drop
ALTER TABLE users DROP CONSTRAINT email_unique;
ALTER TABLE users DROP CONSTRAINT age_check;
Tip: In production, prefer ALTER TABLE for small, fast changes. For large tables, adding a column with a default or changing a type can lock the table  plan for low-traffic windows.
Which statement is true about ALTER TABLE?
NULL means “no valueâ€ÂÂ, “unknownâ€ÂÂ, or “not applicableâ€ÂÂ. Not zero, not empty string. A blank space where a value could be but isn't.
Example: Middle Name
| id | first_name | middle_name | last_name |
|---|---|---|---|
| 1 | Rohan | NULL | Sharma |
| 2 | Ananya | Kaur | Singh |
For id=1, middle_name = NULL  no middle name. Intuition: NULL = There is no value stored here.
0 is a valid number  zero quantity, not “nothingâ€ÂÂ.
| order_id | product_name | quantity |
|---|---|---|
| 101 | Mouse | 2 |
| 102 | Keyboard | 0 |
quantity = 0 means we know the quantity and it is exactly zero. Intuition: 0 = We know the value. It is exactly zero.
'' is a string with length 0  still a valid text value, just no characters. Different from NULL and from ' ' (one space).
| user_id | full_name | nickname |
|---|---|---|
| 1 | Priya Verma | '' |
| 2 | Aman Gupta | 'AG' |
nickname = '' means the field exists but the user left it blank deliberately. Intuition: '' = We know the value. It's an empty piece of text.
Middle name is NULL vs 0 vs ''  which is correct?
| Category | What It Does | Commands | Mental Model |
|---|---|---|---|
| DDL | Defines / changes structure | CREATE, ALTER, DROP, TRUNCATE | Changes the “design†/ schema of the table itself |
| DML | Works with data inside tables | SELECT, INSERT, UPDATE, DELETE | Adds/modifies/retrieves rows, not structure |
DDL = shape of table. DML = data inside the table.
When you run a query, the database follows three steps: Parse → Plan → Execute. You don't control them directly, but they explain performance.
Checks syntax and validity. Which tables? Which columns? Do they exist?
SELECT name, email
FROM customers
WHERE city = 'Mumbai';
SELECT name, email
FROM customers
WHERE city = 'Mumbai';
During parse it checks: valid SELECT ... FROM ... WHERE syntax? Does customers exist? Do name, email, city exist? If not → fail at parse.
Intuition: Parse = Can I understand this query?
Figures out the fastest way  which indexes to use, join order, scan vs index lookup. Output is an execution plan.
SELECT name, email FROM customers WHERE city = 'Mumbai';
SELECT name, email FROM customers WHERE city = 'Mumbai';
Planner thinks: Is there an index on city? If yes → index seek; if no → full table scan. How big is the table? How many rows match?
Intuition: Plan = What is the smartest way to get the answer?
Actually runs the plan  reads data from disk/memory, applies filters/joins/sorting, returns result set.
SELECT name, email FROM customers WHERE city = 'Mumbai';
-- plan says: index scan on city
-- for each match, pick name + email, return to client
SELECT name, email FROM customers WHERE city = 'Mumbai';
-- plan says: index scan on city
-- for each match, pick name + email, return to client
Intuition: Execute = Now do the work and return rows.
An index is a special data structure (often a B-tree) built on one or more columns to make searching faster. Instead of scanning every row, the DB jumps directly to matching rows  like a book index.
Analogy: 1000-page book with no index → flip many pages to find “Binary Search Tree†(full table scan). With an index at the back → topics sorted alphabetically with page numbers → jump directly. That's a DB index.
Parse vs Plan vs Execute  which order?
DDL: CREATE/ALTER/DROP/TRUNCATE/RENAME  structure
DML: INSERT/UPDATE/DELETE  data
DQL: SELECT  query
DCL: GRANT/REVOKE  permissions
TCL: COMMIT/ROLLBACK/SAVEPOINT  transactions
Keywords case-insensitive; object names may be case-sensitive
CREATE DATABASE tuf_sql_2; SHOW DATABASES; USE tuf_sql; DROP DATABASE; CREATE DATABASE IF NOT EXISTS
Types: INT/BIGINT, VARCHAR(n)/TEXT, DATE/DATETIME/TIMESTAMP, BOOLEAN, DECIMAL(p,s)
CREATE TABLE IF NOT EXISTS users (...); SHOW TABLES; DROP TABLE [IF EXISTS]
INSERT INTO users (...) VALUES (...), (...); SELECT * / SELECT col AS alias FROM users
PK: unique + NOT NULL  inline or PRIMARY KEY (col)
FK: REFERENCES users(user_id)  drop child before parent
Composite PK: PRIMARY KEY (email, batch_id)  pair unique, index size ↑
AUTO_INCREMENT must be on a key; NOT NULL = mandatory; UNIQUE = no dupes; CHECK (age >=15) blocks bad data; DEFAULT TRUE auto-fills
DDL to change shape: ADD COLUMN phone VARCHAR(15) / ADD COLUMN status DEFAULT 'active'
DROP COLUMN phone; MODIFY COLUMN name VARCHAR(100) (SQL) vs ALTER COLUMN TYPE VARCHAR(100) (pgsql)
RENAME COLUMN name TO full_name; RENAME TO app_users
ADD CONSTRAINT email_unique UNIQUE / ADD CHECK; DROP CONSTRAINT email_unique
NULL = no value/unknown (middle_name absent); 0 = known number zero; '' = known empty string length 0
NULL != 0 != '' != ' ' (space)
DDL = table shape; DML = rows inside
Parse: syntax + table/col exist? → Plan: index vs scan, join order → Execute: read & return
Index = B-tree on columns → jump to rows like book index; avoids full scan
Primary sources: MySQL CREATE DATABASE · MySQL Data Types · Postgres CREATE TABLE. Ask me anything that's unclear.
Questions? Ask your agent  you can follow up on any concept, quiz answer, or how this applies to a specific query. Review the SQL glossary or syntax quick reference for a compressed overview.