PL/SQL NOT EQUAL Operator: vs !=, NULL Semantics, and Safe Patterns

Last year I watched a “simple” change request turn into a production incident: someone replaced an equality check with a not-equal check to exclude a department, and suddenly a nightly report dropped hundreds of rows. Nothing crashed. The query still returned data. It was just quietly wrong.

That experience is why I treat the PL/SQL/SQL NOT EQUAL operator as more than basic syntax. Yes, it’s just != or . But the moment NULLs, joins, implicit conversions, or “unknown” logic enter the room, “not equal” stops behaving like the mental model most of us carry around.

I’m going to show you how !=/ behaves in Oracle SQL and in PL/SQL, where it shines, where it bites, and what I actually ship in production when I need “different” semantics. You’ll see runnable examples (tables, inserts, queries, PL/SQL blocks), common mistakes I see in code reviews, and a few patterns that make not-equal checks safer and easier to read.

Two Spellings, One Operator: vs !=

Oracle accepts two not-equal spellings:

  • is the SQL-standard operator.
  • != is widely supported in Oracle and many other systems.

In Oracle SQL and PL/SQL, they mean the same thing for normal comparisons.

I generally recommend:

  • Use in shared SQL codebases (portable habits, less debate).
  • Use != only if your team already standardizes on it.

What I don’t recommend is mixing them within the same codebase. Consistency matters more than picking the “better” glyph.

Basic shape

— Both of these are equivalent in Oracle

SELECT CASE WHEN 10 20 THEN ‘different‘ ELSE ‘same‘ END AS result FROM dual;

SELECT CASE WHEN 10 != 20 THEN ‘different‘ ELSE ‘same‘ END AS result FROM dual;

That looks boring (and it is) until you bring NULL into play.

The Part Everyone Trips On: NOT EQUAL and NULL (Three-Valued Logic)

Oracle SQL does not operate in a pure true/false world. It operates in true/false/NULL (often described as “unknown”).

Here’s the key rule:

  • Any comparison to NULL using =, , !=, <, >, etc. yields NULL (unknown), not true or false.

This is not a PL/SQL quirk. It’s the underlying SQL truth model.

Quick reality check

SELECT

CASE WHEN 1 1 THEN ‘T‘ WHEN 1 1 IS NULL THEN ‘NULL‘ ELSE ‘F‘ END AS onevsone,

CASE WHEN 1 2 THEN ‘T‘ WHEN 1 2 IS NULL THEN ‘NULL‘ ELSE ‘F‘ END AS onevstwo,

CASE WHEN 1 NULL THEN ‘T‘ WHEN 1 NULL IS NULL THEN ‘NULL‘ ELSE ‘F‘ END AS onevsnull,

CASE WHEN NULL NULL THEN ‘T‘ WHEN NULL NULL IS NULL THEN ‘NULL‘ ELSE ‘F‘ END AS nullvsnull

FROM dual;

Expected outcomes:

  • 1 1 is false.
  • 1 2 is true.
  • 1 NULL is NULL (unknown).
  • NULL NULL is NULL (unknown).

Why this matters in a WHERE clause

A WHERE clause filters rows by keeping those where the predicate evaluates to TRUE.

  • TRUE: row kept
  • FALSE: row removed
  • NULL (unknown): row removed

So if you write:

SELECT *

FROM employees

WHERE dept_id 101;

you are not saying “department is not 101.”

You are saying:

  • “department is not 101” AND
  • “department is not NULL”

because deptid 101 becomes NULL when deptid is NULL, and NULL doesn’t pass the filter.

In my experience, the fastest way to ship a subtle bug is to forget this behavior and assume automatically includes NULLs as “not equal.” It doesn’t.

Filtering Rows with NOT EQUAL in WHERE (and Why “Not 101” Is More Than It Looks)

Let’s build a small dataset you can run in SQL*Plus, SQLcl, or any Oracle client.

Setup: employees + departments

BEGIN

EXECUTE IMMEDIATE ‘DROP TABLE employees PURGE‘;

EXCEPTION

WHEN OTHERS THEN

IF SQLCODE != -942 THEN

RAISE;

END IF;

END;

/

BEGIN

EXECUTE IMMEDIATE ‘DROP TABLE departments PURGE‘;

EXCEPTION

WHEN OTHERS THEN

IF SQLCODE != -942 THEN

RAISE;

END IF;

END;

/

