Secure connections
Save PostgreSQL connection details with encrypted passwords, SSL settings, URL import, testing, search, edit, and delete actions.
Filter PostgreSQL data
Most filtering problems are one of three things: NULL not behaving like a value, a timestamp range that quietly drops a day, or a pattern match that cannot use the index you built for it.
Short answer: Use ILIKE for case-insensitive matching, IS NULL rather than = NULL, and half-open ranges for timestamps — >= start AND < next_start — because BETWEEN includes the end and will miss everything after midnight on the final day. A leading wildcard in LIKE cannot use a normal B-tree index; that needs pg_trgm.

PgDeck is shaped around PostgreSQL tasks that happen every day: connect, inspect, filter, edit, query, manage structure, copy, and export.
Save PostgreSQL connection details with encrypted passwords, SSL settings, URL import, testing, search, edit, and delete actions.
Browse schemas and tables, search the table list, pin favorites, filter rows, sort columns, resize columns, and paginate results.
Run SELECT and non-SELECT statements from multi-tab SQL editors with syntax highlighting and interactive query results.
Edit cells, add rows, duplicate rows, set NULL, stage deletes, review pending changes, and commit everything in one transaction.
LIKE is case-sensitive. ILIKE is PostgreSQL's case-insensitive version and is almost always what you want when searching names or emails typed by a human. For whole-value comparison, lower() on both sides works too, but it needs an expression index to stay fast.
SELECT * FROM users WHERE email ILIKE '%@example.com';
-- Equivalent for exact matching, needs an expression index to be fast:
SELECT * FROM users WHERE lower(email) = lower('Someone@Example.com');NULL means unknown, so any comparison with it returns unknown rather than true — including = NULL and even NULL = NULL. This is the single most common filtering mistake in SQL. Use IS NULL and IS NOT NULL, and remember that NOT IN with a list containing a NULL returns no rows at all.
SELECT count(*) FROM users WHERE deleted_at IS NULL;
-- Returns nothing if the subquery yields any NULL:
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM bans);
-- Safe version:
SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM bans b WHERE b.user_id = u.id);BETWEEN is inclusive at both ends, so a range ending at a date drops every row after midnight on that day. With timestamps, always use >= the start and < the start of the next period. This bug is invisible until someone notices a report is missing a day.
-- Misses everything after 2026-01-31 00:00:00
SELECT * FROM orders WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31';
-- Correct
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01';->> extracts a field as text, and @> tests containment. Containment is the one that can use a GIN index, so prefer it for filters on jsonb columns. For arrays, = ANY is the readable way to test membership.
-- Field equality
SELECT * FROM events WHERE payload->>'type' = 'signup';
-- Containment, GIN-indexable
SELECT * FROM events WHERE payload @> '{"type": "signup"}';
-- Array membership
SELECT * FROM posts WHERE 'postgres' = ANY(tags);EXPLAIN ANALYZE tells you what actually happened. A sequential scan on a small table is fine; on a large one it means your filter could not use the index. A leading wildcard is the usual cause — 'foo%' can use a B-tree, '%foo' cannot, and that needs a trigram index.
EXPLAIN ANALYZE SELECT * FROM users WHERE email ILIKE '%@example.com';
-- Makes leading-wildcard and ILIKE searches indexable:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_email_trgm ON users USING gin (email gin_trgm_ops);For exploration, a GUI client's filters are faster than writing predicates. PgDeck's filters are type-aware — a boolean column offers true and false, a timestamp offers a date input, and global search uses ILIKE — so the NULL and case-sensitivity traps above are handled for you.
SQL predicates return true, false, or unknown, and only true rows come back. Any comparison involving NULL is unknown, so a row with a NULL in the filtered column is excluded by both col = 'x' and col <> 'x'. That is correct behaviour and it catches everyone. When a filter returns fewer rows than expected, check for NULLs in the column first.
PostgreSQL will ignore an index if the filter cannot use it or if the planner thinks a scan is cheaper. Applying a function to the column — lower(email) — needs a matching expression index. A leading wildcard cannot use a B-tree at all. And on a small table a sequential scan genuinely is faster, so an unused index there is not a problem to solve.
->> gives you a text value and works for straightforward equality, but it cannot use a GIN index. The containment operator @> can, which makes it the right choice on large tables. If you filter on the same jsonb key constantly, an expression index on that specific key is usually better than a general GIN index on the whole column.
Comparing a timestamptz against a bare date string converts using the session TimeZone setting, so the same query can return different rows for two people in different zones. When the boundary matters, be explicit about the zone rather than relying on whatever the session happens to be set to.
Most destructive UPDATE and DELETE incidents come from a WHERE clause that matched more rows than intended. Building the filter as a SELECT and checking the count before attaching an action to it costs nothing, and it is the reason this page is worth reading even if you never write a report.
More on choosing, comparing, and running a PostgreSQL desktop client.
Use ILIKE, which is PostgreSQL's case-insensitive LIKE: WHERE email ILIKE '%@example.com'. For exact comparison you can use lower() on both sides, but that needs an expression index to remain fast on a large table.
Most often NULL. Any comparison with NULL is unknown rather than true, so both col = 'x' and col <> 'x' exclude rows where col is NULL. Use IS NULL to test for it. NOT IN with a list containing any NULL also returns zero rows.
BETWEEN is inclusive of both endpoints, but a date literal means midnight, so BETWEEN '2026-01-01' AND '2026-01-31' excludes everything after 00:00 on the 31st. Use >= '2026-01-01' AND < '2026-02-01' instead.
Common causes: a leading wildcard in LIKE, which a B-tree cannot use and which needs a pg_trgm GIN index; a function applied to the column without a matching expression index; or a table small enough that a sequential scan is genuinely cheaper. EXPLAIN ANALYZE will tell you which.
Use ->> to extract a field as text for simple equality, or the containment operator @> for filters that can use a GIN index. On large tables prefer @>, or create an expression index on the specific key you filter by most.