How do I update existing data in a SQL table?

Data stored in a database rarely stays the same forever. Prices change, customers move, orders are shipped, and inventory goes up and down. When you need to modify rows that already exist in a table, SQL provides the UPDATE statement.

In this tutorial you will learn how to change the value of one or more columns in one or more rows using UPDATE, how to combine it safely with WHERE, how to use expressions and values from other tables, and — most importantly — how to avoid the single most common accident in SQL: updating every row in a table by mistake.

Prerequisites

To follow along, you need:

  • A working SQL database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).
  • A client where you can execute SQL statements, such as DBeaver, psql, mysql, or sqlite3.
  • Basic familiarity with SELECT and the WHERE clause. If you have not read the previous tutorial on filtering rows with WHERE, do that first — UPDATE relies on the same filtering rules.

If you have not created a sample database yet, the setup script in the next section will produce everything you need.

Sample Database

We continue with the small online bookstore schema introduced in earlier tutorials. It has two tables: authors and books.

CREATE TABLE authors (
    author_id   INTEGER      PRIMARY KEY,
    author_name VARCHAR(100) NOT NULL,
    country     VARCHAR(50)
);

CREATE TABLE books (
    book_id      INTEGER       PRIMARY KEY,
    title        VARCHAR(200)  NOT NULL,
    author_id    INTEGER       NOT NULL,
    category     VARCHAR(50),
    price        DECIMAL(8, 2) NOT NULL,
    stock        INTEGER       NOT NULL,
    published_on DATE,
    CONSTRAINT fk_books_author
        FOREIGN KEY (author_id) REFERENCES authors (author_id)
);

INSERT INTO authors (author_id, author_name, country) VALUES
    (1, 'Jane Austen',              'United Kingdom'),
    (2, 'Haruki Murakami',          'Japan'),
    (3, 'Chimamanda Ngozi Adichie', 'Nigeria'),
    (4, 'Gabriel Garcia Marquez',   'Colombia');

INSERT INTO books (book_id, title, author_id, category, price, stock, published_on) VALUES
    (1, 'Pride and Prejudice',           1, 'Classic',      12.50, 20, '1813-01-28'),
    (2, 'Emma',                          1, 'Classic',      10.00,  0, '1815-12-23'),
    (3, 'Norwegian Wood',                2, 'Fiction',      15.75, 12, '1987-09-04'),
    (4, 'Kafka on the Shore',            2, 'Fiction',      18.20,  5, '2002-09-12'),
    (5, 'Half of a Yellow Sun',          3, 'Historical',   16.00,  8, '2006-08-11'),
    (6, 'Americanah',                    3, 'Contemporary', 14.50,  3, '2013-05-14'),
    (7, 'One Hundred Years of Solitude', 4, 'Classic',      22.00, 25, '1967-05-30'),
    (8, 'Love in the Time of Cholera',   4, 'Classic',      19.99,  0, '1985-09-05'),
    (9, 'Unknown Title',                 2, NULL,           13.00,  4, NULL);

Basic Syntax

An UPDATE statement changes the values of specified columns in the rows that match a condition.

UPDATE table_name
SET column1 = value1,
    column2 = value2
WHERE condition;

Each clause has a specific role:

  • UPDATE table_name — the table whose rows will be modified.
  • SET column = value — the columns to change, and the new values or expressions. You can update several columns in a single statement by separating them with commas.
  • WHERE condition — restricts which rows are updated. If you omit WHERE, every row in the table is updated.

Warning: An UPDATE without a WHERE clause modifies every row in the table. Practice these statements only in a disposable learning database or after taking a verified backup.

Verify Before You Modify

Before running an UPDATE, always run a SELECT with the same WHERE clause first. This lets you see exactly which rows will be affected.

SELECT
    book_id,
    title,
    price
FROM books
WHERE book_id = 2;

Only after you have confirmed the target rows should you run the actual UPDATE.

Practical Example

Suppose the bookstore wants to correct the price of Emma (book id 2) to 11.00.

Step 1 — inspect the row:

SELECT
    book_id,
    title,
    price
FROM books
WHERE book_id = 2;
book_id title price
2 Emma 10.00

Step 2 — update the row:

UPDATE books
SET price = 11.00
WHERE book_id = 2;

Step 3 — verify the change:

SELECT
    book_id,
    title,
    price
FROM books
WHERE book_id = 2;

Expected Result

book_id title price
2 Emma 11.00

Most database clients also report the number of rows affected (for example, UPDATE 1), which is a useful sanity check.

Additional Examples

1. Updating Multiple Columns at Once

You can change several columns in one statement by listing them in the SET clause.

UPDATE books
SET price    = 20.00,
    category = 'Literary Classic'
WHERE book_id = 7;

This is more efficient than running two separate updates and guarantees that both changes happen together.

2. Using Expressions in SET

The new value does not have to be a constant. It can be any expression, including one that references the current value of the column.

Apply a 10% discount to every book in the 'Classic' category:

UPDATE books
SET price = price * 0.90
WHERE category = 'Classic';

Increase the stock of Kafka on the Shore by 3 copies:

UPDATE books
SET stock = stock + 3
WHERE book_id = 4;

The right-hand side of = sees the value before the update, so stock = stock + 3 means “new stock equals old stock plus three”.

3. Updating Many Rows That Share a Condition

UPDATE is not limited to a single row. Any WHERE condition that is valid in a SELECT is also valid here.

Mark every out-of-stock book as 'Fiction' no — better example — bump the price of every book published before the year 2000:

UPDATE books
SET price = price + 1.00
WHERE published_on < DATE '2000-01-01';

Before running it, check which rows will change:

SELECT
    book_id,
    title,
    published_on,
    price
FROM books
WHERE published_on < DATE '2000-01-01';

4. Setting a Column to NULL

To clear a value, assign NULL in the SET clause. Note the difference between the assignment = NULL (valid) and the comparison = NULL (invalid — you must use IS NULL for comparisons).

UPDATE books
SET category = NULL
WHERE book_id = 9;

The column must allow NULL (that is, it must not have a NOT NULL constraint) or the statement will fail.

5. Using a Value from Another Table

Sometimes the new value depends on data in a different table. A portable way to express this is a subquery in SET.

Suppose the authors table gains a discount_rate column, and you want every book to inherit its author’s discount. In ANSI-standard SQL you can write:

UPDATE books
SET price = price * (
    SELECT 1 - a.discount_rate
    FROM authors AS a
    WHERE a.author_id = books.author_id
)
WHERE EXISTS (
    SELECT 1
    FROM authors AS a
    WHERE a.author_id = books.author_id
);

PostgreSQL and SQL Server also support the more readable UPDATE ... FROM extension, and MySQL/MariaDB support a joined UPDATE. These are labeled in the Database Compatibility section below.

6. Limiting the Number of Rows Updated