CREATE TABLE employees (

emp_id NUMBER PRIMARY KEY,

emp_name VARCHAR2(50) NOT NULL,

dept_id NUMBER

);

CREATE TABLE departments (

dept_id NUMBER PRIMARY KEY,

dept_name VARCHAR2(50) NOT NULL

);

INSERT INTO employees (empid, empname, dept_id) VALUES (1, ‘John Doe‘, 101);

INSERT INTO employees (empid, empname, dept_id) VALUES (2, ‘Jane Smith‘, 102);

INSERT INTO employees (empid, empname, dept_id) VALUES (3, ‘Emily Davis‘, 103);

INSERT INTO employees (empid, empname, dept_id) VALUES (4, ‘Ravi Patel‘, NULL);

INSERT INTO departments (deptid, deptname) VALUES (101, ‘HR‘);

INSERT INTO departments (deptid, deptname) VALUES (102, ‘Finance‘);

INSERT INTO departments (deptid, deptname) VALUES (104, ‘IT‘);

COMMIT;

Notice what I did on purpose:

  • Employee 3 references department 103, which doesn’t exist in departments.
  • Employee 4 has a NULL dept_id.

Those two conditions create the exact edge cases that make not-equal checks tricky.

Basic NOT EQUAL filter

SELECT empid, empname, dept_id

FROM employees

WHERE dept_id 101

ORDER BY emp_id;

What you’ll get:

  • Jane Smith (102)
  • Emily Davis (103)

What you will not get:

  • John Doe (101) because it is equal
  • Ravi Patel (NULL) because NULL 101 is unknown

If you actually want “everyone except dept 101, including unassigned,” you must say so:

SELECT empid, empname, dept_id

FROM employees

WHERE deptid 101 OR deptid IS NULL

ORDER BY emp_id;

I prefer to make this explicit rather than hiding the intent in a function call. When someone reads it six months later, the meaning is obvious.

NOT EQUAL with strings and date types

Two common surprises I see:

1) Trailing blanks (mostly a CHAR issue)

2) Implicit conversions (a “works until it doesn’t” issue)

Example of an implicit conversion risk:

— If dept_id is numeric, don‘t compare to a quoted literal.

— This forces implicit conversion and can change index usage and error behavior.

SELECT emp_id

FROM employees

WHERE dept_id ‘101‘;

You should compare numbers to numbers:

SELECT emp_id

FROM employees

WHERE dept_id 101;

When I review SQL, I flag quoted numerics and “string dates” immediately. They are a reliability tax.

Joins and Anti-Joins: Getting “Employees Not In Department X” Without Surprises

NOT EQUAL inside joins is where people accidentally create row explosions or lose rows.

Don’t use as a join condition to “exclude” matches

I still see code like this:

— This is a classic mistake.

— It creates many matches because every employee joins to every department

— except the one with the same dept_id.

SELECT e.empname, d.deptname

FROM employees e

JOIN departments d

ON e.deptid d.deptid;

If you have 4 employees and 3 departments, that can return up to 12 rows minus a few “accidental equals.” It’s not a filter; it’s a relationship definition, and it’s almost never the relationship you want.

The normal approach: join on equality, then filter

If you want employees whose department is not HR (101), join on equality and filter on the department you don’t want:

SELECT e.empname, d.deptname

FROM employees e

JOIN departments d

ON d.deptid = e.deptid

WHERE d.dept_id 101

ORDER BY e.emp_id;

That will return employees in known departments other than 101 (in our sample, Jane Smith in Finance). It will not return:

  • employees with missing departments (dept_id 103)
  • employees with NULL dept_id

If you want those too, you probably want an outer join.

Outer join pattern: “not HR, including unassigned or unknown departments”

SELECT e.emp_name,

e.dept_id,

d.dept_name

FROM employees e

LEFT JOIN departments d

ON d.deptid = e.deptid

WHERE e.deptid 101 OR e.deptid IS NULL

ORDER BY e.emp_id;

This keeps:

  • dept_id 102 and 103
  • dept_id NULL

and excludes 101.

Anti-join patterns (safer than NOT IN when NULL is around)

A related need is: “employees whose department does not exist in departments.”

Many people reach for NOT IN, and then they get burned by NULL semantics.

If the subquery can return NULL, NOT IN can filter everything out.

I strongly prefer NOT EXISTS for anti-joins:

SELECT e.empid, e.empname, e.dept_id

FROM employees e

WHERE e.dept_id IS NOT NULL

