SQL GRANT Statement

SQL Tutorial › Security › GRANT

SQL GRANT Statement

IntermediateLesson 67 of 6910 min read7 queries run on MySQL 8.4

GRANT gives an account (or a role) permission to do something: read a table, insert rows, create tables, run a procedure. It is the core of DCL (Data Control Language). Granting exactly what each account needs, and nothing more, is the most effective database security measure you can take.

In this lesson
  • Grant privileges at global, database, table and column level
  • Inspect privileges with SHOW GRANTS
  • Grant through views for row and column security
  • Understand WITH GRANT OPTION and ALL PRIVILEGES

GRANT syntax

MySQL
GRANT privilege [, privilege ...]ON levelTO 'user'@'host' [, ...][WITH GRANT OPTION];
LevelWritten asExample
Global.every database on the server
Databasecompany.*every table in company
Tablecompany.ordersone table
ColumnSELECT (col1, col2) ON company.employeessome columns of one table
RoutineEXECUTE ON PROCEDURE company.place_orderone procedure

Common privileges: SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, EXECUTE, CREATE VIEW, TRIGGER, and ALL PRIVILEGES.

Database-level grant

MySQL
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'Sh0p#App2026';GRANT SELECT, INSERT, UPDATE, DELETE ON company.* TO 'shop_app'@'localhost';SHOW GRANTS FOR 'shop_app'@'localhost';
ResultMySQL 8.4
mysql> CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'Sh0p
Query OK, 0 rows affected
mysql> GRANT SELECT, INSERT, UPDATE, DELETE ON company.* TO 'shop_app'@'local…
Query OK, 0 rows affected
mysql> SHOW GRANTS FOR 'shop_app'@'localhost'
Grants for shop_app@localhost
GRANT USAGE ON *.* TO `shop_app`@`localhost`
GRANT SELECT, INSERT, UPDATE, DELETE ON `company`.* TO `shop_app`@`localhost`
2 rows in set
How this query works, step by step
  1. The account can read and change rows in every table of company.
  2. It cannot create, alter or drop tables, and it cannot touch other databases.
  3. SHOW GRANTS lists exactly what the account may do.

Table-level grant

A reporting account that may read only the shop tables:

MySQL
CREATE USER 'report_reader'@'localhost' IDENTIFIED BY 'Rep0rt#2026';GRANT SELECT ON company.orders TO 'report_reader'@'localhost';GRANT SELECT ON company.order_items TO 'report_reader'@'localhost';GRANT SELECT ON company.products TO 'report_reader'@'localhost';SHOW GRANTS FOR 'report_reader'@'localhost';
ResultMySQL 8.4
mysql> CREATE USER 'report_reader'@'localhost' IDENTIFIED BY 'Rep0rt
Query OK, 0 rows affected
mysql> GRANT SELECT ON company.orders TO 'report_reader'@'localhost'
Query OK, 0 rows affected
mysql> GRANT SELECT ON company.order_items TO 'report_reader'@'localhost'
Query OK, 0 rows affected
mysql> GRANT SELECT ON company.products TO 'report_reader'@'localhost'
Query OK, 0 rows affected
mysql> SHOW GRANTS FOR 'report_reader'@'localhost'
Grants for report_reader@localhost
GRANT USAGE ON *.* TO `report_reader`@`localhost`
GRANT SELECT ON `company`.`order_items` TO `report_reader`@`localhost`
GRANT SELECT ON `company`.`orders` TO `report_reader`@`localhost`
GRANT SELECT ON `company`.`products` TO `report_reader`@`localhost`
4 rows in set

Column-level grant

HR assistants may see employee names and departments, but not salaries:

MySQL
CREATE USER 'hr_assistant'@'localhost' IDENTIFIED BY 'Hr#Asst2026';GRANT SELECT (emp_id, first_name, last_name, dept_id, job_title) ON company.employees TO 'hr_assistant'@'localhost';GRANT UPDATE (job_title) ON company.employees TO 'hr_assistant'@'localhost';SHOW GRANTS FOR 'hr_assistant'@'localhost';
ResultMySQL 8.4
mysql> CREATE USER 'hr_assistant'@'localhost' IDENTIFIED BY 'Hr
Query OK, 0 rows affected
mysql> GRANT SELECT (emp_id, first_name, last_name, dept_id, job_title) ON co…
Query OK, 0 rows affected
mysql> GRANT UPDATE (job_title) ON company.employees TO 'hr_assistant'@'local…
Query OK, 0 rows affected
mysql> SHOW GRANTS FOR 'hr_assistant'@'localhost'
Grants for hr_assistant@localhost
GRANT USAGE ON *.* TO `hr_assistant`@`localhost`
GRANT SELECT (`dept_id`, `emp_id`, `first_name`, `job_title`, `last_name`), UPDATE (`job_title`) ON `company`.`employees` TO `hr_assistant`@`localhost`
2 rows in set
How this query works, step by step
  1. SELECT (columns) allows reading only the listed columns; SELECT salary FROM employees would fail for this account.
  2. UPDATE (job_title) allows changing that one column only.

Security through views

Column grants cannot filter rows. A view can: give a regional manager only Mumbai employees:

MySQL
CREATE VIEW mumbai_staff ASSELECT emp_id, first_name, last_name, job_title FROM employees WHERE city = 'Mumbai';CREATE USER 'mumbai_mgr'@'localhost' IDENTIFIED BY 'Mumb@i#2026';GRANT SELECT ON company.mumbai_staff TO 'mumbai_mgr'@'localhost';SHOW GRANTS FOR 'mumbai_mgr'@'localhost';
ResultMySQL 8.4
mysql> CREATE VIEW mumbai_staff AS
Query OK, 0 rows affected
mysql> CREATE USER 'mumbai_mgr'@'localhost' IDENTIFIED BY 'Mumb@i
Query OK, 0 rows affected
mysql> GRANT SELECT ON company.mumbai_staff TO 'mumbai_mgr'@'localhost'
Query OK, 0 rows affected
mysql> SHOW GRANTS FOR 'mumbai_mgr'@'localhost'
Grants for mumbai_mgr@localhost
GRANT USAGE ON *.* TO `mumbai_mgr`@`localhost`
GRANT SELECT ON `company`.`mumbai_staff` TO `mumbai_mgr`@`localhost`
2 rows in set

The account can read the view but has no privilege on employees itself (Views).

Grant to a role

MySQL
CREATE ROLE 'qa_tester';GRANT SELECT, INSERT, UPDATE, DELETE ON company.* TO 'qa_tester';CREATE USER 'arjun'@'localhost' IDENTIFIED BY 'Arj#un2026';GRANT 'qa_tester' TO 'arjun'@'localhost';SHOW GRANTS FOR 'arjun'@'localhost';
ResultMySQL 8.4
mysql> CREATE ROLE 'qa_tester'
Query OK, 0 rows affected
mysql> GRANT SELECT, INSERT, UPDATE, DELETE ON company.* TO 'qa_tester'
Query OK, 0 rows affected
mysql> CREATE USER 'arjun'@'localhost' IDENTIFIED BY 'Arj
Query OK, 0 rows affected
mysql> GRANT 'qa_tester' TO 'arjun'@'localhost'
Query OK, 0 rows affected
mysql> SHOW GRANTS FOR 'arjun'@'localhost'
Grants for arjun@localhost
GRANT USAGE ON *.* TO `arjun`@`localhost`
GRANT `qa_tester`@`%` TO `arjun`@`localhost`
2 rows in set

See Users & Roles.

WITH GRANT OPTION and ALL PRIVILEGES

  • WITH GRANT OPTION lets the account pass its own privileges on to others. Give it only to administrators.
  • GRANT ALL PRIVILEGES ON company.* TO ... gives every database-level privilege, including DROP. Convenient for a developer’s own test database, dangerous for an application account.
MySQL
CREATE USER 'team_lead'@'localhost' IDENTIFIED BY 'L3ad#2026';GRANT ALL PRIVILEGES ON company.* TO 'team_lead'@'localhost' WITH GRANT OPTION;SHOW GRANTS FOR 'team_lead'@'localhost';
ResultMySQL 8.4
mysql> CREATE USER 'team_lead'@'localhost' IDENTIFIED BY 'L3ad
Query OK, 0 rows affected
mysql> GRANT ALL PRIVILEGES ON company.* TO 'team_lead'@'localhost' WITH GRAN…
Query OK, 0 rows affected
mysql> SHOW GRANTS FOR 'team_lead'@'localhost'
Grants for team_lead@localhost
GRANT USAGE ON *.* TO `team_lead`@`localhost`
GRANT ALL PRIVILEGES ON `company`.* TO `team_lead`@`localhost` WITH GRANT OPTION
2 rows in set

Errors

Granting on an account that does not exist fails in MySQL 8 (older versions silently created it):

MySQL
GRANT SELECT ON company.* TO 'ghost'@'localhost';
ResultMySQL 8.4
ERROR 1410 (42000): You are not allowed to create a user with GRANT

Quick quiz

1. What does ON company.* mean?

2. How do you let an account read only some columns of a table?

3. Which tool restricts which rows an account can see?

4. What does WITH GRANT OPTION allow?

GRANT FAQ

What is the GRANT command in SQL?

A Data Control Language statement that gives privileges, such as SELECT or INSERT, on databases, tables, columns or routines to accounts or roles.

How do I see the privileges of a MySQL user?

SHOW GRANTS FOR 'user'@'host'; For your own account, SHOW GRANTS;.

Do I need FLUSH PRIVILEGES after GRANT?

No. GRANT, REVOKE, CREATE USER and similar statements take effect immediately. FLUSH PRIVILEGES is only needed after editing the grant tables directly, which you should not do.

SQL DROP TABLE and DROP DATABASE

SQL Tutorial › Database Design › DROP TABLE & DATABASE

SQL DROP TABLE and DROP DATABASE

BeginnerLesson 58 of 697 min read7 queries run on MySQL 8.4

DROP removes a database object completely: the table’s structure, data, indexes and triggers, or a whole database with every table in it. It is instant and cannot be undone with ROLLBACK, so it is the one command where a typo hurts most. This lesson shows how to drop safely.

In this lesson
  • Remove a table with DROP TABLE
  • Make scripts re-runnable with IF EXISTS
  • Drop tables that are referenced by foreign keys
  • Drop a database, and compare DROP, TRUNCATE and DELETE

DROP TABLE

