PostgreSQL BETWEEN Operator

Learn how to use the PostgreSQL BETWEEN operator to filter rows within an inclusive range — works with numbers, dates, and timestamps — and common pitfalls with date ranges.

5 min read · PostgreSQL 9.0+ · Back to overview

Quick Answer

The BETWEEN operator checks whether a value falls within a range and is inclusive of both endpoints. BETWEEN low AND high is equivalent to value >= low AND value <= high. Use NOT BETWEEN to exclude a range. Be careful with timestamp columns — the upper date bound is midnight, not end-of-day.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

The BETWEEN operator in PostgreSQL tests whether a value falls within a range. Both endpoints are inclusive, making it a concise and readable replacement for a pair of comparison operators joined by AND. It works with numeric values, character strings (alphabetical ordering), dates, and timestamps.

Syntax

value BETWEEN low AND high

This is exactly equivalent to:

value >= low AND value <= high

To test that a value is outside a range, use NOT BETWEEN:

value NOT BETWEEN low AND high
-- equivalent to: value < low OR value > high

Practical Example

Create a sample invoice table for a subscription billing system:

CREATE TABLE invoices (
  invoice_id   SERIAL PRIMARY KEY,
  customer_ref VARCHAR(20)  NOT NULL,
  amount_usd   NUMERIC(10,2) NOT NULL,
  issued_at    TIMESTAMPTZ   NOT NULL DEFAULT now(),
  due_date     DATE          NOT NULL,
  status       VARCHAR(20)   NOT NULL DEFAULT 'unpaid'
);

INSERT INTO invoices (customer_ref, amount_usd, issued_at, due_date, status) VALUES
  ('ACCT-101', 1200.00, '2026-01-15 09:00+00', '2026-02-15', 'paid'),
  ('ACCT-102',  450.00, '2026-02-01 10:30+00', '2026-03-01', 'unpaid'),
  ('ACCT-103', 3800.00, '2026-02-20 14:00+00', '2026-03-20', 'paid'),
  ('ACCT-104',  990.00, '2026-03-05 08:00+00', '2026-04-05', 'unpaid'),
  ('ACCT-105', 2500.00, '2026-03-18 11:00+00', '2026-04-18', 'paid'),
  ('ACCT-106',  150.00, '2026-04-01 16:00+00', '2026-05-01', 'unpaid');

Find invoices with amounts between $500 and $3000:

SELECT invoice_id, customer_ref, amount_usd, status
FROM invoices
WHERE amount_usd BETWEEN 500.00 AND 3000.00
ORDER BY amount_usd;

Expected result:

 invoice_id | customer_ref | amount_usd | status
------------+--------------+------------+--------
          2 | ACCT-102     |     450.00 | unpaid    ← excluded (< 500)

Wait — 450 < 500, so it is excluded. The result is:

 invoice_id | customer_ref | amount_usd | status
------------+--------------+------------+--------
          4 | ACCT-104     |     990.00 | unpaid
          5 | ACCT-105     |    2500.00 | paid
          1 | ACCT-101     |    1200.00 | paid

Find invoices issued in Q1 2026 (January–March):

SELECT invoice_id, customer_ref, amount_usd, issued_at
FROM invoices
WHERE issued_at BETWEEN '2026-01-01' AND '2026-03-31'
ORDER BY issued_at;

Verify using the equivalent comparison form:

SELECT invoice_id, customer_ref, amount_usd
FROM invoices
WHERE issued_at >= '2026-01-01'
  AND issued_at <= '2026-03-31';
-- Should return identical rows

Date vs. Timestamp Ranges

When filtering a TIMESTAMPTZ column with date literals, the upper bound is interpreted as midnight of that date:

-- This includes events up to 2026-03-31 00:00:00+00 only.
-- Events on March 31 after midnight are EXCLUDED.
WHERE issued_at BETWEEN '2026-01-01' AND '2026-03-31'

-- For full-day coverage at the upper end, use < the next day:
WHERE issued_at >= '2026-01-01'
  AND issued_at  < '2026-04-01'

NOT BETWEEN

-- Invoices with amounts outside the mid-range
SELECT invoice_id, customer_ref, amount_usd
FROM invoices
WHERE amount_usd NOT BETWEEN 500.00 AND 2000.00
ORDER BY amount_usd;

This returns invoices with amount_usd < 500 or amount_usd > 2000.

Testing with Vela

Range-based filters are frequently used in UPDATE and DELETE operations — for example, archiving invoices older than a certain date or adjusting prices in a tier. Before running such operations in production, use Vela’s database branching to validate the BETWEEN predicate against a production-data clone. Run the statement as a SELECT first, inspect the affected rows, then apply the write only after confirming the range captures exactly the right records.

Production Tips

  • Both endpoints of BETWEEN are inclusive — if you need an exclusive upper bound (common for timestamp ranges), use >= low AND < high explicitly.
  • For BETWEEN on timestamp columns paired with date literals, remember the upper bound is midnight. Use < 'next_day' to include the full last day.
  • An index on the filtered column is used as a range scan, making BETWEEN efficient on large tables. Verify with EXPLAIN ANALYZE.
  • NOT BETWEEN on a widely distributed column without an index causes a sequential scan. Add a partial index if you frequently query specific ranges.
  • When the low and high bounds come from user input, validate that low <= high before executing the query to prevent empty result sets from reversed ranges.

Continue in Filtering Data: LIKE.

Related in this section: PostgreSQL WHERE · AND Operator · OR Operator

Frequently Asked Questions

Is BETWEEN inclusive in PostgreSQL?
Yes. BETWEEN in PostgreSQL is inclusive of both endpoints. BETWEEN 1 AND 10 matches values 1, 2, 3 through 10. It is exactly equivalent to value >= 1 AND value <= 10. If you need an exclusive upper bound, write value >= low AND value < high explicitly.
Does BETWEEN lock the table?
No. BETWEEN is a comparison predicate evaluated at query time. It acquires no locks beyond the standard shared read lock used by any SELECT. UPDATE or DELETE statements that use BETWEEN in their WHERE clause acquire the same locks they would without it.
What happens if the low value is greater than the high value in BETWEEN?
The condition can never be true — no value can simultaneously satisfy both >= low and <= high when low > high. The query returns zero rows. Always place the smaller value on the left side of BETWEEN.
Can I use BETWEEN with dates and timestamps in PostgreSQL?
Yes. Use ISO 8601 literals in single quotes: WHERE event_date BETWEEN '2026-01-01' AND '2026-03-31'. For timestamp columns, the upper date bound is interpreted as midnight (00:00:00), so events on the last day after midnight are excluded. Use < 'next_day' for full-day coverage at the upper end.
Is BETWEEN faster than using >= and <= separately?
Performance is identical. BETWEEN is syntactic sugar for value >= low AND value <= high. The planner treats both forms the same way and uses range index scans for both. Choose the form that is most readable to your team.