AND NOT EXISTS (

SELECT 1

FROM departments d

WHERE d.deptid = e.deptid

)

ORDER BY e.emp_id;

That returns Emily Davis (103) and nothing else.

If you also want the NULL department as “does not exist,” say it:

SELECT e.empid, e.empname, e.dept_id

FROM employees e

WHERE e.dept_id IS NULL

OR NOT EXISTS (

SELECT 1

FROM departments d

WHERE d.deptid = e.deptid

)

ORDER BY e.emp_id;

PL/SQL Control Flow: IF, CASE, and Guard Clauses That Read Well

In PL/SQL, the operator works the same way, but the consequences show up as control-flow decisions rather than missing rows.

A runnable PL/SQL block: routing by department

SET SERVEROUTPUT ON;

DECLARE

lempid employees.emp_id%TYPE := 2;

ldeptid employees.dept_id%TYPE;

BEGIN

SELECT dept_id

INTO ldeptid

FROM employees

WHERE empid = lemp_id;

— Guard clause: handle NULL explicitly first.

IF ldeptid IS NULL THEN

DBMSOUTPUT.PUTLINE(‘Employee ‘ |

lempid

‘ has no department assigned.‘);

RETURN;

END IF;

IF ldeptid 101 THEN

DBMSOUTPUT.PUTLINE(‘Employee ‘ |

lempid ‘ is not in HR. deptid=‘

ldept_id);

ELSE

DBMSOUTPUT.PUTLINE(‘Employee ‘ |

lempid

‘ is in HR.‘);

END IF;

END;

/

Why I like this style:

  • The NULL case is handled early and loudly.
  • The check reads as plain English.
  • You avoid the mental overhead of remembering how behaves with NULL.

CASE expressions with NOT EQUAL

CASE is great when you want a value, not a branch:

SELECT e.emp_name,

CASE

WHEN e.dept_id IS NULL THEN ‘UNASSIGNED‘

WHEN e.deptid 101 THEN ‘NONHR‘

ELSE ‘HR‘

END AS hr_bucket

FROM employees e

ORDER BY e.emp_id;

This is the same rule: handle NULL first, then compare.

Looping with a NOT EQUAL stop condition

I’ll show a tiny pattern I use in data repair scripts: loop until a value changes.

SET SERVEROUTPUT ON;

DECLARE

lprevdept employees.dept_id%TYPE;

lcurrdept employees.dept_id%TYPE;

lempid employees.emp_id%TYPE := 1;

ltries PLSINTEGER := 0;

BEGIN

SELECT deptid INTO lprevdept FROM employees WHERE empid = lempid;

LOOP

ltries := ltries + 1;

— Imagine an external process updates dept_id; here we just re-read.

SELECT deptid INTO lcurrdept FROM employees WHERE empid = lempid;

EXIT WHEN lcurrdept lprevdept

OR (lcurrdept IS NULL AND lprevdept IS NOT NULL)

OR (lcurrdept IS NOT NULL AND lprevdept IS NULL);

EXIT WHEN l_tries >= 3; — safety stop

END LOOP;

DBMSOUTPUT.PUTLINE(

‘Stopped after ‘ |

ltries ‘ tries. prev=‘

NVL(TOCHAR(lprevdept),‘NULL‘)

|

‘, curr=‘

NVL(TOCHAR(lcurr_dept),‘NULL‘)

);

END;

/

That EXIT WHEN looks noisy because I’m implementing a “distinct” comparison that treats NULL as a real value. If your Oracle version supports a null-safe distinct operator, use it. If not, you need to spell it out.

Safer Comparisons: Null-Safe Patterns, Data Type Traps, and Collation Gotchas

When someone tells me “I need to check that A is different from B,” my next question is always: “What should happen when one side is NULL?”

Here are patterns I actually use.

Pattern 1: Explicit NULL handling (most readable)

If you mean “different, treating NULL as unknown and excluding it,” the plain is fine.

If you mean “different, and NULL counts as different,” write it:

— Null-safe not-equal logic

WHERE (a b)

OR (a IS NULL AND b IS NOT NULL)

OR (a IS NOT NULL AND b IS NULL)

It’s verbose, but it is unambiguous.

Pattern 2: Sentinel values (fast to write, risky if you pick a bad sentinel)

Sometimes you want a shorter predicate and you control the domain well.

Example with numbers:

— Treat NULL deptid as -1, assuming -1 can never be a real deptid

