· 

Efficiency

1. Which is more efficient?

Which is more efficient?  First, this is in superadmin_summary():

    admins = (

        CustomUser.objects.filter(role="admin")

        .annotate(

            num_clients=Count("clients", distinct=True),

            num_docs=Count("clients__user__documents", distinct=True),

        )

        .order_by("id")    # .order_by("username")

    )

And the following is in superadmin_dashboard():

    admins = CustomUser.objects.filter(role="admin").order_by("username") # sort by name  # get all admins

    client_count = ClientProfile.objects.count()

    document_count = ClientDocument.objects.count()

 

The answer depends on what information you need. They are doing different jobs, so one isn't simply "better" than the other.

superadmin_dashboard()

admins = CustomUser.objects.filter(role="admin").order_by("username")
client_count = ClientProfile.objects.count()
document_count = ClientDocument.objects.count()

This results in approximately:

  • Query 1: fetch all admins
  • Query 2: SELECT COUNT(*) FROM client_profile
  • Query 3: SELECT COUNT(*) FROM client_document

The two .count() calls are very efficient because the database just returns a single integer. It doesn't load every client or document into Python.


superadmin_summary()

admins = (
CustomUser.objects.filter(role="admin")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id")
)

This produces one SQL query that joins several tables and calculates two counts for each admin.

Conceptually, it is similar to:

SELECT
admin.id,
COUNT(DISTINCT client.id),
COUNT(DISTINCT document.id)
FROM ...
GROUP BY admin.id;

This query is more complex, but it gives information like:

Admin Clients Documents
Alice 12 158
Bob 8 73
Charlie 20 412

That information cannot be obtained with simple .count() calls.


Which is faster?

If you only need totals:

client_count = ClientProfile.objects.count()
document_count = ClientDocument.objects.count()

is faster.

If you need counts per admin, then

.annotate(
num_clients=Count(...),
num_docs=Count(...),
)

is the right approach.


One thing I would improve

In superadmin_summary(), you currently have:

for admin in admins:
reset_download_if_needed(admin)
admin.storage_used_mb = get_admin_storage_usage(admin)

The expensive part of this view is probably not the annotate(). Instead, it's:

get_admin_storage_usage(admin)

If that function walks through every document belonging to an admin, then it executes extra work for every admin.

So the rough cost becomes:

annotate()            inexpensive
+
storage calculation potentially expensive
+
download reset inexpensive

I suspect the storage calculation dominates the runtime.


My recommendation

I would keep both views as they are:

Dashboard

admins = ...
client_count = ClientProfile.objects.count()
document_count = ClientDocument.objects.count()

because you only need totals.

Summary

admins.annotate(
num_clients=...,
num_docs=...
)

because you actually display per-admin counts.

That separation is clean and efficient.

 

One optimization I would consider in the future is get_admin_storage_usage(). If you show me that function, I can tell you whether it can be made significantly faster. In many Django applications, storage calculations can be moved from Python loops into a single database aggregation, which scales much better as the number of documents grows.

 

1.(2) get_admin_storage_usage()

You said:

One optimization I would consider in the future is get_admin_storage_usage(). If you show me that function, I can tell you whether it can be made significantly faster. In many Django applications, storage calculations can be moved from Python loops into a single database aggregation, which scales much better as the number of documents grows.

So, here is the code. Can you improve this?

def get_admin_storage_usage(admin_user):

    total = (

        ClientDocument.objects.filter(Q(client__clientprofile__admin=admin_user) | Q(uploader=admin_user))

        .distinct()                       # ensure each document counted once

        .aggregate(total=Sum("file_size"))["total"]

    )

    return total or 0  # bytes

 

 

Your function is already quite good. It lets the database do the summation instead of loading all documents into Python, which is the right approach.

def get_admin_storage_usage(admin_user):
total = (
ClientDocument.objects.filter(
Q(client__clientprofile__admin=admin_user) |
Q(uploader=admin_user)
)
.distinct()
.aggregate(total=Sum("file_size"))["total"]
)
return total or 0

