Rise of the planet of the AI'pes
Just because one isn't running around like an ape who has just discovered fire doesn't mean one doesn't know how to cook.
Read storyLessons from building and maintaining enterprise-scale systems with Django, PostgreSQL, Celery, and RabbitMQ — the hard problems nobody warns you about.

If you can count rows of a table like this SELECT COUNT(*) FROM TABLE, then you probably haven’t worked with large datasets yet.
This article is about my findings after working on a project for almost two years, which used Django, PostgreSQL, Celery, and RabbitMQ to tackle enterprise-level challenges.
In this article:
COUNT(*) will betray you on large PostgreSQL tables — and what to use insteadEXPLAIN to spot query bottlenecks before they hit productionIf you’re a backend engineer who has outgrown toy datasets and is starting to feel the pain, this is for you.
I recall the first time I got a request for a change on my PR, where I used LargeDjangoTable.objects.count(). I thought the reviewer was messing with me and making things up, as the .count() had never failed me … before.
As it turned out, when using SELECT COUNT(*) in PostgreSQL, the planner may choose a sequential scan to read through every data page of the table until it reaches the end. Similar to counting sheep before you fall asleep :). For tables with millions of rows, the query execution may time out sooner before you get a response from the database. So your beautiful code will “never” reach the end of the line.
NB! It is beneficial to familiarize yourself with EXPLAIN or EXPLAIN ANALYSE <your SQL query> to see the query execution plan, so you can identify potential bottlenecks in your queries. Finding something like Seq Scan on tablename (cost=0.00..9,876,540.00 rows=120,000,000 width=128) is definitely a red flag.
SELECT * FROM my_table WHERE id > :last_seen_id ORDER BY id LIMIT 100;SELECT (CASE WHEN c.reltuples < 0 THEN NULL -- never vacuumed
WHEN c.relpages = 0 THEN float8 '0' -- empty table
ELSE c.reltuples / c.relpages END
* (pg_catalog.pg_relation_size(c.oid)
/ pg_catalog.current_setting('block_size')::int)
)::bigint AS estimate_count
FROM pg_catalog.pg_class c
WHERE c.oid = 'myschema.mytable'::regclass;
NB! The estimate query assumes VACUUM is keeping up. On tables with heavy updates or deletes, dead rows inflate the page count and the estimate can drift — run ANALYZE on the table if you need a fresher number.
Indecies, or indexes in modern English! Bear in mind that:
Apples.objects.filter(color="green", size="medium") on the model Meta, you should (if you want :) ) add the following: indexes = [ models.Index(fields=["color", "size"], name="color_size_idx")] One gotcha: the order of fields in the index matters. ["color", "size"] will be used by Postgres when you filter by color, or color + size — but not when filtering by size alone. Put the field you filter on most often first.Email.Add@example.com will not match email.add@example.com. We have fallen into this trap before, where it didn’t use an index scan, and the simplest solution was CREATE INDEX email_lower_idx ON users (LOWER(email)); SELECT * FROM users WHERE LOWER(email)='email.add@example.com';COUNT(*) on a large table can trigger a full sequential scan — always check with EXPLAIN firstWHERE id > :last_seen_id) is your friendpg_class) is fast and good enough for most use cases