WHERE NVL(dept_id, -1) 101

This includes NULLs. But you must be disciplined:

  • Pick a sentinel that cannot appear.
  • Apply the same rule everywhere, or you’ll create inconsistent logic.

For strings:

WHERE NVL(status, ‘<>‘) ‘ACTIVE‘

I’m careful with string sentinels because they can collide during integrations.

Pattern 3: Use a null-safe distinct operator if available

Some Oracle versions support IS DISTINCT FROM / IS NOT DISTINCT FROM semantics, which treat NULL as a comparable value.

When available, it’s the cleanest expression of intent:

— Null-safe: true when different, including NULL vs non-NULL

WHERE a IS DISTINCT FROM b

If your environment doesn’t support it, fall back to Pattern 1.

Data type traps that make NOT EQUAL look broken

These are the issues I see most often:

1) Implicit conversion

— Danger: comparing DATE to string depends on session NLS settings

WHERE hire_date ‘2026-02-03‘

Do this instead:

WHERE hire_date DATE ‘2026-02-03‘

2) Time components

If hire_date is a DATE, it includes a time component (down to seconds). Comparing to midnight can surprise you.

If you mean “not on that calendar day,” compare ranges:

WHERE hire_date < DATE '2026-02-03'

OR hire_date >= DATE ‘2026-02-03‘ + 1

3) Collation and case

If your database uses case-insensitive collation, string inequality may behave differently than you expect, especially across environments.

If you require case-sensitive logic, be explicit and consistent (for example, normalize with UPPER()/LOWER()), but understand that functions can affect index usage.

Traditional vs modern practice (what changed in the last few years)

Topic

Traditional Habit

What I Do Now (2026) —

— NULL behavior

Assume NULL 101 is true

Decide and encode NULL intent explicitly Numeric literals

Compare numbers to quoted strings

Keep types aligned; avoid implicit conversions “Not in set”

NOT IN (subquery) everywhere

Prefer NOT EXISTS to avoid NULL pitfalls Quality

Manually spot-check queries

Add SQL/PLSQL unit tests and generate edge-case matrices

On that last row: I’m not saying “hand your database to an assistant.” I am saying you can generate test cases (NULLs, empty strings, weird NLS cases), and then you codify them with real assertions.

NOT EQUAL vs NOT ( = ): Why These Are Not the Same in SQL

I’m calling this out because it’s a mental trap: in two-valued logic, “A is not equal to B” is identical to “NOT (A equals B).” In SQL’s three-valued logic, they’re not identical when NULL is possible.

Let’s compare these two predicates:

  • a b
  • NOT (a = b)

If either side can be NULL, both predicates can evaluate to NULL (unknown), and both will be filtered out in a WHERE clause. In practice, they often behave similarly, which is why this trap survives.

But the bigger point is this: writing NOT (a = b) does not magically make comparisons “more correct.” It just makes them harder to read.

If you mean “different, treating NULL as unknown,” use .

If you mean “different, and NULL counts as different,” use IS DISTINCT FROM (if available) or spell out the null-safe logic explicitly.

Here’s a concrete demonstration using columns, because that’s where the confusion usually shows up:

— Suppose a and b are nullable columns.

— If a is NULL and b is 5:

— a = b is NULL

— NOT (a = b) is also NULL

— a b is also NULL

So the “NOT around equals” rewrite is not a fix for NULL semantics.

NOT EQUAL in DML: UPDATE, DELETE, and MERGE Gotchas

When a not-equal predicate is in a report, you might notice missing rows. When it’s in a data change statement, it can silently do the wrong write, which is worse.

UPDATE with : avoiding accidental no-ops

A common pattern is “only update when value changed”:

UPDATE employees

SET dept_id = 101

WHERE emp_id = 4

AND dept_id 101;

If deptid is NULL for empid = 4, then dept_id 101 is NULL, which is treated as false in the WHERE clause. Result: the update does not happen.

If your business rule is “set it to 101 unless it’s already 101,” you probably want NULLs included:

UPDATE employees

SET dept_id = 101

WHERE emp_id = 4

AND (deptid 101 OR deptid IS NULL);

Or, if available:

UPDATE employees

SET dept_id = 101

WHERE emp_id = 4

AND dept_id IS DISTINCT FROM 101;

This is one of the most practical places for null-safe distinct semantics.

DELETE with : be careful with “everything except X”

This is a classic footgun:

DELETE FROM employees

