How do I filter values using IN, BETWEEN, and NOT in SQL?

When you filter rows in SQL, you often need to match a value against a list of options, check whether it falls within a range, or exclude rows that satisfy a condition. Chaining many AND/OR comparisons for these cases quickly becomes hard to read. SQL provides three operators — IN, BETWEEN, and NOT — that make these filters expressive and concise.

In this tutorial you will learn how to use IN to match a value against a list, BETWEEN to match a value within an inclusive range, and NOT to negate any condition. You will also see how each one interacts with NULL and where readers commonly get tripped up.

Prerequisites

To follow along you need:

  • A working SQL database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).
  • A client that can execute SQL statements.
  • Basic familiarity with SELECT, WHERE, and comparison/logical operators.

If you followed the previous tutorials in this series, you can reuse the same sample tables.

Sample Database

We continue with the online bookstore schema used earlier in the series.

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);

Row 9 intentionally uses NULL for category and published_on. We will use it to explore how IN, BETWEEN, and NOT handle missing values.

Basic Syntax

All three operators are used inside a boolean expression, most often in a WHERE clause.

SELECT column_list
FROM table_name
WHERE column_name IN (value1, value2, ...);

SELECT column_list
FROM table_name
WHERE column_name BETWEEN low_value AND high_value;

SELECT column_list
FROM table_name
WHERE NOT condition;
  • IN returns true when the column value equals any value in the list.
  • BETWEEN low AND high returns true when the column value is greater than or equal to low and less than or equal to high — both endpoints are included.
  • NOT inverts the boolean result of the expression that follows it.

Each of them can be combined with NOT: NOT IN, NOT BETWEEN, and IS NOT NULL.

Filtering with IN

IN is a readable shortcut for a chain of equality checks joined by OR.

Practical Example

“Show every book whose category is Classic, Fiction, or Historical.”

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

Reading the query in logical order:

  1. FROM books — start with every row in the books table.
  2. WHERE category IN (...) — keep only rows whose category matches one of the listed values.
  3. SELECT title, category — return these two columns.
  4. ORDER BY category, title — sort the final result.

Expected Result

title category
Emma Classic
Love in the Time of Cholera Classic
One Hundred Years of Solitude Classic
Pride and Prejudice Classic
Kafka on the Shore Fiction
Norwegian Wood Fiction
Half of a Yellow Sun Historical

The row with category IS NULL is excluded, because NULL does not equal any listed value.

Equivalent Form with OR

The query above is equivalent to:

SELECT
    title,
    category
FROM books
WHERE category = 'Classic'
   OR category = 'Fiction'
   OR category = 'Historical';

IN communicates intent more clearly and is easier to maintain when the list changes.

Filtering with BETWEEN

BETWEEN matches values inside an inclusive range. It works with numbers, dates, and strings that share a comparable type.

Practical Example: Numeric Range

“Show every book priced between 12.00 and 16.00, inclusive.”

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

Expected Result

title price
Pride and Prejudice 12.50
Unknown Title 13.00
Americanah 14.50
Norwegian Wood 15.75
Half of a Yellow Sun 16.00

Both 12.00 and 16.00 would be included if they matched a row exactly. BETWEEN low AND high is equivalent to column >= low AND column <= high.

Practical Example: Date Range

BETWEEN also works with dates. Use the ANSI-standard DATE '...' literal for portability.

SELECT
    title,
    published_on
FROM books
WHERE published_on BETWEEN DATE '1900-01-01' AND DATE '1999-12-31'
ORDER BY published_on;

This returns twentieth-century books. The row with published_on IS NULL is excluded because comparisons against NULL yield unknown.

Watch the Order of Endpoints

BETWEEN requires the lower value first. If you reverse them, most databases silently return zero rows because no value can be both >= 16.00 and <= 12.00.

-- Returns no rows on standard-compliant databases.
SELECT title, price
FROM books
WHERE price BETWEEN 16.00 AND 12.00;

Negating with NOT

NOT inverts a boolean expression. It is most commonly used together with IN, BETWEEN, and LIKE, but it can negate any condition.

NOT IN

“Show every book whose category is neither Classic nor Fiction.”

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

Expected Result

title category
Americanah Contemporary
Half of a Yellow Sun Historical

Notice that Unknown Title (with category IS NULL) is not in the result. This is a classic pitfall — see the Common Mistakes section.

NOT BETWEEN

“Show every book priced outside the 12.00–16.00 range.”

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

Expected Result

title price
Emma 10.00
Kafka on the Shore 18.20
Love in the Time of Cholera 19.99
One Hundred Years of Solitude 22.00

NOT BETWEEN low AND high is equivalent to column < low OR column > high.

NOT with Other Conditions

You can apply NOT to any boolean expression, including a parenthesized combination:

SELECT
    title,
    category,
    stock
FROM books
WHERE NOT (category = 'Classic' AND stock = 0)
ORDER BY title;

“Show every book that is not an out-of-stock classic.”

Combining IN, BETWEEN, and NOT

These operators combine naturally with AND and OR to describe complex requirements.

“Show Classic or Fiction books, in stock, priced between 12.00 and 20.00.”

SELECT
    title,
    category,
    price,
    stock
FROM books
WHERE category IN ('Classic', 'Fiction')
  AND price BETWEEN 12.00 AND 20.00
  AND stock > 0
ORDER BY price;

Expected Result

title category price stock
Pride and Prejudice Classic 12.50 20
Norwegian Wood Fiction 15.75 12
Kafka on the Shore Fiction 18.20 5
Love in the Time of Cholera Classic 19.99 0

Wait — Love in the Time of Cholera has stock = 0 and should be excluded. Reading the actual result, only the first three rows appear:

title category price stock
Pride and Prejudice Classic 12.50 20
Norwegian Wood Fiction 15.75 12
Kafka on the Shore Fiction 18.20 5

Each clause plays a clear role:

  1. category IN ('Classic', 'Fiction') — restrict to two categories.
  2. price BETWEEN 12.00 AND 20.00 — apply the price range.
  3. stock > 0 — keep only books currently available.

