Skip to content

Commit 8b80c79

Browse files
committed
Use raw sql query with a faster db + Remove SensorDataStat
1 parent 2e51046 commit 8b80c79

File tree

13 files changed

+151
-304
lines changed

13 files changed

+151
-304
lines changed

contrib/start.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ touch /src/logs/gunicorn.log
1313
touch /src/logs/access.log
1414
tail -n 0 -f /src/logs/*.log &
1515

16+
#purge all configured task queues
17+
celery -A sensorsafrica purge
1618
celery -A sensorsafrica beat -l info &> /src/logs/celery.log &
1719
celery -A sensorsafrica worker --hostname=$DOKKU_APP_NAME -l info &> /src/logs/celery.log &
1820
celery -A sensorsafrica flower --basic_auth=$SENSORSAFRICA_FLOWER_ADMIN_USERNAME:$SENSORSAFRICA_FLOWER_ADMIN_PASSWORD &> /src/logs/celery.log &

sensorsafrica/admin.py

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.utils.html import format_html
33
from django.conf.urls import include, url
44
from django.template.response import TemplateResponse
5-
from .api.models import LastActiveNodes, SensorDataStat, City
5+
from .api.models import LastActiveNodes, City
66
from django.db.models import Q
77

88
from feinstaub.sensors.admin import (
@@ -77,57 +77,6 @@ def delete_model(self, request, obj):
7777
def save_related(self, request, form, formsets, change):
7878
pass
7979

80-
81-
@admin.register(SensorDataStat)
82-
class SensorDataStatAdmin(admin.ModelAdmin):
83-
readonly_fields = [
84-
"node",
85-
"sensor",
86-
"location",
87-
"city_slug",
88-
"value_type",
89-
"average",
90-
"maximum",
91-
"minimum",
92-
"timestamp",
93-
]
94-
search_fields = ["city_slug", "value_type"]
95-
list_display = [
96-
"node",
97-
"sensor",
98-
"location",
99-
"city_slug",
100-
"value_type",
101-
"average",
102-
"maximum",
103-
"minimum",
104-
"timestamp",
105-
"created",
106-
"modified",
107-
]
108-
list_filter = ["timestamp", "node", "sensor", "location"]
109-
110-
def get_actions(self, request):
111-
actions = super(SensorDataStatAdmin, self).get_actions(request)
112-
del actions["delete_selected"]
113-
return actions
114-
115-
def has_add_permission(self, request):
116-
return False
117-
118-
def has_delete_permission(self, request, obj=None):
119-
return False
120-
121-
def save_model(self, request, obj, form, change):
122-
pass
123-
124-
def delete_model(self, request, obj):
125-
pass
126-
127-
def save_related(self, request, form, formsets, change):
128-
pass
129-
130-
13180
@admin.register(City)
13281
class CityAdmin(admin.ModelAdmin):
13382
search_fields = ["slug", "name", "country"]

sensorsafrica/api/models.py

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55

66

77
class City(TimeStampedModel):
8-
slug = models.CharField(max_length=255, db_index=True, null=False, blank=False)
9-
name = models.CharField(max_length=255, db_index=True, null=False, blank=False)
10-
country = models.CharField(max_length=255, db_index=True, null=False, blank=False)
11-
location = models.CharField(max_length=255, db_index=True, null=False, blank=False)
12-
latitude = models.DecimalField(max_digits=14, decimal_places=11, null=True, blank=True)
13-
longitude = models.DecimalField(max_digits=14, decimal_places=11, null=True, blank=True)
8+
slug = models.CharField(
9+
max_length=255, db_index=True, null=False, blank=False)
10+
name = models.CharField(
11+
max_length=255, db_index=True, null=False, blank=False)
12+
country = models.CharField(
13+
max_length=255, db_index=True, null=False, blank=False)
14+
location = models.CharField(
15+
max_length=255, db_index=True, null=False, blank=False)
16+
latitude = models.DecimalField(
17+
max_digits=14, decimal_places=11, null=True, blank=True)
18+
longitude = models.DecimalField(
19+
max_digits=14, decimal_places=11, null=True, blank=True)
1420

1521
class Meta:
1622
verbose_name_plural = "Cities"
@@ -20,36 +26,6 @@ def save(self, *args, **kwargs):
2026
return super(City, self).save(*args, **kwargs)
2127

2228

23-
class SensorDataStat(TimeStampedModel):
24-
node = models.ForeignKey(Node)
25-
sensor = models.ForeignKey(Sensor)
26-
location = models.ForeignKey(SensorLocation)
27-
28-
city_slug = models.CharField(max_length=255, db_index=True, null=False, blank=False)
29-
value_type = models.CharField(max_length=255, db_index=True, null=False, blank=False)
30-
31-
average = models.FloatField(null=False, blank=False)
32-
maximum = models.FloatField(null=False, blank=False)
33-
minimum = models.FloatField(null=False, blank=False)
34-
35-
# Number of data points averaged
36-
sample_size = models.IntegerField(null=False, blank=False)
37-
# Last datetime of calculated stats
38-
last_datetime = models.DateTimeField()
39-
40-
timestamp = models.DateTimeField()
41-
42-
def __str__(self):
43-
return "%s %s %s avg=%s min=%s max=%s" % (
44-
self.timestamp,
45-
self.city_slug,
46-
self.value_type,
47-
self.average,
48-
self.minimum,
49-
self.maximum,
50-
)
51-
52-
5329
class LastActiveNodes(TimeStampedModel):
5430
node = models.ForeignKey(Node)
5531
location = models.ForeignKey(SensorLocation)

sensorsafrica/api/v2/router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
data_router = routers.DefaultRouter()
77

8-
data_router.register(r"", SensorDataStatView)
8+
data_router.register(r"", SensorDataStatView, basename="sensor_data_stat_view")
99

1010
city_router = routers.DefaultRouter()
1111

sensorsafrica/api/v2/serializers.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
11
from rest_framework import serializers
22

33

4-
class SensorDataStatSerializer(serializers.Serializer):
5-
average = serializers.FloatField()
6-
minimum = serializers.FloatField()
7-
maximum = serializers.FloatField()
8-
value_type = serializers.CharField(max_length=200)
9-
start_datetime = serializers.DateTimeField()
10-
end_datetime = serializers.DateTimeField()
11-
city_slug = serializers.CharField(max_length=200)
4+
class RawSensorDataStatSerializer(serializers.Serializer):
5+
average = serializers.SerializerMethodField()
6+
minimum = serializers.SerializerMethodField()
7+
maximum = serializers.SerializerMethodField()
8+
value_type = serializers.SerializerMethodField()
9+
start_datetime = serializers.SerializerMethodField()
10+
end_datetime = serializers.SerializerMethodField()
11+
city_slug = serializers.SerializerMethodField()
12+
13+
def get_city_slug(self, obj):
14+
return obj[0]
15+
16+
def get_start_datetime(self, obj):
17+
return obj[1]
18+
19+
def get_end_datetime(self, obj):
20+
return obj[2]
21+
22+
def get_average(self, obj):
23+
return obj[3]
24+
25+
def get_minimum(self, obj):
26+
return obj[4]
27+
28+
def get_maximum(self, obj):
29+
return obj[5]
30+
31+
def get_value_type(self, obj):
32+
return obj[6]
1233

1334

1435
class CitySerializer(serializers.Serializer):

sensorsafrica/api/v2/views.py

Lines changed: 54 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
from django.utils import timezone
99
from dateutil.relativedelta import relativedelta
1010
from django.db.models import ExpressionWrapper, F, FloatField, Max, Min, Sum, Avg, Q
11-
from django.db.models.functions import Cast, TruncDate
11+
from django.db.models.functions import Cast, TruncDay, TruncHour, TruncMinute, TruncMonth
1212
from rest_framework import mixins, pagination, viewsets
1313

14-
from ..models import SensorDataStat, LastActiveNodes, City, Node
15-
from .serializers import SensorDataStatSerializer, CitySerializer
14+
from django.db import connection
15+
16+
from ..models import LastActiveNodes, City, Node
17+
from .serializers import RawSensorDataStatSerializer, CitySerializer
1618

1719
from feinstaub.sensors.views import StandardResultsSetPagination
1820

@@ -76,7 +78,8 @@ def get_paginated_response(self, data_stats):
7678
results[city_slug][value_type] = [] if from_date else {}
7779

7880
values = results[city_slug][value_type]
79-
include_result = getattr(values, "append" if from_date else "update")
81+
include_result = getattr(
82+
values, "append" if from_date else "update")
8083
include_result(
8184
{
8285
"average": data_stat["average"],
@@ -98,8 +101,7 @@ def get_paginated_response(self, data_stats):
98101

99102

100103
class SensorDataStatView(mixins.ListModelMixin, viewsets.GenericViewSet):
101-
queryset = SensorDataStat.objects.none()
102-
serializer_class = SensorDataStatSerializer
104+
serializer_class = RawSensorDataStatSerializer
103105
pagination_class = CustomPagination
104106

105107
@method_decorator(cache_page(3600))
@@ -112,60 +114,31 @@ def get_queryset(self):
112114
city_slugs = self.request.query_params.get("city", None)
113115
from_date = self.request.query_params.get("from", None)
114116
to_date = self.request.query_params.get("to", None)
117+
avg = self.request.query_params.get("avg", 'day')
115118

116119
if to_date and not from_date:
117-
raise ValidationError({"from": "Must be provide along with to query"})
120+
raise ValidationError(
121+
{"from": "Must be provide along with to query"})
118122
if from_date:
119-
validate_date(from_date, {"from": "Must be a date in the format Y-m-d."})
123+
validate_date(
124+
from_date, {"from": "Must be a date in the format Y-m-d."})
120125
if to_date:
121-
validate_date(to_date, {"to": "Must be a date in the format Y-m-d."})
126+
validate_date(
127+
to_date, {"to": "Must be a date in the format Y-m-d."})
122128

123-
value_type_to_filter = self.request.query_params.get("value_type", None)
129+
value_type_to_filter = self.request.query_params.get(
130+
"value_type", None)
124131

125132
filter_value_types = value_types[sensor_type]
126133
if value_type_to_filter:
127-
filter_value_types = set(value_type_to_filter.upper().split(",")) & set(
134+
filter_value_types = ",".join(set(value_type_to_filter.upper().split(",")) & set(
128135
[x.upper() for x in value_types[sensor_type]]
129-
)
136+
))
130137

131138
if not from_date and not to_date:
132-
return self._retrieve_past_24hrs(city_slugs, filter_value_types)
133-
134-
return self._retrieve_range(from_date, to_date, city_slugs, filter_value_types)
135-
136-
@staticmethod
137-
def _retrieve_past_24hrs(city_slugs, filter_value_types):
138-
to_date = timezone.now().replace(minute=0, second=0, microsecond=0)
139-
from_date = to_date - datetime.timedelta(hours=24)
140-
141-
queryset = SensorDataStat.objects.filter(
142-
value_type__in=filter_value_types,
143-
timestamp__gte=from_date,
144-
timestamp__lte=to_date,
145-
)
146-
147-
if city_slugs:
148-
queryset = queryset.filter(city_slug__in=city_slugs.split(","))
149-
150-
return (
151-
queryset.order_by()
152-
.values("value_type", "city_slug")
153-
.annotate(
154-
start_datetime=Min("timestamp"),
155-
end_datetime=Max("timestamp"),
156-
average=ExpressionWrapper(
157-
Sum(F("average") * F("sample_size")) / Sum("sample_size"),
158-
output_field=FloatField(),
159-
),
160-
minimum=Min("minimum"),
161-
maximum=Max("maximum"),
162-
)
163-
.order_by("city_slug")
164-
)
165-
166-
@staticmethod
167-
def _retrieve_range(from_date, to_date, city_slugs, filter_value_types):
168-
if not to_date:
139+
to_date = timezone.now().replace(minute=0, second=0, microsecond=0)
140+
from_date = to_date - datetime.timedelta(hours=24)
141+
elif not to_date:
169142
from_date = beginning_of_day(from_date)
170143
# Get data from_date until the end
171144
# of day yesterday which is the beginning of today
@@ -174,31 +147,36 @@ def _retrieve_range(from_date, to_date, city_slugs, filter_value_types):
174147
from_date = beginning_of_day(from_date)
175148
to_date = end_of_day(to_date)
176149

177-
queryset = SensorDataStat.objects.filter(
178-
value_type__in=filter_value_types,
179-
timestamp__gte=from_date,
180-
timestamp__lt=to_date,
181-
)
182-
183-
if city_slugs:
184-
queryset = queryset.filter(city_slug__in=city_slugs.split(","))
185-
186-
return (
187-
queryset.annotate(date=TruncDate("timestamp"))
188-
.values("date", "value_type")
189-
.annotate(
190-
city_slug=F("city_slug"),
191-
start_datetime=Min("timestamp"),
192-
end_datetime=Max("timestamp"),
193-
average=ExpressionWrapper(
194-
Sum(F("average") * F("sample_size")) / Sum("sample_size"),
195-
output_field=FloatField(),
196-
),
197-
minimum=Min("minimum"),
198-
maximum=Max("maximum"),
199-
)
200-
.order_by("-date")
201-
)
150+
with connection.cursor() as cursor:
151+
cursor.execute(
152+
"""
153+
SELECT
154+
sl.city as city_slug,
155+
min(sd."timestamp") as start_datetime,
156+
max(sd."timestamp") as end_datetime,
157+
sum(CAST("value" as float)) / COUNT(*) AS average,
158+
min(CAST("value" as float)) as minimum,
159+
max(CAST("value" as float)) as maximum,
160+
v.value_type
161+
FROM
162+
sensors_sensordatavalue v
163+
INNER JOIN sensors_sensordata sd ON sd.id = sensordata_id
164+
INNER JOIN sensors_sensorlocation sl ON sl.id = location_id
165+
WHERE
166+
v.value_type IN (%s)
167+
"""
168+
+
169+
("AND sl.city IN (%s)" if city_slugs else "")
170+
+
171+
"""
172+
AND sd."timestamp" >= TIMESTAMP %s
173+
AND sd."timestamp" <= TIMESTAMP %s
174+
GROUP BY
175+
DATE_TRUNC(%s, sd."timestamp"),
176+
v.value_type,
177+
sl.city
178+
""", [filter_value_types, city_slugs, from_date, to_date, avg] if city_slugs else [filter_value_types, from_date, to_date, avg])
179+
return cursor.fetchall()
202180

203181

204182
class CityView(mixins.ListModelMixin, viewsets.GenericViewSet):
@@ -225,7 +203,8 @@ def list(self, request):
225203
moved_to = None
226204
# Get data stats from 5mins before last_data_received_at
227205
if last_data_received_at:
228-
last_5_mins = last_data_received_at - datetime.timedelta(minutes=5)
206+
last_5_mins = last_data_received_at - \
207+
datetime.timedelta(minutes=5)
229208
stats = (
230209
SensorDataValue.objects.filter(
231210
Q(sensordata__sensor__node=last_active.node.id),

0 commit comments

Comments
 (0)