Module F-3·22 min read

WHERE, ORDER BY, LIMIT, OFFSET, string functions, and date arithmetic — the tools you will use on every single query.

F-3 — Filtering, Sorting, and Finding What You Need

Who this module is for: You completed F-2, can create tables and perform basic INSERT/SELECT/UPDATE/DELETE. Now you need to find specific rows, sort results, and use the built-in functions that every SQL developer relies on daily. This module covers the tools you will use in almost every query you ever write.


The WHERE Clause — Your Primary Filter

WHERE filters rows before they are returned. Only rows where the condition is TRUE are included.

sql

BETWEEN — Range Check

sql

BETWEEN is inclusive — it includes the boundary values.

IN — Match Any Value in a List

sql

LIKE and ILIKE — Pattern Matching

LIKE matches a pattern where % means "any sequence of characters" and _ means "any single character".

sql

LIKE is case-sensitive. ILIKE is the PostgreSQL extension for case-insensitive matching:

sql

IS NULL and IS NOT NULL

sql

Combining Conditions — AND, OR, NOT

sql

Parentheses Are Critical

Without parentheses, AND has higher precedence than OR — this catches many beginners:

sql

Rule: when mixing AND and OR, always use parentheses to make intent explicit.


ORDER BY — Sorting Results

Without ORDER BY, PostgreSQL returns rows in no guaranteed order. Do not assume the order will be consistent between queries.

sql

NULLS FIRST and NULLS LAST

When sorting a column that contains NULL, you control where nulls appear:

sql

By default in PostgreSQL: NULL sorts last with ASC, first with DESC.


LIMIT and OFFSET — Pagination

sql

⚠️ The performance trap with large OFFSET:

OFFSET 1000 means PostgreSQL reads 1000 rows and discards them before returning your 5. At large offsets, this gets slow. For pagination in real applications, use cursor-based pagination instead (covered in later modules).

Always use ORDER BY with LIMIT — without it, the rows you get are unpredictable and vary between runs.


DISTINCT — Remove Duplicate Rows

sql

DISTINCT is applied across all selected columns — it removes rows where all selected columns are identical.


String Functions

These are the functions you will reach for constantly:

sql

Real-world string examples

sql

Date and Time Functions

sql

Date arithmetic

sql

Type Casting — Converting Between Types

Sometimes you need to explicitly convert a value from one type to another:

sql

Practical Exercise: Product Search Queries

Using the products table from F-2, answer these questions with SQL:

sql

Putting It All Together

A query can chain all of these together. The order of clauses is:

sql
sql

Summary

ConceptWhat it does
WHERE col = valExact match filter
WHERE col BETWEEN a AND bRange filter (inclusive)
WHERE col IN (a, b, c)Match any value in list
WHERE col LIKE '%pattern%'Case-sensitive pattern match
WHERE col ILIKE '%pattern%'Case-insensitive pattern match
WHERE col IS NULLCheck for missing value
AND / OR / NOTCombine conditions
ORDER BY col ASC/DESCSort results
LIMIT nReturn at most n rows
OFFSET nSkip first n rows
DISTINCTRemove duplicate result rows
UPPER() / LOWER()Change string case
LENGTH()String length
TRIM()Remove whitespace
CONCAT() / ||Join strings
NOW()Current timestamp
EXTRACT()Get part of a date/time
DATE_TRUNC()Round down to precision
INTERVALDuration arithmetic
::typeType casting

Module F-4 covers aggregation — GROUP BY, HAVING, COUNT, SUM, AVG, and the mental model that trips up almost every beginner learning SQL.

Next: F-4 — Aggregation — Summarising Your Data →


Knowledge Check

Question 1: A developer writes the following SQL query to find products that are either out of stock (in_stock = false) OR have no description, AND are priced under $50:

sql

Which of the following statements accurately describes the issues with this query and how to correctly achieve the intended filtering?

  • A) The condition description = NULL is incorrect; it should be description IS NULL. Additionally, due to operator precedence, the AND clause binds tighter than OR, requiring parentheses around the OR conditions to group them correctly.
  • B) The condition description = NULL is correct for checking nulls. The primary issue is that OR has higher precedence than AND, leading to (in_stock = false OR description = NULL) being evaluated first.
  • C) The condition description = NULL is incorrect; it should be description IS NULL. However, parentheses are not needed because AND naturally groups description IS NULL with price < 50 before OR is applied.
  • D) The query's logic is fundamentally flawed because NULL values cannot be combined with AND or OR operators, requiring separate WHERE clauses for each condition.
Reveal Answer

Correct Answer: A