Using These Operators in Application Code

When the values come from user input or a variable, always use parameterized queries.

String sql = """
        SELECT book_id, title, category, price
        FROM books
        WHERE category IN (?, ?, ?)
          AND price BETWEEN ? AND ?
        """;

try (PreparedStatement statement = connection.prepareStatement(sql)) {
    statement.setString(1, "Classic");
    statement.setString(2, "Fiction");
    statement.setString(3, "Historical");
    statement.setBigDecimal(4, new BigDecimal("12.00"));
    statement.setBigDecimal(5, new BigDecimal("20.00"));

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

Note that most JDBC drivers do not accept a Java collection directly for IN (...). You typically expand placeholders to match the list size or use database-specific features such as PostgreSQL’s = ANY(?) with an array parameter.

Common Mistakes

NOT IN with a List That Contains NULL

This is the most surprising pitfall in this tutorial. If any value in the NOT IN list is NULL, the whole condition evaluates to unknown for every row, so no rows are returned.

Incorrect:

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

This query returns zero rows on every standard-compliant database. It is equivalent to:

WHERE category <> 'Classic'
  AND category <> 'Fiction'
  AND category <> NULL   -- always unknown

Because category <> NULL is always unknown, the entire AND chain never evaluates to true.

Correct — filter out NULL in the source or the list before applying NOT IN:

SELECT title, category
FROM books
WHERE category IS NOT NULL
  AND category NOT IN ('Classic', 'Fiction');

Forgetting That NOT IN Excludes NULL Rows Too

Even without NULL in the list, NOT IN excludes rows where the column itself is NULL, because NULL <> 'Classic' is unknown.

If you want those rows included, add an explicit check:

SELECT title, category
FROM books
WHERE category NOT IN ('Classic', 'Fiction')
   OR category IS NULL;

Reversing the BETWEEN Endpoints

BETWEEN 16.00 AND 12.00 is not the same as BETWEEN 12.00 AND 16.00. The lower value must come first, otherwise the range is empty.

Assuming BETWEEN Is Exclusive

BETWEEN is inclusive on both ends. If you need an exclusive upper bound (common with dates), write the comparison explicitly:

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

This is safer than BETWEEN DATE '2000-01-01' AND DATE '2009-12-31', which can behave differently when the column stores a timestamp with a time component.

Comparing to NULL with = or <>

NOT does not rescue NULL comparisons. Both column = NULL and NOT (column = NULL) evaluate to unknown. Always use IS NULL / IS NOT NULL for missing-value checks.

Database Compatibility

IN, BETWEEN, NOT, NOT IN, and NOT BETWEEN are part of ANSI SQL and work in:

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

Notable considerations:

  • List length limits for IN. Oracle Database limits the IN list to 1000 expressions. Other systems allow more but may perform poorly with very long lists. For large sets, consider a subquery or a temporary table instead.
  • IN with a subquery. All major systems support WHERE column IN (SELECT ...). Be aware of NULL in the subquery result — the same NOT IN pitfall applies.
  • Date literals in BETWEEN. The DATE '2000-01-01' form is portable to PostgreSQL, MySQL, MariaDB, SQLite, and Oracle Database. SQL Server accepts the plain string '2000-01-01'.
  • Arrays and ANY/ALL. PostgreSQL supports = ANY(array) as a convenient alternative to IN when passing a single array parameter from application code.

When in doubt, consult the documentation for your database and version.

Best Practices

  • Prefer IN over long OR chains. It reads better and is easier to maintain.
  • Prefer BETWEEN for inclusive numeric ranges. For date/timestamp ranges, prefer explicit >= and < comparisons to avoid boundary bugs.
  • Handle NULL explicitly with NOT IN. Filter with IS NOT NULL first, or ensure your list never contains NULL.
  • Keep IN lists short. Very long lists reduce readability and may hit database-specific limits. Consider a subquery, a join, or a temporary table for large sets.
  • Parenthesize when combining with AND and OR. Even though IN, BETWEEN, and NOT bind clearly on their own, mixing them with OR can still cause precedence bugs.
  • Use parameters, not string concatenation. This prevents SQL injection and lets the database reuse the execution plan.
  • Verify destructive filters with SELECT first. Especially when using NOT IN or NOT BETWEEN, run a SELECT to confirm the target rows before any UPDATE or DELETE.

Warning: Running UPDATE or DELETE with NOT IN on a column that contains NULL values can produce unexpected results. Practice these statements only in a disposable learning database or after taking a verified backup.

Conclusion

You learned how to use IN to match a value against a list, BETWEEN to match an inclusive range, and NOT to negate any of these conditions. You also saw how each operator interacts with NULL — most importantly, why NOT IN combined with a NULL in the list returns no rows at all.

How do I search for patterns using LIKE and wildcards in SQL?

When you filter data with WHERE, exact comparisons such as = are not always enough. You often need to find rows where a text column starts with, ends with, or contains a certain fragment. For example, you may want every customer whose email ends with @gmail.com, or every book whose title begins with “The”.

SQL provides the LIKE operator together with wildcard characters to solve this problem. In this tutorial, you will learn how LIKE works, what the % and _ wildcards mean, how to escape special characters, and how different databases handle case sensitivity.

Prerequisites

To follow along, you need:

  • A working database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).
  • Basic knowledge of the SELECT and WHERE clauses.
  • A client such as psql, MySQL Workbench, DBeaver, or the IntelliJ IDEA Database tool window.

If you are new to filtering rows, review the earlier tutorial How Do I Filter SQL Query Results Using WHERE? first.

Sample Database

We will continue with the online bookstore domain used earlier in this series. The examples use a single customers table so that pattern matching remains the focus.

CREATE TABLE customers (
    customer_id   INTEGER PRIMARY KEY,
    customer_name VARCHAR(100) NOT NULL,
    email         VARCHAR(150),
    city          VARCHAR(100),
    phone_number  VARCHAR(20)
);