Some databases allow you to cap the number of updated rows, which is useful for controlled batch updates.

MySQL, MariaDB, SQLite
UPDATE books
SET stock = stock + 1
WHERE category = 'Classic'
ORDER BY book_id
LIMIT 3;

PostgreSQL, SQL Server, and Oracle Database do not support LIMIT directly in UPDATE. On those systems you typically restrict the target rows through a subquery on the primary key.

7. Updating from Application Code

When the new value comes from user input, always use a parameterized query. This prevents SQL injection and lets the database reuse the prepared plan.

String sql = """
        UPDATE books
        SET price = ?
        WHERE book_id = ?
        """;

try (PreparedStatement statement = connection.prepareStatement(sql)) {
    statement.setBigDecimal(1, newPrice);
    statement.setLong(2, bookId);

    int updatedRows = statement.executeUpdate();
    if (updatedRows == 0) {
        // No row matched the WHERE clause — decide how to handle it.
    }
}

executeUpdate() returns the number of affected rows, which you should check when you expect exactly one row to change.

8. Wrapping Related Updates in a Transaction

When two or more updates must succeed together — or fail together — run them inside a transaction.

BEGIN;

UPDATE books
SET stock = stock - 1
WHERE book_id = 3;

UPDATE books
SET stock = stock + 1
WHERE book_id = 4;

COMMIT;

If something goes wrong between the two statements, ROLLBACK reverts both changes so the total stock across the two books stays consistent. Transaction behavior varies between database systems and storage engines; consult your database’s documentation for details.

Common Mistakes

Forgetting the WHERE Clause

Incorrect:

UPDATE books
SET price = 9.99;

This is syntactically valid, but it sets the price of every book in the table to 9.99. Always double-check that a WHERE clause is present before executing an UPDATE.

Comparing to NULL with =

Incorrect:

UPDATE books
SET category = 'Uncategorized'
WHERE category = NULL;

The expression category = NULL evaluates to unknown, never true, so this statement updates zero rows.

Correct:

UPDATE books
SET category = 'Uncategorized'
WHERE category IS NULL;

Assuming an UPDATE Always Affects a Row

If the WHERE condition matches nothing, UPDATE succeeds and reports zero rows affected. It does not raise an error. In application code, check the returned row count when you expect exactly one row to change.

Mixing AND and OR Without Parentheses

Incorrect:

UPDATE books
SET price = price * 0.90
WHERE category = 'Classic' OR category = 'Fiction'
  AND stock > 0;

Because AND binds tighter than OR, this discounts all classic books plus in-stock fiction books, which is probably not what you want.

Correct:

UPDATE books
SET price = price * 0.90
WHERE (category = 'Classic' OR category = 'Fiction')
  AND stock > 0;

Reading a Column Twice in One SET

Incorrect assumption:

UPDATE books
SET price = price * 1.10,
    stock = price;         -- Which "price" is this — old or new?

In standard SQL, all right-hand-side expressions in a single UPDATE see the row’s values as they were before the statement started. So stock = price here uses the old price, not the newly increased one. Behavior in edge cases can differ between databases, so avoid relying on evaluation order — use two statements (inside a transaction) or a temporary variable if you need the intermediate value.

Database Compatibility

The basic form of UPDATE shown in the Basic Syntax section is part of ANSI SQL and works in:

  • PostgreSQL
  • MySQL
  • MariaDB
  • SQL Server
  • Oracle Database
  • SQLite

Notable differences you may encounter:

  • UPDATE ... FROM. PostgreSQL and SQL Server support an extension that joins other tables directly:
PostgreSQL, SQL Server
UPDATE books
  SET price = price * (1 - a.discount_rate)
  FROM authors AS a
  WHERE a.author_id = books.author_id;
  • Multi-table UPDATE. MySQL and MariaDB use a JOIN directly in the UPDATE:
MySQL, MariaDB
UPDATE books AS b
  JOIN authors AS a
      ON a.author_id = b.author_id
  SET b.price = b.price * (1 - a.discount_rate);
  • UPDATE ... LIMIT. Supported by MySQL, MariaDB, and SQLite. PostgreSQL, SQL Server, and Oracle Database do not support it directly; use a subquery to restrict the target rows.
  • RETURNING. PostgreSQL, Oracle Database, and modern SQLite/MariaDB support UPDATE ... RETURNING, which returns the updated rows in the same statement. SQL Server offers the OUTPUT clause with similar semantics.
  • Boolean and date literals. The DATE '2000-01-01' form is portable to most modern databases, but Oracle Database users sometimes prefer TO_DATE(...).

When in doubt, consult the documentation for the database and version you are using.

Best Practices

  • Always test the WHERE clause with SELECT first. Confirm the exact set of rows before modifying them.
  • Never rely on the absence of a WHERE clause. If you truly want to change every row, say so explicitly with a comment and a code review.
  • Update only the columns that must change. Leaving other columns out of the SET list prevents accidental overwrites.
  • Use parameters, not string concatenation. This prevents SQL injection and improves plan reuse in application code.
  • Wrap related updates in a transaction. If two changes must be consistent with each other, they belong in one transaction with an explicit COMMIT or ROLLBACK.
  • Check the affected-row count. In application code, verify that the number of updated rows matches your expectation.
  • Handle NULL explicitly. Use IS NULL and IS NOT NULL in the WHERE clause when a column can be missing.
  • Prefer readable expressions. price = price * 0.90 communicates a 10% discount more clearly than a precomputed constant.

Conclusion

You learned how to use the SQL UPDATE statement to modify existing rows, how to change multiple columns at once, how to use expressions that reference the current value, and how database-specific extensions can make cross-table updates more readable. Above all, remember that an UPDATE without a WHERE clause changes every row in the table — always verify the target rows with a SELECT first, and wrap related changes in a transaction when consistency matters.

How do I find unique values using DISTINCT?

When you query a table, the result set often contains repeated values. For example, if you list the countries of every customer, the same country name appears many times. The DISTINCT keyword tells the database to return only unique values, removing duplicates from the result.

In this tutorial you will learn what DISTINCT does, how to apply it to one or several columns, how it interacts with NULL, and when to prefer alternatives such as GROUP BY.

Prerequisites

To follow along you should be comfortable with:

  • writing a basic SELECT statement;
  • filtering rows with WHERE;
  • running SQL against a local database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).

If you are new to SELECT, review a beginner SELECT tutorial first.

Sample Database

We will use a small online bookstore schema. Create the table and insert the sample data below.

CREATE TABLE books (
    book_id      INTEGER      PRIMARY KEY,
    title        VARCHAR(200) NOT NULL,
    author_name  VARCHAR(100) NOT NULL,
    genre        VARCHAR(50),
    language     VARCHAR(30)  NOT NULL,
    published_year INTEGER
);