Option A is correct. The lesson explicitly states that description = NULL does not work and returns 0 rows; the correct syntax is description IS NULL. Furthermore, SQL's operator precedence dictates that AND binds tighter than OR. Without parentheses, the query in_stock = false OR description = NULL AND price < 50 is interpreted as in_stock = false OR (description = NULL AND price < 50). To achieve the intended logic of (out of stock OR no description) AND under $50, parentheses are critical: (in_stock = false OR description IS NULL) AND price < 50;. Option B is incorrect because description = NULL is wrong and AND has higher precedence than OR. Option C is incorrect because parentheses are needed to override AND's higher precedence. Option D is incorrect because NULL values can be combined with AND/OR using IS NULL/IS NOT NULL.


Question 2: A high-traffic e-commerce platform uses PostgreSQL for its product catalog. The product listing page implements pagination using LIMIT and OFFSET to display 20 products per page. The query structure is SELECT id, name, price FROM products ORDER BY created_at DESC LIMIT 20 OFFSET X;. As the platform scales to millions of products, users report that navigating to deeper pages (e.g., page 5000, where OFFSET would be 99980) becomes progressively slower, eventually leading to timeouts.

Which of the following statements best explains this performance degradation and suggests the most appropriate architectural solution for production-grade pagination?

  • A) The ORDER BY created_at DESC clause is the primary bottleneck because sorting millions of rows is inherently slow. Removing ORDER BY would resolve the performance issue, as LIMIT and OFFSET are optimized for speed.
  • B) The OFFSET clause forces the database to scan and discard a large number of rows before retrieving the desired subset. The recommended solution is cursor-based pagination, where the WHERE clause filters based on the last seen created_at value from the previous page.
  • C) The LIMIT clause itself is inefficient when combined with large datasets, as it requires a full table scan to determine the total number of rows before limiting. A better approach is to pre-calculate page numbers and store them in a separate index.
  • D) The issue is likely due to insufficient memory allocated to PostgreSQL for caching intermediate results of the OFFSET operation. Increasing work_mem and shared_buffers would mitigate the problem, allowing OFFSET to perform efficiently even at deep pages.
Reveal Answer

Correct Answer: B

Option B is correct. The lesson explicitly highlights the 'performance trap with large OFFSET', explaining that OFFSET N requires PostgreSQL to read and discard N rows before returning the requested LIMIT amount. This becomes extremely inefficient for deep pagination. The recommended solution for real-world applications is cursor-based pagination (also known as keyset pagination), which involves filtering results using a WHERE clause based on the values of the last row from the previous page (e.g., WHERE created_at < 'last_seen_timestamp' ORDER BY created_at DESC LIMIT 20). Option A is incorrect because ORDER BY is essential for consistent pagination results, and while sorting can be expensive, the primary issue described is the OFFSET's discarding behavior. Option C is incorrect; LIMIT does not require a full table scan to determine total rows, and pre-calculating page numbers is not a standard or scalable pagination strategy. Option D is incorrect; while memory tuning can help general query performance, it does not fundamentally address the algorithmic inefficiency of OFFSET discarding rows.


Question 3: A data analytics team needs to generate a report showing all unique product categories that have had at least one product added in the last 90 days. Additionally, they want to exclude any categories whose name (case-insensitive) contains the word "legacy". The final list of categories should be sorted alphabetically.

Given the products table with category (TEXT) and created_at (TIMESTAMPTZ) columns, which of the following SQL queries correctly fulfills these requirements?

  • A) ```sql SELECT DISTINCT category FROM products WHERE created_at > NOW() - INTERVAL '90 days' AND category NOT LIKE '%legacy%' ORDER BY category ASC;
text
  • C) ```sql SELECT DISTINCT category FROM products WHERE created_at > NOW() - INTERVAL '90 days' AND category NOT ILIKE '%legacy%' ORDER BY category ASC;
text
Reveal Answer

Correct Answer: C

Option C is correct. It correctly uses DISTINCT category to get unique categories. The date filter created_at > NOW() - INTERVAL '90 days' accurately identifies products created in the last 90 days. Most importantly, category NOT ILIKE '%legacy%' correctly performs a case-insensitive pattern match to exclude categories containing 'legacy', as ILIKE is the PostgreSQL-specific extension for case-insensitive matching, as taught in the lesson. Finally, ORDER BY category ASC sorts the results alphabetically. Option A is incorrect because LIKE is case-sensitive, violating the 'case-insensitive' requirement. Option B achieves case-insensitivity using LOWER(category) NOT LIKE '%legacy%', which is functionally correct but ILIKE (as used in C) is the more idiomatic and often more performant way in PostgreSQL for this specific task. Option D is incorrect because while GROUP BY category would also yield unique categories, DISTINCT is the more direct and appropriate clause for simply selecting unique values of columns, especially when no aggregation is involved. GROUP BY is typically used with aggregate functions, which are covered in the next module.

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.