The Ultimate Guide to Learn SQL

So, you’ve decided to learn SQL? Welcome to the world of databases, where SELECT statements are your bread, JOINs are your butter, and window functions are the sprinkles on top.

Before we dive in, I’m assuming you’ve already set up your SQL environment (if not, Google is your best friend – it’s like the Hogwarts of tech tutorials). Now, let’s get you querying like a pro.

1. Start with the Basics: SELECT * FROM Awesomeness

Every SQL journey begins with SELECT *. It’s like saying “Hello, World!” but for databases. But here’s a pro tip: ditch the * early. Be specific. Your future self (and your database) will thank you.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT TOP 10 first_name, last_name
FROM users
WHERE signup_date > '2024-01-01'
ORDER BY last_name;
SELECT TOP 10 first_name, last_name FROM users WHERE signup_date > '2024-01-01' ORDER BY last_name;
SELECT TOP 10 first_name, last_name 
FROM users 
WHERE signup_date > '2024-01-01' 
ORDER BY last_name;

Learn these keywords like the back of your hand: SELECTFROMWHEREGROUP BYHAVING, and ORDER BY. And yes, be curious about the order SQL processes your queries. (Spoiler: It’s not the order you write them in.)

2. Aggregate Like a Boss

Counting rows is fun, but why stop there? Learn to SUM()AVG()MIN(), and MAX() your way through data. Pair these with GROUP BY and HAVING to slice and dice your data like a chef.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 50000;
SELECT department, AVG(salary) AS avg_salary FROM employees GROUP BY department HAVING AVG(salary) > 50000;
SELECT department, AVG(salary) AS avg_salary 
FROM employees 
GROUP BY department 
HAVING AVG(salary) > 50000;

3. JOIN the Party

Bring tables together with JOINs. Start with INNER JOIN, flirt with LEFT JOIN, and maybe even try the APPLY operator if you’re feeling adventurous.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT u.first_name, o.order_date
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id;
SELECT u.first_name, o.order_date FROM users u INNER JOIN orders o ON u.user_id = o.user_id;
SELECT u.first_name, o.order_date 
FROM users u 
INNER JOIN orders o ON u.user_id = o.user_id;

And hey, if you remember set theory from middle school, UNIONEXCEPT, and INTERSECT will feel like a reunion with an old friend.

4. Understand the Magic: Logical Query Processing

Ever wonder why your WHERE clause runs before your GROUP BY? Dive into logical query processing. It’s like learning how a magician does their tricks – prepare for a lot of “hmm” and “aha!” moments.

5. Window Functions: The Icing on the Cake