INSERT INTO books (book_id, title, author_name, genre, language, published_year) VALUES
    (1, 'The Silent River',       'Anna Novak',      'Fiction',   'English', 2019),
    (2, 'Cooking with Herbs',     'Marco Bianchi',   'Cooking',   'English', 2021),
    (3, 'Distant Galaxies',       'Anna Novak',      'Science',   'English', 2022),
    (4, 'La Cucina Italiana',     'Marco Bianchi',   'Cooking',   'Italian', 2020),
    (5, 'Silent Mountains',       'Chen Wei',        'Fiction',   'English', 2019),
    (6, 'Introduction to Botany', 'Priya Sharma',    'Science',   'English', 2018),
    (7, 'Recipes from Home',      'Marco Bianchi',   'Cooking',   'Italian', 2023),
    (8, 'Untitled Draft',         'Anna Novak',       NULL,       'English', 2024),
    (9, 'Notes on Rivers',        'Chen Wei',         NULL,       'English', 2020);

Basic Syntax

Place DISTINCT immediately after SELECT:

SELECT DISTINCT column_name
FROM table_name;
  • DISTINCT applies to the entire row produced by the SELECT list, not to a single column in isolation.
  • When you list multiple columns, uniqueness is evaluated across the whole combination.

Practical Example

Suppose the marketing team wants a list of every genre carried by the bookstore, without duplicates.

SELECT DISTINCT genre
FROM books;

The database reads every row in books, extracts the genre value, and returns each unique value once.

Expected Result

genre
Fiction
Cooking
Science
NULL

Two rows contain NULL in genre, but DISTINCT collapses them into a single NULL in the output. DISTINCT treats NULL values as equal to each other for the purpose of removing duplicates, even though NULL = NULL is not true in general SQL comparisons.

Additional Examples

DISTINCT on Multiple Columns

Uniqueness is calculated across the combination of listed columns.

SELECT DISTINCT
    author_name,
    language
FROM books;

Expected result:

author_name language
Anna Novak English
Marco Bianchi English
Marco Bianchi Italian
Chen Wei English
Priya Sharma English

Marco Bianchi appears twice because he has books in two different languages. The pair (Marco Bianchi, English) is distinct from (Marco Bianchi, Italian).

Combining DISTINCT with WHERE

Filter rows first, then remove duplicates:

SELECT DISTINCT author_name
FROM books
WHERE genre = 'Cooking';

Expected result:

author_name
Marco Bianchi

Combining DISTINCT with ORDER BY

You can sort the unique values. Every column in ORDER BY must also appear in the SELECT list when using DISTINCT.

SELECT DISTINCT genre
FROM books
WHERE genre IS NOT NULL
ORDER BY genre ASC;

Counting Unique Values

To count how many unique values exist, wrap the column in COUNT(DISTINCT ...):

SELECT COUNT(DISTINCT author_name) AS unique_authors
FROM books;

Expected result:

unique_authors
4

Note that COUNT(DISTINCT column_name) ignores NULL values, which is different from how DISTINCT behaves in a SELECT list.

DISTINCT vs. GROUP BY

The following two queries produce the same set of unique genres:

SELECT DISTINCT genre
FROM books;
SELECT genre
FROM books
GROUP BY genre;

Prefer DISTINCT when you only need unique values. Prefer GROUP BY when you also need aggregate values per group, such as counts or sums:

SELECT
    genre,
    COUNT(*) AS book_count
FROM books
GROUP BY genre;

Common Mistakes

Applying DISTINCT to One Column Expecting per-Column Uniqueness

Incorrect assumption:

SELECT DISTINCT author_name, title
FROM books;

Readers sometimes expect this to return one row per unique author_name. It does not. DISTINCT considers the entire selected row, and every title is unique, so no rows are collapsed.

If you want one author per row, select only author_name:

SELECT DISTINCT author_name
FROM books;

Using DISTINCT to Hide a Faulty JOIN

Adding DISTINCT to remove duplicate rows caused by an incorrect join often masks a real problem. Instead, review the join condition, the relationships between tables, and whether a one-to-many relationship is producing the extra rows.

Assuming DISTINCT Sorts the Result

DISTINCT does not guarantee any specific ordering. If the order matters, add an explicit ORDER BY clause.

Database Compatibility

SELECT DISTINCT is part of standard SQL and works in:

  • PostgreSQL;
  • MySQL;
  • MariaDB;
  • SQL Server;
  • Oracle Database;
  • SQLite.

PostgreSQL additionally supports a non-standard extension, DISTINCT ON, which keeps the first row per group according to ORDER BY:

SELECT DISTINCT ON (author_name)
    author_name,
    title,
    published_year
FROM books
ORDER BY author_name, published_year DESC;

This returns the most recently published book per author. DISTINCT ON is not portable, so avoid it in code that must run on other databases.

Best Practices

  • List only the columns you need. DISTINCT on many columns is expensive because the database must compare every column value.
  • Filter first with WHERE. Reducing the row set before applying DISTINCT lowers the work required.
  • Prefer GROUP BY when you need aggregates. Do not combine DISTINCT with GROUP BY unless you have a specific reason.
  • Add ORDER BY when order matters. DISTINCT does not sort.
  • Measure performance on large tables. DISTINCT may require sorting or hashing the entire result. Inspect the execution plan with EXPLAIN to understand the cost. The exact syntax and output of EXPLAIN vary between database systems.
  • Handle NULL deliberately. Remember that DISTINCT treats all NULL values as one group, while COUNT(DISTINCT column_name) ignores them entirely.

Conclusion

You learned how to use the SQL DISTINCT keyword to remove duplicate rows from a result set, how it applies to one or more columns, how it interacts with NULL, and how it compares to GROUP BY. Use DISTINCT when you need a clean list of unique values, and switch to GROUP BY when you also need aggregate information per group.

How do I limit the number of rows returned by a query?

A SELECT statement can easily return thousands or millions of rows, but in most real applications you only need a handful of them at a time — the ten cheapest books, the top five customers, the most recent orders, or a single page of search results. Downloading everything and discarding the rest wastes memory, network bandwidth, and time.

SQL provides a way to tell the database, “return only the first N rows of the result.” The keyword is not the same in every product — you will see LIMIT, TOP, and FETCH FIRST — but the idea is identical. In this tutorial you will learn how to limit rows portably, how to combine limiting with ORDER BY, how to page through a large result set with OFFSET, and which dialect uses which keyword.

Prerequisites

To follow along, you need:

  • A working SQL database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).
  • A client where you can execute SQL statements, such as DBeaver, psql, mysql, or sqlite3.
  • Basic familiarity with SELECT, WHERE, and ORDER BY.

If you do not have a sample database yet, the setup script in the next section creates everything the tutorial needs.

Sample Database

We reuse the small online bookstore schema from the previous tutorials in this series. It contains two tables: authors and books.

