Mastering Postgres
Introduction
Introduction to the course
Overview of course structure
Postgres vs. everyone
The psql CLI
Data Types
Introduction to schema
Integers
Numeric
Floating point
Storing money
NaNs and infinity
Casting types
Characters types
Check constraints
Domain types
Charsets and collations
Binary data
UUIDs
Boolean
Enums
Timestamps
Timezones
Dates and times
Advanced Data Types
Intervals
Serial type
Sequences
Identity
Network and mac addresses
JSON
Arrays
Generated columns
Text search types
Bit string
Ranges
Composite types
Nulls
Unique constraints
Exclusion constraints
Foreign key constraints
Indexing
Introduction to indexes
Heaps and CTIDs
B-Tree overview
Primary keys vs. secondary indexes
Primary key types
Where to add indexes
Index selectivity
Composite indexes
Composite range
Combining multiple indexes
Covering indexes
Partial indexes
Index ordering
Ordering nulls in indexes
Advanced Indexing
Functional indexes
Duplicate indexes
Hash indexes
Naming indexes
Understanding Query Plans
Introduction to explain
Explain structure
Scan nodes
Costs and rows
Explain analyze
Generating Results
Introduction to queries
Cross joins
Inner joins
Outer joins
Subqueries
Lateral joins
ROWS FROM
Filling gaps in sequences
Subquery elimination
Combining queries
Set generating functions
Indexing joins
Advanced SQL
Introduction to advanced SQL
Grouping
Grouping sets, rollups, cubes
Window functions
CTEs
CTEs with window functions
Recursive CTE
Hierarchical recursive CTE
Handling nulls
Row value syntax
Views
Materialized views
Removing duplicate rows
Upsert
Returning keyword
COALESCE + generated column
Full Text Search
Introduction to full text search
Searching with LIKE
Vectors, queries, and ranks
Websearch
Ranking
Indexing full text search
Highlighting
Advanced JSON
Intro to JSON
JSON vs JSONB
Validating JSON
Creating JSON objects + arrays
JSON extraction
JSON containment
JSON existence
JSON recordset
Updating JSON
Indexing JSON parts
GIN index
Vectors (pgvector)
Intro to pgvector
Vector embedding columns
Find related articles
Upsert vector embedding
Semantic search
Other operators
Vector indexes
Outro
Thank you
Bonus interviews
Heroku's glory days & Postgres vs the world (with Craig Kerstiens)
Creating a Postgres platform with Monica & Tudor from Xata.io
Bootstrapping an email service provider (with Jesse Hanley)
Locked video

Please purchase the course to watch this video.

Video thumbnail
Indexing
Ordering nulls in indexes

Full Course

$
349
$399
USD, one-time fee
Mastering Postgres has dramatically improved my understanding of indexes. It has helped me make informed decisions around my queries and schema rather than blindly adding indexes and hoping for the best.
Dwight Watson
Dwight Watson

PostgreSQL database platform

Shorten dev cycles with branching and zero-downtime schema migrations.

Test Data

I've made my test data available for you to use and follow along.

Download
or
Use on Xata

Summary

Learn how to control where nulls appear in your database results by using `NULLS FIRST` or `NULLS LAST` in your query, allowing you to place them at the top or bottom. By default, nulls are treated as larger than other values, so they appear last when sorting in ascending order. If your queries often require nulls to be in a specific order, creating an index that matches this order can help improve query performance.

Video Transcript

If you can believe it, there's even another option we have, and that's controlling where nulls end up in our result set. By default, nulls are treated as larger than any other value, but we can change that both in the query and in the index construction! We can do anything we want. Let me show you.

Let's start out by proving that nulls are greater than every other value because I'll tell you what, I often forget that. This is explicitly saying order by birthday ascending, which means all those nulls would be at the bottom if they are of the greatest value. Let's switch it around to descending, which should, there you go, put it at the top. Birthday being null comes after every other birthday because it is treated as the largest value. You can change this. You can say, in this case, nulls last, and the null will go to the very, very bottom. In this case, it's filtered out because we have a limit of 10 on here.

Now, if we did say nulls first, that doesn't do a whole lot because nulls first is the default when it is descending. When it is ascending, you can say, "Yeah, do it ascending, "but instead put all the nulls at the top "and then do the ascending value." Likewise, this is useless because that is the default behavior. We're gonna leave it as nulls first, and then we can create an index that mirrors that.

We'll do create index, we'll call it birthday_nulls_first, null first is fine on users. We're gonna say birthday. And we can say ascending, which is fine. That's the default, but we'll go ahead and leave it for clarity's sake. We'll say nulls first. If we run that, and then get out of there, and then we run this, it's pretty fast. That doesn't tell us anything. Let's take a look at the explain index scan on birthday_null_first users. If we switch this to nulls last, whap-whap, we're back to a sequence scan on users; switch it back to nulls first, and we're good; switch this to descending, and we're not good, descending nulls last.

We are using a backward index scan, so those rules about forward and backwards still pertain. If you were to switch both of these things, then you're fine because that can read the index the opposite direction. If you skip just one of them, then the nulls are in the wrong spot, everything is in the wrong spot, so it's not going to work. If you find yourself reaching for nulls first or nulls_last in a query relatively often, you might consider creating an index that represents that same exact order.