Window functions are where SQL gets fancy. Use them for running totals, rankings, and more.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT
employee_id,
department,
employee_name,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_salary_rank,
LAG(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS previous_salary,
LEAD(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS next_salary,
SUM(salary) OVER (PARTITION BY department) AS total_department_salary,
AVG(salary) OVER (PARTITION BY department) AS avg_department_salary,
NTILE(4) OVER (PARTITION BY department ORDER BY salary DESC) AS salary_quartile,
FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS highest_salary
FROM employees;
SELECT employee_id, department, employee_name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_salary_rank, LAG(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS previous_salary, LEAD(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS next_salary, SUM(salary) OVER (PARTITION BY department) AS total_department_salary, AVG(salary) OVER (PARTITION BY department) AS avg_department_salary, NTILE(4) OVER (PARTITION BY department ORDER BY salary DESC) AS salary_quartile, FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS highest_salary FROM employees;
SELECT
    employee_id,
    department,
    employee_name,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank,
    DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_salary_rank,
    LAG(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS previous_salary,
    LEAD(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS next_salary,
    SUM(salary) OVER (PARTITION BY department) AS total_department_salary,
    AVG(salary) OVER (PARTITION BY department) AS avg_department_salary,
    NTILE(4) OVER (PARTITION BY department ORDER BY salary DESC) AS salary_quartile,
    FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS highest_salary
FROM employees;

6. Subqueries & CTEs: Your New Best Friend

Subqueries are like nesting dolls – a query inside another query. Use them in WHEREFROM, or even SELECT clauses.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
-- Find users who spent more than the average.
SELECT user_id, total_spent
FROM orders
WHERE total_spent > (SELECT AVG(total_spent) FROM orders);
-- Find users who spent more than the average. SELECT user_id, total_spent FROM orders WHERE total_spent > (SELECT AVG(total_spent) FROM orders);
-- Find users who spent more than the average.
SELECT user_id, total_spent  
FROM orders  
WHERE total_spent > (SELECT AVG(total_spent) FROM orders);  

Common Table Expressions (CTEs) are like sticky notes for your SQL code. They make complex queries readable. 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
-- Calculate monthly sales with a CTE.
WITH monthly_sales AS (
SELECT MONTH(order_date) AS month, SUM(total) AS revenue
FROM orders
GROUP BY MONTH(order_date)
SELECT month, revenue
FROM monthly_sales
WHERE revenue > 10000;
-- Calculate monthly sales with a CTE. WITH monthly_sales AS ( SELECT MONTH(order_date) AS month, SUM(total) AS revenue FROM orders GROUP BY MONTH(order_date) SELECT month, revenue FROM monthly_sales WHERE revenue > 10000;
-- Calculate monthly sales with a CTE.
WITH monthly_sales AS (  
    SELECT MONTH(order_date) AS month, SUM(total) AS revenue  
    FROM orders  
    GROUP BY MONTH(order_date)  
SELECT month, revenue  
FROM monthly_sales  
WHERE revenue > 10000;  

7. SQL Functions Unplugged

String Functions: Making Your Text Dance

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT SUBSTR('Hello, world!', 1, 5) AS greeting,
CONCAT('Hello, ', 'world!') AS full_greeting,
REPLACE('twe t', ' ', 'e') AS sanitized_tweet,
TRIM(' Hello, world! ') AS cleaned_text,
LENGTH('Hello') AS string_length;
SELECT SUBSTR('Hello, world!', 1, 5) AS greeting, CONCAT('Hello, ', 'world!') AS full_greeting, REPLACE('twe t', ' ', 'e') AS sanitized_tweet, TRIM(' Hello, world! ') AS cleaned_text, LENGTH('Hello') AS string_length;
SELECT SUBSTR('Hello, world!', 1, 5) AS greeting,
       CONCAT('Hello, ', 'world!') AS full_greeting,
       REPLACE('twe t', ' ', 'e') AS sanitized_tweet,
       TRIM('   Hello, world!   ') AS cleaned_text,
       LENGTH('Hello') AS string_length;     

Date Functions: Taming the Time Machine

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT DATEADD(day, 7, GETDATE()) AS deadline,
DATEPART(month, GETDATE()) AS date_month,
EXTRACT(minute from GETDATE());
SELECT DATEADD(day, 7, GETDATE()) AS deadline, DATEPART(month, GETDATE()) AS date_month, EXTRACT(minute from GETDATE());
SELECT DATEADD(day, 7, GETDATE()) AS deadline,
       DATEPART(month, GETDATE()) AS date_month,
       EXTRACT(minute from GETDATE());

Conditional Logic: The SQL Decision-Maker

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT
CASE
WHEN salary > 100000 THEN 'Cha-ching!'
WHEN salary BETWEEN 50000 AND 100000 THEN 'Surviving'
ELSE 'Intern?'
END AS salary_mood,
COALESCE(nickname, first_name, 'Hey You') AS name
FROM employees;
SELECT CASE WHEN salary > 100000 THEN 'Cha-ching!' WHEN salary BETWEEN 50000 AND 100000 THEN 'Surviving' ELSE 'Intern?' END AS salary_mood, COALESCE(nickname, first_name, 'Hey You') AS name FROM employees;
SELECT  
  CASE  
    WHEN salary > 100000 THEN 'Cha-ching!'  
    WHEN salary BETWEEN 50000 AND 100000 THEN 'Surviving'  
    ELSE 'Intern?'  
  END AS salary_mood,
  COALESCE(nickname, first_name, 'Hey You') AS name
FROM employees;  

EXISTS: The Data Detective

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT order_id, customer_id
FROM orders
WHERE EXISTS (
SELECT 1 FROM customers
WHERE customers.id = orders.customer_id
);
SELECT order_id, customer_id FROM orders WHERE EXISTS ( SELECT 1 FROM customers WHERE customers.id = orders.customer_id );
SELECT order_id, customer_id
FROM orders
WHERE EXISTS (
    SELECT 1 FROM customers
    WHERE customers.id = orders.customer_id
);

Pivoting Data: Need to turn rows into columns (like an Excel pivot table)? Because stakeholders love pretty tables. Meet PIVOT

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
-- Show sales by product category per month.
SELECT *
FROM (
SELECT category, MONTH(order_date) AS month, total
FROM orders) AS src
PIVOT (SUM(total) FOR month IN ([1], [2], [3])) AS pvt;
-- Show sales by product category per month. SELECT * FROM ( SELECT category, MONTH(order_date) AS month, total FROM orders) AS src PIVOT (SUM(total) FOR month IN ([1], [2], [3])) AS pvt;
-- Show sales by product category per month.
SELECT *  
FROM (  
    SELECT category, MONTH(order_date) AS month, total  
    FROM orders) AS src  
PIVOT (SUM(total) FOR month IN ([1], [2], [3])) AS pvt;  

8. Stored Procedures & UDFs: SQL’s Reusable Code Blocks

Store your SQL scripts as reusable procedures. Think of them as LEGO blocks for databases. 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
-- Create a proc to fetch recent orders.
CREATE PROC GetRecentOrders @days INT
AS
SELECT *
FROM orders
WHERE order_date >= DATEADD(DAY, -@days, GETDATE());
-- Execute it
EXEC GetRecentOrders 7;
-- Create a proc to fetch recent orders. CREATE PROC GetRecentOrders @days INT AS SELECT * FROM orders WHERE order_date >= DATEADD(DAY, -@days, GETDATE()); -- Execute it EXEC GetRecentOrders 7;
-- Create a proc to fetch recent orders.
CREATE PROC GetRecentOrders @days INT  
AS  
SELECT *  
FROM orders  
WHERE order_date >= DATEADD(DAY, -@days, GETDATE());  

-- Execute it 
EXEC GetRecentOrders 7;

UDFs let you create reusable functions – like building your own Lego blocks. There are two types:

Scalar UDFs

Return a single value. Perfect for calculations or formatting.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
CREATE FUNCTION GetFormattedDate (@date DATE)
RETURNS NVARCHAR(20)
AS
BEGIN
RETURN FORMAT(@date, 'yyyy-MM-dd');
END;
-- Use it
SELECT dbo.GetFormattedDate(order_date) AS pretty_date
FROM orders;
CREATE FUNCTION GetFormattedDate (@date DATE) RETURNS NVARCHAR(20) AS BEGIN RETURN FORMAT(@date, 'yyyy-MM-dd'); END; -- Use it SELECT dbo.GetFormattedDate(order_date) AS pretty_date FROM orders;
CREATE FUNCTION GetFormattedDate (@date DATE)  
RETURNS NVARCHAR(20)  
AS  
BEGIN  
    RETURN FORMAT(@date, 'yyyy-MM-dd');  
END;  

-- Use it
SELECT dbo.GetFormattedDate(order_date) AS pretty_date  
FROM orders;  

Table-Valued UDFs

Return a table. Ideal for parameterized queries.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
CREATE FUNCTION GetHighSpenders (@threshold DECIMAL)
RETURNS TABLE
AS
RETURN
SELECT user_id, total_spent
FROM orders
WHERE total_spent >= @threshold;
-- Use it
SELECT * FROM GetHighSpenders(1000);
CREATE FUNCTION GetHighSpenders (@threshold DECIMAL) RETURNS TABLE AS RETURN SELECT user_id, total_spent FROM orders WHERE total_spent >= @threshold; -- Use it SELECT * FROM GetHighSpenders(1000);
CREATE FUNCTION GetHighSpenders (@threshold DECIMAL)  
RETURNS TABLE  
AS  
RETURN  
    SELECT user_id, total_spent  
    FROM orders  
    WHERE total_spent >= @threshold;  

-- Use it
SELECT * FROM GetHighSpenders(1000);  

Pro Tip: Use UDFs sparingly – they can slow down queries if overused.

9. Dynamic SQL: The Shape-Shifter

Dynamic SQL lets you build queries on the fly. It’s like being a mad scientist with strings.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
-- Filter orders by a dynamic column.
DECLARE @column NVARCHAR(50) = 'total';
DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @column + ' FROM orders';
EXEC sp_executesql @sql;
-- Filter orders by a dynamic column. DECLARE @column NVARCHAR(50) = 'total'; DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @column + ' FROM orders'; EXEC sp_executesql @sql;
-- Filter orders by a dynamic column.
DECLARE @column NVARCHAR(50) = 'total';  
DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @column + ' FROM orders';  
EXEC sp_executesql @sql;  

Handle with care: Great power = great responsibility (and potential SQL injection risks).

10. Create, Drop, and Modify Like a Wizard

Once you’re comfortable querying, it’s time to play god (or DBA). Create tables, drop them (carefully!), and insert, update, or delete data.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
CREATE TABLE pets (
pet_id INT PRIMARY KEY,
name VARCHAR(100),
species VARCHAR(50)
);
CREATE TABLE pets ( pet_id INT PRIMARY KEY, name VARCHAR(100), species VARCHAR(50) );
CREATE TABLE pets (
    pet_id INT PRIMARY KEY,
    name VARCHAR(100),
    species VARCHAR(50)
);

11. Views: Your Database’s Favorite Filter

Views are like virtual tables – they don’t store data but save you from writing the same query repeatedly. Think of them as your favorite playlist: curated, reusable, and easy to share.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
-- Create a view for active users.
CREATE VIEW ActiveUsers AS
SELECT user_id, first_name, last_name
FROM users
WHERE last_login_date >= '2024-01-01';
-- Now query it like a table
SELECT * FROM ActiveUsers;
-- Create a view for active users. CREATE VIEW ActiveUsers AS SELECT user_id, first_name, last_name FROM users WHERE last_login_date >= '2024-01-01'; -- Now query it like a table SELECT * FROM ActiveUsers;
-- Create a view for active users.
CREATE VIEW ActiveUsers AS  
SELECT user_id, first_name, last_name  
FROM users  
WHERE last_login_date >= '2024-01-01';  

-- Now query it like a table
SELECT * FROM ActiveUsers;  

Why? Views simplify complex queries and keep your code DRY (Don’t Repeat Yourself).

12. Indexes: The Secret Sauce for Speed

Indexes are your database’s GPS, they help it find data faster. But not all indexes are created equal.

Clustered Indexes

  • The physical order of data in a table. Only one per table (like a primary key).
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
CREATE CLUSTERED INDEX IX_Users_UserId
ON users(user_id);
CREATE CLUSTERED INDEX IX_Users_UserId ON users(user_id);
CREATE CLUSTERED INDEX IX_Users_UserId  
ON users(user_id);  

Non-Clustered Indexes

  • separate structure that points to data. Create multiple per table.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate
ON orders(order_date);
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate ON orders(order_date);
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate  
ON orders(order_date);  

Covering Indexes

  • Includes all columns needed for a query. No need to visit the actual table.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
CREATE NONCLUSTERED INDEX IX_Orders_Covering
ON orders(order_id)
INCLUDE (order_date, total);
CREATE NONCLUSTERED INDEX IX_Orders_Covering ON orders(order_id) INCLUDE (order_date, total);
CREATE NONCLUSTERED INDEX IX_Orders_Covering  
ON orders(order_id)  
INCLUDE (order_date, total);  

Pro Tips:

  • Don’t over-index: Too many indexes slow down writes (INSERT/UPDATE/DELETE).
  • Index what you search: Focus on columns in WHEREJOIN, or ORDER BY clauses.

13. SQL Statements: The Family Tree

SQL statements can be grouped into families:

  • DDL (Data Definition Language): CREATEALTERDROP
  • DML (Data Manipulation Language): INSERTUPDATEDELETESELECT
  • DCL (Data Control Language): GRANTREVOKE
  • TCL (Transaction Control Language): COMMITROLLBACK

Pro Tips for SQL Wizards

  1. Execution Plans Are Your Crystal Ball
    • Use SET SHOWPLAN_ALL ON; (SQL Server) or EXPLAIN (PostgreSQL) to see how your query runs.
    • Look for “table scans” (bad) vs. “index seeks” (good). If your query is taking a scenic route, add an index or rewrite the JOIN!

Final Words of Wisdom

SQL is like a Swiss Army knife for data. Whether you’re querying a small table or wrangling a data lake, these skills will serve you well. And remember, the best way to learn SQL is by doing. So, fire up your SQL environment and start querying!