WHERE dept_id 101;

It will not delete rows with NULL dept_id. That may be good or bad depending on what you meant.

When I’m deleting “everything except,” I force myself to decide explicitly:

  • “Except X, keep NULLs”:

DELETE FROM employees

WHERE deptid 101 OR deptid IS NULL;

  • “Except X, ignore NULLs”:

DELETE FROM employees

WHERE dept_id IS NOT NULL

AND dept_id 101;

MERGE: comparing source vs target safely

In MERGE, you often want to update only if source and target values differ. If you write:

— Sketch only: focus is the predicate

WHEN MATCHED THEN

UPDATE SET t.deptid = s.deptid

WHERE t.deptid s.deptid;

then rows where one side is NULL will not update. Sometimes that’s correct. Often it isn’t.

My rule: if the column is nullable, I treat “changed” as “distinct,” not “not equal.” That means IS DISTINCT FROM when I can, or the explicit null-safe pattern when I can’t.

NOT EQUAL and Empty Strings in Oracle: The Special Case You Must Know

Oracle treats the empty string ‘‘ as NULL for VARCHAR2 (and many related contexts). This is another reason NULL semantics and not-equal semantics are inseparable in Oracle.

So if you think you’re comparing “empty vs not empty,” you’re actually comparing “NULL vs not NULL.”

That affects things like:

  • col ‘‘ is effectively col NULL, which is NULL/unknown and filters out everything.

If you want “non-empty string,” the clearer intent is:

WHERE col IS NOT NULL

And if you want “empty or null,” in Oracle those collapse to the same thing for VARCHAR2:

WHERE col IS NULL

This is one of those Oracle-specific behaviors that makes portability discussions feel academic. In the real world, you need to know what your database does.

Practical NOT EQUAL Recipes I Actually Use

This section is deliberately copy/paste-friendly. These patterns show up constantly.

Recipe 1: “Exclude one value, but keep NULLs”

WHERE col 😡 OR col IS NULL

When the value is numeric:

WHERE deptid 101 OR deptid IS NULL

Recipe 2: “Exclude one value, and also exclude NULLs”

WHERE col IS NOT NULL AND col 😡

That reads cleanly and avoids relying on three-valued logic to do “the right thing by accident.”

Recipe 3: “Different columns, NULL counts as different”

Use IS DISTINCT FROM if available:

WHERE a IS DISTINCT FROM b

Fallback:

WHERE (a b)

OR (a IS NULL AND b IS NOT NULL)

OR (a IS NOT NULL AND b IS NULL)

Recipe 4: “Not in a list, with NULL rules spelled out”

If you mean “exclude these values, keep NULLs”:

WHERE col NOT IN (101, 102) OR col IS NULL

If you mean “exclude these values, also exclude NULLs”:

WHERE col IS NOT NULL AND col NOT IN (101, 102)

Note: NOT IN has its own NULL trap when the list/subquery contains NULL. For literal lists like (101, 102) you’re safe. For subqueries, I default to NOT EXISTS.

Recipe 5: “Not in subquery (anti-join) safely”

WHERE NOT EXISTS (

SELECT 1

FROM some_table t

WHERE t.key = s.key

)

If you want NULL keys treated as “no match,” decide explicitly:

  • Treat NULL as “no match” and include it:

WHERE s.key IS NULL

OR NOT EXISTS (

SELECT 1

FROM some_table t

WHERE t.key = s.key

)

  • Treat NULL as “not eligible,” exclude it:

WHERE s.key IS NOT NULL

AND NOT EXISTS (

SELECT 1

FROM some_table t

WHERE t.key = s.key

)

Performance and Maintainability: When Is Fine and When I Reach for Another Shape

NOT EQUAL predicates can be perfectly fine, but they can also produce plans you didn’t expect.

Here’s the mental model I use:

  • dept_id = 101 is usually highly selective and index-friendly.
  • dept_id 101 can be low selectivity (it might match most rows), so the database may prefer a full scan.

That’s not “bad.” If most rows match, a full scan can be exactly the right plan. But it’s different than what people expect when they think “I have an index, so it’ll be fast.” Indexes help most when you’re selecting a small portion of the table.

Why often isn’t selective

When you filter with dept_id 101, you are asking for “almost everything except one slice.” That can be the majority of rows.

So I think in terms of shapes:

  • Equality: narrow slice (good candidate for index range/unique lookups)
  • Not-equal: broad slice (often not a good candidate for index access)

Rewriting for performance (only when the semantics match)

Sometimes you can rewrite a not-equal filter into something more index-friendly, but only if your data has a natural ordering and only if you’re careful with NULL.

For numeric values, you can rewrite col 101 into two ranges:

WHERE col 101

This can allow range scans, but it can also be worse (two index probes plus table access) and it still excludes NULL unless you add OR col IS NULL.

If you need NULLs included:

WHERE col IS NULL OR col 101

I don’t do this rewrite automatically. I do it when:

  • I’ve measured that the predicate is a hot path.
  • The column has an index and the distribution makes the rewrite worthwhile.
  • The team is comfortable with the readability trade-off.

Function calls can break index usage

If you use sentinels like NVL(col, -1) 101, you’ve wrapped the column in a function. Depending on indexes and optimizer behavior, that may prevent efficient index usage unless you have a function-based index.

So I choose between:

  • Clarity first: col 101 OR col IS NULL
  • Performance tuning: sentinel + function-based index (only when justified)

Maintainability: make intent obvious

Not-equal logic is easy to misunderstand. I optimize for “the next person can’t misread it.”

That’s why you’ll see me write:

  • col IS NOT NULL AND col :x instead of just col :x when NULLs should not match.
  • col :x OR col IS NULL when NULLs should match.

Those extra words are a cheap insurance policy.

Common Pitfalls I Flag in Code Reviews

These are the patterns that correlate strongly with defects.

Pitfall 1: in join conditions

If I see ON a.id b.id, I immediately assume it’s wrong until proven otherwise.

Almost every time, the author meant:

  • join on equality, then filter
  • or use NOT EXISTS for an anti-join

Pitfall 2: used as “changed” logic for nullable columns

If the column can be NULL, a b does not mean “changed.” It means “changed AND neither side is NULL.”

If you want “changed,” you want “distinct.”

Pitfall 3: implicit conversions

  • Comparing numbers to ‘101‘
  • Comparing dates to ‘2026-02-03‘
  • Comparing timestamps to strings

This causes session-dependent behavior and can produce surprising errors in production.

Pitfall 4: misunderstanding NOT IN with subqueries

If the subquery can return NULL, NOT IN (subquery) can return no rows at all.

When I see NOT IN (SELECT ...), I ask:

  • Can that SELECT return NULL?
  • If yes, should we change to NOT EXISTS?

Pitfall 5: assuming empty string is different from NULL

In Oracle, empty string often behaves like NULL for VARCHAR2. That changes how not-equal comparisons behave.

A Simple Testing Matrix for NOT EQUAL Logic (SQL and PL/SQL)

When I’m writing or reviewing not-equal logic, I like to force a tiny truth table. You don’t need a full testing framework to benefit from this; you just need discipline.

Here’s the minimum set of cases that catch most mistakes:

Case

a

b

What many people expect

What SQL does (a b) —

—:

—:

— equal

5

5

false

false different

5

6

true

true null-left

NULL

5

true

NULL null-right

5

NULL

true

NULL null-both

NULL

NULL

false or true (varies)

NULL

Then I choose semantics:

  • If I want NULL treated as “unknown”: a b is fine.
  • If I want NULL treated as a comparable value (distinctness): use IS DISTINCT FROM or the explicit null-safe predicate.

In PL/SQL, I apply the same thinking, but I make it visible with guard clauses. In SQL, I make it visible in the predicate.

Quick “What I Ship” Checklist

When I’m about to merge code that uses not-equal logic, I scan for:

  • Nullable columns: do we want NULL included or excluded?
  • Implicit conversions: are literals typed correctly (DATE ‘YYYY-MM-DD‘, numeric literals unquoted)?
  • Joins: is used in an ON clause (usually wrong)?
  • Anti-joins: can NOT EXISTS express the intent better?
  • Readability: would a future reader understand NULL semantics without re-deriving it?

Closing Thoughts

The NOT EQUAL operator in PL/SQL and Oracle SQL is simple on paper: or !=. The hard part is not the glyph; it’s the truth model.

Once you internalize three-valued logic, a lot of “mystery bugs” stop being mysterious. You start writing predicates that say what you mean, especially around NULL. And you stop trusting “it looks right” as a substitute for a two-minute edge-case check.

If you remember one thing, make it this: whenever you write , ask yourself out loud—what should happen when the value is NULL? Then encode that answer directly in the SQL or PL/SQL.

Scroll to Top