Testing SQL Performance in PostgreSQL

Ask questions Research chat →

https://thoughtbot.com/blog/test-sql-performance · scraped

databases

Attachments

Scraped Content

— 559 words · 2026-02-14 17:38:34 UTC ·

Excerpt

A few weeks ago I had a really great bad idea: What if I tried to test SQL outside of a framework, using only SQL. Was it possible? Did it even make sense? I would soon accept what many developers come to terms with at some point in their careers: Yes my idea is possible and yes, it’s not a very good one. However, there was a silver lining! I learned that while it didn’t quite make sense to test with SQL, I could test the performance of different queries that retrieved the same data. Enter PostgreSQL’s’ EXPLAIN ANALYZE command. EXPLAIN ANALYZE is somewhat self explanatory: it breaks down how Postgres will execute a given query. The most valuable part of this output is an estimated time of how long a query will take. So, while it might not make sense to test your big ol’ SQL query outside of a development framework, getting dirty in the Postgres terminal with EXPLAIN could save you from painfully slow data retrieval. Let’s look at an example. The other day I wrote this fancy pants que
A few weeks ago I had a really great bad idea: What if I tried to test SQL outside of a framework, using only SQL. Was it possible? Did it even make sense? I would soon accept what many developers come to terms with at some point in their careers: Yes my idea is possible and yes, it’s not a very good one. However, there was a silver lining! I learned that while it didn’t quite make sense to test with SQL, I could test the performance of different queries that retrieved the same data. Enter PostgreSQL’s’ EXPLAIN ANALYZE command. EXPLAIN ANALYZE is somewhat self explanatory: it breaks down how Postgres will execute a given query. The most valuable part of this output is an estimated time of how long a query will take. So, while it might not make sense to test your big ol’ SQL query outside of a development framework, getting dirty in the Postgres terminal with EXPLAIN could save you from painfully slow data retrieval. Let’s look at an example. The other day I wrote this fancy pants query using a common table expression and asked my colleagues if there was a less verbose way to write it: ```plain text WITH user_visits_table AS ( SELECT row_number() over(ORDER BY created_at DESC) AS visit_date, created_at FROM visits WHERE visits.user_id = 1 ) SELECT DISTINCT CAST(created_at AS DATE) FROM user_visits_table ``` My colleague Summer suggested the following: ```plain text SELECT COUNT(*) FROM (SELECT DISTINCT CAST(created_at AS DATE) FROM visits) dates; ``` Definitely less verbose! But do they perform the same? Let’s EXPLAIN ANALYZE and compare. ### Query 1: ```plain text EXPLAIN ANALYZE WITH user_visits_table AS ( SELECT row_number() over(ORDER BY created_at DESC) AS visit_date, created_at FROM visits WHERE visits.user_id = 1 ) SELECT DISTINCT CAST(created_at AS DATE) FROM user_visits_table; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------- Unique (cost=11.34..11.36 rows=3 width=4) (actual time=0.017..0.018 rows=0 loops=1) -> Sort (cost=11.34..11.35 rows=3 width=4) (actual time=0.017..0.018 rows=0 loops=1) Sort Key: ((user_visits_table.created_at)::date) Sort Method: quicksort Memory: 25kB -> Subquery Scan on user_visits_table (cost=4.17..11.32 rows=3 width=4) (actual time=0.014..0.014 rows=0 loops=1) -> Bitmap Heap Scan on visits (cost=4.17..11.28 rows=3 width=16) (actual time=0.013..0.014 rows=0 loops=1) Recheck Cond: (user_id = 1) -> Bitmap Index Scan on index_visits_on_user_id (cost=0.00..4.17 rows=3 width=0) (actual time=0.003..0.003 rows=0 loops=1) Index Cond: (user_id = 1) Planning Time: 0.087 ms Execution Time: 0.044 ms (11 rows) Time: 0.893 ms ``` ### Query 2: ```plain text EXPLAIN ANALYZE SELECT COUNT(*) FROM (SELECT DISTINCT CAST(created_at AS DATE) FROM visits) dates; QUERY PLAN --------------------------------------------------------------------------------------------------------------- Aggregate (cost=22.80..22.81 rows=1 width=8) (actual time=0.011..0.011 rows=1 loops=1) -> HashAggregate (cost=17.80..20.30 rows=200 width=4) (actual time=0.006..0.007 rows=0 loops=1) Group Key: (visits.created_at)::date -> Seq Scan on visits (cost=0.00..16.50 rows=520 width=4) (actual time=0.005..0.005 rows=0 loops=1) Planning Time: 0.700 ms Execution Time: 0.207 ms (6 rows) Time: 2.450 ms ``` I did not expect such a difference in performance between the two queries, which goes to show that EXPLAIN ANALYZE may be a useful addition to your workflow when writing and refactoring SQL. Keep in mind results on a toy-sized table cannot always be extrapolated to a larger, production database table, but it’s a good place to start. Please consult the official documentation for caveats and best practices. And if you prefer writing queries using Active Record, you can write explain queries in Rails as well. ![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/33d31aa8-d71a-4d77-8cb7-58809793d279/support_and_maintenance-f11bf830c0a5d79d09ffa47201396c1ad872d67e186ff795a428709fb3fb2add.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466YONC45PX%2F20260214%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260214T173834Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEEaCXVzLXdlc3QtMiJGMEQCID377Hjc7z%2Fc14A3%2F6SLIFER71YtecyEKnB99nRZNf93AiBd2ccG7ekvPEldXr3DrWHQJimZndHvS6vl8lCpAa1T6Sr%2FAwgKEAAaDDYzNzQyMzE4MzgwNSIMXIxc8OKC4QiGC8zSKtwD01YQxX2D5SK86CGGqdpdj1Vlr7P4DIKC%2Bo8wvThfEbuhhCLYojg5PQJ8UyzoE6qTFczx1iC9yPcV62yHcVW0V6mOvIxiiwOCqn8PqybuZDrKBWFwP07WCjD3QMlaLIN5lzF2o0A%2FZONWzn53HsjgNy0RdcPcQPVpm%2Fs4shAyaPOBnjzIw58og5vu7zxO3P%2BLz%2FY5ePFd0eLQEoLIrLDTM1TUJnGU0F7Bv0su9FTwm0DsHUBtEB02o8O792%2FcvPALwYLXzZIpsisXhFz02pC5qR3KbQMUkmfqAN5CmU5UoQnXSjh9BFSq6%2BDzrwpiG%2FjKH6d%2FsB9V9qONmVohyEQIxe8NncBXrSXvkYagPbQK32YfFsS%2BfqiMY9SzOtnEriPtQA9UpfLN6qwBSouKHUitXZvHOVg3%2FDKZQmt4orljX2T3ZNBmVYglpjCiYp%2BxfY2pe%2BCBCO971LNr0upLx4rWhOm53NhDh6gC%2Fal2BFtfLDCcXxT0P7muH6xe0bNF2nFUaFTrfZsGLOLo9Dfx4mIMbi5F52QwnXvx8EHEJo1KVxuIaay2wQNh9qdUUEUqlt17XFxMS2O8%2FBVOuEWGbWCiR8tWYpUJKtEOxHW3rVRqY85e%2B%2Br0OG8RkQmqdCEw29HCzAY6pgGN4qMVsDUFy8CrJiIoOsYoLKYHaibGA7NmlAUbTRG7IZZKyZ%2FM3RM%2FSf4SjlIqxXy9mI3w1tR%2F1LRvP9KQB0WcYYGTacLSNKxtejWFdL6OZ3ISOFeICClN3aSKPCu2RvQwTyTak3wI9z9O0YkytLXoIGWPkBcQRgOxy9dNroUUSHLzEfXs0tKrCcAqq1xAkPTRiS1ln0TxqF1fyY1wIdNcWIFXKpzJ&X-Amz-Signature=82845abac1a0834fa3a0fb10caf2052b538ec517b0d4d1d9b7ddcb4789d0a218&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) A developer checks that code runs properly on both a laptop and a mobile device

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation