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
Generating Results
Inner joins

Full Course

$
349
$399
USD, one-time fee
Just finished Aaron Francis' Mastering Postgres course. Highly recommend 🙂
Savannah

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 an inner join works in SQL, the default type of join, where rows are matched based on a specified condition and unmatched rows are excluded from the result. I also demonstrate how to use shorthand syntax with the `USING` keyword when joining columns with the same name, simplifying your query and improving readability.

Video Transcript

When we talked about cross joins, I said it's the default type of join for an unqualified join, which hopefully leads you to believe that there's such thing as a qualified join, which there is.

When it comes to a qualified join, the default type of join is an inner join, which is what we're gonna talk about now. An inner join takes two tables, a left and a right table, and it matches them up on something, on a qualification. That's why it's a qualified join. It matches them up on some qualification, usually two columns matching each other. Then any rows that don't have a match on either table are eliminated. Any rows on the left table that don't have a match on the right side, boom, gone. Any rows on the right side that don't have a match on the left side, boom, gone. Let's take a look at that now.

We're working with another table here. If we say select * from bookmarks and we'll limit it to 10, we see we've got a user ID and a URL. It's all made up information, but we can join the users and their bookmarks together by saying select * from users. We're gonna say that the left table is users and then we can just write the word join, join bookmarks. We need to introduce our qualifier, our condition, our joined condition here. We're gonna say on users.id equals bookmarks.user_id. If we do that, let's limit that down. Let's limit that down and we'll see that Kyleigh Runolfsson has a bunch of bookmarks over here. It does look like that matched up because if we look at the user ID 652655, when we come over here, Kyleigh's user ID is 652655. We did match them up correctly.

We can slim this down a little bit. Let's slim this down a little bit. We can use the qualified name here because we do have, if we were to say id, it's gonna say, ooh, I don't know what you're talking about man. Because we have two tables and they both have a column named id, we do have to introduce the table qualifier, which is totally fine. Users.id, and then we'll say users.first_name and then let's do bookmarks.user_id. We can see all of those right next to each other. There you go. When we switched to Wayland, we're down, Waylon, sorry, 652656. Those match up quite nicely.

Notice that we just wrote the word join. We just wrote the word join and then we had our condition at the end. We didn't have to write inner, outer, left, right, any of that stuff. Inner join is the default type of join. And lemme see if I can prove to you that rows that don't match up are eliminated. We don't need this duplicate because they are guaranteed to be the same. I'm gonna drop that off. We'll keep first_name. I do wanna put bookmarks.id on there and we'll put bookmarks.url on there as well. Limit it down to 10. And we're gonna take this ID and we're just gonna say where bookmarks.id equals that one. Because that's the one that we're gonna, too much space. That's the one that we're gonna monkey around with.

With this bookmark here, what happens if this bookmark becomes disassociated with the user, i.e. what happens if the user ID of this bookmark goes to null? Let's figure it out. We will do update bookmarks set user_id equal to null where id equals this one. If we run that, and now let's just read it back, select * from bookmarks where id is that. It's been disassociated with this user Waylon. If we tried to do an inner join again, it's just not there. Because it doesn't have a corresponding match in the user's table because it has no user ID. If we drop this where off, you'll see, oh, that was a Kyleigh URL. Sorry, Kyleigh, that was a Kyleigh URL and it is now gone. We do not see that anymore. It does still exist, anderson.org, but it no longer exists in this joined table because orphans have been eliminated.

There's one other syntax on this join condition that I wanna show you. I don't use it very often 'cause this is not how I name my columns. But if you name your columns this way, you can use this syntax, and the syntax is using.

Note: When I show you how to use "using", there's a small error in the query I show on the screen. The keyword "on" shouldn't be included in the query, so the correct query is:

select * from users join bookmarks using(user_id) limit 10;

Using is a shorthand. Here is our join condition right here. We said users.id equals bookmarks.user_id. Pretend with me for just one second that in the users table we actually named the primary key user_id, which a lot of people do. There's absolutely nothing wrong with that. A lot of people do that, that's totally fine. I like to name my primary keys id instead of table_id. That's just a personal preference thing.

Here's the situation. If you had this right here where both columns are named the same thing in both tables, you can get away with this. You can say using user_id and that's gonna say, great, I'm gonna look for that in both tables. The one benefit you get out of this is if you were to do a select *, it's not gonna bring back both user IDs because then you would have two columns and you'd run into that ambiguous thing again. If you have identically named columns that you are joining on and you use using, hard to say, and you use using, it's going to drop one of those for your select * when it comes back.

Inner join recap. It is the default type of conditional join. You don't have to write the word inner, but you can. If you want to be more clear, that's totally fine. You can also just write join. The condition goes at the end where you specify which columns should be used to join. We joined with column from each table. You can join with two, three, four, it doesn't matter as long as they match. When you have an inner join, it takes a left table and a right table. Using that join condition finds the rows that match, eliminates the ones that don't have matches, and then returns that result set to you.