top of page

Welcome
to NumpyNinja Blogs

NumpyNinja: Blogs. Demystifying Tech,

One Blog at a Time.
Millions of views. 

Four PostgreSQL Features That Instantly Simplified My SQL Queries


If you’ve worked with PostgreSQL for a while, you’ve probably reached a point where a simple query slowly turns into something much bigger. It starts with a couple of joins, then another subquery, followed by a GROUP BY, and before long you’re scrolling through dozens of lines trying to remember what each part of the query is doing.

I’ve been in that situation more times than I’d like to admit.

Most of us learn SQL by mastering the fundamentals. We write SELECT statements, join tables, filter data with WHERE, and summarize results using GROUP BY. Those skills are essential, but they’re only the beginning of what PostgreSQL can do.

A few days ago, I was looking for a dataset that would let me practice some PostgreSQL concepts without falling back on the classic Employee and Department tables that appear in almost every tutorial. After a bit of searching, I came across a Gym Management dataset online. It included members, membership plans, payments, trainers, attendance records, and gym locations. It wasn’t a large database, but it had enough relationships to make the examples feel realistic.

Initially, my plan was simple. I wanted to understand the schema, write a few interesting queries, and move on.

That didn’t happen.

As I explored the dataset, every new question naturally introduced another PostgreSQL feature that simplified the solution. A ranking report led me to Window Functions. Reusing the same insert logic pointed me toward Stored Procedures. Monthly reporting became a perfect opportunity to use Recursive Queries, and presenting those results in a dashboard-friendly format introduced Crosstabs.

By the time I finished exploring the database, I realized I hadn’t just practiced SQL. I had rediscovered four PostgreSQL features that consistently made my queries cleaner, easier to read, and much simpler to maintain.

Exploring the Dataset

The Gym Management dataset follows a typical transactional design. Members enroll in membership plans, make payments, attend training sessions, and belong to specific gym locations. Although the database is small, it contains enough relationships to demonstrate many of PostgreSQL’s advanced capabilities.

Figure 1. Entity Relationship Diagram of the Gym Management database.

After restoring the database into PostgreSQL, I spent a little time exploring each table and understanding how they were connected. Looking through the relationships first made it much easier to decide where to begin.

One table immediately caught my attention: payment.

It looked like a straightforward transactional table containing payment amounts, payment methods, payment dates, and the membership plan associated with each payment.

Naturally, the first question I wanted to answer was:

Which payments are the highest within each membership plan?

Finding the maximum payment is easy enough using MAX().

Ranking every payment while still displaying every record is where the query starts becoming more interesting.

That turned out to be the perfect introduction to one of PostgreSQL’s most useful analytical features.

Window Functions

When I first started learning SQL, I had a habit of solving ranking problems with nested subqueries. They worked, but they were rarely enjoyable to read a few weeks later.

Looking at the payment table, my initial instinct was exactly the same. I started thinking about subqueries, self joins, and temporary result sets before reminding myself that PostgreSQL already has a feature designed specifically for this type of analysis.

Window Functions

For this example, I wanted to rank payment amounts within each membership plan while keeping every payment record visible.

At first glance, the query looks similar to a regular ranking query, but the OVER() clause changes everything.

PARTITION BY divides the data into separate groups based on the membership plan. Instead of calculating one ranking across the entire table, PostgreSQL creates an independent ranking for each plan.

Within every partition, ORDER BY payment_amt DESC sorts the payments from highest to lowest before assigning the rankings.

The important detail is what doesn’t happen.

No rows disappear.

Every payment record remains in the result while PostgreSQL adds an additional column containing the ranking.

That’s a very different way of thinking compared to GROUP BY.

GROUP BY summarizes data by combining rows.

Window Functions analyze data while preserving the original rows.

That distinction completely changed how I approached analytical SQL.

Result:

The first time this clicked for me, I realized I had been reaching for GROUP BY far too often. Many reports don’t actually need summarized data. They need analytical information added to detailed data, and that’s exactly where Window Functions shine.

Once I became comfortable with OVER() and PARTITION BY, several queries that previously relied on nested subqueries became noticeably shorter and much easier to understand.

Tip: If your SQL contains multiple subqueries just to calculate rankings, running totals, or moving averages, it’s worth asking whether a Window Function can simplify the solution.

Ranking the payments answered my first question about the dataset, but it also led to another thought.

Exploring data is only one side of working with a database.

Eventually, every system needs to insert new data as well.

The next table I explored was members, and that naturally introduced another PostgreSQL feature that has been around for years but is often overlooked: Stored Procedures.

Stored Procedures

The payment table answered my first reporting question, but as I continued exploring the dataset, I realized I had spent all my time reading data.

Eventually, every database needs to write data as well.

The members table was the obvious place to continue. At first glance, registering a new member looked simple enough. It’s just an INSERT statement with a handful of columns.

Or at least that’s how it starts.

The more I thought about it, the more I realized that inserting a member isn’t something that happens in only one place. A gym could have a website where new members register online, a mobile application for existing customers, an administrative portal used by staff, and a reception system at the front desk.

If every application contains its own version of the same INSERT statement, keeping them synchronized becomes difficult over time. A small change to the registration process suddenly needs to be updated in multiple places.

That’s exactly the kind of problem Stored Procedures were designed to solve.

Instead of repeating the SQL everywhere, PostgreSQL allows us to place the logic inside the database and call it whenever it’s needed.

Once the procedure has been created, adding a member becomes much simpler.

simply calls the procedure and passes the required values.

That’s a subtle change, but an important one.

This example only inserts a single row, so the benefit might not seem obvious yet. Production systems, however, rarely stop there.

A registration process often grows over time. It might validate phone numbers, prevent duplicate registrations, generate membership IDs, create the first payment record, update audit tables, or perform all of those operations inside a single transaction.

When that logic lives inside a stored procedure, every application automatically follows the same business rules.

Recursive Queries

Ask someone for an example of a recursive query, and there’s a good chance they’ll show an employee hierarchy.

There’s nothing wrong with that example, but after seeing it so many times, I wanted to find a use case that felt more practical for this dataset.

The Gym Management database doesn’t contain managers or reporting structures.

What it does contain is member registration dates.

That raised a simple question.

How many members joined each month?

The query itself wasn’t difficult.

The challenge was generating a complete list of months.

Without a calendar table, months with no registrations would simply disappear from the report.

Fortunately, PostgreSQL can generate that list dynamically.

The query starts with a single row representing January.

From there, PostgreSQL repeatedly adds one month until it reaches December.

Jan

Feb

Mar

Apr

Dec

I like this example because the recursion is easy to visualize.

Each new month is generated from the previous month, and the process stops once the ending condition is reached.

Generating the months was only the first step.

The next task was combining those months with the members table.

By this point, the monthly registration report was complete.

Crosstabs

By this point, the monthly registration report was doing exactly what I wanted.

It showed how many members joined each month, included months with no registrations, and the SQL was much cleaner than I expected when I first started exploring the dataset.

There was just one problem.

The report still looked like transactional data.

Gym Month Members

 — — — — — — — — — — — -

1010 Jan 12

1010 Feb 15

1010 Mar 18

1020 Jan 8

1020 Feb 11

1020 Mar 13

From a database perspective, this structure is perfect. Each row represents a single observation, making it easy to store, update, and query but from a reporting perspective, not so much.

If I wanted to compare January, February, and March side by side, I’d probably end up exporting the results into Excel and creating a Pivot Table.

Then I remembered PostgreSQL already had a feature designed for exactly this purpose is crosstab.

Before using crosstab(), PostgreSQL requires the tablefunc extension.

CREATE EXTENSION IF NOT EXISTS tablefunc;

Once the extension is available, transforming the report becomes surprisingly straightforward.

The difference in the output is immediately obvious.

Instead of reading down the page, the report becomes much easier to scan horizontally.

Seeing the results side by side made it much easier to compare activity across different gym locations.

What I liked most about this feature is that the transformation happens inside PostgreSQL itself. There was no need to export the data into another tool just to reshape it into a presentation-friendly format.

If you’ve ever copied SQL results into Excel just to build a Pivot Table, crosstab() can often eliminate that extra step.

That was probably my biggest takeaway from this section. PostgreSQL isn’t just capable of storing and retrieving data. It also provides features that prepare the data for reporting before it ever leaves the database.


"Happy Querying"


"The best SQL developers aren't the ones who write the longest queries. They're the ones who know when not to"

+1 (302) 200-8320

NumPy_Ninja_Logo (1).png

Numpy Ninja Inc. 8 The Grn Ste A Dover, DE 19901

© Copyright 2025 by Numpy Ninja Inc.

  • Twitter
  • LinkedIn
bottom of page