SQL Queries - Filtering, Sorting, and Aggregating
The SELECT Statement
The SELECT statement is the heart of SQL querying. Let's master it from basics to advanced.
Basic SELECT
-- All columns
SELECT * FROM Products;
-- Specific columns
SELECT ProductName, Price FROM Products;
-- With alias
SELECT
ProductName AS Name,
Price AS Cost
FROM Products;
-- Calculated columns
SELECT
ProductName,
Price,
Price * 1.2 AS PriceWithTax
FROM Products;
WHERE Clause - Filtering Data
Comparison Operators
-- Equal
SELECT * FROM Products WHERE Price = 19.99;
-- Not equal
SELECT * FROM Products WHERE Price <> 19.99;
SELECT * FROM Products WHERE Price != 19.99; -- Same thing
-- Greater than / Less than
SELECT * FROM Products WHERE Price > 50;
SELECT * FROM Products WHERE Price < 100;
SELECT * FROM Products WHERE Price >= 50;
SELECT * FROM Products WHERE Price <= 100;
-- Between
SELECT * FROM Products
WHERE Price BETWEEN 10 AND 50;
Logical Operators
-- AND
SELECT * FROM Products
WHERE Price > 10 AND Stock > 0;
-- OR
SELECT * FROM Products
WHERE CategoryID = 1 OR CategoryID = 2;
-- NOT
SELECT * FROM Products
WHERE NOT (Price > 100);
-- Combination
SELECT * FROM Products
WHERE (Price < 20 OR Price > 100)
AND Stock > 0;
IN Operator
-- Instead of multiple ORs
SELECT * FROM Products
WHERE CategoryID IN (1, 2, 3, 5);
-- NOT IN
SELECT * FROM Products
WHERE CategoryID NOT IN (4, 6);
-- With subquery
SELECT * FROM Products
WHERE CategoryID IN (
SELECT CategoryID
FROM Categories
WHERE IsActive = 1
);
LIKE Operator - Pattern Matching
-- Starts with 'Pro'
SELECT * FROM Products
WHERE ProductName LIKE 'Pro%';
-- Ends with 'er'
SELECT * FROM Products
WHERE ProductName LIKE '%er';
-- Contains 'phone'
SELECT * FROM Products
WHERE ProductName LIKE '%phone%';
-- Second character is 'r'
SELECT * FROM Products
WHERE ProductName LIKE '_r%';
-- Exactly 5 characters
SELECT * FROM Products
WHERE SKU LIKE '_____';
-- Case-insensitive by default in most collations
SELECT * FROM Products
WHERE ProductName LIKE '%PHONE%'; -- Matches 'phone', 'Phone', 'PHONE'
NULL Handling
-- Check for NULL
SELECT * FROM Products
WHERE Description IS NULL;
-- Check for NOT NULL
SELECT * FROM Products
WHERE Description IS NOT NULL;
-- COALESCE - return first non-null value
SELECT
ProductName,
COALESCE(Description, 'No description available') AS Description
FROM Products;
-- ISNULL - T-SQL specific
SELECT
ProductName,
ISNULL(Description, 'No description') AS Description
FROM Products;
ORDER BY - Sorting Results
-- Ascending (default)
SELECT * FROM Products
ORDER BY Price;
SELECT * FROM Products
ORDER BY Price ASC;
-- Descending
SELECT * FROM Products
ORDER BY Price DESC;
-- Multiple columns
SELECT * FROM Products
ORDER BY CategoryID ASC, Price DESC;
-- By column position (avoid in production)
SELECT ProductName, Price FROM Products
ORDER BY 2 DESC; -- Orders by Price (2nd column)
-- By expression
SELECT * FROM Products
ORDER BY LEN(ProductName) DESC;
-- With CASE
SELECT * FROM Products
ORDER BY
CASE CategoryID
WHEN 1 THEN 1
WHEN 5 THEN 2
ELSE 3
END;
DISTINCT - Unique Values
-- Unique categories
SELECT DISTINCT CategoryID
FROM Products;
-- Unique combinations
SELECT DISTINCT CategoryID, SupplierID
FROM Products;
-- Count unique values
SELECT COUNT(DISTINCT CategoryID) AS UniqueCategoriesCount
FROM Products;
TOP - Limiting Results
-- First 10 rows
SELECT TOP 10 * FROM Products;
-- First 10 with ORDER BY
SELECT TOP 10 * FROM Products
ORDER BY Price DESC;
-- Top 5 percent
SELECT TOP 5 PERCENT * FROM Products
ORDER BY Price DESC;
-- With TIES (includes ties for last position)
SELECT TOP 10 WITH TIES * FROM Products
ORDER BY Price DESC;
OFFSET and FETCH - Pagination
-- Skip first 10, get next 10 (rows 11-20)
SELECT * FROM Products
ORDER BY ProductID
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;
-- Page 1 (rows 1-20)
SELECT * FROM Products
ORDER BY ProductID
OFFSET 0 ROWS
FETCH NEXT 20 ROWS ONLY;
-- Page 2 (rows 21-40)
SELECT * FROM Products
ORDER BY ProductID
OFFSET 20 ROWS
FETCH NEXT 20 ROWS ONLY;
-- Page 3 (rows 41-60)
SELECT * FROM Products
ORDER BY ProductID
OFFSET 40 ROWS
FETCH NEXT 20 ROWS ONLY;
Aggregate Functions
Basic Aggregates
-- COUNT
SELECT COUNT(*) AS TotalProducts FROM Products;
SELECT COUNT(Description) AS ProductsWithDescription FROM Products;
SELECT COUNT(DISTINCT CategoryID) AS UniqueCategories FROM Products;
-- SUM
SELECT SUM(Price) AS TotalValue FROM Products;
SELECT SUM(Stock * Price) AS TotalInventoryValue FROM Products;
-- AVG
SELECT AVG(Price) AS AveragePrice FROM Products;
-- MIN and MAX
SELECT MIN(Price) AS CheapestProduct FROM Products;
SELECT MAX(Price) AS MostExpensive FROM Products;
-- Multiple aggregates
SELECT
COUNT(*) AS TotalProducts,
AVG(Price) AS AvgPrice,
MIN(Price) AS MinPrice,
MAX(Price) AS MaxPrice,
SUM(Stock) AS TotalStock
FROM Products;
GROUP BY
-- Group by single column
SELECT
CategoryID,
COUNT(*) AS ProductCount,
AVG(Price) AS AvgPrice
FROM Products
GROUP BY CategoryID;
-- Group by multiple columns
SELECT
CategoryID,
SupplierID,
COUNT(*) AS ProductCount
FROM Products
GROUP BY CategoryID, SupplierID;
-- With ORDER BY
SELECT
CategoryID,
COUNT(*) AS ProductCount
FROM Products
GROUP BY CategoryID
ORDER BY ProductCount DESC;
HAVING - Filtering Groups
-- Categories with more than 5 products
SELECT
CategoryID,
COUNT(*) AS ProductCount
FROM Products
GROUP BY CategoryID
HAVING COUNT(*) > 5;
-- Categories where average price > 50
SELECT
CategoryID,
AVG(Price) AS AvgPrice
FROM Products
GROUP BY CategoryID
HAVING AVG(Price) > 50;
-- Combination of WHERE and HAVING
SELECT
CategoryID,
COUNT(*) AS ProductCount
FROM Products
WHERE IsActive = 1 -- Filter before grouping
GROUP BY CategoryID
HAVING COUNT(*) > 5 -- Filter after grouping
ORDER BY ProductCount DESC;
String Functions
-- Concatenation
SELECT
FirstName + ' ' + LastName AS FullName
FROM Users;
-- CONCAT (handles NULL better)
SELECT
CONCAT(FirstName, ' ', LastName) AS FullName
FROM Users;
-- CONCAT_WS (with separator)
SELECT
CONCAT_WS(' ', FirstName, MiddleName, LastName) AS FullName
FROM Users;
-- UPPER and LOWER
SELECT UPPER(ProductName) FROM Products;
SELECT LOWER(ProductName) FROM Products;
-- LEN and DATALENGTH
SELECT LEN(ProductName) FROM Products;
-- SUBSTRING
SELECT SUBSTRING(ProductName, 1, 10) AS ShortName FROM Products;
-- LEFT and RIGHT
SELECT LEFT(ProductName, 5) FROM Products;
SELECT RIGHT(ProductName, 5) FROM Products;
-- TRIM, LTRIM, RTRIM
SELECT TRIM(' Hello ') AS Trimmed;
SELECT LTRIM(' Hello') AS LeftTrim;
SELECT RTRIM('Hello ') AS RightTrim;
-- REPLACE
SELECT REPLACE(ProductName, 'old', 'new') FROM Products;
-- CHARINDEX (find position)
SELECT CHARINDEX('phone', ProductName) FROM Products;
Date Functions
-- Current date/time
SELECT GETDATE() AS CurrentDateTime;
SELECT SYSDATETIME() AS CurrentDateTime2; -- More precise
-- Date parts
SELECT
YEAR(OrderDate) AS Year,
MONTH(OrderDate) AS Month,
DAY(OrderDate) AS Day
FROM Orders;
-- DATEPART
SELECT DATEPART(QUARTER, OrderDate) AS Quarter FROM Orders;
SELECT DATEPART(WEEK, OrderDate) AS WeekNumber FROM Orders;
SELECT DATEPART(WEEKDAY, OrderDate) AS DayOfWeek FROM Orders;
-- DATENAME
SELECT DATENAME(MONTH, OrderDate) AS MonthName FROM Orders;
SELECT DATENAME(WEEKDAY, OrderDate) AS DayName FROM Orders;
-- DATEADD (add interval)
SELECT DATEADD(DAY, 7, OrderDate) AS DueDatePlus7 FROM Orders;
SELECT DATEADD(MONTH, 1, GETDATE()) AS NextMonth;
SELECT DATEADD(YEAR, -1, GETDATE()) AS LastYear;
-- DATEDIFF (difference between dates)
SELECT DATEDIFF(DAY, OrderDate, ShipDate) AS DaysToShip FROM Orders;
SELECT DATEDIFF(HOUR, CreatedDate, GETDATE()) AS HoursSinceCreation FROM Users;
-- Format dates
SELECT FORMAT(OrderDate, 'yyyy-MM-dd') AS ISODate FROM Orders;
SELECT FORMAT(OrderDate, 'MMMM dd, yyyy') AS FriendlyDate FROM Orders;
SELECT CONVERT(VARCHAR, OrderDate, 101) AS USDate FROM Orders; -- MM/DD/YYYY
Conditional Logic
CASE Statement
-- Simple CASE
SELECT
ProductName,
Price,
CASE
WHEN Price < 20 THEN 'Budget'
WHEN Price < 50 THEN 'Mid-Range'
WHEN Price < 100 THEN 'Premium'
ELSE 'Luxury'
END AS PriceCategory
FROM Products;
-- CASE in aggregate
SELECT
CategoryID,
SUM(CASE WHEN Stock > 0 THEN 1 ELSE 0 END) AS InStock,
SUM(CASE WHEN Stock = 0 THEN 1 ELSE 0 END) AS OutOfStock
FROM Products
GROUP BY CategoryID;
-- CASE in WHERE
SELECT * FROM Products
WHERE
CASE
WHEN CategoryID = 1 THEN Price > 50
WHEN CategoryID = 2 THEN Price > 30
ELSE Price > 20
END = 1;
IIF (Inline IF)
-- T-SQL shorthand for simple CASE
SELECT
ProductName,
IIF(Stock > 0, 'Available', 'Out of Stock') AS Availability
FROM Products;
-- Nested IIF
SELECT
ProductName,
IIF(Price < 20, 'Cheap',
IIF(Price < 50, 'Moderate', 'Expensive')
) AS PriceRange
FROM Products;
Subqueries
-- In WHERE clause
SELECT ProductName, Price
FROM Products
WHERE Price > (SELECT AVG(Price) FROM Products);
-- In SELECT clause
SELECT
ProductName,
Price,
(SELECT AVG(Price) FROM Products) AS AvgPrice,
Price - (SELECT AVG(Price) FROM Products) AS DiffFromAvg
FROM Products;
-- Correlated subquery
SELECT
p1.ProductName,
p1.Price,
(
SELECT AVG(p2.Price)
FROM Products p2
WHERE p2.CategoryID = p1.CategoryID
) AS CategoryAvgPrice
FROM Products p1;
-- EXISTS
SELECT * FROM Categories c
WHERE EXISTS (
SELECT 1 FROM Products p
WHERE p.CategoryID = c.CategoryID
);
-- NOT EXISTS
SELECT * FROM Categories c
WHERE NOT EXISTS (
SELECT 1 FROM Products p
WHERE p.CategoryID = c.CategoryID
);
Common Table Expressions (CTE)
-- Basic CTE
WITH HighPriceProducts AS (
SELECT *
FROM Products
WHERE Price > 100
)
SELECT * FROM HighPriceProducts
WHERE Stock > 0;
-- Multiple CTEs
WITH
AvgPrices AS (
SELECT CategoryID, AVG(Price) AS AvgPrice
FROM Products
GROUP BY CategoryID
),
ProductsWithAvg AS (
SELECT
p.ProductName,
p.Price,
ap.AvgPrice
FROM Products p
INNER JOIN AvgPrices ap ON p.CategoryID = ap.CategoryID
)
SELECT * FROM ProductsWithAvg
WHERE Price > AvgPrice;
Performance Tips
-- Use column names instead of *
SELECT ProductID, ProductName, Price FROM Products;
-- Filter early with WHERE
SELECT * FROM Products
WHERE IsActive = 1; -- Filter first
-- Use appropriate indexes
CREATE INDEX IX_Products_Price ON Products(Price);
-- Avoid functions on indexed columns in WHERE
-- Bad
SELECT * FROM Products WHERE YEAR(CreatedDate) = 2025;
-- Good
SELECT * FROM Products
WHERE CreatedDate >= '2025-01-01'
AND CreatedDate < '2026-01-01';
-- Use EXISTS instead of COUNT
-- Bad
IF (SELECT COUNT(*) FROM Products WHERE ProductID = 1) > 0
-- Good
IF EXISTS (SELECT 1 FROM Products WHERE ProductID = 1)
Next Topics
This covers querying fundamentals. Next articles will explore: - JOINs: Combining data from multiple tables - Reporting: Advanced aggregations and pivots - Optimization: Execution plans and indexing strategies
Practice these concepts - they're the foundation for all SQL work!