There are only a few possible improvements.


1. The biggest issue is not this function itself

The expensive part is that you call it once for every admin.

Suppose you have

  • 500 admins
  • 50,000 documents

Your dashboard executes something like

SELECT SUM(...)

500 separate times.

That is called the N+1 query problem.

So even though each query is efficient, there are many of them.


2. Remove .distinct() if possible

Ask yourself:

Can the same document satisfy both conditions?

client__clientprofile__admin = admin_user
OR
uploader = admin_user

For example

  • Admin uploads a client's document.

Then

  • uploader == admin
  • client's admin == admin

The same document matches twice.

Without .distinct(), SQL would count it twice.

So in your application, .distinct() is actually necessary.

I would leave it.


3. Slightly cleaner version

Instead of

.aggregate(total=Sum("file_size"))["total"]

I usually write

.aggregate(total_size=Sum("file_size"))["total_size"] or 0

which is just a little clearer.

def get_admin_storage_usage(admin_user):
return (
ClientDocument.objects.filter(
Q(client__clientprofile__admin=admin_user) |
Q(uploader=admin_user)
)
.distinct()
.aggregate(total_size=Sum("file_size"))["total_size"]
or 0
)

No performance difference.


4. A much faster approach for hundreds of admins

If your SaaS grows, instead of

for admin in admins:
get_admin_storage_usage(admin)

you can calculate all admins' storage usage in one query.

For example, something like

from django.db.models import Sum

admins = (
CustomUser.objects
.filter(role="admin")
.annotate(
storage_used=Sum(
"clients__user__documents__file_size",
distinct=True
)
)
)

Now every admin already has

admin.storage_used

without calling another function.

Instead of

500 SQL queries

you get

1 SQL query

which is dramatically faster.

Whether this works exactly depends on your model relationships. I'd need to see your ClientDocument, ClientProfile, and related models to produce the correct annotation.


5. The fastest approach (recommended for commercial SaaS)

Many production SaaS applications don't calculate storage every time someone opens the dashboard.

Instead they store it.

For example:

AdminProfile.storage_used_bytes

Whenever

  • a document is uploaded
storage_used += file_size

Whenever

  • a document is deleted
storage_used -= file_size

Now your dashboard simply does

admin.admin_profile.storage_used_bytes

No aggregation.

No scanning documents.

Almost instant.

This is how systems like Dropbox, Google Drive, and many commercial SaaS products handle storage quotas.


What I would recommend for your project

Since I know you're building a commercial Django client portal, I'd recommend these stages:

 

  • Current stage (good enough): Keep your current function. It's correct and easy to understand.
  • When you have a few hundred admins: Replace the per-admin function calls with a single annotate() query if practical.
  • When you launch publicly and expect significant usage: Add a storage_used_bytes field to AdminProfile and update it whenever documents are uploaded or deleted. This gives constant-time (O(1)) lookups and scales far better than recalculating totals on every dashboard request.

 

1.(3) Code explained

Can you explain the code in details? I don't understand what "Q" does.

def get_admin_storage_usage(admin_user):

    total = (

        ClientDocument.objects.filter(Q(client__clientprofile__admin=admin_user) | Q(uploader=admin_user))

        .distinct()                       # ensure each document counted once

        .aggregate(total=Sum("file_size"))["total"]

    )

    return total or 0  # bytes

 

Certainly. The Q object is one of the most useful features in Django's ORM, and understanding it will help you write much more flexible database queries.

Let's go through your function line by line.

def get_admin_storage_usage(admin_user):

The function receives one argument:

admin_user

which is a CustomUser object representing one admin.

For example,

admin_user.username

might be

alice

Step 1

ClientDocument.objects

This means

Start a query on the ClientDocument table.

Suppose the table contains

id file client uploader file_size
1 a.pdf Client A alice 1000
2 b.pdf Client B alice 2000
3 c.pdf Client C bob 500
4 d.pdf Client A alice 1500

Nothing has been retrieved yet.


Step 2

Now comes the interesting part.

.filter(
Q(client__clientprofile__admin=admin_user)
|
Q(uploader=admin_user)
)

Let's first ignore the |.


Without Q

Normally you write

.filter(username="alice")

or

.filter(email="[email protected]")

If you write

.filter(
username="alice",
email="[email protected]"
)

Django automatically uses AND.

It becomes

WHERE username='alice'
AND email='[email protected]'

There is no way to get OR like this.


What is Q?

A Q object represents one condition.

For example

Q(username="alice")

means

username = "alice"

Another one

Q(email="[email protected]")

means

email = "[email protected]"

Notice these are just objects.

They haven't queried the database yet.


Why use Q?

Because you can combine them.

Example

Q(username="alice") | Q(email="[email protected]")

means

username = "alice"
OR
email = "[email protected]"

The | means OR.


Likewise

Q(username="alice") & Q(is_active=True)

means

username="alice"
AND
is_active=True

The & means AND.


You can even negate one.

~Q(is_active=True)

means

NOT active

In your function

The first condition is

Q(client__clientprofile__admin=admin_user)

Let's read it slowly.

client

means

Follow the foreign key named client.

Then

clientprofile

means

Go to that user's ClientProfile.

Then

admin

means

Look at the admin assigned to that client.

Finally

= admin_user

means

Is that admin the admin we're looking for?

So this condition means

Find documents whose client belongs to this admin.


The second condition

Q(uploader=admin_user)

means

Find documents uploaded by this admin.


Then you combine them

Q(client__clientprofile__admin=admin_user)
|
Q(uploader=admin_user)

The vertical bar

|

means

OR

So Django generates something equivalent to

WHERE
client.admin = admin_user
OR
uploader = admin_user

Why is OR needed?

Suppose your data is

Document Client's Admin Uploader
Doc A Alice Alice
Doc B Alice Client
Doc C Bob Bob
Doc D Bob Alice

For Alice

Condition 1

client.admin = Alice

matches

  • Doc A
  • Doc B

Condition 2

uploader = Alice

matches

  • Doc A
  • Doc D

Using OR gives

  • Doc A
  • Doc B
  • Doc D

Exactly what you want.


Step 3

.distinct()

Remember Doc A?

It satisfies both conditions.

Without distinct()

Condition 1
Doc A

Condition 2
Doc A

Doc A could be counted twice.

distinct() removes duplicates.

Result

Doc A
Doc B
Doc D

each appearing only once.


Step 4

.aggregate(total=Sum("file_size"))

This tells the database

Add up the values in the file_size column.

Suppose the remaining rows are

Document Size
Doc A 1000
Doc B 2500
Doc D 700

The database computes

1000
+2500
+700
------
4200

The result returned is

{
"total": 4200
}

Step 5

["total"]

extracts the value

4200

from the dictionary.


Step 6

return total or 0

If there are no matching documents,

Sum(...)

returns

None

instead of zero.

So

None or 0

becomes

0

If

total = 4200

then

4200 or 0

is

4200

So the function always returns an integer number of bytes.


One important thing about |

Earlier you asked about | versus ||.

In Python:

  • | is the bitwise OR operator, and Django overloads it for Q objects to mean SQL OR.
  • || does not exist in Python. If you write it, you'll get a syntax error.

So in Django, whenever you see

Q(...) | Q(...)

you can mentally read it as:

Condition A OR Condition B

and

Q(...) & Q(...)

as:

Condition A AND Condition B

 

This is the most common use of Q objects, and you'll see this pattern frequently in Django projects.

 

2. superadmin_summary()

This is part of superadmin_summary():

    for admin in page_obj:

        admin_profile = getattr(admin, "admin_profile", None)

        reset_download_if_needed(admin)  # ✅ keep usage accurate.  It is defined in utils.py

        # Create custom attributes and attach usage and limit

        admin.storage_used_mb = get_admin_storage_usage(admin) / (1024 * 1024)

        admin.storage_limit_mb = admin_profile.storage_limit_mb if admin_profile else 0

        admin.download_used_mb = admin.download_bytes_used / (1024 * 1024)

        admin.download_limit_mb = admin.download_bytes_limit / (1024 * 1024)

Why does the line of code admin_profile = getattr(admin, "admin_profile", None) work? It seems there is no attribute of admin_profile in CustomUser.  admin is a CustomUser, which is defined as:

(Code omitted)

 

This is one of Django's nicest features. It looks like admin has an admin_profile attribute, but it is actually being created automatically by Django through a OneToOneField.

I suspect your AdminProfile model looks something like this:

class AdminProfile(models.Model):
user = models.OneToOneField(
CustomUser,
on_delete=models.CASCADE,
related_name="admin_profile",
)

storage_limit_mb = models.PositiveIntegerField(default=5000)

or perhaps

user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)

How does admin.admin_profile exist?

Suppose you have these database rows.

CustomUser

id username
5 alice

AdminProfile

id user_id storage_limit_mb
1 5 5000

Because AdminProfile.user is a OneToOneField, Django automatically creates a reverse relationship.

So after retrieving the user

admin = CustomUser.objects.get(id=5)

you can write

admin.admin_profile

and Django automatically performs something equivalent to

SELECT *
FROM admin_profile
WHERE user_id = 5;

and returns the corresponding AdminProfile object.

You never defined admin_profile inside CustomUser, but Django added it for you.


Why does getattr() work?

Your code is

admin_profile = getattr(admin, "admin_profile", None)

getattr() is simply Python's built-in function.

Normally you might write

admin_profile = admin.admin_profile

However, if the user doesn't have an AdminProfile, Django raises an exception:

RelatedObjectDoesNotExist

instead of returning None.

Using

getattr(admin, "admin_profile", None)

is an attempt to avoid an error if the attribute doesn't exist.


But there is a small catch

For Django reverse OneToOne relations,

getattr(admin, "admin_profile", None)

does not always return None.

When Django knows the relation exists but no row is present, it raises

AdminProfile.DoesNotExist

(or RelatedObjectDoesNotExist).

So if an admin somehow has no profile, this can still fail.

The safest pattern is

try:
admin_profile = admin.admin_profile
except AdminProfile.DoesNotExist:
admin_profile = None

Why hasn't your code crashed?

Most likely because every admin in your system has an AdminProfile.

For example, when you create an admin you probably do something like

admin = CustomUser.objects.create(...)
AdminProfile.objects.create(
user=admin,
storage_limit_mb=5000,
)

or perhaps you create it in a signal.

Therefore

admin.admin_profile

always exists, so getattr() simply returns it.


You can verify this

In the Django shell:

admin = CustomUser.objects.filter(role="admin").first()
print(admin.admin_profile)

If it prints something like

AdminProfile object (3)

then Django is following the OneToOne relationship automatically.


A performance improvement

Since every admin appears to have an AdminProfile, you can avoid one database query per admin by fetching the related profile up front:

admins = (
CustomUser.objects
.filter(role="admin")
.select_related("admin_profile")
)

Then later:

admin_profile = admin.admin_profile

 

will not hit the database again—it will already have been loaded. This is a good optimization for your superadmin_summary() view because it loops over many admins.

 

2.(2) More efficient version

The following code can be modified into the one below, right?

    admins = (

        CustomUser.objects.filter(role="admin")

        .annotate(

            num_clients=Count("clients", distinct=True),

            num_docs=Count("clients__user__documents", distinct=True),

        )

        .order_by("id")    # .order_by("username")

    )

    ....

    for admin in page_obj:

        admin_profile = getattr(admin, "admin_profile", None)

    ....

[Modified version]

    admins = (

        CustomUser.objects.filter(role="admin")

        .select_related("admin_profile")

        .annotate(

            num_clients=Count("clients", distinct=True),

            num_docs=Count("clients__user__documents", distinct=True),

        )

        .order_by("id")    # .order_by("username")

    )

    ...

    for admin in page_obj:

        admin_profile = admin.admin_profile

    ...

And the modified one is more efficient, right?

 

Yes, that's the right direction, and it is more efficient. There are just two small points I'd change.

1. select_related("admin_profile") is definitely an improvement

This:

admins = (
CustomUser.objects.filter(role="admin")
.select_related("admin_profile")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id")
)

is better than before.

Previously, for every admin on the page Django would execute another query when you did

admin.admin_profile

so if you displayed 50 admins, you'd have roughly

  • 1 query for the admins
  • 50 more queries for their profiles

With select_related(), Django performs one SQL JOIN and retrieves everything in one query.

So this is a worthwhile optimization.


2. admin_profile = admin.admin_profile

Assuming every admin always has an AdminProfile (which appears to be true in your project), then

admin_profile = admin.admin_profile

is perfectly fine.

You can even simplify the next line.

Instead of

admin.storage_limit_mb = (
admin_profile.storage_limit_mb if admin_profile else 0
)

you can simply write

admin.storage_limit_mb = admin.admin_profile.storage_limit_mb

because you're assuming the profile always exists.


3. One thing I would change

You wrote

Q(id__icontains=search)

This is the only line I would change.

id is an integer field.

icontains is intended for text fields.

On many databases this either

  • raises an error,
  • performs poorly,
  • or forces Django to cast the integer to text.

A better approach is

from django.db.models import Q
if search:
filters = (
Q(username__icontains=search) |
Q(email__icontains=search)
)
if search.isdigit():
filters |= Q(id=int(search))
admins = admins.filter(filters)

This gives you

  • searching "mk" → username/email
  • searching "18" → id=18 or username/email containing "18"

This is both cleaner and faster.


4. Another tiny improvement

Your comment says

page_number = request.GET.get("page")    # if the result is None, 1 is returned

Actually,

request.GET.get("page")

returns None if there is no page parameter.

It is

page_obj = paginator.get_page(page_number)

that interprets None as page 1.

So I'd change the comment to

page_number = request.GET.get("page")    # None if not specified
page_obj = paginator.get_page(page_number) # None → page 1

My final version

I'd write it like this:

admins = (
CustomUser.objects
.filter(role="admin")
.select_related("admin_profile")
.annotate(
num_clients=Count("clients", distinct=True),
num_docs=Count("clients__user__documents", distinct=True),
)
.order_by("id")
)
if search:
filters = (
Q(username__icontains=search) |
Q(email__icontains=search)
)
if search.isdigit():
filters |= Q(id=int(search))
admins = admins.filter(filters)

Then inside the loop:

admin.storage_limit_mb = admin.admin_profile.storage_limit_mb

 

Overall, I think this version is cleaner and more efficient than your original one. Using select_related() is exactly the kind of optimization that's appropriate for a page like superadmin_summary(), which displays many admins and accesses each admin's profile.

 

2.(3) Abandoned signup does not have a profile

What should I do if I can't assume the profile always exists?

 

That's a good question. It depends on how robust you want your application to be.

If an admin should always have an AdminProfile, then there are two approaches.

Option 1 (recommended): Treat a missing profile as a bug

If a missing AdminProfile indicates your database is in an inconsistent state, don't silently ignore it. Instead:

for admin in page_obj:
try:
admin_profile = admin.admin_profile
except AdminProfile.DoesNotExist:
logger.error(
f"AdminProfile missing for admin id={admin.id}, username={admin.username}"
)
continue # or raise an exception

This lets you discover and fix the underlying problem.


Option 2: Continue gracefully

If it's acceptable that some admins don't yet have a profile, then use a fallback:

for admin in page_obj:
try:
admin_profile = admin.admin_profile
storage_limit_mb = admin_profile.storage_limit_mb
except AdminProfile.DoesNotExist:
storage_limit_mb = 0

admin.storage_limit_mb = storage_limit_mb

The page still renders, and that admin simply shows a storage limit of 0.


Option 3: Use getattr() (what you currently have)

admin_profile = getattr(admin, "admin_profile", None)

However, this is not my favorite for a reverse OneToOneField.

The reason is that admin_profile isn't a normal Python attribute—it is a Django descriptor. Depending on the situation, accessing it can raise RelatedObjectDoesNotExist (which subclasses AdminProfile.DoesNotExist) rather than simply returning None.

An explicit try/except makes it clearer what you're handling.


Option 4: Ensure the profile always exists (my preferred solution)

Rather than handling the missing profile everywhere, make it impossible for one to be missing.

For example, when creating an admin:

admin = CustomUser.objects.create(...)
AdminProfile.objects.get_or_create(
user=admin,
defaults={
"storage_limit_mb": 5000,
},
)

or use a post_save signal:

@receiver(post_save, sender=CustomUser)
def create_admin_profile(sender, instance, created, **kwargs):
if created and instance.role == "admin":
AdminProfile.objects.get_or_create(user=instance)

Then your view can simply do:

admin_profile = admin.admin_profile

without any defensive code.


Which approach fits your project?

From everything we've discussed over the past several months, your application seems designed so that:

  • every admin has exactly one AdminProfile,
  • storage limits are stored there,
  • download limits depend on it,
  • many views rely on it.

 

In that architecture, I would enforce the existence of AdminProfile rather than repeatedly checking for its absence in every view. It's simpler, faster, and if a profile is ever missing, that's something you want to know about and fix rather than quietly masking.

 

3. One aggregation and two aggregations

Which is more efficient?
[Pattern 1] separate addregation
total_download_used_mb = (CustomUser.objects.aggregate( total=Sum("download_bytes_used"))["total"] or 0 ) / (1024 * 1024)
total_download_limit_mb = (CustomUser.objects.aggregate( total=Sum("download_bytes_limit"))["total"] or 0 ) / (1024 * 1024)
[Pattern 2] Combined aggregation
stats = (CustomUser.objects.aggregate(
    total_download_used_bytes=Sum("download_bytes_used"),
    total_download_limit_bytes=Sum("download_bytes_limit"), )  

Pattern 2 is more efficient because it performs one database query instead of two.

Pattern 1 (2 SQL queries)

total_download_used_mb = (
CustomUser.objects.aggregate(
total=Sum("download_bytes_used")
)["total"] or 0
) / (1024 * 1024)

total_download_limit_mb = (
CustomUser.objects.aggregate(
total=Sum("download_bytes_limit")
)["total"] or 0
) / (1024 * 1024)

Django executes:

SELECT SUM(download_bytes_used) FROM custom_user;

and then

SELECT SUM(download_bytes_limit) FROM custom_user;

So the database scans the table twice.


Pattern 2 (1 SQL query)

stats = CustomUser.objects.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
)

total_download_used_mb = (
stats["total_download_used_bytes"] or 0
) / (1024 * 1024)

total_download_limit_mb = (
stats["total_download_limit_bytes"] or 0
) / (1024 * 1024)

Django generates a single query similar to:

SELECT
SUM(download_bytes_used) AS total_download_used_bytes,
SUM(download_bytes_limit) AS total_download_limit_bytes
FROM custom_user;

The table is scanned only once.


Which should you use?

For your superadmin dashboard, I recommend Pattern 2. It's:

  • ✅ Faster (one query instead of two)
  • ✅ Easier to extend
  • ✅ Easier to read

For example, if you later add totals for other fields:

stats = CustomUser.objects.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
paid_admins=Count("id", filter=Q(has_paid=True)),
active_admins=Count("id", filter=Q(is_active=True)),
)

Everything is computed in one query.


My recommendation for your dashboard

Your superadmin_dashboard() is starting to compute many statistics:

  • total download used
  • total download limit
  • total storage limit
  • admin count
  • client count
  • document count
  • total storage used (from documents)

It's a good idea to group related statistics into as few aggregation queries as possible. This keeps the number of database round trips low and will scale much better as your number of admins, clients, and documents grows.

3.(2) One Q() and two Q()s

Is the syntax of the following code correct?

