Skip to content

Commit 5c7ed41

Browse files
committed
Use raw sql query with a faster db
1 parent e1ae955 commit 5c7ed41

File tree

2 files changed

+84
-73
lines changed

2 files changed

+84
-73
lines changed

sensorsafrica/api/v2/serializers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,37 @@ class SensorDataStatSerializer(serializers.Serializer):
1111
city_slug = serializers.CharField(max_length=200)
1212

1313

14+
class RawSensorDataStatSerializer(serializers.Serializer):
15+
average = serializers.SerializerMethodField()
16+
minimum = serializers.SerializerMethodField()
17+
maximum = serializers.SerializerMethodField()
18+
value_type = serializers.SerializerMethodField()
19+
start_datetime = serializers.SerializerMethodField()
20+
end_datetime = serializers.SerializerMethodField()
21+
city_slug = serializers.SerializerMethodField()
22+
23+
def get_city_slug(self, obj):
24+
return obj[0]
25+
26+
def get_start_datetime(self, obj):
27+
return obj[1]
28+
29+
def get_end_datetime(self, obj):
30+
return obj[2]
31+
32+
def get_average(self, obj):
33+
return obj[3]
34+
35+
def get_minimum(self, obj):
36+
return obj[4]
37+
38+
def get_maximum(self, obj):
39+
return obj[5]
40+
41+
def get_value_type(self, obj):
42+
return obj[6]
43+
44+
1445
class CitySerializer(serializers.Serializer):
1546
latitude = serializers.DecimalField(max_digits=14, decimal_places=11)
1647
longitude = serializers.DecimalField(max_digits=14, decimal_places=11)

sensorsafrica/api/v2/views.py

Lines changed: 53 additions & 73 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 django.db import connection
15+
1416
from ..models import SensorDataStat, LastActiveNodes, City, Node
15-
from .serializers import SensorDataStatSerializer, CitySerializer
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"],
@@ -99,7 +102,7 @@ def get_paginated_response(self, data_stats):
99102

100103
class SensorDataStatView(mixins.ListModelMixin, viewsets.GenericViewSet):
101104
queryset = SensorDataStat.objects.none()
102-
serializer_class = SensorDataStatSerializer
105+
serializer_class = RawSensorDataStatSerializer
103106
pagination_class = CustomPagination
104107

105108
@method_decorator(cache_page(3600))
@@ -112,60 +115,31 @@ def get_queryset(self):
112115
city_slugs = self.request.query_params.get("city", None)
113116
from_date = self.request.query_params.get("from", None)
114117
to_date = self.request.query_params.get("to", None)
118+
avg = self.request.query_params.get("avg", 'day')
115119

116120
if to_date and not from_date:
117-
raise ValidationError({"from": "Must be provide along with to query"})
121+
raise ValidationError(
122+
{"from": "Must be provide along with to query"})
118123
if from_date:
119-
validate_date(from_date, {"from": "Must be a date in the format Y-m-d."})
124+
validate_date(
125+
from_date, {"from": "Must be a date in the format Y-m-d."})
120126
if to_date:
121-
validate_date(to_date, {"to": "Must be a date in the format Y-m-d."})
127+
validate_date(
128+
to_date, {"to": "Must be a date in the format Y-m-d."})
122129

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

125133
filter_value_types = value_types[sensor_type]
126134
if value_type_to_filter:
127-
filter_value_types = set(value_type_to_filter.upper().split(",")) & set(
135+
filter_value_types = ",".join(set(value_type_to_filter.upper().split(",")) & set(
128136
[x.upper() for x in value_types[sensor_type]]
129-
)
137+
))
130138

131139
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:
140+
to_date = timezone.now().replace(minute=0, second=0, microsecond=0)
141+
from_date = to_date - datetime.timedelta(hours=24)
142+
elif not to_date:
169143
from_date = beginning_of_day(from_date)
170144
# Get data from_date until the end
171145
# of day yesterday which is the beginning of today
@@ -174,31 +148,36 @@ def _retrieve_range(from_date, to_date, city_slugs, filter_value_types):
174148
from_date = beginning_of_day(from_date)
175149
to_date = end_of_day(to_date)
176150

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-
)
151+
with connection.cursor() as cursor:
152+
cursor.execute(
153+
"""
154+
SELECT
155+
sl.city as city_slug,
156+
min(sd."timestamp") as start_datetime,
157+
max(sd."timestamp") as end_datetime,
158+
sum(CAST("value" as float)) / COUNT(*) AS average,
159+
min(CAST("value" as float)) as minimum,
160+
max(CAST("value" as float)) as maximum,
161+
v.value_type
162+
FROM
163+
sensors_sensordatavalue v
164+
INNER JOIN sensors_sensordata sd ON sd.id = sensordata_id
165+
INNER JOIN sensors_sensorlocation sl ON sl.id = location_id
166+
WHERE
167+
v.value_type IN (%s)
168+
"""
169+
+
170+
("AND sl.city IN (%s)" if city_slugs else "")
171+
+
172+
"""
173+
AND sd."timestamp" >= TIMESTAMP %s
174+
AND sd."timestamp" <= TIMESTAMP %s
175+
GROUP BY
176+
DATE_TRUNC(%s, sd."timestamp"),
177+
v.value_type,
178+
sl.city
179+
""", [filter_value_types, city_slugs, from_date, to_date, avg] if city_slugs else [filter_value_types, from_date, to_date, avg])
180+
return cursor.fetchall()
202181

203182

204183
class CityView(mixins.ListModelMixin, viewsets.GenericViewSet):
@@ -225,7 +204,8 @@ def list(self, request):
225204
moved_to = None
226205
# Get data stats from 5mins before last_data_received_at
227206
if last_data_received_at:
228-
last_5_mins = last_data_received_at - datetime.timedelta(minutes=5)
207+
last_5_mins = last_data_received_at - \
208+
datetime.timedelta(minutes=5)
229209
stats = (
230210
SensorDataValue.objects.filter(
231211
Q(sensordata__sensor__node=last_active.node.id),

0 commit comments

Comments
 (0)