INSERT INTO customers (customer_id, customer_name, email, city, phone_number) VALUES
    (1, 'Alice Johnson',   '[email protected]',       'London',    '+44-20-7946-0001'),
    (2, 'Bob Smith',       '[email protected]',   'Liverpool', '+44-20-7946-0002'),
    (3, 'Charlie Brown',   '[email protected]',     'Leeds',     '+44-20-7946-0003'),
    (4, 'Diana Prince',    '[email protected]',     'Manchester','+44-20-7946-0004'),
    (5, 'Ethan Hunt',      '[email protected]',     'Bristol',   NULL),
    (6, 'Fiona O''Neill',  '[email protected]',   'Dublin',    '+353-1-555-0006'),
    (7, 'George Miller',   NULL,                    'London',    '+44-20-7946-0007');

Basic Syntax

The general form of a pattern-matching query is:

SELECT column_list
FROM table_name
WHERE column_name LIKE 'pattern';

LIKE compares a text value to a pattern that may contain wildcard characters:

Wildcard Meaning
% Matches zero or more characters of any kind.
_ Matches exactly one character of any kind.

You can also use NOT LIKE to invert the match.

Practical Example

Suppose we want every customer whose email is hosted on Gmail:

SELECT
    customer_id,
    customer_name,
    email
FROM customers
WHERE email LIKE '%@gmail.com';

How to read this query:

  1. FROM customers supplies the rows.
  2. WHERE email LIKE '%@gmail.com' keeps only rows whose email ends with @gmail.com. The % matches any characters (including none) that appear before @gmail.com.
  3. The SELECT list returns three columns for each matching row.

Expected Result

customer_id customer_name email
1 Alice Johnson [email protected]
3 Charlie Brown [email protected]
5 Ethan Hunt [email protected]

Note that George Miller does not appear because his email is NULL. LIKE never matches NULL.

Additional Examples

Starts With

Find every customer whose name starts with the letter A:

SELECT customer_id, customer_name
FROM customers
WHERE customer_name LIKE 'A%';

Expected result:

customer_id customer_name
1 Alice Johnson

Ends With

Find every customer living in a city ending with pool:

SELECT customer_id, customer_name, city
FROM customers
WHERE city LIKE '%pool';

Expected result:

customer_id customer_name city
2 Bob Smith Liverpool

Contains

Find every customer whose name contains on:

SELECT customer_id, customer_name
FROM customers
WHERE customer_name LIKE '%on%';

Expected result:

customer_id customer_name
1 Alice Johnson
4 Diana Prince
6 Fiona O’Neill

Fixed-Length Match with _

The _ wildcard matches exactly one character. To find every UK phone number where the area code is three digits after +44- and starts with 20-79, and the next two digits can be anything:

SELECT customer_name, phone_number
FROM customers
WHERE phone_number LIKE '+44-20-79__-____';

Expected result:

customer_name phone_number
Alice Johnson +44-20-7946-0001
Bob Smith +44-20-7946-0002
Charlie Brown +44-20-7946-0003
Diana Prince +44-20-7946-0004
Ethan Hunt is skipped because his phone_number is NULL.

Negation with NOT LIKE

Find customers whose email is not a Gmail address (and is known):

SELECT customer_id, customer_name, email
FROM customers
WHERE email NOT LIKE '%@gmail.com'
  AND email IS NOT NULL;

Expected result:

customer_id customer_name email
2 Bob Smith [email protected]
4 Diana Prince [email protected]
6 Fiona O’Neill [email protected]

The extra IS NOT NULL is required because NOT LIKE still returns UNKNOWN for NULL values, and rows with UNKNOWN conditions are excluded from the result.

Escaping Wildcard Characters

What if the text you are searching for actually contains a % or _ character? Use the ESCAPE clause to define an escape character.

For example, to find emails that literally contain an underscore:

SELECT customer_id, email
FROM customers
WHERE email LIKE '%\_%' ESCAPE '\';

Expected result:

customer_id email
5 [email protected]

Any character can serve as the escape character; \ is a common choice.

Common Mistakes

Using = Instead of LIKE

Incorrect:

SELECT *
FROM customers
WHERE email = '%@gmail.com';

This looks for a literal string %@gmail.com and returns no rows. Wildcards work only with LIKE, not with =.

Correct:

SELECT *
FROM customers
WHERE email LIKE '%@gmail.com';

Forgetting That LIKE Ignores NULL

A condition such as email LIKE '%' does not match rows where email IS NULL. If you need those rows too, add an explicit IS NULL check:

SELECT *
FROM customers
WHERE email LIKE '%'
   OR email IS NULL;

Assuming LIKE Is Always Case-Insensitive

Case sensitivity depends on the database and column collation. LIKE 'a%' may or may not match Alice. See the compatibility notes below.

Leading Wildcards and Performance

A pattern such as LIKE '%gmail.com' prevents most databases from using a standard B-tree index on the column, because the search does not start from the beginning of the string. On small learning datasets this is fine, but for large tables it can be slow. Consider full-text search or specialized indexes when this becomes a problem, and always inspect the execution plan with EXPLAIN before drawing conclusions.

Database Compatibility

The LIKE operator and the % and _ wildcards are part of the SQL standard and are supported by every major database. However, some behaviors differ.

PostgreSQL

  • LIKE is case-sensitive.
  • Use ILIKE for a case-insensitive match:
SELECT *
FROM customers
WHERE customer_name ILIKE 'a%';

MySQL / MariaDB

  • LIKE is case-insensitive by default because most character collations end in _ci (case-insensitive).
  • Use LIKE BINARY to force case-sensitive comparison:
SELECT *
FROM customers
WHERE customer_name LIKE BINARY 'a%';

SQL Server

  • Case sensitivity depends on the column or database collation.
  • Supports additional bracket wildcards such as [abc], [^abc], and character ranges like [a-c]:
SELECT *
FROM customers
WHERE customer_name LIKE '[A-C]%';