stats = CustomUser.objects.aggregate(

    total_download_used_bytes=Sum("download_bytes_used"),

    total_download_limit_bytes=Sum("download_bytes_limit"),

    active_admin_count=Count("id", filter=Q(role="admin") & Q(is_active=True)),

    # paid_admin_count=Count("id", filter=Q(has_paid=True)),

    # cnclatPE_count=Count("id", filter=Q(cancel_at_period_end=True)),

)

 

Yes, the syntax is correct. Django supports filter= inside Count() (Django 2.0+), and combining Q() objects with & is the proper way to express an AND condition.

Your code:

stats = CustomUser.objects.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
active_admin_count=Count(
"id",
filter=Q(role="admin") & Q(is_active=True),
),
# paid_admin_count=Count("id", filter=Q(has_paid=True)),
# cnclatPE_count=Count("id", filter=Q(cancel_at_period_end=True)),
)

will work.

However, I'd recommend making the other counts consistent.

For example:

stats = CustomUser.objects.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),

active_admin_count=Count(
"id",
filter=Q(role="admin", is_active=True),
),

paid_admin_count=Count(
"id",
filter=Q(role="admin", has_paid=True),
),

cancel_at_period_end_count=Count(
"id",
filter=Q(role="admin", cancel_at_period_end=True),
),
)

Notice two small improvements:

  1. One Q() instead of two

Instead of:

Q(role="admin") & Q(is_active=True)

you can write:

Q(role="admin", is_active=True)

They're equivalent, but the latter is shorter and easier to read.

  1. Restrict counts to admins

If your database contains superadmins and clients (which it does), then

Count("id", filter=Q(has_paid=True))

would count everyone with has_paid=True, including clients if they ever have that flag set.

It's safer to write:

Count("id", filter=Q(role="admin", has_paid=True))

so you're explicitly counting only admins.


 

Overall, this is a nice use of aggregate(). You're calculating several statistics in a single query, which is both efficient and easy to maintain.

 

3.(3) Use .filter(role="admin")

Is it safer to add role="admin" as a filter to the lines of total_download_used_bytes... and total_download_limit_bytes...? Does that reduce the efficiency?

 

Yes, I would recommend filtering to role="admin" if those totals are intended to represent admins only.

However, don't use the filter= argument inside Sum(). Instead, filter the queryset before calling aggregate().

stats = (
CustomUser.objects
.filter(role="admin")
.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),
active_admin_count=Count("id", filter=Q(is_active=True)),
paid_admin_count=Count("id", filter=Q(has_paid=True)),
cancel_at_period_end_count=Count("id", filter=Q(cancel_at_period_end=True)),
)
)

Why is this better?

The database only considers rows where role="admin".

Conceptually, it executes something like:

SELECT
SUM(download_bytes_used),
SUM(download_bytes_limit),
COUNT(...) ...
FROM custom_user
WHERE role = 'admin';

instead of

SELECT ...
FROM custom_user;

Does it reduce efficiency?

Quite the opposite.

  • If you have only a few superadmins and many admins, the difference is tiny.
  • If you eventually have 100,000 clients and 500 admins, filtering by role="admin" means the database ignores almost all of the client rows.

So it is:

  • ✅ more correct (you're computing admin statistics)
  • ✅ usually more efficient
  • ✅ simpler to read

One more improvement

Because you've already filtered to admins, you can simplify the Count() filters:

stats = (
CustomUser.objects
.filter(role="admin")
.aggregate(
total_download_used_bytes=Sum("download_bytes_used"),
total_download_limit_bytes=Sum("download_bytes_limit"),

active_admin_count=Count("id", filter=Q(is_active=True)),
paid_admin_count=Count("id", filter=Q(has_paid=True)),
cancel_at_period_end_count=Count("id", filter=Q(cancel_at_period_end=True)),
)
)

Notice there's no need to repeat role="admin" inside every Q(...).


 

For your superadmin dashboard, this is the approach I'd use. It keeps the code clean, avoids accidentally including clients or superadmins in the statistics, and will scale well as your SaaS grows.