CREATE TABLE authors
(
    author_id   INTEGER PRIMARY KEY,
    author_name VARCHAR(100) NOT NULL,
    country     VARCHAR(50)
);

CREATE TABLE books
(
    book_id      INTEGER PRIMARY KEY,
    title        VARCHAR(200)  NOT NULL,
    author_id    INTEGER       NOT NULL,
    category     VARCHAR(50),
    price        DECIMAL(8, 2) NOT NULL,
    stock        INTEGER       NOT NULL,
    published_on DATE,
    CONSTRAINT fk_books_author
        FOREIGN KEY (author_id) REFERENCES authors (author_id)
);

INSERT INTO authors (author_id, author_name, country)
VALUES (1, 'Jane Austen', 'United Kingdom'),
       (2, 'Haruki Murakami', 'Japan'),
       (3, 'Chimamanda Ngozi Adichie', 'Nigeria'),
       (4, 'Gabriel Garcia Marquez', 'Colombia');

INSERT INTO books (book_id, title, author_id, category, price, stock,
                   published_on)
VALUES (1, 'Pride and Prejudice', 1, 'Classic', 12.50, 20, '1813-01-28'),
       (2, 'Emma', 1, 'Classic', 10.00, 0, '1815-12-23'),
       (3, 'Norwegian Wood', 2, 'Fiction', 15.75, 12, '1987-09-04'),
       (4, 'Kafka on the Shore', 2, 'Fiction', 18.20, 5, '2002-09-12'),
       (5, 'Half of a Yellow Sun', 3, 'Historical', 16.00, 8, '2006-08-11'),
       (6, 'Americanah', 3, 'Contemporary', 14.50, 3, '2013-05-14'),
       (7, 'One Hundred Years of Solitude', 4, 'Classic', 22.00, 25,
        '1967-05-30'),
       (8, 'Love in the Time of Cholera', 4, 'Classic', 19.99, 0, '1985-09-05');

Eight books is enough to make the effect of row-limiting easy to see and check by hand.

Basic Syntax

Row limiting is written in different ways depending on the database, but every dialect requires two things to be useful:

  1. A defined order for the rows (via ORDER BY), so that “the first N rows” has a meaningful answer.
  2. A limit value — the maximum number of rows to return.

The three main forms are shown below.

PostgreSQL, MySQL, MariaDB, SQLite

SELECT column_list
FROM table_name
ORDER BY sort_expression LIMIT row_count;

SQL Server

SELECT TOP(row_count)
       column_list
FROM table_name
ORDER BY sort_expression;

Oracle Database and ANSI SQL

SELECT column_list
FROM table_name
ORDER BY sort_expression
    FETCH FIRST row_count ROWS ONLY;

FETCH FIRST ... ROWS ONLY is defined by the SQL standard and is also supported by PostgreSQL and modern versions of MySQL, MariaDB, and SQL Server. When portability matters, prefer this form.

Important: without an ORDER BY clause, “the first N rows” is not really defined. The database is free to return any N rows it wants, and that choice can change between runs. Always combine row limiting with an explicit order when the answer must be meaningful.

Practical Example

The bookstore manager wants to display the three cheapest books on the home page.

SELECT book_id,
       title,
       price
FROM books
ORDER BY price ASC LIMIT 3;

Reading the query in logical execution order:

  1. FROM books — start from every row in the books table.
  2. ORDER BY price ASC — sort the rows from the smallest to the largest price.
  3. LIMIT 3 — keep only the first three rows of that sorted result.
  4. SELECT book_id, title, price — return the three columns for those rows.

Expected Result

book_id title price
2 Emma 10.00
1 Pride and Prejudice 12.50
6 Americanah 14.50

Additional Examples

1. Top N in Descending Order

To show the three most expensive books, keep the same LIMIT and reverse the sort direction.

SELECT book_id,
       title,
       price
FROM books
ORDER BY price DESC LIMIT 3;

Expected result:

book_id title price
7 One Hundred Years of Solitude 22.00
8 Love in the Time of Cholera 19.99
4 Kafka on the Shore 18.20

2. Portable Syntax with FETCH FIRST

The same result written in ANSI-standard form:

SELECT book_id,
       title,
       price
FROM books
ORDER BY price DESC
    FETCH FIRST 3 ROWS ONLY;

This runs on PostgreSQL, Oracle Database, SQL Server 2012+, MySQL 8.0.31+, MariaDB 10.6+, and SQLite 3.30+.

3. SQL Server TOP

SQL Server uses TOP in the SELECT list. Parentheses around the count are recommended and required when the count is an expression or parameter.

SELECT TOP(3)
       book_id,
       title,
       price
FROM books
ORDER BY price DESC;

4. Pagination with OFFSET

To display page 2 of a list where each page shows three books, skip the first three rows and take the next three.

-- PostgreSQL, MySQL, MariaDB, SQLite
SELECT book_id,
       title,
       price
FROM books
ORDER BY price ASC, book_id ASC LIMIT 3
OFFSET 3;

Portable version:

SELECT book_id,
       title,
       price
FROM books
ORDER BY price ASC, book_id ASC
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;

Expected result (rows 4 through 6 of the price-ascending list):

book_id title price
3 Norwegian Wood 15.75
5 Half of a Yellow Sun 16.00
4 Kafka on the Shore 18.20

Notice the tie-breaker book_id ASC in the ORDER BY. Without it, two books that share the same price could switch positions between page 1 and page 2, so a row might appear twice or not at all.

5. Limiting After Filtering

Row limiting is applied after WHERE, so you can safely combine both.

SELECT title,
       price,
       stock
FROM books
WHERE category = 'Classic'
  AND stock > 0
ORDER BY price ASC LIMIT 2;

“Show me the two cheapest classic books that are still in stock.”

Expected result:

title price stock
Pride and Prejudice 12.50 20
One Hundred Years of Solitude 22.00 25

6. Sampling Without an ORDER BY

Sometimes you genuinely do not care which rows you get — you just want a small, cheap sample while exploring a large table:

SELECT book_id,
       title
FROM books LIMIT 5;

This is fine for ad-hoc inspection, but do not rely on the row order or on which rows you get. For anything a user will see, add an ORDER BY.

7. Using LIMIT from Java

When your application shows one page of results at a time, pass the page size and offset as parameters — never build the SQL by concatenating them.

String sql = """
    SELECT book_id, title, price
    FROM books
    WHERE category = ?
    ORDER BY price ASC, book_id ASC
    LIMIT ? OFFSET ?
    """;

try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, category);
statement.setInt(2, pageSize);
statement.setInt(3, pageSize * (pageIndex - 1));

    try (ResultSet resultSet = statement.executeQuery()) {
        while (resultSet.next()) {
            long bookId = resultSet.getLong("book_id");
            String title = resultSet.getString("title");
            BigDecimal price = resultSet.getBigDecimal("price");
            // process the row...
        }
    }
}

Parameterizing the values keeps the query cache-friendly and eliminates SQL-injection risk.

Common Mistakes

Using LIMIT Without ORDER BY

Incorrect:

SELECT title, price
FROM books LIMIT 3;
-- "Give me the three cheapest books."

The query does not ask for the cheapest books — it asks for any three. The database is free to return whichever three rows are convenient. Add the sort:

SELECT title, price
FROM books
ORDER BY price ASC LIMIT 3;

Forgetting a Tie-Breaker When Paging

If several rows share the same value in the sort column, their relative order is undefined. During pagination this can cause rows to appear twice or be skipped.

Fragile:

SELECT book_id, title, price
FROM books
ORDER BY price ASC LIMIT 3
OFFSET 3;

Reliable:

SELECT book_id, title, price
FROM books
ORDER BY price ASC, book_id ASC LIMIT 3
OFFSET 3;

Mixing Dialects in the Same Statement

SELECT TOP 3 ... LIMIT 3 is not valid SQL in any product. Choose one form based on your database:

  • PostgreSQL, MySQL, MariaDB, SQLite → LIMIT (or FETCH FIRST).
  • SQL Server → TOP (or OFFSET ... FETCH NEXT).
  • Oracle Database → FETCH FIRST (or ROWNUM in older versions).

Assuming LIMIT Runs Before WHERE

LIMIT is applied to the already-filtered, already-sorted result set, not to the raw table. LIMIT 10 does not mean “read only 10 rows from disk” — the database may still scan the whole table if there is no supporting index. Use WHERE to reduce the working set, and consider indexes on the filter and sort columns for large tables.

Database Compatibility

Database Preferred syntax
PostgreSQL LIMIT n [OFFSET m] or FETCH FIRST n ROWS ONLY
MySQL LIMIT n [OFFSET m] or LIMIT m, n
MariaDB LIMIT n [OFFSET m] or LIMIT m, n
SQLite LIMIT n [OFFSET m]
SQL Server SELECT TOP (n) ... or OFFSET m ROWS FETCH NEXT n ROWS ONLY
Oracle Database FETCH FIRST n ROWS ONLY (12c+); ROWNUM <= n on older versions

Notes:

  • FETCH FIRST / FETCH NEXT is part of ANSI SQL and is the most portable option across modern versions.
  • LIMIT m, n (MySQL and MariaDB shorthand) treats the first value as the offset and the second as the count. This ordering is easy to mix up; prefer LIMIT n OFFSET m for clarity.
  • Older Oracle Database versions (pre-12c) do not support FETCH FIRST. Use WHERE ROWNUM <= n around a sorted subquery.
  • SQL Server requires an ORDER BY clause when using OFFSET ... FETCH NEXT.

When in doubt, consult the documentation for the specific database and version you are using.

Best Practices

  • Combine row limiting with an explicit ORDER BY whenever the identity of the returned rows matters.
  • Add a deterministic tie-breaker, such as the primary key, so that ordering and pagination remain stable across runs.
  • Prefer the portable FETCH FIRST n ROWS ONLY form when writing SQL that must run on more than one database.
  • Use parameters for the limit and offset values, especially in application code — do not concatenate them into the SQL string.
  • Consider indexes on filter and sort columns for large tables, and measure with your database’s execution-plan tool before assuming a benefit.
  • Be careful with very large OFFSET values. Most databases still read and discard all the skipped rows, so deep pagination can be surprisingly slow. Keyset pagination — filtering with WHERE (sort_key, id) > (last_sort_key, last_id) — is often faster for large data sets.

Conclusion

You learned how to restrict the number of rows a query returns using LIMIT, TOP, and FETCH FIRST, how to combine row limiting with ORDER BY and OFFSET for pagination, and how the syntax differs across PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, and SQLite. Always sort explicitly and add a deterministic tie-breaker before you rely on “top N” or paged results.

How do I sort query results using ORDER BY?

When you run a SELECT statement, the database does not promise to return rows in any particular order. If you want a predictable sequence — the cheapest books first, the newest orders on top, or authors listed alphabetically — you have to ask for it explicitly. The tool SQL gives you for that is the ORDER BY clause.

In this tutorial you will learn how to sort rows in ascending or descending order, sort by more than one column, sort by expressions and aliases, and control where NULL values appear. You will also see the most common mistakes and how each dialect handles the differences.

Prerequisites

To follow along, you need:

  • A working SQL database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).
  • A client where you can execute SQL statements (a GUI tool like DBeaver, or a command-line client).
  • Basic familiarity with the SELECT and WHERE clauses.

If you have not created a sample database yet, the setup script in the next section will produce everything you need.

Sample Database

We continue with the small online bookstore schema used in the previous tutorial. It contains two tables: authors and books.

CREATE TABLE authors (
    author_id   INTEGER      PRIMARY KEY,
    author_name VARCHAR(100) NOT NULL,
    country     VARCHAR(50)
);

CREATE TABLE books (
    book_id      INTEGER       PRIMARY KEY,
    title        VARCHAR(200)  NOT NULL,
    author_id    INTEGER       NOT NULL,
    category     VARCHAR(50),
    price        DECIMAL(8, 2) NOT NULL,
    stock        INTEGER       NOT NULL,
    published_on DATE,
    CONSTRAINT fk_books_author
        FOREIGN KEY (author_id) REFERENCES authors (author_id)
);

INSERT INTO authors (author_id, author_name, country) VALUES
    (1, 'Jane Austen',              'United Kingdom'),
    (2, 'Haruki Murakami',          'Japan'),
    (3, 'Chimamanda Ngozi Adichie', 'Nigeria'),
    (4, 'Gabriel Garcia Marquez',   'Colombia');

INSERT INTO books (book_id, title, author_id, category, price, stock, published_on) VALUES
    (1, 'Pride and Prejudice',           1, 'Classic',      12.50, 20, '1813-01-28'),
    (2, 'Emma',                          1, 'Classic',      10.00,  0, '1815-12-23'),
    (3, 'Norwegian Wood',                2, 'Fiction',      15.75, 12, '1987-09-04'),
    (4, 'Kafka on the Shore',            2, 'Fiction',      18.20,  5, '2002-09-12'),
    (5, 'Half of a Yellow Sun',          3, 'Historical',   16.00,  8, '2006-08-11'),
    (6, 'Americanah',                    3, 'Contemporary', 14.50,  3, '2013-05-14'),
    (7, 'One Hundred Years of Solitude', 4, 'Classic',      22.00, 25, '1967-05-30'),
    (8, 'Love in the Time of Cholera',   4, 'Classic',      19.99,  0, '1985-09-05'),
    (9, 'Unknown Title',                 2, NULL,           13.00,  4, NULL);

The last row contains NULL values for category and published_on. We will use it to see how ORDER BY treats missing data.

Basic Syntax

The ORDER BY clause appears after WHERE (and after GROUP BY / HAVING when they are present), and before LIMIT.

SELECT column_list
FROM table_name
WHERE condition
ORDER BY sort_expression [ASC | DESC] [, sort_expression [ASC | DESC] ...];
  • sort_expression is usually a column name, but it can also be an expression, an alias, or a column position number.
  • ASC sorts from smallest to largest and is the default.
  • DESC sorts from largest to smallest.
  • Without ORDER BY, the returned row order is undefined, even if it looks stable during testing.

Practical Example

The bookstore manager wants a price list of every book, from the cheapest to the most expensive.

SELECT
    book_id,
    title,
    price
FROM books
ORDER BY price ASC;

Reading the query in logical order:

  1. FROM books — start with every row in the books table.
  2. SELECT book_id, title, price — pick the three columns to return.
  3. ORDER BY price ASC — sort the result set by price from lowest to highest.

Expected Result

book_id title price
2 Emma 10.00
1 Pride and Prejudice 12.50
9 Unknown Title 13.00
6 Americanah 14.50
3 Norwegian Wood 15.75
5 Half of a Yellow Sun 16.00
4 Kafka on the Shore 18.20
8 Love in the Time of Cholera 19.99
7 One Hundred Years of Solitude 22.00

Additional Examples

1. Descending Order

Use DESC when the largest value should come first, for example a “most expensive first” list.

SELECT
    title,
    price
FROM books
ORDER BY price DESC;

2. Sorting by Multiple Columns

You can sort by more than one column. Each column can independently use ASC or DESC. The database sorts by the first expression, then breaks ties with the second, and so on.

SELECT
    category,
    title,
    price
FROM books
WHERE category IS NOT NULL
ORDER BY
    category ASC,
    price    DESC;

“Group books by category alphabetically, and within each category show the most expensive book first.”

Expected result:

category title price
Classic One Hundred Years of Solitude 22.00
Classic Love in the Time of Cholera 19.99
Classic Pride and Prejudice 12.50
Classic Emma 10.00
Contemporary Americanah 14.50
Fiction Kafka on the Shore 18.20
Fiction Norwegian Wood 15.75
Historical Half of a Yellow Sun 16.00

3. Sorting by an Expression

ORDER BY accepts any expression that the database can evaluate — arithmetic, string operations, function calls, and so on.

SELECT
    title,
    price,
    stock,
    price * stock AS inventory_value
FROM books
ORDER BY price * stock DESC;

The rows are ordered by the computed inventory value even though the raw price and stock columns are not sorted individually.

4. Sorting by an Alias

Most databases allow you to use the alias defined in the SELECT list, because ORDER BY is logically evaluated after SELECT.

SELECT
    title,
    price * stock AS inventory_value
FROM books
ORDER BY inventory_value DESC;

This is often more readable than repeating the full expression. Availability varies slightly — see the Database Compatibility section for details.

5. Sorting Strings

String columns are sorted according to the column’s collation, which controls case sensitivity and locale-aware rules.

SELECT
    author_name,
    country
FROM authors
ORDER BY author_name ASC;

Expected result:

author_name country
Chimamanda Ngozi Adichie Nigeria
Gabriel Garcia Marquez Colombia
Haruki Murakami Japan
Jane Austen United Kingdom

If your data mixes upper- and lower-case letters and the ordering surprises you, check the collation for your column or database.

6. Sorting Dates

Dates and timestamps sort chronologically. ASC produces oldest-first; DESC produces newest-first.

SELECT
    title,
    published_on
FROM books
WHERE published_on IS NOT NULL
ORDER BY published_on DESC;

7. Controlling NULL Placement

NULL values need special treatment because they are neither less than nor greater than any real value. Different databases place them in different positions by default:

  • PostgreSQL and Oracle Database place NULL values last in ASC order and first in DESC order.
  • MySQL, MariaDB, and SQLite place NULL values first in ASC order and last in DESC order.
  • SQL Server places NULL values first in ASC order and last in DESC order.

ANSI SQL defines the optional NULLS FIRST and NULLS LAST modifiers to make the placement explicit.

-- PostgreSQL, Oracle Database, SQLite 3.30+
SELECT
    title,
    published_on
FROM books
ORDER BY published_on ASC NULLS LAST;

If your database does not support NULLS FIRST / NULLS LAST, you can emulate them with a helper expression:

-- Portable emulation: put NULLs last in ascending order
SELECT
    title,
    published_on
FROM books
ORDER BY
    CASE WHEN published_on IS NULL THEN 1 ELSE 0 END,
    published_on ASC;

8. Combining ORDER BY with LIMIT

ORDER BY is what makes LIMIT meaningful. Asking for the “top 3” without sorting first is not really a top-3 query.

-- PostgreSQL, MySQL, MariaDB, SQLite
SELECT
    title,
    price
FROM books
ORDER BY price DESC
LIMIT 3;

Equivalent examples for other systems:

-- SQL Server
SELECT TOP 3
    title,
    price
FROM books
ORDER BY price DESC;
-- Oracle Database and ANSI SQL
SELECT
    title,
    price
FROM books
ORDER BY price DESC
FETCH FIRST 3 ROWS ONLY;

9. Using ORDER BY in Application Code

When your application performs paging or user-selected sorting, keep the sort expression on the server side and pass only the values you actually want to filter by as parameters.

String sql = """
        SELECT book_id, title, price
        FROM books
        WHERE category = ?
        ORDER BY price DESC
        """;

try (PreparedStatement statement = connection.prepareStatement(sql)) {
    statement.setString(1, category);

    try (ResultSet resultSet = statement.executeQuery()) {
        while (resultSet.next()) {
            long bookId = resultSet.getLong("book_id");
            String title = resultSet.getString("title");
            BigDecimal price = resultSet.getBigDecimal("price");
            // process the row...
        }
    }
}

Never build the ORDER BY clause by concatenating user input directly into the SQL string. If users may choose the sort column, validate the incoming value against a fixed allow-list of column names before assembling the query.

Common Mistakes

Assuming a Query Is Sorted Without ORDER BY

Incorrect:

SELECT title, price
FROM books;
-- "The rows come back sorted by book_id anyway."

The order you happen to observe is an artifact of the current storage layout, statistics, and execution plan. It can change silently after an update, an index change, or an upgrade. If order matters, always request it:

SELECT title, price
FROM books
ORDER BY book_id;

Sorting by Column Position Numbers

Some databases allow ordering by the position of a column in the SELECT list, for example ORDER BY 2. This is fragile — inserting a new column silently changes the sort. Prefer explicit column names or aliases.

Fragile:

SELECT title, price
FROM books
ORDER BY 2 DESC;

Better:

SELECT title, price
FROM books
ORDER BY price DESC;

Forgetting a Tie-Breaker for Deterministic Ordering