These bracket expressions are not portable to other databases.

Oracle Database

  • LIKE is case-sensitive.
  • Combine with UPPER or LOWER for case-insensitive matches:
SELECT *
FROM customers
WHERE UPPER(customer_name) LIKE 'A%';

SQLite

  • LIKE is case-insensitive for ASCII characters by default.
  • Use GLOB for case-sensitive, Unix-style wildcard matching (* and ?).

When you need advanced pattern matching that goes beyond LIKE, most databases also support regular expressions through operators or functions such as ~ (PostgreSQL), REGEXP (MySQL, MariaDB, SQLite), and REGEXP_LIKE (Oracle). Consult your database documentation before relying on them.

Best Practices

  • Use LIKE only when you actually need pattern matching. For exact matches, use = because it is simpler and can use indexes efficiently.
  • Anchor patterns when possible. LIKE 'gmail%' is usually faster than LIKE '%gmail%' because the leading part is fixed.
  • Always add IS NULL handling when your logic must include or exclude unknown values.
  • Escape user input properly. When using LIKE from application code, use parameterized queries and escape the % and _ characters inside the input:
String sql = """
        SELECT customer_id, customer_name, email
        FROM customers
        WHERE email LIKE ? ESCAPE '\\'
        """;

try (PreparedStatement statement = connection.prepareStatement(sql)) {
    String safeInput = userInput
            .replace("\\", "\\\\")
            .replace("%",  "\\%")
            .replace("_",  "\\_");

    statement.setString(1, "%" + safeInput + "%");

    try (ResultSet resultSet = statement.executeQuery()) {
        while (resultSet.next()) {
            long   customerId   = resultSet.getLong("customer_id");
            String customerName = resultSet.getString("customer_name");
            String email        = resultSet.getString("email");
        }
    }
}

This prevents both SQL injection and accidental wildcard behavior caused by raw user input.

  • Document the intended case sensitivity of a query, especially when the same code runs against multiple database systems.

Conclusion

You learned how to search for text patterns in SQL using the LIKE operator with the % and _ wildcards, how to use NOT LIKE, how to escape special characters with ESCAPE, and how case sensitivity differs across database systems. Remember that LIKE never matches NULL, and that patterns starting with % may slow down queries on large tables.

How do I use comparison and logical operators in SQL?

When you write SQL, every meaningful query eventually needs a condition — a boolean expression that decides which rows are returned, updated, or deleted. Conditions are built from two small but essential building blocks: comparison operators (=, <>, <, >, <=, >=) and logical operators (AND, OR, NOT).

In this tutorial you will learn what each operator does, how they combine, how operator precedence affects the result, and how NULL interacts with them through SQL’s three-valued logic. By the end, you will be able to build precise conditions with confidence.

Prerequisites

To follow along you need:

  • A working SQL database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle Database, or SQLite).
  • A client that can execute SQL statements.
  • Basic familiarity with the SELECT and WHERE clauses.

If you already followed the previous tutorial on WHERE, you can reuse the same sample tables.

Sample Database

We continue with the online bookstore schema so the examples stay consistent across the series.

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);

Row 9 intentionally has NULL in category — we will use it to show how logical operators behave when a value is missing.

Comparison Operators

A comparison operator returns one of three values: true, false, or unknown (when either side is NULL).

Operator Meaning Example
= Equal to price = 12.50
<> or != Not equal to category <> 'Classic'
< Less than stock < 5
> Greater than price > 15.00
<= Less than or equal to stock <= 10
>= Greater than or equal to published_on >= DATE '2000-01-01'

<> is the ANSI-standard “not equal” operator. != is accepted by most databases but is not part of the standard.

Example: Equality and Inequality

Find every classic book:

SELECT title, category
FROM books
WHERE category = 'Classic';

Find every book that is not a classic:

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

Notice that this second query does not return the row where category IS NULL. Comparing NULL with <> produces unknown, and WHERE keeps only rows where the condition is true. We will return to this behavior in the common mistakes section.

Example: Ordered Comparisons

Ordered operators (<, >, <=, >=) work on numbers, dates, and strings (using the column’s collation for strings).

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

Expected result:

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

Logical Operators

Logical operators combine boolean expressions into more precise conditions.

Operator Meaning
AND True when both sides are true.
OR True when at least one side is true.
NOT Inverts a boolean: true becomes false, false becomes true, NULL stays NULL.

Example: AND

Return classic books that are currently in stock:

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

Expected result:

title category stock
Pride and Prejudice Classic 20
One Hundred Years of Solitude Classic 25

Emma and Love in the Time of Cholera are excluded because their stock is 0.

Example: OR

Return every book that is either a Classic or a Fiction title:

SELECT title, category
FROM books
WHERE category = 'Classic'
   OR category = 'Fiction';

Example: NOT

Return every book that is not cheap (using NOT to negate a condition):

SELECT title, price
FROM books
WHERE NOT price < 15.00;

This is equivalent to WHERE price >= 15.00, but NOT is useful when the inner expression is more complex — for example, NOT (category = 'Classic' AND stock = 0).

Operator Precedence

When you combine operators in one condition, SQL evaluates them in this order (highest to lowest):

  1. Comparison operators (=, <>, <, >, <=, >=)
  2. NOT
  3. AND
  4. OR

That means AND binds tighter than OR. The following two conditions are not equivalent:

-- Interpreted as: category = 'Classic'
--                 OR (category = 'Fiction' AND price < 20.00)
WHERE category = 'Classic'
OR category = 'Fiction'
AND price < 20.00;
-- Every classic OR fiction book cheaper than 20.00
WHERE (category = 'Classic' OR category = 'Fiction')
AND price < 20.00;

When you mix AND and OR, always use parentheses. They make intent explicit and prevent subtle bugs.

Three-Valued Logic and NULL

SQL logic has three possible outcomes: true, false, and unknown. Any comparison involving NULL produces unknown.

The truth tables below explain how AND, OR, and NOT handle unknown values.

AND

Left Right Result
true true true
true false false
true unknown unknown
false anything false
unknown unknown unknown

OR

Left Right Result
true anything true
false false false
false unknown unknown
unknown unknown unknown

NOT

Operand Result
true false
false true
unknown unknown

WHERE keeps a row only when its condition evaluates to true. Rows with an unknown result are silently excluded — this is often surprising to newcomers.

Example: NULL and Inequality

You might expect the following query to return the row Unknown Title (which has category = NULL):

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

It does not. NULL <> 'Classic' evaluates to unknown, so the row is filtered out. To include rows with missing categories, add an explicit check:

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

Use IS NULL and IS NOT NULL to test for missing values — never = NULL or <> NULL.

Combining Comparison and Logical Operators

Here is a realistic query that uses several operators together. “Show contemporary or historical books that are in stock and cost at most 16.00.”

SELECT title,
       category,
       price,
       stock
FROM books
WHERE (category = 'Contemporary' OR category = 'Historical')
  AND stock > 0
  AND price <= 16.00
ORDER BY price;

Expected result:

title category price stock
Americanah Contemporary 14.50 3
Half of a Yellow Sun Historical 16.00 8

Reading the condition clause by clause:

  1. (category = 'Contemporary' OR category = 'Historical') — restrict to two categories.
  2. AND stock > 0 — only books currently available.
  3. AND price <= 16.00 — apply the budget constraint.

The parentheses around the OR are required; without them, AND would bind first and produce a very different result.

Common Mistakes

Comparing to NULL with = or <>

Incorrect:

SELECT *
FROM books
WHERE category = NULL;

Correct:

SELECT *
FROM books
WHERE category IS NULL;

category = NULL always evaluates to unknown, so the query returns zero rows on every standard-compliant database.

Forgetting Parentheses Around OR

Incorrect (probably not what was intended):

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

Correct:

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

Assuming NOT (a = b) Is the Same as a <> b When NULL Is Possible

Both NOT (category = 'Classic') and category <> 'Classic' evaluate to unknown when category is NULL, so both exclude those rows. If you want to include rows with missing values, add OR category IS NULL explicitly.

Mixing AND/OR in UPDATE and DELETE

Before running a destructive statement, verify the filter with SELECT:

SELECT *
FROM books
WHERE category = 'Classic'
  AND stock = 0;

Only after confirming the target rows should you run:

DELETE FROM books
WHERE category = 'Classic'
  AND stock = 0;

Warning: Running UPDATE or DELETE with a wrong combination of AND/OR can silently modify far more rows than expected. Practice these statements only in a disposable learning database or after taking a verified backup.

Database Compatibility

The comparison operators (=, <>, <, >, <=, >=) and the logical operators (AND, OR, NOT) are part of ANSI SQL and are supported by:

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

Minor differences to be aware of:

  • != versus <>. Both are widely supported, but only <> is standard. Prefer <> for portability.
  • String comparison case sensitivity. = and <> on text depend on the column collation. PostgreSQL is case-sensitive by default; MySQL and SQL Server are usually case-insensitive.
  • Boolean expressions. PostgreSQL has a native BOOLEAN type, so WHERE is_active works directly. In SQL Server and Oracle Database you typically compare against 1/0 or 'Y'/'N' flags.
  • Three-valued logic. All major systems follow it, but some client tools display unknown as an empty cell rather than NULL.

When in doubt, consult the documentation for your database and version.

Best Practices

  • Use <> instead of != for portable code.
  • Always parenthesize when mixing AND and OR. Readers should never have to remember precedence rules.
  • Handle NULL explicitly with IS NULL / IS NOT NULL when a column is nullable and those rows matter.
  • Prefer set-based operators when they read better. For fixed lists, IN (...) is clearer than a chain of ORs. For inclusive ranges, BETWEEN low AND high is clearer than two comparisons.
  • Verify destructive filters with SELECT first. This is especially important when the condition mixes AND and OR.
  • Use parameters, not string concatenation, when comparison values come from application input. This prevents SQL injection and improves plan reuse.

Conclusion

You learned how SQL comparison operators (=, <>, <, >, <=, >=) and logical operators (AND, OR, NOT) work together to form the conditions that drive every WHERE clause. You also saw how operator precedence and three-valued logic affect the outcome, and how to protect yourself with parentheses and explicit NULL checks.

How do I work with NULL values in SQL?

Real data is rarely complete. A customer may not have provided a phone number, an order may not yet have a shipping date, and a book may not yet have been assigned a category. SQL uses a special marker, NULL, to represent this missing or unknown information. Working with NULL correctly is one of the most important skills in SQL, because NULL does not behave like ordinary values in comparisons, arithmetic, or aggregation.

In this tutorial, you will learn what NULL really means, how to test for it, how it interacts with operators and functions, and how to replace it with sensible defaults. You will also see the most common mistakes beginners make with NULL and how to avoid them.

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 have not created a sample database yet, the setup script below will produce everything you need.

Sample Database

We will 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),
    discount      DECIMAL(4, 2),
    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',  NULL),
    (4, 'Gabriel Garcia Marquez',   'Colombia');

INSERT INTO books (book_id, title, author_id, category, price, discount, published_on) VALUES
    (1, 'Pride and Prejudice',           1, 'Classic',    12.50, 1.00, '1813-01-28'),
    (2, 'Emma',                          1, 'Classic',    10.00, NULL, '1815-12-23'),
    (3, 'Norwegian Wood',                2, 'Fiction',    15.75, 2.00, '1987-09-04'),
    (4, 'Kafka on the Shore',            2, 'Fiction',    18.20, NULL, '2002-09-12'),
    (5, 'Half of a Yellow Sun',          3, 'Historical', 16.00, 1.50, '2006-08-11'),
    (6, 'Americanah',                    3, 'Contemporary', NULL, NULL, '2013-05-14'),
    (7, 'One Hundred Years of Solitude', 4, 'Classic',    22.00, 3.00, '1967-05-30'),
    (8, 'Untitled Draft',                2,  NULL,         NULL, NULL,  NULL);

Notice that several columns intentionally contain NULL:

  • authors.country is NULL for one author (unknown country).
  • books.category, books.price, books.discount, and books.published_on are NULL for one or more books.

We will use these rows to demonstrate how NULL behaves.

What Does NULL Actually Mean?

NULL is a marker that represents missing, unknown, or unavailable information. It is not:

  • the number zero;
  • an empty string '';
  • the boolean value false;
  • a “default” value.

Two NULL values are not considered equal to each other in comparisons, because “unknown = unknown” is itself unknown.

Three-Valued Logic

Most languages use two-valued boolean logic: a condition is either true or false. SQL uses three-valued logic: true, false, and unknown. Any comparison involving NULL returns unknown.

Expression Result
1 = 1 true
1 = 2 false
1 = NULL unknown
NULL = NULL unknown
NULL <> 'x' unknown

The WHERE clause keeps a row only when the condition evaluates to true. Rows for which the condition is false or unknown are excluded.

Basic Syntax

To test whether a column contains NULL, use IS NULL or IS NOT NULL:

SELECT column_list
FROM table_name
WHERE column_name IS NULL;
SELECT column_list
FROM table_name
WHERE column_name IS NOT NULL;

Do not use = or <> with NULL. They will not raise an error, but they will never match anything.

Practical Example

Suppose the bookstore manager wants to see every book that is missing a category, so the catalog team can fill in the gaps.

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

Read the query in logical order:

  1. FROM books — start with every row in the books table.
  2. WHERE category IS NULL — keep only rows whose category is missing.
  3. SELECT book_id, title, category — return these three columns.

Expected Result

book_id title category
8 Untitled Draft NULL

Only one row is returned because only Untitled Draft has a missing category.

Additional Examples

Finding Rows Where a Column Is Not NULL

To list every book that already has a category:

SELECT
    book_id,
    title,
    category
FROM books
WHERE category IS NOT NULL
ORDER BY book_id;

NULL and Inequality

A common surprise is that <> does not return rows where the column is NULL:

SELECT
    book_id,
    title,
    category
FROM books
WHERE category <> 'Classic';

This query returns fiction, historical, and contemporary books, but not Untitled Draft, because NULL <> 'Classic' is unknown. To include rows with a missing category, add an explicit NULL check:

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

NULL in Arithmetic

Any arithmetic expression involving NULL produces NULL:

SELECT
    book_id,
    title,
    price,
    discount,
    price - discount AS final_price
FROM books;

For any row where either price or discount is NULL, final_price is also NULL. Zero and NULL are not interchangeable.

Replacing NULL with COALESCE

COALESCE returns the first non-NULL argument. It is the portable, ANSI-standard way to substitute a default value for a missing one.

SELECT
    book_id,
    title,
    price,
    discount,
    price - COALESCE(discount, 0) AS final_price
FROM books;

Now the discount is treated as 0 when it is missing, and final_price is computed correctly for every row that has a price.

You can pass more than two arguments; COALESCE returns the first one that is not NULL:

SELECT
    author_id,
    author_name,
    COALESCE(country, 'Unknown') AS country
FROM authors;

NULLIF: The Inverse of COALESCE

NULLIF(a, b) returns NULL when a equals b, and returns a otherwise. It is useful for avoiding division by zero:

SELECT
    book_id,
    title,
    price / NULLIF(discount, 0) AS price_to_discount_ratio
FROM books;

If discount is 0, NULLIF converts it to NULL, and the division produces NULL instead of raising an error.

NULL and Aggregate Functions

Most aggregate functions ignore NULL values:

SELECT
    COUNT(*)          AS total_rows,
    COUNT(price)      AS rows_with_price,
    AVG(price)        AS average_price,
    SUM(discount)     AS total_discount
FROM books;
  • COUNT(*) counts every row, including rows where all columns are NULL.
  • COUNT(price) counts only rows where price IS NOT NULL.
  • AVG(price) is the average of the non-NULL prices; it is not affected by rows where price is NULL.
  • SUM(discount) adds only non-NULL discounts. If every value is NULL, SUM returns NULL, not 0.

This distinction matters when you report metrics: AVG over a partially-null column is the average of the values that exist, not the average of “value or zero”.

NULL and ORDER BY

Databases differ in where NULL values appear when you sort:

  • PostgreSQL and Oracle Database place NULL last in ascending order by default.
  • MySQL, MariaDB, SQL Server, and SQLite place NULL first in ascending order by default.

To control the placement portably where supported, use NULLS FIRST or NULLS LAST (PostgreSQL, Oracle Database, and SQLite 3.30+):

SELECT
    book_id,
    title,
    published_on
FROM books
ORDER BY published_on ASC NULLS LAST;

MySQL, MariaDB, and SQL Server do not support NULLS LAST directly; you can emulate it with an expression:

SELECT
    book_id,
    title,
    published_on
FROM books
ORDER BY
    CASE WHEN published_on IS NULL THEN 1 ELSE 0 END,
    published_on ASC;

NULL and DISTINCT / GROUP BY

DISTINCT and GROUP BY treat all NULL values as belonging to a single group, even though NULL = NULL is not true in a comparison:

SELECT DISTINCT category
FROM books;

This query returns each category once, plus a single row for NULL.

NULL and IN

Be careful when a subquery used with IN can return NULL:

SELECT title
FROM books
WHERE author_id NOT IN (SELECT author_id FROM authors WHERE country IS NULL);

If the subquery returns any NULL, the entire NOT IN condition becomes unknown for every row, and no rows are returned. Prefer NOT EXISTS when the subquery may produce NULL:

SELECT b.title
FROM books AS b
WHERE NOT EXISTS (
    SELECT 1
    FROM authors AS a
    WHERE a.author_id = b.author_id
      AND a.country IS NULL
);

Using NULL from Application Code

When you fetch nullable columns in JDBC, primitive types cannot represent NULL. Use the wrapper getters, or explicitly test the result with ResultSet.wasNull() after reading the value:

String sql = """
        SELECT book_id, title, price, discount
        FROM books
        WHERE book_id = ?
        """;

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

    try (ResultSet resultSet = statement.executeQuery()) {
        if (resultSet.next()) {
            BigDecimal price = resultSet.getBigDecimal("price");
            BigDecimal discount = resultSet.getBigDecimal("discount");

            // getBigDecimal returns null for SQL NULL,
            // so an explicit wasNull() check is optional here.
            if (discount == null) {
                discount = BigDecimal.ZERO;
            }

            BigDecimal finalPrice = price == null
                    ? null
                    : price.subtract(discount);
        }
    }
}

When inserting a NULL, use PreparedStatement.setNull(index, sqlType) rather than passing an empty string or zero.

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 because category = NULL evaluates to unknown, never true.

Assuming NULL Equals NULL

The following query does not return every row:

SELECT *
FROM books
WHERE category = category;

For rows where category IS NULL, the expression NULL = NULL is unknown, so those rows are excluded. If you truly want every row, remove the WHERE clause.

Treating NULL as Zero in Arithmetic

Incorrect assumption:

SELECT
    book_id,
    price - discount AS final_price
FROM books;

For rows where discount IS NULL, final_price will also be NULL — not price. Use COALESCE:

SELECT
    book_id,
    price - COALESCE(discount, 0) AS final_price
FROM books;

Using NOT IN with a Nullable Subquery

If a subquery inside NOT IN can return NULL, the outer query silently returns no rows. Prefer NOT EXISTS when nullability is possible.

Expecting Aggregates to Fail on NULL

Aggregates skip NULL instead of raising an error. If AVG(price) looks “too high” or SUM(discount) looks “too low”, check whether the source column contains NULL values and whether that is the behavior you want.

Database Compatibility

NULL, IS NULL, IS NOT NULL, COALESCE, and NULLIF are part of standard SQL and work in:

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

Notable differences:

  • NULLS FIRST / NULLS LAST are supported by PostgreSQL, Oracle Database, and SQLite 3.30 and later. MySQL, MariaDB, and SQL Server require a CASE expression in ORDER BY to emulate the behavior.
  • Default NULL ordering differs between databases, as noted earlier.
  • ISNULL exists in both SQL Server and MySQL, but with different meanings. In SQL Server, ISNULL(a, b) behaves like COALESCE(a, b). In MySQL, ISNULL(x) is a single-argument function that returns 1 when x IS NULL. Prefer the portable COALESCE and IS NULL forms.
  • NVL is Oracle Database’s non-standard equivalent of COALESCE with two arguments. Prefer COALESCE for portability.
  • Empty string vs. NULL. Oracle Database historically treats an empty string '' as NULL for VARCHAR2 columns. Other databases treat '' and NULL as distinct values.

Consult the documentation for your database and version when in doubt.

Best Practices

  • Model missing data deliberately. Decide up front whether a column should allow NULL. If a value is always required, declare the column NOT NULL.
  • Use IS NULL / IS NOT NULL for tests. Never write = NULL or <> NULL.
  • Use COALESCE for portable defaults. It is standard SQL and easier to read than vendor-specific functions such as NVL or ISNULL.
  • Guard against NULL in arithmetic. Wrap potentially null operands with COALESCE(column, 0) when zero is the correct substitute.
  • Beware of NOT IN with nullable subqueries. Prefer NOT EXISTS.
  • Document nullability at the application boundary. In Java, use wrapper types or Optional for columns that can be NULL, and use PreparedStatement.setNull when inserting.
  • Check aggregate semantics. Know that COUNT(*), COUNT(column), and SUM(column) treat NULL differently.
  • Be explicit about ordering. If sort order matters, specify NULLS FIRST or NULLS LAST where supported, or emulate it with CASE.

Conclusion

You learned that NULL represents missing or unknown information in SQL, that comparisons involving NULL use three-valued logic, and that you must use IS NULL and IS NOT NULL instead of = and <> to test for it. You also saw how COALESCE and NULLIF help you substitute defaults and guard against division by zero, and how NULL affects arithmetic, aggregation, sorting, and NOT IN. Treating NULL carefully will save you from a large class of subtle, silent bugs.

How do I delete data safely from a SQL table?

Removing data from a database sounds simple, but a single missing clause can wipe out an entire table. In this tutorial, you will learn how to use the SQL DELETE statement safely: how to target the exact rows you want to remove, how to verify your target before deleting, and how to use transactions to recover from mistakes.

By the end, you will be able to confidently delete rows without risking accidental data loss.

Prerequisites

To follow along, you should:

  • Have a working SQL database (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, or SQLite).
  • Know how to run SQL statements in a client such as psql, MySQL Workbench, DBeaver, or the command line.
  • Be familiar with SELECT and WHERE from earlier tutorials.

If you are new to SQL, start with How do I retrieve data using a SELECT statement? and How do I filter SQL query results using WHERE?

Sample Database

We will use an online bookstore as our sample domain. Create a customers table and insert a few rows so you can practice deletions safely.

CREATE TABLE customers (
    customer_id   INTEGER PRIMARY KEY,
    customer_name VARCHAR(100) NOT NULL,
    email         VARCHAR(150) NOT NULL,
    status        VARCHAR(20)  NOT NULL,
    created_at    DATE         NOT NULL
);

INSERT INTO customers (customer_id, customer_name, email, status, created_at) VALUES
    (1, 'Alice Johnson',   '[email protected]',   'active',   '2024-01-15'),
    (2, 'Bob Smith',       '[email protected]',     'inactive', '2023-06-20'),
    (3, 'Carol Davis',     '[email protected]',   'active',   '2024-03-10'),
    (4, 'David Miller',    '[email protected]',   'inactive', '2022-11-05'),
    (5, 'Eva Thompson',    '[email protected]',     'active',   '2025-02-28');

This gives us five customers with a mix of active and inactive statuses.

Warning: Run every example in this tutorial in a disposable learning database, never in production.

Basic Syntax

The general form of a DELETE statement is:

DELETE FROM table_name
WHERE condition;

Key points:

  • DELETE FROM table_name names the table you want to remove rows from.
  • WHERE condition decides which rows are removed.
  • Without a WHERE clause, every row in the table is deleted.

DELETE removes rows but keeps the table structure (columns, indexes, constraints) intact.

Step 1: Verify the Target Rows First

Before running any DELETE, always run a SELECT with the same WHERE clause. This is the single most important habit to prevent data loss.

Suppose we want to remove the customer with customer_id = 4. First, check exactly which rows match:

SELECT customer_id, customer_name, email, status
FROM customers
WHERE customer_id = 4;

Expected Result

customer_id customer_name email status
4 David Miller [email protected] inactive

The result shows exactly one row, which is what we expect. Now the DELETE is safe to run.

Step 2: Perform the Delete

Reuse the same WHERE clause you just verified:

DELETE FROM customers
WHERE customer_id = 4;

The database will report the number of rows affected (for example, 1 row deleted). Confirm the row is gone:

SELECT customer_id, customer_name
FROM customers
ORDER BY customer_id;

Expected Result

customer_id customer_name
1 Alice Johnson
2 Bob Smith
3 Carol Davis
5 Eva Thompson

Additional Examples

Deleting Multiple Rows with a Condition

Remove all customers whose status is inactive:

SELECT customer_id, customer_name, status
FROM customers
WHERE status = 'inactive';

If the result matches the rows you intend to delete, run:

DELETE FROM customers
WHERE status = 'inactive';

Deleting Rows That Match a Date Condition

Remove customers created before 2024:

DELETE FROM customers
WHERE created_at < DATE '2024-01-01';

Handling NULL in Delete Conditions

If a column allows NULL, remember that NULL is not a value you can compare with =. To delete rows where email is missing:

DELETE FROM customers
WHERE email IS NULL;

Never write WHERE email = NULL. That condition is never true, so no rows are deleted, and you might incorrectly assume the table has no such rows.

Using a Transaction to Stay Safe

A transaction lets you delete rows and then decide whether to keep the change (COMMIT) or undo it (ROLLBACK).

BEGIN;

DELETE FROM customers
WHERE status = 'inactive';

-- Inspect the effect before committing
SELECT customer_id, customer_name, status
FROM customers;

-- If something looks wrong:
ROLLBACK;

-- If everything looks correct:
-- COMMIT;

Explanation:

  1. BEGIN starts a transaction.
  2. The DELETE removes matching rows, but the change is not yet permanent.
  3. You verify the result with a SELECT.
  4. ROLLBACK reverts the deletion. COMMIT makes it permanent.

Transaction behavior varies between database systems and storage engines. In MySQL, for example, the underlying engine must be transactional (InnoDB) for ROLLBACK to work.

Using Parameters from Application Code

When you delete from Java, always use parameterized queries instead of string concatenation:

String sql = """
        DELETE FROM customers
        WHERE customer_id = ?
        """;

try (PreparedStatement statement = connection.prepareStatement(sql)) {
    statement.setLong(1, customerId);
    int rowsDeleted = statement.executeUpdate();
    System.out.println(rowsDeleted + " row(s) deleted.");
}

Parameters protect against SQL injection and correctly handle data types.

Common Mistakes

Forgetting the WHERE Clause

Incorrect:

DELETE FROM customers;

This removes every row in the customers table. Unless you truly intend that, always include a WHERE clause.

Comparing to NULL with =

Incorrect:

DELETE FROM customers
WHERE email = NULL;

Correct:

DELETE FROM customers
WHERE email IS NULL;

NULL represents missing information, so equality comparisons with NULL always return unknown, not true.

Confusing DELETE with TRUNCATE and DROP

  • DELETE removes rows and can be rolled back inside a transaction.
  • TRUNCATE removes all rows quickly but is usually non-transactional and may reset auto-increment counters.
  • DROP TABLE removes the entire table, including its structure.

Do not use TRUNCATE or DROP TABLE when you only want to remove selected rows.

Ignoring Foreign Key Constraints

If another table references the row you are deleting (for example, an orders table with a customer_id foreign key), the database may:

  • reject the delete;
  • cascade the delete to related rows (ON DELETE CASCADE);
  • set related columns to NULL (ON DELETE SET NULL).

Understand the constraints before deleting parent rows.

Database Compatibility

The basic DELETE ... WHERE ... syntax is standard SQL and works in:

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

Differences to be aware of:

  • DELETE with JOIN: MySQL, MariaDB, and SQL Server support multi-table delete syntax. PostgreSQL uses DELETE ... USING. Oracle and SQLite require subqueries.
  • RETURNING clause: PostgreSQL and Oracle (RETURNING INTO) support returning deleted rows. MySQL and SQLite do not.
  • Auto-commit: Some clients auto-commit each statement. Explicitly use BEGIN / COMMIT when you need transactional safety.

When in doubt, consult the documentation for your database version.

Best Practices

  • Always run SELECT first with the same WHERE clause you plan to use in DELETE.
  • Never omit WHERE unless you deliberately want to empty the table.
  • Wrap risky deletes in a transaction so you can ROLLBACK on mistakes.
  • Back up important data before large or irreversible deletions.
  • Use parameterized queries in application code to avoid SQL injection.
  • Understand foreign keys and cascading rules before deleting parent rows.
  • Prefer a soft delete (for example, status = 'inactive' or a deleted_at timestamp) when you may need to recover the data later.
  • Use least-privilege accounts: application users should only have DELETE rights on tables where it is necessary.

Conclusion

You learned how to delete data safely from a SQL table by verifying the target rows with SELECT, applying DELETE with a precise WHERE clause, and using transactions to guard against mistakes. The most important rule is simple: never run a DELETE you have not first previewed with SELECT.