MySQL
CREATE TABLE temp_report (id INT, total INT);SHOW TABLES LIKE 'temp%';DROP TABLE temp_report;SHOW TABLES LIKE 'temp%';
ResultMySQL 8.4
mysql> CREATE TABLE temp_report (id INT, total INT)
Query OK, 0 rows affected
mysql> SHOW TABLES LIKE 'temp%'
Tables_in_company (temp%)
temp_report
1 row in set
mysql> DROP TABLE temp_report
Query OK, 0 rows affected
mysql> SHOW TABLES LIKE 'temp%'
Empty set
How this query works, step by step
  1. The table is created and SHOW TABLES lists it.
  2. DROP TABLE removes the table and all its rows.
  3. The second SHOW TABLES returns an Empty set.

IF EXISTS

Dropping a table that does not exist is an error:

MySQL
DROP TABLE no_such_table;
ResultMySQL 8.4
ERROR 1051 (42S02): Unknown table 'company.no_such_table'

IF EXISTS turns it into a note, so setup scripts can run again and again:

MySQL
DROP TABLE IF EXISTS no_such_table;SHOW WARNINGS;
ResultMySQL 8.4
mysql> DROP TABLE IF EXISTS no_such_table
Query OK, 0 rows affected, 1 warning
mysql> SHOW WARNINGS
LevelCodeMessage
Note1051Unknown table 'company.no_such_table'
1 row in set

Several tables at once

MySQL
CREATE TABLE t1 (a INT);CREATE TABLE t2 (a INT);DROP TABLE IF EXISTS t1, t2;SHOW TABLES LIKE 't_';
ResultMySQL 8.4
mysql> CREATE TABLE t1 (a INT)
Query OK, 0 rows affected
mysql> CREATE TABLE t2 (a INT)
Query OK, 0 rows affected
mysql> DROP TABLE IF EXISTS t1, t2
Query OK, 0 rows affected
mysql> SHOW TABLES LIKE 't_'
Empty set

Tables referenced by foreign keys

A parent table cannot be dropped while a child table references it:

MySQL
DROP TABLE customers;
ResultMySQL 8.4
ERROR 3730 (HY000): Cannot drop table 'customers' referenced by a foreign key constraint 'orders_ibfk_1' on table 'orders'.

Drop the child first, then the parent:

MySQL
DROP TABLE order_items;DROP TABLE orders;DROP TABLE customers;SHOW TABLES;
ResultMySQL 8.4
mysql> DROP TABLE order_items
Query OK, 0 rows affected
mysql> DROP TABLE orders
Query OK, 0 rows affected
mysql> DROP TABLE customers
Query OK, 0 rows affected
mysql> SHOW TABLES
Tables_in_company
departments
employees
products
3 rows in set
How this query works, step by step
  1. order_items references orders, and orders references customers.
  2. Dropping in child-to-parent order satisfies every foreign key.
  3. The three tables are gone; departments, employees and products remain.
Watch out

You may see scripts that run SET FOREIGN_KEY_CHECKS = 0; to drop tables in any order. That also lets broken data in, so use it only in throw-away development databases and switch it back on immediately.

DROP DATABASE

MySQL
CREATE DATABASE scratch_db;DROP DATABASE scratch_db;DROP DATABASE IF EXISTS scratch_db;SHOW DATABASES;
ResultMySQL 8.4
mysql> CREATE DATABASE scratch_db
Query OK, 1 row affected
mysql> DROP DATABASE scratch_db
Query OK, 0 rows affected
mysql> DROP DATABASE IF EXISTS scratch_db
Query OK, 0 rows affected, 1 warning
mysql> SHOW DATABASES
Database
information_schema
mysql
performance_schema
sys
4 rows in set
How this query works, step by step
  1. DROP DATABASE deletes the database and every table in it: Query OK, 0 rows affected (the count is the number of tables dropped).
  2. Running it again with IF EXISTS produces only a warning.

DROP vs TRUNCATE vs DELETE

DROP TABLETRUNCATE TABLEDELETE FROM
Removes datayesyes, all rowschosen rows
Removes structureyesnono
Can use WHEREnonoyes
Can be rolled backnonoyes
Fires DELETE triggersnonoyes

Safety checklist

  1. Run SELECT DATABASE(); to be sure which database you are in.
  2. Take a backup (mysqldump) of anything you might need.
  3. Prefer renaming (RENAME TABLE orders TO orders_old_2026) and dropping later, once nobody has complained.
  4. In production, grant the DROP privilege only to the accounts that need it (GRANT).

Quick quiz

1. What does DROP TABLE remove?

2. Why use DROP TABLE IF EXISTS in scripts?

3. In which order must you drop orders and customers?

DROP FAQ

What is the difference between DROP and TRUNCATE?

DROP TABLE deletes the table completely, structure included. TRUNCATE TABLE deletes all rows but keeps the empty table.

Can I undo DROP TABLE?

Not with ROLLBACK. Restore from a backup or point-in-time recovery using the binary log.

How do I drop a table with a foreign key?

Drop the referencing (child) tables first, or remove the foreign key with ALTER TABLE child DROP FOREIGN KEY name;.

SQL ALTER TABLE

SQL Tutorial › Database Design › ALTER TABLE

SQL ALTER TABLE

IntermediateLesson 57 of 6911 min read8 queries run on MySQL 8.4

Requirements change after a table is created: a new column for phone numbers, a longer name field, a renamed column, a new index. ALTER TABLE changes the structure of an existing table without losing its data. This lesson runs each kind of change on a copy of the sample customers table and shows the result with DESCRIBE.

In this lesson
  • Add, modify, rename and drop columns
  • Rename a table
  • Add and drop keys, indexes and constraints
  • Change large tables safely

Sample table

Every example works on a copy, so the original stays intact:

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name, email, city FROM customers;DESCRIBE clients;
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name, email, city…
Query OK, 7 rows affected
mysql> DESCRIBE clients
FieldTypeNullKeyDefaultExtra
customer_idintNONULL
customer_namevarchar(50)NONULL
emailvarchar(60)YESNULL
cityvarchar(30)YESNULL
4 rows in set

ADD COLUMN

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name, email, city FROM customers;ALTER TABLE clients  ADD COLUMN phone VARCHAR(15),  ADD COLUMN loyalty_points INT NOT NULL DEFAULT 0 AFTER customer_name;SELECT customer_id, customer_name, loyalty_points, phone FROM clients LIMIT 3;
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name, email, city…
Query OK, 7 rows affected
mysql> ALTER TABLE clients
Query OK, 0 rows affected
mysql> SELECT customer_id, customer_name, loyalty_points, phone FROM clients …
customer_idcustomer_nameloyalty_pointsphone
1Ananya Sharma0NULL
2Rohit Kumar0NULL
3Fatima Khan0NULL
3 rows in set
How this query works, step by step
  1. One ALTER TABLE can make several changes, separated by commas.
  2. phone is added at the end and is NULL for existing rows.
  3. loyalty_points is NOT NULL DEFAULT 0, so every existing row gets 0; AFTER customer_name places it right after that column (FIRST puts it first).

MODIFY COLUMN: change type or constraints

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name, email, city FROM customers;ALTER TABLE clients MODIFY customer_name VARCHAR(100) NOT NULL;DESCRIBE clients;
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name, email, city…
Query OK, 7 rows affected
mysql> ALTER TABLE clients MODIFY customer_name VARCHAR(100) NOT NULL
Query OK, 7 rows affected
mysql> DESCRIBE clients
FieldTypeNullKeyDefaultExtra
customer_idintNONULL
customer_namevarchar(100)NONULL
emailvarchar(60)YESNULL
cityvarchar(30)YESNULL
4 rows in set
Watch out

MODIFY replaces the whole column definition. If the column had NOT NULL or a DEFAULT and you leave it out, it is gone. Always restate everything you want to keep.

Shrinking a column fails when existing data would not fit (in strict mode):

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name, email, city FROM customers;ALTER TABLE clients MODIFY customer_name VARCHAR(10);
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name, email, city…
Query OK, 7 rows affected
mysql> ALTER TABLE clients MODIFY customer_name VARCHAR(10)
ERROR 1265 (01000): Data truncated for column 'customer_name' at row 1

RENAME COLUMN and CHANGE

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name, email, city FROM customers;ALTER TABLE clients RENAME COLUMN customer_name TO full_name;ALTER TABLE clients CHANGE city home_city VARCHAR(40);SELECT customer_id, full_name, home_city FROM clients LIMIT 2;
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name, email, city…
Query OK, 7 rows affected
mysql> ALTER TABLE clients RENAME COLUMN customer_name TO full_name
Query OK, 0 rows affected
mysql> ALTER TABLE clients CHANGE city home_city VARCHAR(40)
Query OK, 0 rows affected
mysql> SELECT customer_id, full_name, home_city FROM clients LIMIT 2
customer_idfull_namehome_city
1Ananya SharmaMumbai
2Rohit KumarDelhi
2 rows in set
How this query works, step by step
  1. RENAME COLUMN old TO new only renames (MySQL 8.0+).
  2. CHANGE old new definition renames and redefines the column in one step (here VARCHAR(40)).

DROP COLUMN

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name, email, city FROM customers;ALTER TABLE clients DROP COLUMN email;SELECT * FROM clients LIMIT 2;
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name, email, city…
Query OK, 7 rows affected
mysql> ALTER TABLE clients DROP COLUMN email
Query OK, 0 rows affected
mysql> SELECT * FROM clients LIMIT 2
customer_idcustomer_namecity
1Ananya SharmaMumbai
2Rohit KumarDelhi
2 rows in set

The column and all its data are deleted permanently.

Rename a table

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name FROM customers;ALTER TABLE clients RENAME TO client_list;SHOW TABLES LIKE 'client%';
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name FROM customers
Query OK, 7 rows affected
mysql> ALTER TABLE clients RENAME TO client_list
Query OK, 0 rows affected
mysql> SHOW TABLES LIKE 'client%'
Tables_in_company (client%)
client_list
1 row in set

RENAME TABLE a TO b; does the same and can rename several tables at once.

Keys, indexes and constraints

MySQL
CREATE TABLE clients AS SELECT customer_id, customer_name, email, city FROM customers;ALTER TABLE clients  ADD PRIMARY KEY (customer_id),  ADD UNIQUE KEY uq_client_email (email),  ADD INDEX idx_client_city (city),  ADD CONSTRAINT chk_name CHECK (customer_name <> '');SHOW INDEX FROM clients;
ResultMySQL 8.4
mysql> CREATE TABLE clients AS SELECT customer_id, customer_name, email, city…
Query OK, 7 rows affected
mysql> ALTER TABLE clients
Query OK, 7 rows affected
mysql> SHOW INDEX FROM clients
TableNon_uniqueKey_nameSeq_in_indexColumn_nameCollationCardinalitySub_partPackedNullIndex_typeCommentIndex_commentVisibleExpression
clients0PRIMARY1customer_idA7NULLNULLBTREEYESNULL
clients0uq_client_email1emailA7NULLNULLYESBTREEYESNULL
clients1idx_client_city1cityA7NULLNULLYESBTREEYESNULL
3 rows in set
How this query works, step by step
  1. CREATE TABLE ... AS SELECT copied data but no keys, so they are added now.
  2. One statement adds a primary key, a unique key, a normal index and a check.
  3. SHOW INDEX lists the three indexes; the check constraint is not an index.

Remove them with DROP PRIMARY KEY, DROP INDEX name, DROP CHECK name and DROP FOREIGN KEY name.

Changing big tables safely

  • Many changes in MySQL 8 are instant or in-place (adding a column at the end, renaming, adding an index) and do not block reads and writes for long.
  • Others (changing a column’s type) rebuild the table, copying every row. On a large table that can take a long time.
  • Ask for the cheap algorithm and let MySQL refuse if it cannot: ALTER TABLE big ADD COLUMN note VARCHAR(50), ALGORITHM=INSTANT;
  • Test schema changes on a copy, take a backup first, and run them in a quiet period.
  • ALTER TABLE is DDL: it commits any open transaction and cannot be rolled back.

Exercise: Extend products

On a copy of products called catalog, add a discount_pct INT NOT NULL DEFAULT 0 column after price, rename stock to units_in_stock, and show the structure.

Expected resultMySQL 8.4
mysql> CREATE TABLE catalog AS SELECT * FROM products
Query OK, 9 rows affected
mysql> ALTER TABLE catalog
Query OK, 0 rows affected
mysql> DESCRIBE catalog
FieldTypeNullKeyDefaultExtra
product_idintNONULL
product_namevarchar(50)NONULL
categoryvarchar(30)YESNULL
priceintYESNULL
discount_pctintNO0
units_in_stockintYESNULL
6 rows in set
Hint 1

Create the copy with CREATE TABLE catalog AS SELECT * FROM products;, then one ALTER TABLE with two changes.

Quick quiz

1. Which clause renames a column without changing its type?

2. What happens to a NOT NULL constraint you forget to repeat in MODIFY?

3. Can ALTER TABLE be rolled back inside a transaction?

ALTER TABLE FAQ

How do I add a column to an existing table in SQL?

ALTER TABLE table_name ADD COLUMN column_name datatype [constraints]; Use AFTER other_column or FIRST to choose its position in MySQL.

What is the difference between MODIFY and CHANGE in MySQL?

MODIFY redefines a column but keeps its name. CHANGE can rename it and redefine it at the same time.

How do I rename a table in MySQL?

ALTER TABLE old_name RENAME TO new_name; or RENAME TABLE old_name TO new_name;

Does ALTER TABLE lock the table?

It depends on the change. Instant and in-place changes allow normal work; changes that rebuild the table can block writes for their duration. Specify ALGORITHM and LOCK to control it.

SQL DELETE and TRUNCATE

SQL Tutorial › Modifying Data › DELETE & TRUNCATE

SQL DELETE and TRUNCATE

BeginnerLesson 20 of 6910 min read9 queries run on MySQL 8.4

DELETE removes rows from a table; TRUNCATE empties a table completely. Both are permanent once committed, so the habits from the UPDATE lesson matter even more here: check the WHERE with a SELECT first, and use a transaction when the data is valuable.

In this lesson
  • Delete rows that match a condition
  • Delete using a subquery or a join
  • Understand foreign key errors on delete
  • Choose between DELETE, TRUNCATE and DROP

DELETE syntax

MySQL
DELETE FROM table_nameWHERE condition;
Watch out

DELETE FROM table_name; without WHERE removes every row. The table itself stays, empty.

Sample data

products · 9 rows
product_idproduct_namecategorypricestock
1Wireless MouseElectronics799120
2Mechanical KeyboardElectronics349945
3USB-C HubElectronics19990
4Office ChairFurniture899912
5Standing DeskFurniture189995
6Notebook PackStationery249300
7Gel PensStationery199250
8Desk LampFurniture149930
9Monitor ArmFurniture279918

Delete one row

Product 9 (Monitor Arm) has never been ordered and is discontinued:

SQL
DELETE FROM productsWHERE product_id = 9; SELECT product_id, product_name FROM products;
ResultMySQL 8.4
mysql> DELETE FROM products
Query OK, 1 row affected
mysql> SELECT product_id, product_name FROM products
product_idproduct_name
1Wireless Mouse
2Mechanical Keyboard
3USB-C Hub
4Office Chair
5Standing Desk
6Notebook Pack
7Gel Pens
8Desk Lamp
8 rows in set
How this query works, step by step
  1. WHERE product_id = 9 matches exactly one row.
  2. MySQL removes it and reports 1 row affected.
  3. The SELECT shows the 8 remaining products.

Delete several rows

A WHERE that matches several rows deletes them all. Remove every line of order 1001:

SQL
DELETE FROM order_itemsWHERE order_id = 1001; SELECT * FROM order_items WHERE order_id IN (1001, 1002);
ResultMySQL 8.4
mysql> DELETE FROM order_items
Query OK, 2 rows affected
mysql> SELECT * FROM order_items WHERE order_id IN (1001, 1002)
order_idproduct_idquantity
100241
1 row in set
How this query works, step by step
  1. Order 1001 has two lines (products 2 and 6), so 2 rows affected.
  2. Order 1002’s line is untouched.

Check first with SELECT

Before deleting, run the same WHERE as a SELECT:

SQL
SELECT * FROM orders WHERE status = 'Cancelled';
ResultMySQL 8.4
order_idcustomer_idorder_datestatus
100432025-02-14Cancelled
1 row in set

If the result is what you expect, change SELECT * to DELETE and run it.

Foreign keys block orphan rows

Order 1004 still has an order line. The foreign key from order_items to orders stops you from deleting the order and leaving the line pointing at nothing:

MySQL
DELETE FROM orders WHERE order_id = 1004;
ResultMySQL 8.4
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`company`.`order_items`, CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`order_id`))

Delete the child rows first, then the parent:

SQL
DELETE FROM order_items WHERE order_id = 1004;DELETE FROM orders WHERE order_id = 1004;SELECT order_id, status FROM orders WHERE status = 'Cancelled';
ResultMySQL 8.4
mysql> DELETE FROM order_items WHERE order_id = 1004
Query OK, 1 row affected
mysql> DELETE FROM orders WHERE order_id = 1004
Query OK, 1 row affected
mysql> SELECT order_id, status FROM orders WHERE status = 'Cancelled'
Empty set
How this query works, step by step
  1. The first DELETE removes the order’s line (1 row).
  2. Now nothing references order 1004, so the second DELETE succeeds.
  3. No cancelled orders are left: Empty set.
Tip

A foreign key declared with ON DELETE CASCADE deletes the child rows automatically. It is convenient, but make sure that is really what you want.

Delete with a subquery

Delete customers who have never placed an order:

SQL
DELETE FROM customersWHERE customer_id NOT IN (SELECT customer_id FROM orders); SELECT customer_id, customer_name FROM customers;
ResultMySQL 8.4
mysql> DELETE FROM customers
Query OK, 2 rows affected
mysql> SELECT customer_id, customer_name FROM customers
customer_idcustomer_name
1Ananya Sharma
2Rohit Kumar
3Fatima Khan
4David Fernandes
5Ishita Bose
5 rows in set
How this query works, step by step
  1. The subquery lists every customer id that appears in orders.
  2. NOT IN matches customers 6 and 7, who have no orders. (orders.customer_id is NOT NULL, so the NOT IN NULL trap cannot happen here.)
  3. Two rows are deleted.

Delete with JOIN (MySQL)

Delete the order lines of every delivered order placed before February 2025:

MySQL
DELETE oiFROM order_items oiJOIN orders o ON o.order_id = oi.order_idWHERE o.status = 'Delivered' AND o.order_date < '2025-02-01'; SELECT COUNT(*) AS remaining_lines FROM order_items;
ResultMySQL 8.4
mysql> DELETE oi
Query OK, 3 rows affected
mysql> SELECT COUNT(*) AS remaining_lines FROM order_items
remaining_lines
13
1 row in set
How this query works, step by step
  1. DELETE oi names the table to delete from; orders is only used for the condition.
  2. Orders 1001 and 1002 match, with three lines between them.
  3. 16 lines minus 3 leaves 13.

Delete with ORDER BY and LIMIT

MySQL can delete a limited number of rows. This is useful for removing huge amounts of old data in small batches:

MySQL
DELETE FROM order_itemsORDER BY order_idLIMIT 2; SELECT COUNT(*) AS remaining_lines FROM order_items;
ResultMySQL 8.4
mysql> DELETE FROM order_items
Query OK, 2 rows affected
mysql> SELECT COUNT(*) AS remaining_lines FROM order_items
remaining_lines
14
1 row in set
How this query works, step by step
  1. ORDER BY order_id decides which rows go first: the two lines of order 1001.
  2. LIMIT 2 stops after two rows, leaving 14.
  3. In a real clean-up job you would use something like LIMIT 1000 and repeat until no rows are affected, so each batch holds locks only briefly.

TRUNCATE TABLE

TRUNCATE TABLE removes all rows at once and resets AUTO_INCREMENT counters:

MySQL
CREATE TABLE audit_log (id INT AUTO_INCREMENT PRIMARY KEY, msg VARCHAR(50));INSERT INTO audit_log (msg) VALUES ('login'), ('logout');TRUNCATE TABLE audit_log;INSERT INTO audit_log (msg) VALUES ('fresh start');SELECT * FROM audit_log;
ResultMySQL 8.4
mysql> CREATE TABLE audit_log (id INT AUTO_INCREMENT PRIMARY KEY, msg VARCHAR…
Query OK, 0 rows affected
mysql> INSERT INTO audit_log (msg) VALUES ('login'), ('logout')
Query OK, 2 rows affected
mysql> TRUNCATE TABLE audit_log
Query OK, 0 rows affected
mysql> INSERT INTO audit_log (msg) VALUES ('fresh start')
Query OK, 1 row affected
mysql> SELECT * FROM audit_log
idmsg
1fresh start
1 row in set
How this query works, step by step
  1. Two rows are inserted with ids 1 and 2.
  2. TRUNCATE empties the table and resets the counter.
  3. The next insert gets id 1 again. After a DELETE FROM audit_log it would have got id 3.

DELETE vs TRUNCATE vs DROP

DELETETRUNCATEDROP TABLE
Removeschosen rows (WHERE)all rowsthe whole table
Table structurekeptkeptremoved
WHERE allowedyesnono
Speed on big tablesslow (row by row)fastfast
Resets AUTO_INCREMENTnoyesn/a
Fires triggersyesnono
Rollback in a transactionyesno (implicit commit)no (implicit commit)
Command typeDMLDDLDDL

Exercise: Remove out-of-stock products

Delete every product whose stock is 0. Run the matching SELECT first in your head, then show the remaining products.

Expected resultMySQL 8.4
mysql> DELETE FROM order_items WHERE product_id IN (SELECT product_id FROM pr…
Query OK, 1 row affected
mysql> DELETE FROM products WHERE stock = 0
Query OK, 1 row affected
mysql> SELECT product_id, product_name, stock FROM products
product_idproduct_namestock
1Wireless Mouse120
2Mechanical Keyboard45
4Office Chair12
5Standing Desk5
6Notebook Pack300
7Gel Pens250
8Desk Lamp30
9Monitor Arm18
8 rows in set
Hint 1

DELETE FROM products WHERE stock = 0;. The USB-C Hub has order lines, so delete those first.

Quick quiz

1. What does DELETE FROM customers; do?

2. Which command resets the AUTO_INCREMENT counter?

3. Why does deleting a parent row fail with error 1451?

4. Which can be rolled back inside a transaction?

DELETE FAQ

What is the difference between DELETE and TRUNCATE?

DELETE removes rows one by one, can use WHERE, fires triggers and can be rolled back. TRUNCATE removes all rows at once, resets AUTO_INCREMENT and cannot be rolled back in MySQL.

How do I delete duplicate rows in SQL?

Keep one row per duplicate group and delete the rest, for example with a self join on the duplicate columns and a.id > b.id, or with ROW_NUMBER() in a CTE. See Top N per Group.

Can I recover deleted rows?

Inside an uncommitted transaction, yes: ROLLBACK. After a commit, only from a backup or the binary log.

How do I delete rows from two tables at once in MySQL?

Name both tables after DELETE: DELETE o, oi FROM orders o JOIN order_items oi ON ... WHERE ...;

SQL UPDATE Statement

SQL Tutorial › Modifying Data › UPDATE

SQL UPDATE Statement

BeginnerLesson 19 of 6911 min read7 queries run on MySQL 8.4

UPDATE changes values in rows that already exist: give a raise, fix a typo, mark an order as shipped. It is simple to write and easy to get dangerously wrong, because without a WHERE clause it changes every row. This lesson shows the safe way to do it, step by step.

In this lesson
  • Update one or several columns with SET
  • Target the right rows with WHERE
  • Use calculations, CASE and joins in an UPDATE
  • Check your change before and after

UPDATE syntax

MySQL
UPDATE table_nameSET column1 = value1, column2 = value2, ...WHERE condition;

Sample data

employees · 12 rows (some columns)
emp_idfirst_namedept_idjob_titlesalarycity
100MeeraNULLCEO250000Mumbai
101Asha1Engineering Manager125000Bengaluru
102Rahul1Senior Developer98000Bengaluru
103Priya1QA Engineer72000Pune
104Karan1Developer72000Bengaluru
105Vikram2Sales Manager105000Mumbai
106Neha2Sales Executive54000Mumbai
107Arjun2Sales Executive48000Kolkata
108Kavya3Marketing Lead88000Pune
109Rohan3Content Writer45000Pune
110Sneha4HR Manager78000Delhi
111Amit1Intern25000Bengaluru

Update one row

Karan has been promoted:

SQL
UPDATE employeesSET job_title = 'Senior Developer'WHERE emp_id = 104; SELECT emp_id, first_name, job_title FROM employees WHERE emp_id = 104;
ResultMySQL 8.4
mysql> UPDATE employees
Query OK, 1 row affected
mysql> SELECT emp_id, first_name, job_title FROM employees WHERE emp_id = 104
emp_idfirst_namejob_title
104KaranSenior Developer
1 row in set
How this query works, step by step
  1. WHERE emp_id = 104 selects exactly one row, because emp_id is the primary key.
  2. SET job_title = 'Senior Developer' replaces the old value in that row.
  3. MySQL reports 1 row affected. The SELECT confirms the change.

Update several columns

Separate the assignments with commas:

SQL
UPDATE employeesSET city = 'Hyderabad', salary = 80000WHERE emp_id = 103; SELECT emp_id, first_name, city, salary FROM employees WHERE emp_id = 103;
ResultMySQL 8.4
mysql> UPDATE employees
Query OK, 1 row affected
mysql> SELECT emp_id, first_name, city, salary FROM employees WHERE emp_id = …
emp_idfirst_namecitysalary
103PriyaHyderabad80000
1 row in set

Calculated updates

The new value can use the old one. A 10% raise for the Marketing department:

MySQL
UPDATE employeesSET salary = salary * 1.10WHERE dept_id = 3; SELECT first_name, salary FROM employees WHERE dept_id = 3;
ResultMySQL 8.4
mysql> UPDATE employees
Query OK, 2 rows affected
mysql> SELECT first_name, salary FROM employees WHERE dept_id = 3
first_namesalary
Kavya96800
Rohan49500
2 rows in set
How this query works, step by step
  1. WHERE dept_id = 3 picks the two Marketing employees.
  2. For each, MySQL reads the current salary, multiplies it by 1.10 and stores the result.
  3. salary is an INT column, so MySQL rounds the result to a whole number: 88,000 becomes 96,800, and 45,000 becomes 49,500.

Update with CASE

Different raises for different job levels, in one statement:

MySQL
UPDATE employeesSET salary = CASE               WHEN salary < 50000 THEN salary + 5000               WHEN salary < 100000 THEN salary + 3000               ELSE salary             ENDWHERE dept_id = 2; SELECT first_name, salary FROM employees WHERE dept_id = 2;
ResultMySQL 8.4
mysql> UPDATE employees
Query OK, 2 rows affected
mysql> SELECT first_name, salary FROM employees WHERE dept_id = 2
first_namesalary
Vikram105000
Neha57000
Arjun53000
3 rows in set
How this query works, step by step
  1. WHERE dept_id = 2 picks the three Sales employees.
  2. The CASE works out each person’s new salary: Arjun (48,000) gets 5,000 more, Neha (54,000) gets 3,000 more.
  3. Vikram (105,000) falls into ELSE salary, so his value stays the same.
  4. MySQL reports 2 rows affected although 3 rows matched: it counts only rows whose value actually changed.

UPDATE with JOIN

Update one table using values from another. Move everyone in the Sales department to the department’s own location:

MySQL
UPDATE employees eJOIN departments d ON d.dept_id = e.dept_idSET e.city = d.locationWHERE d.dept_name = 'Sales'; SELECT e.first_name, e.city, d.locationFROM employees e JOIN departments d ON d.dept_id = e.dept_idWHERE d.dept_name = 'Sales';
ResultMySQL 8.4
mysql> UPDATE employees e
Query OK, 1 row affected
mysql> SELECT e.first_name, e.city, d.location
first_namecitylocation
VikramMumbaiMumbai
NehaMumbaiMumbai
ArjunMumbaiMumbai
3 rows in set
How this query works, step by step
  1. The join pairs each employee with their department row.
  2. WHERE d.dept_name = 'Sales' keeps the Sales employees.
  3. SET e.city = d.location copies Mumbai into each one’s city. Arjun moves from Kolkata; Vikram and Neha were already in Mumbai, so only 1 row changes.
Note

UPDATE ... JOIN is MySQL syntax. PostgreSQL uses UPDATE employees SET ... FROM departments WHERE ..., and SQL Server uses UPDATE e SET ... FROM employees e JOIN ....

Update with a subquery

The WHERE clause can use a subquery. Put every product that has never been ordered on sale, 300 off:

SQL
UPDATE productsSET price = price - 300WHERE product_id NOT IN (SELECT product_id FROM order_items); SELECT product_id, product_name, price FROM products WHERE product_id = 9;
ResultMySQL 8.4
mysql> UPDATE products
Query OK, 1 row affected
mysql> SELECT product_id, product_name, price FROM products WHERE product_id …
product_idproduct_nameprice
9Monitor Arm2499
1 row in set

Safe UPDATE habits

  1. Run the WHERE as a SELECT first. SELECT * FROM employees WHERE dept_id = 3; shows exactly which rows will change.
  2. Use a transaction for important changes, so you can ROLLBACK a mistake (Transactions).
  3. Turn on safe updates in interactive sessions: SET SQL_SAFE_UPDATES = 1; makes MySQL refuse UPDATE and DELETE without a key-based WHERE or LIMIT. MySQL Workbench enables it by default.
MySQL
SET SQL_SAFE_UPDATES = 1;UPDATE employees SET salary = 0;
ResultMySQL 8.4
mysql> SET SQL_SAFE_UPDATES = 1
Query OK, 0 rows affected
mysql> UPDATE employees SET salary = 0
ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column. 

Exercise: Restock

Set the stock of the USB-C Hub (product 3) to 75 and show the product.

Expected resultMySQL 8.4
mysql> UPDATE products
Query OK, 1 row affected
mysql> SELECT product_id, product_name, stock FROM products WHERE product_id …
product_idproduct_namestock
3USB-C Hub75
1 row in set
Hint 1

UPDATE products SET stock = 75 WHERE product_id = 3;

Exercise: Price rise

Raise the price of every Stationery product by 20 and show the Stationery products afterwards.

Expected resultMySQL 8.4
mysql> UPDATE products
Query OK, 2 rows affected
mysql> SELECT product_name, price FROM products WHERE category = 'Stationery'
product_nameprice
Notebook Pack269
Gel Pens219
2 rows in set
Hint 1

SET price = price + 20 with a WHERE on category.

Quick quiz

1. What does UPDATE products SET stock = 0; do?

2. How do you update two columns in one statement?

3. MySQL says “Rows matched: 3 Changed: 2”. Why only 2?

4. What is the best first step before a risky UPDATE?

UPDATE FAQ

How do I update multiple columns in SQL?

List them after SET, separated by commas: UPDATE t SET a = 1, b = 'x' WHERE id = 5;

How do I update a table using values from another table?

In MySQL, join the tables in the UPDATE: UPDATE t1 JOIN t2 ON ... SET t1.col = t2.col WHERE ...;

Can I undo an UPDATE?

Only if it ran inside a transaction that has not been committed: run ROLLBACK. With autocommit on (the default), the change is permanent, so restore from a backup.

What is safe update mode in MySQL?

SQL_SAFE_UPDATES = 1 blocks UPDATE and DELETE statements that have no WHERE on a key column and no LIMIT, protecting you from changing every row by accident.

SQL SELECT Statement

SQL Tutorial › Querying Data › SELECT

SQL SELECT Statement

BeginnerLesson 7 of 6912 min read7 queries run on MySQL 8.4

SELECT is the SQL statement you will write more than any other. It reads rows from a table and returns only the columns you ask for. In this lesson every query runs on a real MySQL 8.4 server, and you see the table it reads, the result it returns and what happens at each step.

In this lesson
  • Read specific columns, or every column with SELECT *
  • Calculate new columns such as yearly salary
  • Understand the order in which MySQL runs a query
  • Avoid the most common SELECT mistakes

What does SELECT do?

A database stores data in tables: rows and columns, like a spreadsheet. SELECT asks the database a question and gets a result set back, which is also a table. It never changes the stored data, so it is always safe to run.

Every query in this lesson reads the employees table of our sample company database. Here are the columns we use:

employees · 12 rows (some columns)
emp_idfirst_namelast_namejob_titlesalarycity
100MeeraKapoorCEO250000Mumbai
101AshaRaoEngineering Manager125000Bengaluru
102RahulMehtaSenior Developer98000Bengaluru
103PriyaNairQA Engineer72000Pune
104KaranShahDeveloper72000Bengaluru
105VikramSinghSales Manager105000Mumbai
106NehaGuptaSales Executive54000Mumbai
107ArjunDasSales Executive48000Kolkata
108KavyaIyerMarketing Lead88000Pune
109RohanJoshiContent Writer45000Pune
110SnehaKulkarniHR Manager78000Delhi
111AmitVermaIntern25000Bengaluru

SELECT syntax

MySQL
SELECT column1, column2, ...FROM table_name;
  • SELECT lists the columns you want, separated by commas.
  • FROM names the table to read.
  • The semicolon ; ends the statement. MySQL needs it when you send several statements together.

Select specific columns

Show each employee’s first name and job title:

SQL
SELECT first_name, job_titleFROM employees;
ResultMySQL 8.4
first_namejob_title
MeeraCEO
AshaEngineering Manager
RahulSenior Developer
PriyaQA Engineer
KaranDeveloper
VikramSales Manager
NehaSales Executive
ArjunSales Executive
KavyaMarketing Lead
RohanContent Writer
SnehaHR Manager
AmitIntern
12 rows in set
How this query works, step by step
  1. FROM employees runs first: MySQL opens the employees table and takes all 12 rows.
  2. SELECT first_name, job_title keeps only those two columns and drops the rest.
  3. The columns appear in the order you listed them, not the order they have in the table.
  4. There is no WHERE, so every row comes back: 12 rows in set.
Tip

Click Try it, change the column list (for example add salary), and press Run. The query runs in your browser on a copy of the same table.

Select all columns with SELECT *

The asterisk * means “every column, in table order”:

SQL
SELECT * FROM employees;
ResultMySQL 8.4
emp_idfirst_namelast_namedept_idmanager_idjob_titlesalarycommissionhire_datecity
100MeeraKapoorNULLNULLCEO250000NULL2015-01-05Mumbai
101AshaRao1100Engineering Manager125000NULL2018-04-12Bengaluru
102RahulMehta1101Senior Developer98000NULL2019-07-01Bengaluru
103PriyaNair1101QA Engineer72000NULL2021-01-18Pune
104KaranShah1102Developer72000NULL2022-08-29Bengaluru
105VikramSingh2100Sales Manager105000120002017-11-20Mumbai
106NehaGupta2105Sales Executive5400060002022-03-14Mumbai
107ArjunDas2105Sales Executive4800045002023-06-05Kolkata
108KavyaIyer3100Marketing Lead88000NULL2020-09-09Pune
109RohanJoshi3108Content Writer45000NULL2024-01-22Pune
110SnehaKulkarni4100HR Manager78000NULL2019-02-25Delhi
111AmitVerma1101Intern25000NULL2025-06-02Bengaluru
12 rows in set
How this query works, step by step
  1. FROM employees reads all 12 rows.
  2. * expands to all 10 columns: emp_id, first_name, last_name, dept_id, manager_id, job_title, salary, commission, hire_date, city.
  3. Empty values show as NULL. Only the three sales staff have a commission, and the CEO has no dept_id or manager_id.
Watch out

SELECT * is fine for exploring a table, but avoid it in application code and reports. It sends columns you do not need, makes queries slower on wide tables, and breaks code silently when someone adds or reorders a column.

Calculated columns and aliases

A column in the SELECT list can be an expression. MySQL calculates it for every row. Use AS to give it a readable name (an alias):

SQL
SELECT first_name,       salary,       salary * 12 AS yearly_salaryFROM employees;
ResultMySQL 8.4
first_namesalaryyearly_salary
Meera2500003000000
Asha1250001500000
Rahul980001176000
Priya72000864000
Karan72000864000
Vikram1050001260000
Neha54000648000
Arjun48000576000
Kavya880001056000
Rohan45000540000
Sneha78000936000
Amit25000300000
12 rows in set
How this query works, step by step
  1. FROM employees reads all 12 rows.
  2. For each row, MySQL multiplies that row’s salary by 12.
  3. AS yearly_salary names the new column. Without it, the header would be the expression text salary * 12.
  4. The stored salary values do not change: the calculation exists only in the result.

You can also join text with CONCAT():

SQL
SELECT emp_id,       CONCAT(first_name, ' ', last_name) AS full_name,       cityFROM employees;
ResultMySQL 8.4
emp_idfull_namecity
100Meera KapoorMumbai
101Asha RaoBengaluru
102Rahul MehtaBengaluru
103Priya NairPune
104Karan ShahBengaluru
105Vikram SinghMumbai
106Neha GuptaMumbai
107Arjun DasKolkata
108Kavya IyerPune
109Rohan JoshiPune
110Sneha KulkarniDelhi
111Amit VermaBengaluru
12 rows in set
How this query works, step by step
  1. For every row, CONCAT() glues first_name, a space and last_name into one string.
  2. The result column is called full_name because of the alias.
  3. emp_id and city are copied as they are.

SELECT without a table

SELECT can also evaluate expressions without reading any table. That is handy for testing a function before you use it in a real query:

MySQL
SELECT 7 * 6 AS answer, UPPER('sqatools') AS site, CURDATE() IS NOT NULL AS has_date;
ResultMySQL 8.4
answersitehas_date
42SQATOOLS1
1 row in set
How this query works, step by step
  1. There is no FROM, so MySQL evaluates each expression once and returns one row.
  2. 7 * 6 is 42, UPPER() turns the text to capitals, and a comparison returns 1 for true.

The order MySQL runs a query

You write a query in one order but MySQL runs it in another. Knowing this explains most beginner errors, such as “unknown column” when you use an alias in WHERE.

StepClauseWhat happens
1FROM / JOINPick the tables and combine them
2WHEREKeep only the rows that match the condition
3GROUP BYPut rows into groups
4HAVINGKeep only the groups that match
5SELECTCalculate the output columns and aliases
6DISTINCTRemove duplicate rows
7ORDER BYSort the result
8LIMITReturn only the first N rows

Because SELECT (step 5) runs after WHERE (step 2), an alias created in SELECT does not exist yet inside WHERE:

MySQL
SELECT first_name, salary * 12 AS yearly_salaryFROM employeesWHERE yearly_salary > 1000000;
ResultMySQL 8.4
ERROR 1054 (42S22): Unknown column 'yearly_salary' in 'where clause'
Note

Repeat the expression in WHERE (WHERE salary * 12 > 1000000) or use a subquery or CTE. ORDER BY runs after SELECT, so ORDER BY yearly_salary does work.

Common SELECT mistakes

  • Missing comma between columns. SELECT first_name job_title FROM employees does not fail: MySQL reads job_title as an alias for first_name, so you get one column with the wrong header.
  • Comma after the last column. SELECT first_name, FROM employees is a syntax error.
  • Quoting column names with single quotes. SELECT 'salary' FROM employees returns the text salary on every row. Write column names without quotes; wrap a name in backticks only when it has spaces or is a reserved word (see SQL Syntax).
  • Expecting a fixed row order. Without ORDER BY the database may return rows in any order. Never rely on the order you happen to see.

Here is the missing-comma trap in action:

SQL
SELECT first_name job_titleFROM employeesLIMIT 3;
ResultMySQL 8.4
job_title
Meera
Asha
Rahul
3 rows in set

Predict the result

What does this query return? Think about how many columns, and what the header is.

SELECT last_name, salary / 1000 AS salary_kFROM employeesLIMIT 2;

Exercise: List names and cities

Write a query that shows every employee’s emp_id, first_name and city, with city renamed to office.

Expected resultMySQL 8.4
emp_idfirst_nameoffice
100MeeraMumbai
101AshaBengaluru
102RahulBengaluru
103PriyaPune
104KaranBengaluru
105VikramMumbai
106NehaMumbai
107ArjunKolkata
108KavyaPune
109RohanPune
110SnehaDelhi
111AmitBengaluru
12 rows in set
Hint 1

Put the three columns after SELECT, separated by commas.

Hint 2

Rename a column with AS office.

Exercise: Monthly bonus

Show each employee’s first_name, salary and a column called bonus that is 10% of the salary.

Expected resultMySQL 8.4
first_namesalarybonus
Meera25000025000.00
Asha12500012500.00
Rahul980009800.00
Priya720007200.00
Karan720007200.00
Vikram10500010500.00
Neha540005400.00
Arjun480004800.00
Kavya880008800.00
Rohan450004500.00
Sneha780007800.00
Amit250002500.00
12 rows in set
Hint 1

10% of a number is the number multiplied by 0.10.

Quick quiz

1. Which clause does MySQL run first?

2. What does SELECT * return?

3. You run SELECT first_name job_title FROM employees. What happens?

4. Does SELECT change the data stored in the table?

SELECT FAQ

What is the SELECT statement in SQL?

SELECT is the SQL command that reads data from one or more tables. You list the columns you want after SELECT and the table after FROM, and the database returns a result set.

Is SELECT * bad practice?

It is fine for quick exploration, but in applications and reports list the columns you need. SELECT * transfers extra data and can break code when the table changes.

Can I use an alias from SELECT in the WHERE clause?

No. WHERE runs before SELECT, so the alias does not exist yet. Repeat the expression, or wrap the query in a subquery or CTE. You can use the alias in ORDER BY.

Is SQL case sensitive?

Keywords such as SELECT and FROM are not case sensitive, so select works too. Writing keywords in capitals is a convention that makes queries easier to read. Whether table names are case sensitive depends on the operating system and MySQL settings.

What is the order of execution of a SQL query?

FROM and JOIN, then WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY and finally LIMIT.

SQL INSERT INTO Statement

SQL Tutorial › Modifying Data › INSERT

SQL INSERT INTO Statement

BeginnerLesson 18 of 6911 min read10 queries run on MySQL 8.4

INSERT INTO adds new rows to a table. You can insert one row, many rows in one statement, or the result of a query. This lesson inserts into the sample customers and products tables and shows exactly what MySQL reports, including the errors you get when a row breaks a rule.

In this lesson
  • Insert one row and many rows
  • Name the columns so inserts survive table changes
  • Rely on defaults and NULLs for missing columns
  • Copy rows with INSERT INTO ... SELECT

INSERT syntax

MySQL
INSERT INTO table_name (column1, column2, ...)VALUES (value1, value2, ...);

The values are matched to the columns by position: the first value goes into the first listed column, and so on.

Sample data

customers · 7 rows
customer_idcustomer_nameemailcityjoined_on
1Ananya Sharmaananya@example.comMumbai2024-01-15
2Rohit Kumarrohit.k@example.comDelhi2024-02-03
3Fatima Khanfatima@example.comHyderabad2024-03-21
4David FernandesNULLGoa2024-05-09
5Ishita Boseishita@example.comKolkata2024-07-30
6Manoj Pillaimanoj.p@example.comChennai2025-01-11
7Sara Thomassara.t@example.comBengaluru2025-03-02

Insert one row

SQL
INSERT INTO customers (customer_id, customer_name, email, city, joined_on)VALUES (8, 'Kiran Rao', 'kiran.rao@example.com', 'Bengaluru', '2025-06-15'); SELECT * FROM customers WHERE customer_id = 8;
ResultMySQL 8.4
mysql> INSERT INTO customers (customer_id, customer_name, email, city, joined…
Query OK, 1 row affected
mysql> SELECT * FROM customers WHERE customer_id = 8
customer_idcustomer_nameemailcityjoined_on
8Kiran Raokiran.rao@example.comBengaluru2025-06-15
1 row in set
How this query works, step by step
  1. The column list names the five columns that receive values.
  2. VALUES (...) supplies one value per column, in the same order. Text and dates are quoted; numbers are not.
  3. MySQL reports Query OK, 1 row affected.
  4. The SELECT reads the new row back.

Insert several rows at once

Separate the row groups with commas. One multi-row INSERT is much faster than many single-row statements:

SQL
INSERT INTO products (product_id, product_name, category, price, stock)VALUES (10, 'Webcam HD', 'Electronics', 2499, 40),       (11, 'Sticky Notes', 'Stationery', 99, 500),       (12, 'Footrest', 'Furniture', 1299, 25); SELECT product_id, product_name, price FROM products WHERE product_id >= 10;
ResultMySQL 8.4
mysql> INSERT INTO products (product_id, product_name, category, price, stock)
Query OK, 3 rows affected
mysql> SELECT product_id, product_name, price FROM products WHERE product_id …
product_idproduct_nameprice
10Webcam HD2499
11Sticky Notes99
12Footrest1299
3 rows in set
How this query works, step by step
  1. Each parenthesised group is one row: three rows here.
  2. MySQL inserts them in one statement and reports 3 rows affected.

Insert only some columns

Columns you leave out get their default value, or NULL if they have no default:

SQL
INSERT INTO customers (customer_id, customer_name, city)VALUES (9, 'Leela Nair', 'Kochi'); SELECT * FROM customers WHERE customer_id = 9;
ResultMySQL 8.4
mysql> INSERT INTO customers (customer_id, customer_name, city)
Query OK, 1 row affected
mysql> SELECT * FROM customers WHERE customer_id = 9
customer_idcustomer_nameemailcityjoined_on
9Leela NairNULLKochiNULL
1 row in set
How this query works, step by step
  1. Only three columns are named.
  2. email and joined_on are not given and have no default, so they are stored as NULL.

Insert without a column list

You may leave out the column list, but then you must give a value for every column in table order:

SQL
INSERT INTO departments VALUES (6, 'Legal', 'Delhi');SELECT * FROM departments;
ResultMySQL 8.4
mysql> INSERT INTO departments VALUES (6, 'Legal', 'Delhi')
Query OK, 1 row affected
mysql> SELECT * FROM departments
dept_iddept_namelocation
1EngineeringBengaluru
2SalesMumbai
3MarketingPune
4HRDelhi
5FinanceMumbai
6LegalDelhi
6 rows in set
Watch out

Avoid this form in application code. If someone adds a column to the table later, every insert without a column list breaks, or worse, puts values into the wrong columns.

Errors you will meet

A duplicate primary key:

MySQL
INSERT INTO customers (customer_id, customer_name) VALUES (1, 'Duplicate Person');
ResultMySQL 8.4
ERROR 1062 (23000): Duplicate entry '1' for key 'customers.PRIMARY'

A required column left empty (customer_name is NOT NULL):

MySQL
INSERT INTO customers (customer_id, city) VALUES (10, 'Pune');
ResultMySQL 8.4
ERROR 1364 (HY000): Field 'customer_name' doesn't have a default value

A value count that does not match the column count:

MySQL
INSERT INTO departments (dept_id, dept_name) VALUES (7, 'Support', 'Pune');
ResultMySQL 8.4
ERROR 1136 (21S01): Column count doesn't match value count at row 1

A foreign key that points nowhere (there is no customer 99):

MySQL
INSERT INTO orders (order_id, customer_id, order_date, status) VALUES (1011, 99, '2025-06-10', 'Pending');
ResultMySQL 8.4
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`company`.`orders`, CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`customer_id`))
How this query works, step by step
  1. Error 1062: the primary key value already exists.
  2. Error 1364: a NOT NULL column without a default was not given a value.
  3. Error 1136: three values for two columns.
  4. Error 1452: the foreign key on customer_id refuses an order for a customer that does not exist.
Note

An INSERT that fails inserts nothing. In a multi-row insert, one bad row makes the whole statement fail (for InnoDB tables).

INSERT INTO … SELECT

Insert the result of a query instead of literal values. Archive all cancelled orders into a new table:

SQL
CREATE TABLE cancelled_orders (  order_id    INT PRIMARY KEY,  customer_id INT,  order_date  DATE); INSERT INTO cancelled_orders (order_id, customer_id, order_date)SELECT order_id, customer_id, order_dateFROM ordersWHERE status = 'Cancelled'; SELECT * FROM cancelled_orders;
ResultMySQL 8.4
mysql> CREATE TABLE cancelled_orders (
Query OK, 0 rows affected
mysql> INSERT INTO cancelled_orders (order_id, customer_id, order_date)
Query OK, 1 row affected
mysql> SELECT * FROM cancelled_orders
order_idcustomer_idorder_date
100432025-02-14
1 row in set
How this query works, step by step
  1. CREATE TABLE makes an empty archive table.
  2. The SELECT finds the cancelled orders (one row).
  3. INSERT INTO ... SELECT writes every row the query returns into the archive. There is no VALUES keyword.

Insert calculated values

Values can be expressions and functions:

SQL
INSERT INTO products (product_id, product_name, category, price, stock)VALUES (13, UPPER('laptop stand'), 'Furniture', 1500 + 499, 10 * 2); SELECT * FROM products WHERE product_id = 13;
ResultMySQL 8.4
mysql> INSERT INTO products (product_id, product_name, category, price, stock)
Query OK, 1 row affected
mysql> SELECT * FROM products WHERE product_id = 13
product_idproduct_namecategorypricestock
13LAPTOP STANDFurniture199920
1 row in set

Exercise: Add a department

Insert a new department with dept_id 6, name 'Support' and location 'Hyderabad', then show all departments.

Expected resultMySQL 8.4
mysql> INSERT INTO departments (dept_id, dept_name, location)
Query OK, 1 row affected
mysql> SELECT * FROM departments
dept_iddept_namelocation
1EngineeringBengaluru
2SalesMumbai
3MarketingPune
4HRDelhi
5FinanceMumbai
6SupportHyderabad
6 rows in set
Hint 1

Name the three columns, then give three values.

Exercise: Two new customers

Insert customers 8 and 9 in one statement: 'Om Prakash' from 'Jaipur' and 'Rhea Das' from 'Kolkata'. Leave the email and date empty.

Expected resultMySQL 8.4
mysql> INSERT INTO customers (customer_id, customer_name, city)
Query OK, 2 rows affected
mysql> SELECT customer_id, customer_name, city, email FROM customers WHERE cu…
customer_idcustomer_namecityemail
8Om PrakashJaipurNULL
9Rhea DasKolkataNULL
2 rows in set
Hint 1

One INSERT with two row groups separated by a comma.

Quick quiz

1. How are values matched to columns in INSERT INTO t (a, b) VALUES (1, 2)?

2. A column is left out of the INSERT and has no DEFAULT. What is stored?

3. Which statement copies rows from one table into another existing table?

4. Why is a multi-row INSERT preferred to many single-row INSERTs?

INSERT FAQ

How do I insert multiple rows in SQL?

List several value groups after VALUES, separated by commas: INSERT INTO t (a, b) VALUES (1, 'x'), (2, 'y');

How do I insert data from one table into another?

Use INSERT INTO target (columns) SELECT columns FROM source WHERE ...; The selected columns must match the target columns in number and order.

How do I get the ID of the row I just inserted?

For an AUTO_INCREMENT column, call SELECT LAST_INSERT_ID(); right after the insert, in the same connection. See AUTO_INCREMENT.

How do I insert a row only if it does not exist?

Use INSERT IGNORE, or INSERT ... ON DUPLICATE KEY UPDATE to update the existing row instead. See Upsert.

SQL Introduction

SQL Tutorial › Getting Started › SQL Introduction

SQL Introduction

BeginnerLesson 1 of 6910 min read2 queries run on MySQL 8.4

SQL (Structured Query Language) is the standard language for storing, reading and changing data in relational databases. Banks, shops, hospitals, apps and test frameworks all keep their data in databases, and SQL is how you talk to them. This lesson explains what SQL is, how a database is organised and what kinds of commands SQL has, then you run your first queries.

In this lesson
  • What SQL, a database and an RDBMS are
  • How tables, rows, columns and keys fit together
  • The difference between SQL and MySQL
  • The five groups of SQL commands: DQL, DML, DDL, DCL and TCL

What is SQL?

SQL stands for Structured Query Language. It is pronounced “S-Q-L” or “sequel”. You use it to:

  • read data: which customers ordered this month?
  • add, change and delete data: record a new order, update a price, remove a test account.
  • define the structure: create tables and the rules the data must follow.
  • control access: decide who may read or change what.

SQL is declarative: you describe what result you want, and the database works out how to get it. You never write loops to search a table yourself.

Databases, tables, rows and columns

A relational database stores data in tables. A table looks like a spreadsheet:

  • each column has a name and a data type, for example salary INT;
  • each row is one record, for example one department;
  • a primary key column holds a value that identifies each row uniquely, such as dept_id.

Here is the departments table from the sample database used throughout this tutorial:

departments · 5 rows
dept_iddept_namelocation
1EngineeringBengaluru
2SalesMumbai
3MarketingPune
4HRDelhi
5FinanceMumbai

Tables are linked by keys. Every employee row stores a dept_id, which points at a row in departments. That link is called a foreign key, and it is what makes the database relational.

What is an RDBMS?

An RDBMS (Relational Database Management System) is the software that stores the tables and runs your SQL. Popular ones:

RDBMSMade byTypical use
MySQLOracle (open source)Websites, SaaS apps, WordPress
PostgreSQLOpen source communityApps, analytics, GIS
SQL ServerMicrosoftEnterprise and .NET apps
Oracle DatabaseOracleLarge enterprise systems
SQLiteOpen sourceMobile apps, browsers, small tools

SQL vs MySQL

SQL is the language; MySQL is a database system that understands it. The core of SQL (SELECT, INSERT, JOIN, GROUP BY …) is an ISO standard and works almost the same everywhere. Each product adds its own extras. This tutorial uses MySQL 8.4 LTS, and every result you see was produced by a real MySQL 8.4 server. Where MySQL differs from other databases, the lesson says so.

Your first SQL queries

Ask the database which departments exist and where they are:

SQL
SELECT dept_name, locationFROM departments;
ResultMySQL 8.4
dept_namelocation
EngineeringBengaluru
SalesMumbai
MarketingPune
HRDelhi
FinanceMumbai
5 rows in set
How this query works, step by step
  1. FROM departments tells MySQL which table to read.
  2. SELECT dept_name, location picks two of its three columns.
  3. MySQL returns a result set with one row per department: 5 rows.

Now a question with a condition and a sort: who earns more than 90,000, highest first?

SQL
SELECT first_name, job_title, salaryFROM employeesWHERE salary > 90000ORDER BY salary DESC;
ResultMySQL 8.4
first_namejob_titlesalary
MeeraCEO250000
AshaEngineering Manager125000
VikramSales Manager105000
RahulSenior Developer98000
4 rows in set
How this query works, step by step
  1. FROM employees reads all 12 employees.
  2. WHERE salary > 90000 keeps only the rows whose salary is above 90,000.
  3. SELECT picks the three columns to show.
  4. ORDER BY salary DESC sorts from the highest salary to the lowest.
Tip

Every example with a Try it button runs in your browser. Edit the query, press Run (or Ctrl+Enter), and Reset to get the original back.

The five types of SQL commands

GroupStands forCommandsUsed to
DQLData Query LanguageSELECTread data
DMLData Manipulation LanguageINSERT, UPDATE, DELETEchange rows
DDLData Definition LanguageCREATE, ALTER, DROP, TRUNCATEdefine tables and other objects
DCLData Control LanguageGRANT, REVOKEcontrol permissions
TCLTransaction Control LanguageSTART TRANSACTION, COMMIT, ROLLBACKgroup changes into all-or-nothing units

You will learn each group in this tutorial: queries first, because reading data is what you will do most.

Why learn SQL?

  • It is one of the most requested skills for developers, testers, data analysts and data engineers.
  • It has barely changed in decades, so what you learn now stays useful.
  • Testers use SQL to set up test data and check what an application really saved, not just what the screen shows.
  • Python, Java and every other language talk to databases by sending SQL.

Quick quiz

1. What does SQL stand for?

2. Which statement best describes MySQL?

3. Which command group does INSERT belong to?

4. What links an employee row to its department?

SQL introduction FAQ

What is SQL used for?

SQL is used to read, insert, update and delete data in relational databases, to create tables and other database objects, and to control who can access the data.

Is SQL a programming language?

SQL is a declarative query language. Standard SQL describes what data you want rather than step-by-step instructions, although database systems add procedural features such as stored procedures.

What is the difference between SQL and MySQL?

SQL is the language. MySQL is a relational database management system that stores data and executes SQL statements. PostgreSQL, SQL Server and Oracle also use SQL.

Is SQL hard to learn?

No. Basic queries read almost like English, and most people write useful queries within a day. Joins, grouping and window functions take more practice, which is why this tutorial explains each query step by step.

Which database should a beginner start with?

MySQL is a good first choice: it is free, widely used and well documented. The SQL you learn here transfers to PostgreSQL, SQL Server and others with small changes.

SQL Views

SQL Tutorial › Performance & Objects › Views

SQL Views

IntermediateLesson 62 of 6911 min read6 queries run on MySQL 8.4

A view is a saved SELECT with a name. You query it like a table, but it stores no data of its own: every time you read it, MySQL runs the underlying query on the current data. Views hide complex joins behind a simple name, give every report the same definition of “revenue”, and let you expose only some columns to some users.

In this lesson
  • Create and query a view
  • Change and drop views
  • Know when a view is updatable and what WITH CHECK OPTION does
  • Use views for simplicity and security

CREATE VIEW

An order summary that every report can reuse:

SQL
CREATE VIEW order_summary ASSELECT o.order_id, o.order_date, o.status, c.customer_name,       SUM(oi.quantity * p.price) AS order_totalFROM orders oJOIN customers c    ON c.customer_id = o.customer_idJOIN order_items oi ON oi.order_id = o.order_idJOIN products p     ON p.product_id = oi.product_idGROUP BY o.order_id, o.order_date, o.status, c.customer_name; SELECT * FROM order_summary WHERE status = 'Delivered' ORDER BY order_total DESC;
ResultMySQL 8.4
mysql> CREATE VIEW order_summary AS
Query OK, 0 rows affected
mysql> SELECT * FROM order_summary WHERE status = 'Delivered' ORDER BY order_…
order_idorder_datestatuscustomer_nameorder_total
10022025-01-17DeliveredRohit Kumar8999
10012025-01-05DeliveredAnanya Sharma4744
10032025-02-02DeliveredAnanya Sharma4298
10092025-05-15DeliveredFatima Khan3499
10052025-03-08DeliveredDavid Fernandes3286
10102025-06-01DeliveredIshita Bose1145
10072025-04-10DeliveredRohit Kumar799
7 rows in set
How this query works, step by step
  1. CREATE VIEW order_summary AS SELECT ... saves the query under a name. No rows are copied.
  2. SELECT * FROM order_summary WHERE ... reads it like a table.
  3. MySQL combines the view’s query with your WHERE and ORDER BY and runs them on the live tables.

A view always shows current data

SQL
CREATE VIEW low_stock ASSELECT product_id, product_name, stock FROM products WHERE stock < 20;SELECT * FROM low_stock;UPDATE products SET stock = 5 WHERE product_id = 2;SELECT * FROM low_stock;
ResultMySQL 8.4
mysql> CREATE VIEW low_stock AS
Query OK, 0 rows affected
mysql> SELECT * FROM low_stock
product_idproduct_namestock
3USB-C Hub0
4Office Chair12
5Standing Desk5
9Monitor Arm18
4 rows in set
mysql> UPDATE products SET stock = 5 WHERE product_id = 2
Query OK, 1 row affected
mysql> SELECT * FROM low_stock
product_idproduct_namestock
2Mechanical Keyboard5
3USB-C Hub0
4Office Chair12
5Standing Desk5
9Monitor Arm18
5 rows in set
How this query works, step by step
  1. The first read lists the products with fewer than 20 in stock.
  2. After the keyboard’s stock drops to 5, the same view includes it, without any change to the view.

Change and drop a view

MySQL
CREATE VIEW low_stock AS SELECT product_id, product_name, stock FROM products WHERE stock < 20;CREATE OR REPLACE VIEW low_stock ASSELECT product_id, product_name, category, stock FROM products WHERE stock < 10;SELECT * FROM low_stock;DROP VIEW low_stock;
ResultMySQL 8.4
mysql> CREATE VIEW low_stock AS SELECT product_id, product_name, stock FROM p…
Query OK, 0 rows affected
mysql> CREATE OR REPLACE VIEW low_stock AS
Query OK, 0 rows affected
mysql> SELECT * FROM low_stock
product_idproduct_namecategorystock
3USB-C HubElectronics0
5Standing DeskFurniture5
2 rows in set
mysql> DROP VIEW low_stock
Query OK, 0 rows affected

SHOW FULL TABLES WHERE Table_type = 'VIEW'; lists the views in a database, and SHOW CREATE VIEW name; shows a view’s query.

Updatable views

A simple view over one table, without GROUP BY, DISTINCT, aggregates or UNION, is updatable: changes go straight to the base table.

MySQL
CREATE VIEW furniture ASSELECT product_id, product_name, price, stock FROM products WHERE category = 'Furniture';UPDATE furniture SET price = price - 500 WHERE product_id = 4;SELECT product_id, product_name, price FROM products WHERE product_id = 4;
ResultMySQL 8.4
mysql> CREATE VIEW furniture AS
Query OK, 0 rows affected
mysql> UPDATE furniture SET price = price – 500 WHERE product_id = 4
Query OK, 1 row affected
mysql> SELECT product_id, product_name, price FROM products WHERE product_id …
product_idproduct_nameprice
4Office Chair8499
1 row in set

A view with an aggregate is not:

MySQL
CREATE VIEW category_stock AS SELECT category, SUM(stock) AS units FROM products GROUP BY category;UPDATE category_stock SET units = 0 WHERE category = 'Furniture';
ResultMySQL 8.4
mysql> CREATE VIEW category_stock AS SELECT category, SUM(stock) AS units FRO…
Query OK, 0 rows affected
mysql> UPDATE category_stock SET units = 0 WHERE category = 'Furniture'
ERROR 1288 (HY000): The target table category_stock of the UPDATE is not updatable

WITH CHECK OPTION

Without it, you can insert rows through a view that the view itself will not show. WITH CHECK OPTION refuses them:

MySQL
CREATE VIEW furniture ASSELECT product_id, product_name, category, price, stock FROM products WHERE category = 'Furniture'WITH CHECK OPTION;INSERT INTO furniture VALUES (20, 'Bookshelf', 'Furniture', 4999, 8);INSERT INTO furniture VALUES (21, 'Stapler', 'Stationery', 299, 40);
ResultMySQL 8.4
mysql> CREATE VIEW furniture AS
Query OK, 0 rows affected
mysql> INSERT INTO furniture VALUES (20, 'Bookshelf', 'Furniture', 4999, 8)
Query OK, 1 row affected
mysql> INSERT INTO furniture VALUES (21, 'Stapler', 'Stationery', 299, 40)
ERROR 1369 (HY000): CHECK OPTION failed 'company.furniture'
How this query works, step by step
  1. The bookshelf is Furniture, matches the view’s WHERE, and is inserted.
  2. The stapler is Stationery, which the view would not show, so WITH CHECK OPTION rejects it.

Views for security

Give an analyst access to staff names and departments but not salaries:

MySQL
CREATE VIEW staff_directory ASSELECT emp_id, first_name, last_name, dept_id, city FROM employees; GRANT SELECT ON company.staff_directory TO 'analyst'@'localhost';

The analyst can query staff_directory but has no privilege on employees itself. See GRANT.

View vs table

TableView
Stores datayesno (only the query)
Always currentyesyes, computed on every read
Can have its own indexesyesno, it uses the base tables’ indexes
Speeddirectsame as running the query

MySQL has no materialized views. To store a precomputed result, write it into a summary table (for example with CREATE TABLE ... AS SELECT) and refresh it on a schedule.

Quick quiz

1. Does a view store a copy of the data?

2. Which view can be updated?

3. What does WITH CHECK OPTION prevent?

Views FAQ

What is a view in SQL?

A virtual table defined by a SELECT query. It has a name and columns like a table but stores no data; its rows are computed when it is queried.

What is the difference between a view and a table?

A table stores data. A view stores a query and shows the current result of that query each time you read it.

Can we insert data into a view?

Yes, if the view is updatable: based on one table, with no aggregates, GROUP BY, DISTINCT or UNION. The rows go into the base table.

Does MySQL support materialized views?

No. Use a summary table refreshed by a scheduled event or batch job instead.

SQL Triggers

SQL Tutorial › Performance & Objects › Triggers

SQL Triggers

AdvancedLesson 65 of 6912 min read5 queries run on MySQL 8.4

A trigger is code the database runs automatically when a row is inserted, updated or deleted in a table. Triggers can validate or adjust data before it is saved and react after it is saved: writing an audit log, keeping a stock level in sync, blocking an invalid change. They run no matter which application or user made the change.

In this lesson
  • Create BEFORE and AFTER triggers for INSERT, UPDATE and DELETE
  • Read the row values with NEW and OLD
  • Build an audit log and validate data with SIGNAL
  • Know when not to use triggers

Trigger syntax

MySQL
CREATE TRIGGER trigger_name{BEFORE | AFTER} {INSERT | UPDATE | DELETE}ON table_nameFOR EACH ROWtrigger_body;
EventOLD (row before)NEW (row after)
INSERTnot availablethe row being inserted
UPDATEvalues before the changevalues after the change
DELETEthe row being deletednot available

In a BEFORE trigger you can change NEW values; in an AFTER trigger they are read-only.

BEFORE INSERT: clean data

Store every new customer email in lower case:

MySQL
CREATE TRIGGER trg_customers_biBEFORE INSERT ON customersFOR EACH ROWSET NEW.email = LOWER(TRIM(NEW.email)); INSERT INTO customers (customer_id, customer_name, email, city)VALUES (8, 'Kiran Rao', '  Kiran.Rao@Example.COM ', 'Pune');SELECT customer_id, email FROM customers WHERE customer_id = 8;
ResultMySQL 8.4
mysql> CREATE TRIGGER trg_customers_bi
Query OK, 0 rows affected
mysql> INSERT INTO customers (customer_id, customer_name, email, city)
Query OK, 1 row affected
mysql> SELECT customer_id, email FROM customers WHERE customer_id = 8
customer_idemail
8kiran.rao@example.com
1 row in set
How this query works, step by step
  1. Before the row is written, the trigger replaces NEW.email with the trimmed, lower-case version.
  2. The application sent messy input, but the table stores kiran.rao@example.com.
  3. A single-statement body needs no BEGIN ... END or DELIMITER.

AFTER UPDATE: audit log

Record every salary change, who it was for, and the old and new values:

MySQL
CREATE TABLE salary_audit (  audit_id   INT AUTO_INCREMENT PRIMARY KEY,  emp_id     INT,  old_salary INT,  new_salary INT,  changed_by VARCHAR(80),  changed_at DATETIME DEFAULT CURRENT_TIMESTAMP); DELIMITER //CREATE TRIGGER trg_salary_auditAFTER UPDATE ON employeesFOR EACH ROWBEGIN  IF NEW.salary <> OLD.salary THEN    INSERT INTO salary_audit (emp_id, old_salary, new_salary, changed_by)    VALUES (NEW.emp_id, OLD.salary, NEW.salary, CURRENT_USER());  END IF;END //DELIMITER ; UPDATE employees SET salary = salary + 5000 WHERE dept_id = 3;UPDATE employees SET city = 'Pune' WHERE emp_id = 101;SELECT audit_id, emp_id, old_salary, new_salary, changed_by FROM salary_audit;
ResultMySQL 8.4
mysql> CREATE TABLE salary_audit (
Query OK, 0 rows affected
mysql> CREATE TRIGGER trg_salary_audit
Query OK, 0 rows affected
mysql> UPDATE employees SET salary = salary + 5000 WHERE dept_id = 3
Query OK, 2 rows affected
mysql> UPDATE employees SET city = 'Pune' WHERE emp_id = 101
Query OK, 1 row affected
mysql> SELECT audit_id, emp_id, old_salary, new_salary, changed_by FROM salar…
audit_idemp_idold_salarynew_salarychanged_by
11088800093000root@localhost
21094500050000root@localhost
2 rows in set
How this query works, step by step
  1. The trigger fires once for each row the UPDATE changes.
  2. Raising Marketing salaries changes two rows, so two audit rows are written.
  3. Changing Asha’s city fires the trigger too, but the IF sees the salary did not change and writes nothing.
  4. CURRENT_USER() records which account made the change.

BEFORE UPDATE: validate with SIGNAL

Reject salary cuts of more than 20%:

MySQL
DELIMITER //CREATE TRIGGER trg_salary_checkBEFORE UPDATE ON employeesFOR EACH ROWBEGIN  IF NEW.salary < OLD.salary * 0.8 THEN    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Salary cut above 20% needs HR approval';  END IF;END //DELIMITER ; UPDATE employees SET salary = 90000 WHERE emp_id = 102;UPDATE employees SET salary = 50000 WHERE emp_id = 102;
ResultMySQL 8.4
mysql> CREATE TRIGGER trg_salary_check
Query OK, 0 rows affected
mysql> UPDATE employees SET salary = 90000 WHERE emp_id = 102
Query OK, 1 row affected
mysql> UPDATE employees SET salary = 50000 WHERE emp_id = 102
ERROR 1644 (45000): Salary cut above 20% needs HR approval
How this query works, step by step
  1. Cutting Rahul’s salary from 98,000 to 90,000 (8%) passes.
  2. Cutting it to 50,000 is more than 20%, so SIGNAL raises error 1644 with the custom message and the update is cancelled.

AFTER INSERT: keep stock in sync

Reduce a product’s stock whenever an order line is added:

MySQL
CREATE TRIGGER trg_order_items_aiAFTER INSERT ON order_itemsFOR EACH ROWUPDATE products SET stock = stock - NEW.quantity WHERE product_id = NEW.product_id; INSERT INTO order_items (order_id, product_id, quantity) VALUES (1009, 6, 10);SELECT product_id, product_name, stock FROM products WHERE product_id = 6;
ResultMySQL 8.4
mysql> CREATE TRIGGER trg_order_items_ai
Query OK, 0 rows affected
mysql> INSERT INTO order_items (order_id, product_id, quantity) VALUES (1009,…
Query OK, 1 row affected
mysql> SELECT product_id, product_name, stock FROM products WHERE product_id …
product_idproduct_namestock
6Notebook Pack290
1 row in set
How this query works, step by step
  1. The new line orders 10 notebook packs.
  2. The trigger subtracts 10 from product 6’s stock: 300 becomes 290.
  3. A matching AFTER DELETE trigger (adding OLD.quantity back) would complete the logic.

AFTER DELETE: keep a history

MySQL
CREATE TABLE deleted_customers AS SELECT * FROM customers WHERE 1 = 0;CREATE TRIGGER trg_customers_adAFTER DELETE ON customersFOR EACH ROWINSERT INTO deleted_customers VALUES (OLD.customer_id, OLD.customer_name, OLD.email, OLD.city, OLD.joined_on); DELETE FROM customers WHERE customer_id = 7;SELECT * FROM deleted_customers;
ResultMySQL 8.4
mysql> CREATE TABLE deleted_customers AS SELECT * FROM customers WHERE 1 = 0
Query OK, 0 rows affected
mysql> CREATE TRIGGER trg_customers_ad
Query OK, 0 rows affected
mysql> DELETE FROM customers WHERE customer_id = 7
Query OK, 1 row affected
mysql> SELECT * FROM deleted_customers
customer_idcustomer_nameemailcityjoined_on
7Sara Thomassara.t@example.comBengaluru2025-03-02
1 row in set

WHERE 1 = 0 copies the table’s columns without any rows.

Manage triggers

TaskStatement
ListSHOW TRIGGERS FROM company;
See the codeSHOW CREATE TRIGGER trg_salary_audit;
RemoveDROP TRIGGER IF EXISTS trg_salary_audit;

Pitfalls

  • Hidden logic: someone reading INSERT INTO order_items cannot see that stock changes. Document triggers well.
  • Performance: triggers run for every row; a bulk update of a million rows runs the trigger a million times.
  • Not fired by TRUNCATE, DROP TABLE or changes made by foreign-key CASCADE actions.
  • A trigger cannot modify the same table it is defined on (other than NEW values in BEFORE triggers).

Quick quiz

1. Which row values are available in a DELETE trigger?

2. Where can you change NEW values?

3. An UPDATE changes 50 rows. How often does a FOR EACH ROW trigger run?

4. How does a trigger reject a change?

Triggers FAQ

What is a trigger in SQL?

A stored program that the database executes automatically before or after an INSERT, UPDATE or DELETE on a table, once for each affected row.

What is the difference between BEFORE and AFTER triggers?

A BEFORE trigger runs before the row is written and can change or reject it. An AFTER trigger runs after the change and is typically used to update other tables or write logs.

What are NEW and OLD in MySQL triggers?

NEW holds the row’s values after an INSERT or UPDATE; OLD holds the values before an UPDATE or DELETE.

Can a trigger call a stored procedure?

Yes, with CALL, as long as the procedure does not return result sets or use transaction statements.