SQL Views - Virtual Tables
Learning Objectives
- Understand views as virtual tables
- Create and manage simple and complex views
- Implement updatable views with restrictions
- Use views for security and access control
- Apply views for data abstraction
- Understand materialized views concepts
- Optimize view performance
- Handle view dependencies and maintenance
Understanding Database Views
Views are virtual tables that present data from one or more tables through a predefined query. They simplify complex queries and enhance security! 🔍
View Key Concepts
Creating and Managing Views
graph TB
VIEW[Database Views] --> SIMPLE[Simple Views]
VIEW --> COMPLEX[Complex Views]
VIEW --> UPDATE[Updatable Views]
VIEW --> MAT[Materialized Views]
SIMPLE --> S1[Single table]
SIMPLE --> S2[No aggregates]
COMPLEX --> C1[Multiple tables]
COMPLEX --> C2[Aggregates/Groups]
UPDATE --> U1[Restrictions apply]
UPDATE --> U2[WITH CHECK OPTION]
MAT --> M1[Stored results]
MAT --> M2[Refresh needed]
style VIEW fill:#dcfce7
style SIMPLE fill:#dbeafe
style COMPLEX fill:#fef3c7
style UPDATE fill:#fee2e2
View Creation Examples
-- ========================================
-- SIMPLE VIEWS
-- ========================================
-- Basic view creation
CREATE VIEW active_customers AS
SELECT
customer_id,
customer_name,
email,
registration_date
FROM customers
WHERE is_active = TRUE;
-- Using the view
SELECT * FROM active_customers;
SELECT * FROM active_customers WHERE registration_date >= '2024-01-01';
-- View with column aliases
CREATE VIEW product_inventory AS
SELECT
p.product_id AS id,
p.product_name AS name,
p.price AS unit_price,
i.quantity_available AS stock,
p.price * i.quantity_available AS inventory_value
FROM products p
JOIN inventory i ON p.product_id = i.product_id;
-- ========================================
-- COMPLEX VIEWS
-- ========================================
-- View with aggregations
CREATE VIEW customer_statistics AS
SELECT
c.customer_id,
c.customer_name,
c.customer_segment,
COUNT(DISTINCT o.order_id) AS total_orders,
SUM(o.amount) AS lifetime_value,
AVG(o.amount) AS avg_order_value,
MAX(o.order_date) AS last_order_date,
DATEDIFF(CURDATE(), MAX(o.order_date)) AS days_since_last_order
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id;
-- Multi-table complex view
CREATE VIEW order_details_view AS
SELECT
o.order_id,
o.order_date,
c.customer_name,
c.email AS customer_email,
p.product_name,
oi.quantity,
oi.unit_price,
oi.quantity * oi.unit_price AS line_total,
o.status AS order_status
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id;
-- ========================================
-- REPLACE/ALTER VIEWS
-- ========================================
-- Replace existing view
CREATE OR REPLACE VIEW active_customers AS
SELECT
customer_id,
customer_name,
email,
phone, -- Added phone
registration_date,
last_login
FROM customers
WHERE is_active = TRUE
AND last_login >= DATE_SUB(CURDATE(), INTERVAL 90 DAY);
-- ALTER VIEW (MySQL 5.0.1+)
ALTER VIEW product_inventory AS
SELECT
p.product_id,
p.product_name,
p.category, -- Added category
p.price,
i.quantity_available,
i.reorder_level -- Added reorder level
FROM products p
JOIN inventory i ON p.product_id = i.product_id
WHERE p.is_discontinued = FALSE;
-- ========================================
-- VIEW METADATA
-- ========================================
-- Show all views in database
SHOW FULL TABLES WHERE Table_type = 'VIEW';
-- Show view definition
SHOW CREATE VIEW customer_statistics;
-- View information from information_schema
SELECT
TABLE_NAME,
VIEW_DEFINITION,
CHECK_OPTION,
IS_UPDATABLE,
DEFINER,
SECURITY_TYPE
FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = DATABASE();
-- Check view columns
DESCRIBE customer_statistics;
-- ========================================
-- DROP VIEWS
-- ========================================
-- Drop single view
DROP VIEW IF EXISTS customer_statistics;
-- Drop multiple views
DROP VIEW IF EXISTS view1, view2, view3;
Updatable Views
Working with Updatable Views
-- ========================================
-- UPDATABLE VIEW EXAMPLES
-- ========================================
-- Simple updatable view
CREATE VIEW editable_products AS
SELECT
product_id,
product_name,
price,
category,
is_active
FROM products
WHERE category = 'Electronics';
-- INSERT through view
INSERT INTO editable_products (product_name, price, category, is_active)
VALUES ('New Laptop', 999.99, 'Electronics', TRUE);
-- UPDATE through view
UPDATE editable_products
SET price = price * 1.1
WHERE product_id = 123;
-- DELETE through view
DELETE FROM editable_products
WHERE product_id = 456;
-- ========================================
-- WITH CHECK OPTION
-- ========================================
-- LOCAL check (only this view's WHERE)
CREATE VIEW premium_products AS
SELECT * FROM products
WHERE price > 1000
WITH LOCAL CHECK OPTION;
-- Cascaded check (all underlying views)
CREATE VIEW electronics_premium AS
SELECT * FROM premium_products
WHERE category = 'Electronics'
WITH CASCADED CHECK OPTION;
-- This INSERT will fail (price <= 1000)
-- INSERT INTO premium_products (product_name, price)
-- VALUES ('Cheap Item', 50.00); -- Error!
-- ========================================
-- VIEW WITH DEFAULT VALUES
-- ========================================
CREATE VIEW customer_orders AS
SELECT
o.order_id,
o.customer_id,
COALESCE(o.status, 'pending') AS status,
COALESCE(o.amount, 0) AS amount,
o.order_date
FROM orders o;
-- ========================================
-- INSTEAD OF TRIGGERS (Not in MySQL)
-- ========================================
-- MySQL doesn't support INSTEAD OF triggers
-- Alternative: Use stored procedures for complex updates
DELIMITER $$
CREATE PROCEDURE update_complex_view(
IN p_customer_id INT,
IN p_new_email VARCHAR(255),
IN p_new_phone VARCHAR(20)
)
BEGIN
-- Update customer table
UPDATE customers
SET email = p_new_email,
phone = p_new_phone
WHERE customer_id = p_customer_id;
-- Log the change
INSERT INTO audit_log (table_name, action, user, timestamp)
VALUES ('customers', 'UPDATE', USER(), NOW());
END$$
DELIMITER ;
-- ========================================
-- CHECK VIEW UPDATABILITY
-- ========================================
-- Check if view is updatable
SELECT
TABLE_NAME,
IS_UPDATABLE,
CHECK_OPTION
FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'editable_products';
-- Test updatability
-- Try to update and check for errors
BEGIN;
UPDATE editable_products SET price = 100 WHERE product_id = 1;
-- If no error, view is updatable
ROLLBACK;
Views for Security and Access Control
Security Through Views
Security Implementation with Views
-- ========================================
-- COLUMN-LEVEL SECURITY
-- ========================================
-- Hide sensitive columns
CREATE VIEW employee_public AS
SELECT
employee_id,
first_name,
last_name,
department,
job_title
-- Excludes: salary, ssn, birth_date, personal info
FROM employees;
-- Grant access to view, not table
GRANT SELECT ON employee_public TO 'app_user'@'localhost';
-- Don't grant on employees table
-- ========================================
-- ROW-LEVEL SECURITY
-- ========================================
-- Department-specific views
CREATE VIEW sales_team_view AS
SELECT * FROM employees
WHERE department = 'Sales';
CREATE VIEW hr_employee_view AS
SELECT * FROM employees
WHERE department = 'HR'
OR employee_id = SUBSTRING_INDEX(USER(), '@', 1);
-- Regional data access
CREATE VIEW regional_sales_west AS
SELECT * FROM sales
WHERE region = 'West';
-- ========================================
-- DATA MASKING
-- ========================================
CREATE VIEW customer_masked AS
SELECT
customer_id,
CONCAT(LEFT(customer_name, 1), REPEAT('*', LENGTH(customer_name) - 2),
RIGHT(customer_name, 1)) AS customer_name,
CONCAT(LEFT(email, 3), '***@***',
SUBSTRING_INDEX(email, '.', -1)) AS email,
CONCAT('***-***-', RIGHT(phone, 4)) AS phone,
registration_date
FROM customers;
-- ========================================
-- MULTI-TENANT SECURITY
-- ========================================
-- Create tenant-specific views
CREATE VIEW tenant_1_data AS
SELECT * FROM multi_tenant_table
WHERE tenant_id = 1;
CREATE VIEW tenant_2_data AS
SELECT * FROM multi_tenant_table
WHERE tenant_id = 2;
-- User can only see their tenant's data
GRANT SELECT ON tenant_1_data TO 'tenant1_user'@'localhost';
-- ========================================
-- AUDIT VIEWS
-- ========================================
CREATE VIEW user_activity_summary AS
SELECT
user_id,
COUNT(*) AS total_actions,
MAX(action_timestamp) AS last_activity,
COUNT(CASE WHEN action_type = 'LOGIN' THEN 1 END) AS login_count,
COUNT(CASE WHEN action_type = 'ERROR' THEN 1 END) AS error_count
FROM audit_log
GROUP BY user_id;
-- ========================================
-- DEFINER vs INVOKER Security
-- ========================================
-- DEFINER security (default) - runs with creator's privileges
CREATE DEFINER = 'admin'@'localhost'
SQL SECURITY DEFINER
VIEW sensitive_data AS
SELECT * FROM confidential_table;
-- INVOKER security - runs with caller's privileges
CREATE SQL SECURITY INVOKER
VIEW public_data AS
SELECT * FROM public_table;
-- ========================================
-- ROLE-BASED ACCESS VIEWS
-- ========================================
-- Manager view - sees team data
CREATE VIEW manager_dashboard AS
SELECT
e.employee_id,
e.employee_name,
e.performance_score,
d.department_budget,
d.department_goals
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.manager_id = (
SELECT employee_id
FROM employees
WHERE user_account = CURRENT_USER()
);
-- Executive view - sees aggregate data
CREATE VIEW executive_summary AS
SELECT
department,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary,
SUM(budget) AS total_budget
FROM departments d
JOIN employees e ON d.department_id = e.department_id
GROUP BY department;
Advanced View Patterns
Complex View Patterns and Optimization
-- ========================================
-- MATERIALIZED VIEW SIMULATION IN MySQL
-- ========================================
-- MySQL doesn't have native materialized views
-- Simulate with tables and triggers
-- Create materialized view table
CREATE TABLE mv_daily_sales_summary (
sale_date DATE PRIMARY KEY,
total_orders INT,
total_revenue DECIMAL(10,2),
unique_customers INT,
avg_order_value DECIMAL(10,2),
last_refreshed TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_revenue (total_revenue)
);
-- Refresh procedure
DELIMITER $$
CREATE PROCEDURE refresh_daily_sales_summary()
BEGIN
TRUNCATE TABLE mv_daily_sales_summary;
INSERT INTO mv_daily_sales_summary (
sale_date,
total_orders,
total_revenue,
unique_customers,
avg_order_value
)
SELECT
DATE(order_date) AS sale_date,
COUNT(*) AS total_orders,
SUM(amount) AS total_revenue,
COUNT(DISTINCT customer_id) AS unique_customers,
AVG(amount) AS avg_order_value
FROM orders
WHERE status = 'completed'
GROUP BY DATE(order_date);
END$$
DELIMITER ;
-- Schedule refresh (using events)
CREATE EVENT refresh_sales_summary
ON SCHEDULE EVERY 1 HOUR
DO CALL refresh_daily_sales_summary();
-- ========================================
-- NESTED VIEWS
-- ========================================
-- Base view
CREATE VIEW order_basics AS
SELECT
order_id,
customer_id,
order_date,
amount,
status
FROM orders;
-- View built on another view
CREATE VIEW recent_orders AS
SELECT * FROM order_basics
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);
-- Another level of nesting
CREATE VIEW recent_large_orders AS
SELECT * FROM recent_orders
WHERE amount > 1000;
-- ========================================
-- UNION VIEWS
-- ========================================
CREATE VIEW all_transactions AS
SELECT
'Order' AS transaction_type,
order_id AS transaction_id,
customer_id,
amount,
order_date AS transaction_date
FROM orders
WHERE status = 'completed'
UNION ALL
SELECT
'Refund' AS transaction_type,
refund_id AS transaction_id,
customer_id,
-amount AS amount,
refund_date AS transaction_date
FROM refunds
UNION ALL
SELECT
'Credit' AS transaction_type,
credit_id AS transaction_id,
customer_id,
amount,
credit_date AS transaction_date
FROM customer_credits;
-- ========================================
-- PARAMETERIZED VIEW WORKAROUND
-- ========================================
-- MySQL views don't support parameters
-- Use session variables as workaround
SET @customer_id = 123;
SET @start_date = '2024-01-01';
SET @end_date = '2024-12-31';
CREATE VIEW parameterized_orders AS
SELECT *
FROM orders
WHERE customer_id = @customer_id
AND order_date BETWEEN @start_date AND @end_date;
-- Better approach: Use stored procedure
DELIMITER $$
CREATE PROCEDURE get_customer_orders(
IN p_customer_id INT,
IN p_start_date DATE,
IN p_end_date DATE
)
BEGIN
SELECT *
FROM orders
WHERE customer_id = p_customer_id
AND order_date BETWEEN p_start_date AND p_end_date;
END$$
DELIMITER ;
-- ========================================
-- VIEW DEPENDENCIES
-- ========================================
-- Find all views that depend on a table
SELECT
TABLE_NAME AS view_name,
VIEW_DEFINITION
FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = DATABASE()
AND VIEW_DEFINITION LIKE '%products%';
-- Find view hierarchy
WITH RECURSIVE view_deps AS (
SELECT
TABLE_NAME AS view_name,
VIEW_DEFINITION,
1 AS level
FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = DATABASE()
AND VIEW_DEFINITION NOT LIKE '%information_schema.VIEWS%'
UNION ALL
SELECT
v.TABLE_NAME,
v.VIEW_DEFINITION,
vd.level + 1
FROM information_schema.VIEWS v
JOIN view_deps vd ON v.VIEW_DEFINITION LIKE CONCAT('%', vd.view_name, '%')
WHERE v.TABLE_SCHEMA = DATABASE()
AND vd.level < 5
)
SELECT DISTINCT view_name, level
FROM view_deps
ORDER BY level, view_name;
View Performance Optimization
Performance Considerations
-- ========================================
-- VIEW PERFORMANCE OPTIMIZATION
-- ========================================
-- Analyze view performance
EXPLAIN SELECT * FROM customer_statistics;
-- Create indexes for view performance
-- Identify columns used in view's WHERE/JOIN
CREATE INDEX idx_view_support
ON orders(customer_id, order_date, status);
-- ========================================
-- OPTIMIZE COMPLEX VIEWS
-- ========================================
-- Instead of complex view with many JOINs
-- CREATE VIEW complex_report AS
-- SELECT ... FROM t1 JOIN t2 JOIN t3 JOIN t4 ...
-- Use summary tables updated periodically
CREATE TABLE report_summary (
report_date DATE,
metric1 DECIMAL(10,2),
metric2 INT,
-- ... other columns
PRIMARY KEY (report_date)
);
-- Update via stored procedure or triggers
DELIMITER $$
CREATE TRIGGER update_report_summary
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
INSERT INTO report_summary (report_date, metric1, metric2)
VALUES (DATE(NEW.order_date), NEW.amount, 1)
ON DUPLICATE KEY UPDATE
metric1 = metric1 + NEW.amount,
metric2 = metric2 + 1;
END$$
DELIMITER ;
-- ========================================
-- VIEW CACHING STRATEGIES
-- ========================================
-- Use query cache (deprecated in MySQL 8.0)
-- For older versions:
SET GLOBAL query_cache_type = 1;
SET GLOBAL query_cache_size = 67108864; -- 64MB
-- Application-level caching
-- Cache view results in Redis/Memcached
-- ========================================
-- MONITOR VIEW USAGE
-- ========================================
-- Track slow queries involving views
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
-- Check view execution frequency
SELECT
SUBSTRING_INDEX(
SUBSTRING_INDEX(argument, 'FROM', -1),
' ', 2
) AS view_name,
COUNT(*) AS execution_count,
AVG(query_time) AS avg_time,
MAX(query_time) AS max_time
FROM mysql.general_log
WHERE argument LIKE '%FROM%VIEW%'
GROUP BY view_name;
Practice Exercise
View Challenges
💡 Click for Solutions
-- 1. Top customers with data masking
CREATE VIEW top_customers_masked AS
SELECT
customer_id,
CONCAT(LEFT(customer_name, 2), REPEAT('*', LENGTH(customer_name) - 4),
RIGHT(customer_name, 2)) AS customer_name,
CONCAT(LEFT(email, 3), '****@****.***') AS email,
total_orders,
total_revenue,
CASE
WHEN total_revenue > 10000 THEN 'Platinum'
WHEN total_revenue > 5000 THEN 'Gold'
WHEN total_revenue > 1000 THEN 'Silver'
ELSE 'Bronze'
END AS tier
FROM (
SELECT
c.customer_id,
c.customer_name,
c.email,
COUNT(o.order_id) AS total_orders,
SUM(o.amount) AS total_revenue
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'completed'
GROUP BY c.customer_id
ORDER BY total_revenue DESC
LIMIT 100
) AS customer_summary;
-- 2. Updatable product management view
CREATE VIEW product_management AS
SELECT
product_id,
sku,
product_name,
category,
price,
is_active,
last_updated
FROM products
WHERE is_active = TRUE
AND price > 0
WITH CHECK OPTION;
-- Test updates
UPDATE product_management
SET price = 99.99
WHERE product_id = 1;
-- 3. Role-based views
-- Admin view
CREATE VIEW admin_dashboard AS
SELECT
DATE(order_date) AS date,
COUNT(*) AS orders,
SUM(amount) AS revenue,
AVG(amount) AS avg_order,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
GROUP BY DATE(order_date);
-- Sales view
CREATE VIEW sales_dashboard AS
SELECT
product_name,
category,
SUM(quantity) AS units_sold,
SUM(quantity * unit_price) AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.product_id;
-- Customer service view
CREATE VIEW customer_service_view AS
SELECT
c.customer_id,
c.customer_name,
c.email,
COUNT(o.order_id) AS total_orders,
MAX(o.order_date) AS last_order,
COUNT(r.review_id) AS reviews_written
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
LEFT JOIN reviews r ON c.customer_id = r.user_id
GROUP BY c.customer_id;
-- 4. Complex reporting view
CREATE VIEW sales_report AS
SELECT
YEAR(o.order_date) AS year,
QUARTER(o.order_date) AS quarter,
MONTH(o.order_date) AS month,
p.category,
COUNT(DISTINCT o.order_id) AS order_count,
COUNT(DISTINCT o.customer_id) AS unique_customers,
SUM(oi.quantity) AS units_sold,
SUM(oi.quantity * oi.unit_price) AS gross_revenue,
SUM(oi.quantity * (oi.unit_price - p.cost)) AS gross_profit,
AVG(oi.unit_price) AS avg_selling_price
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.status = 'completed'
GROUP BY
YEAR(o.order_date),
QUARTER(o.order_date),
MONTH(o.order_date),
p.category;
-- 5. Materialized view simulation
-- Create table to store view results
CREATE TABLE mv_product_performance (
product_id INT PRIMARY KEY,
product_name VARCHAR(200),
total_quantity_sold INT,
total_revenue DECIMAL(10,2),
avg_rating DECIMAL(3,2),
review_count INT,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Procedure to refresh
DELIMITER $$
CREATE PROCEDURE refresh_product_performance()
BEGIN
TRUNCATE TABLE mv_product_performance;
INSERT INTO mv_product_performance
SELECT
p.product_id,
p.product_name,
COALESCE(SUM(oi.quantity), 0),
COALESCE(SUM(oi.quantity * oi.unit_price), 0),
COALESCE(AVG(r.rating), 0),
COUNT(DISTINCT r.review_id),
NOW()
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
LEFT JOIN reviews r ON p.product_id = r.product_id
GROUP BY p.product_id;
END$$
DELIMITER ;
-- 6. Nested views for hierarchy
CREATE VIEW category_base AS
SELECT
category_id,
category_name,
parent_category_id
FROM categories;
CREATE VIEW category_with_products AS
SELECT
c.*,
COUNT(p.product_id) AS product_count
FROM category_base c
LEFT JOIN products p ON c.category_id = p.category_id
GROUP BY c.category_id;
CREATE VIEW category_hierarchy AS
SELECT
c1.category_name AS main_category,
c2.category_name AS sub_category,
c2.product_count
FROM category_with_products c1
JOIN category_with_products c2 ON c1.category_id = c2.parent_category_id;
-- 7. Security view
CREATE VIEW employee_safe_view AS
SELECT
employee_id,
first_name,
last_name,
department,
hire_date,
CASE
WHEN USER() LIKE '%hr%' THEN salary
ELSE 'RESTRICTED'
END AS salary_info,
CASE
WHEN USER() LIKE '%hr%' THEN ssn
ELSE 'XXX-XX-' || RIGHT(ssn, 4)
END AS ssn_masked
FROM employees;
-- 8. Performance-optimized dashboard view
CREATE VIEW dashboard_metrics AS
SELECT
(SELECT COUNT(*) FROM orders WHERE DATE(order_date) = CURDATE()) AS orders_today,
(SELECT SUM(amount) FROM orders WHERE DATE(order_date) = CURDATE()) AS revenue_today,
(SELECT COUNT(*) FROM customers WHERE DATE(registration_date) = CURDATE()) AS new_customers_today,
(SELECT COUNT(*) FROM products WHERE is_active = TRUE) AS active_products,
(SELECT AVG(rating) FROM reviews WHERE DATE(created_at) = CURDATE()) AS avg_rating_today;