If several rows share the same value in the sort column, their relative order is undefined. For paging or reproducible reports, add a unique tie-breaker such as the primary key.

SELECT
    title,
    price
FROM books
ORDER BY
    price   DESC,
    book_id ASC;

Ignoring NULL Placement Differences

If your query is portable across databases, do not rely on the default position of NULL values. Use NULLS FIRST / NULLS LAST where supported, or the CASE-based emulation shown earlier.

Database Compatibility

The core ORDER BY syntax with ASC and DESC is part of ANSI SQL and is supported by:

  • PostgreSQL
  • MySQL
  • MariaDB
  • SQL Server
  • Oracle Database
  • SQLite

Notable differences you may encounter:

  • NULLS FIRST / NULLS LAST. Supported by PostgreSQL, Oracle Database, and SQLite 3.30+. MySQL, MariaDB, and SQL Server do not accept these keywords; use a CASE expression to control placement.
  • Default NULL position. As described in section 7, defaults differ across engines. When in doubt, be explicit.
  • Row limiting. Combine ORDER BY with LIMIT (PostgreSQL, MySQL, MariaDB, SQLite), TOP (SQL Server), or FETCH FIRST n ROWS ONLY (Oracle Database and ANSI SQL).
  • Sorting by alias. PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server accept SELECT-list aliases in ORDER BY. Oracle Database also accepts them in most cases, but repeating the expression is always safe.
  • Collation and case sensitivity. String ordering depends on the column’s collation. Two databases with different default collations can order the same data differently.

When in doubt, consult the documentation for the specific database and version you are using.

Best Practices

  • Always sort explicitly when order matters. The database is free to return rows in any order otherwise.
  • Add a deterministic tie-breaker. A unique column such as the primary key guarantees stable ordering, especially with LIMIT or paging.
  • Prefer column names over positions. Ordering by 1, 2, 3 breaks silently when the SELECT list changes.
  • Be explicit about NULL placement when writing portable SQL.
  • Sort on indexed columns when possible. An index on the sort column can let the database skip an expensive sort step. Measure with your database’s execution-plan tool before assuming a benefit.
  • Sort in the database, not in the application. The database can use indexes and streaming; loading everything into memory to sort in Java is slower and wastes bandwidth.
  • Validate user-supplied sort keys against an allow-list. Never inject raw user input into an ORDER BY clause.

Conclusion

You learned how to use the SQL ORDER BY clause to sort query results in ascending or descending order, sort by multiple columns and expressions, and control how NULL values are positioned. You also saw why a deterministic tie-breaker matters for paging and how dialects differ in NULL handling and row limiting.

How do I filter SQL query results using WHERE?

When you query a table, you rarely want every row it contains. Most of the time you want specific rows — customers from a certain city, orders above a certain price, or books written by a particular author. The WHERE clause is the tool SQL gives you to express exactly which rows should be returned.

In this tutorial you will learn how to use WHERE with comparison operators, logical operators, ranges, lists, pattern matching, and NULL checks. You will also see the most common mistakes beginners make and how to avoid them.

Prerequisites

To follow along, you need:

  • A working SQL database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).
  • A client where you can execute SQL statements (a GUI tool like DBeaver, or a command-line client).
  • Basic familiarity with the SELECT statement.

If you have not created a sample database yet, the setup script in the next section will produce everything you need.

Sample Database

Throughout this tutorial we use a small online bookstore schema with two tables: authors and books.

CREATE TABLE authors (
    author_id   INTEGER      PRIMARY KEY,
    author_name VARCHAR(100) NOT NULL,
    country     VARCHAR(50)
);

CREATE TABLE books (
    book_id      INTEGER       PRIMARY KEY,
    title        VARCHAR(200)  NOT NULL,
    author_id    INTEGER       NOT NULL,
    category     VARCHAR(50),
    price        DECIMAL(8, 2) NOT NULL,
    stock        INTEGER       NOT NULL,
    published_on DATE,
    CONSTRAINT fk_books_author
        FOREIGN KEY (author_id) REFERENCES authors (author_id)
);

INSERT INTO authors (author_id, author_name, country) VALUES
    (1, 'Jane Austen',     'United Kingdom'),
    (2, 'Haruki Murakami', 'Japan'),
    (3, 'Chimamanda Ngozi Adichie', 'Nigeria'),
    (4, 'Gabriel Garcia Marquez',   'Colombia');

INSERT INTO books (book_id, title, author_id, category, price, stock, published_on) VALUES
    (1, 'Pride and Prejudice',        1, 'Classic',        12.50, 20,   '1813-01-28'),
    (2, 'Emma',                       1, 'Classic',        10.00,  0,   '1815-12-23'),
    (3, 'Norwegian Wood',             2, 'Fiction',        15.75, 12,   '1987-09-04'),
    (4, 'Kafka on the Shore',         2, 'Fiction',        18.20,  5,   '2002-09-12'),
    (5, 'Half of a Yellow Sun',       3, 'Historical',     16.00,  8,   '2006-08-11'),
    (6, 'Americanah',                 3, 'Contemporary',   14.50,  3,   '2013-05-14'),
    (7, 'One Hundred Years of Solitude', 4, 'Classic',     22.00, 25,   '1967-05-30'),
    (8, 'Love in the Time of Cholera',   4, 'Classic',     19.99,  0,   '1985-09-05'),
    (9, 'Unknown Title',              2, NULL,             13.00,  4,   NULL);

The final row uses NULL for both category and published_on on purpose — we will use it to demonstrate how WHERE handles missing values.

Basic Syntax

The WHERE clause appears after FROM and before clauses like ORDER BY or GROUP BY.

SELECT column_list
FROM table_name
WHERE condition;
  • condition is a boolean expression evaluated once per row.
  • A row is included in the result only when the condition evaluates to true.
  • Rows that evaluate to false or unknown (which is what NULL comparisons return) are excluded.

Practical Example

Suppose the bookstore manager wants to see every book that costs more than 15.00.

SELECT
    book_id,
    title,
    price
FROM books
WHERE price > 15.00;

Reading the query in logical order:

  1. FROM books — start with every row in the books table.
  2. WHERE price > 15.00 — keep only rows where the price is greater than 15.00.
  3. SELECT book_id, title, price — return these three columns.

Expected Result

book_id title price
3 Norwegian Wood 15.75
4 Kafka on the Shore 18.20
5 Half of a Yellow Sun 16.00
7 One Hundred Years of Solitude 22.00
8 Love in the Time of Cholera 19.99

Additional Examples

1. Comparison Operators

SQL supports the standard set of comparison operators.

Operator Meaning
= Equal to
<> or != Not equal to
<, > Less than / greater than
<=, >= Less than or equal / etc.
SELECT title, category
FROM books
WHERE category = 'Classic';

