Skip to content

Commit f7d9bf9

Browse files
committed
nullable but not null post is written
1 parent 20aad03 commit f7d9bf9

File tree

1 file changed

+104
-0
lines changed

1 file changed

+104
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
title: "Nullable but not null"
3+
publishedAt: '2025-07-25'
4+
category: 'django'
5+
---
6+
7+
When working on backend applications, especially those with evolving database schemas, it's common to see a recurring pattern:
8+
9+
1. A new field is added to a model.
10+
2. To avoid locking the table during the migration, the field is added as **nullable**.
11+
3. The application logic is updated to start filling in this field.
12+
4. A backfill job runs to populate the existing records.
13+
5. The field is left as nullable.
14+
15+
People often forget the final step which is updating the schema to make the field non-nullable once the data is fully populated.
16+
17+
### Why it matters
18+
19+
Leaving a field marked as nullable when it no longer contains nulls creates a mismatch between your schema and your actual data. This can lead to confusion, missed constraints and unnecessary complexity in your code. Over time, your schema becomes harder to trust. If a field is supposed to be required, your database should enforce it.
20+
21+
This happens because marking a field as non-nullable in production often requires care. Teams delay or skip it to avoid risk and the field stays nullable indefinitely. But a field that is **nullable in the schema** and **never null in practice is a silent lie**. This is a missed opportunity to make your data model clearer and safer.
22+
23+
### How to fix it?
24+
25+
To help find these fields, I wrote a simple script that checks all models in a project. It scans for nullable fields and calculates how many rows actually contain null values. The idea is to identify cases where the field could safely be changed to non-nullable.
26+
27+
Here’s the code (written for Django, but the logic can be adapted to any ORM or raw SQL setup):
28+
29+
```python
30+
from django.apps import apps
31+
from django.db import connection, transaction
32+
33+
def get_nullable_fields(model):
34+
nullable_fields = []
35+
for field in model._meta.fields:
36+
if getattr(field, 'null', False) and not field.auto_created:
37+
nullable_fields.append(field.name)
38+
return nullable_fields
39+
40+
def get_null_percentage_for_field(model, field_name, total_rows):
41+
if total_rows == 0:
42+
return 0.0
43+
null_count = model.objects.filter(**{f"{field_name}__isnull": True}).count()
44+
return round((null_count / total_rows) * 100, 2)
45+
46+
def analyze_all_models_fields_nulls_with_timeout(timeout_ms=5000):
47+
"""
48+
Analyze every nullable field in every model with a DB-level timeout in ms.
49+
"""
50+
results = []
51+
52+
for model in apps.get_models():
53+
model_label = f"{model._meta.app_label}.{model.__name__}"
54+
55+
with connection.cursor() as cursor:
56+
cursor.execute(f"SET LOCAL statement_timeout = {timeout_ms};")
57+
with transaction.atomic():
58+
nullable_fields = get_nullable_fields(model)
59+
total_rows = model.objects.count()
60+
61+
if total_rows == 0:
62+
continue
63+
64+
for field in nullable_fields:
65+
null_percent = get_null_percentage_for_field(model, field, total_rows)
66+
results.append({
67+
"model": model_label,
68+
"field": field,
69+
"null_percentage": null_percent,
70+
})
71+
return results
72+
```
73+
74+
### How to use it?
75+
76+
You can integrate this logic into a command-line tool, admin report, or health check. The output shows each nullable field and what percentage of rows actually contain nulls.
77+
78+
If a field has **0.0%** nulls, that’s a strong signal it should no longer be nullable. That change makes your schema more accurate, improves validation, and simplifies your codebase.
79+
80+
```json
81+
[
82+
{
83+
"model": "users.Profile",
84+
"field": "birth_date",
85+
"null_percentage": 0.0
86+
},
87+
{
88+
"model": "orders.Order",
89+
"field": "coupon_code",
90+
"null_percentage": 92.4
91+
},
92+
{
93+
"model": "products.Product",
94+
"field": "description",
95+
"null_percentage": 0.0
96+
}
97+
]
98+
```
99+
100+
### Final thoughts
101+
102+
Nullable fields that never hold nulls are often the result of incomplete migrations or forgotten cleanup steps. Catching and fixing them keeps your data model honest and your application healthier.
103+
104+
If a field is meant to be required, your database should say so. Nullable but not null? Time to clean that up.

0 commit comments

Comments
 (0)