Returns all books whose category is exactly 'Classic'. String comparisons are case-sensitive in some databases (for example, PostgreSQL) and case-insensitive in others (for example, SQL Server with default collation). Consult your database documentation if this matters for your data.

2. Combining Conditions with AND, OR, and NOT

You can combine multiple conditions to express more precise rules.

SELECT title, price, stock
FROM books
WHERE category = 'Classic'
  AND stock > 0;

“Return classic books that are currently in stock.”

Use parentheses when mixing AND and ORAND has higher precedence than OR, and forgetting this is a common source of bugs.

SELECT title, category, price
FROM books
WHERE (category = 'Classic' OR category = 'Fiction')
  AND price < 20.00;

3. Ranges with BETWEEN

BETWEEN is a readable shortcut for a closed range. Both endpoints are included.

SELECT title, price
FROM books
WHERE price BETWEEN 12.00 AND 16.00;

This is equivalent to:

SELECT title, price
FROM books
WHERE price >= 12.00
  AND price <= 16.00;

4. Lists with IN

Use IN when you want to match any value from a fixed list.

SELECT title, category
FROM books
WHERE category IN ('Classic', 'Fiction');

This is much cleaner than chaining OR conditions.

5. Pattern Matching with LIKE

LIKE matches strings against a pattern. The two standard wildcards are:

  • % — matches any sequence of characters (including none).
  • _ — matches exactly one character.
SELECT title
FROM books
WHERE title LIKE '%Love%';

Returns any title containing the substring Love.

SELECT author_name
FROM authors
WHERE author_name LIKE 'J%';

Returns authors whose name starts with J.

Case sensitivity of LIKE varies between databases. PostgreSQL provides ILIKE for case-insensitive matching; other systems rely on the column’s collation.

6. Handling NULL

NULL is not equal to anything — not even to another NULL. Comparisons involving NULL return unknown, and rows with an unknown result are excluded by WHERE.

To test for missing values, use IS NULL or IS NOT NULL.

SELECT title, published_on
FROM books
WHERE published_on IS NULL;

To exclude rows with a missing category:

SELECT title, category
FROM books
WHERE category IS NOT NULL;

7. Filtering with Dates

Date comparisons work like number comparisons, but the literal format depends on the database.

SELECT title, published_on
FROM books
WHERE published_on >= DATE '2000-01-01';

The DATE '...' prefix is the ANSI-standard form and is accepted by PostgreSQL, MySQL 8+, MariaDB, and SQLite. SQL Server and Oracle Database accept the plain string '2000-01-01' in most contexts as well.

8. Using WHERE in Application Code

When the filter value comes from user input or another variable, use a parameterized query rather than string concatenation. This prevents SQL injection and lets the database reuse the prepared plan.

String sql = """
        SELECT book_id, title, price
        FROM books
        WHERE category = ?
          AND price < ?
        """;

try (PreparedStatement statement = connection.prepareStatement(sql)) {
    statement.setString(1, category);
    statement.setBigDecimal(2, maxPrice);

    try (ResultSet resultSet = statement.executeQuery()) {
        while (resultSet.next()) {
            long bookId = resultSet.getLong("book_id");
            String title = resultSet.getString("title");
            BigDecimal price = resultSet.getBigDecimal("price");
            // process the row...
        }
    }
}

Common Mistakes

Comparing a Value to NULL with =

Incorrect:

SELECT *
FROM books
WHERE category = NULL;

Correct:

SELECT *
FROM books
WHERE category IS NULL;

The first query returns zero rows on every standard-compliant database because category = NULL evaluates to unknown, never true.

Mixing AND and OR Without Parentheses

Incorrect:

SELECT title, category, price
FROM books
WHERE category = 'Classic' OR category = 'Fiction'
  AND price < 20.00;

Because AND binds tighter than OR, this is interpreted as “all classic books, plus fiction books cheaper than 20.00”, which is probably not what you want.

Correct:

SELECT title, category, price
FROM books
WHERE (category = 'Classic' OR category = 'Fiction')
  AND price < 20.00;

Forgetting WHERE in an UPDATE or DELETE

Before you delete or update rows, verify the filter with a SELECT first.

SELECT *
FROM books
WHERE stock = 0;

Only after confirming the target rows should you run:

DELETE FROM books
WHERE stock = 0;

Warning: Running UPDATE or DELETE without a WHERE clause modifies every row in the table. Practice these statements only in a disposable learning database or after taking a verified backup.

Using <> with NULL

The expression category <> 'Classic' will not return rows where category IS NULL, because comparing NULL to anything yields unknown. If you need those rows too, add an explicit NULL check:

SELECT title, category
FROM books
WHERE category <> 'Classic'
   OR category IS NULL;

Database Compatibility

The core WHERE clause and the operators shown here (=, <>, <, >, <=, >=, AND, OR, NOT, BETWEEN, IN, LIKE, IS NULL, IS NOT NULL) are part of ANSI SQL and work in:

  • PostgreSQL
  • MySQL
  • MariaDB
  • SQL Server
  • Oracle Database
  • SQLite

Notable differences you may encounter:

  • Case sensitivity. String equality and LIKE are case-sensitive in PostgreSQL by default but often case-insensitive in MySQL and SQL Server, depending on collation.
  • Case-insensitive LIKE. PostgreSQL offers ILIKE. Other systems typically rely on collation or functions such as LOWER().
  • Date literals. The DATE '2000-01-01' form is portable to most systems; Oracle Database also accepts it, while some tools prefer TO_DATE('2000-01-01', 'YYYY-MM-DD').
  • Boolean values. PostgreSQL has a native BOOLEAN type; SQL Server and Oracle Database traditionally use BIT or numeric flags.

When in doubt, consult the documentation for the specific database and version you are using.

Best Practices

  • List the columns you need. Combine a specific SELECT list with a precise WHERE clause instead of scanning SELECT *.
  • Use parameters, not string concatenation. This prevents SQL injection and improves plan reuse.
  • Handle NULL explicitly. If a column can be NULL, decide whether those rows should be included and add IS NULL / IS NOT NULL accordingly.
  • Parenthesize mixed logical expressions. It makes intent obvious and avoids precedence bugs.
  • Verify destructive filters with SELECT first. Never run an UPDATE or DELETE before you have seen the rows it will affect.
  • Prefer readable operators. BETWEEN and IN communicate intent better than long chains of AND/OR.
  • Beware of functions on filtered columns. Wrapping a column in a function (for example, WHERE LOWER(title) = 'emma') can prevent an index from being used. If this matters, measure with your database’s execution-plan tool before optimizing.

Conclusion

You learned how to use the SQL WHERE clause to filter rows with comparison, logical, range, list, pattern-matching, and NULL conditions. You also saw how operator precedence, three-valued logic, and parameterized queries affect real-world usage. Always verify an UPDATE or DELETE condition with SELECT before modifying data.