I am building a backend GraphQL service with Django & Graphene and I am having trouble with figuring out what the best way would be to fetch thousands of data points in a performant manner.
Some context, we have an experiment that has 35,000 data points that needs to be displayed on a Plotly plot. The data points are stored in a database table called data_point. Here is the schema of this table:
data_point_id | value | type | created_at
In my Graphene schema, I have a resolver to get all data points for a given experiment as shown below
class ExperimentNode(DjangoObjectType):
users = generic.GenericScalar()
aliases = generic.GenericScalar()
properties = generic.GenericScalar()
data_points = graphene.List(DataPointType)
class Meta:
model = Experiment
# what other filter fields would we want for a batch?
filter_fields = {
'experiment_id': ['exact']
}
interfaces = (graphene.relay.Node, )
def resolve_data_points(self, info, **kwargs):
try:
data_points_for_experiment = DataPoint.objects.filter(experiment_id=self.experiment_id)
data_points = []
for data_point in data_points_for_experiment:
value = data_point.numeric_value
measured_at = data_point.measured_at
attribute = data_point.measurement_type_id
data_points.append({
"value": value,
"measured_at": measured_at,
"attribute": attribute
})
return data_points
except Exception:
return None
Here is my query class
class Query(graphene.ObjectType):
all_batches = DjangoFilterConnectionField(BatchNode)
When I run the query, as expected, it takes a very long time to fetch all 35,000 data points (longer than 10 seconds). I'm still pretty new to Graphene and I'm wondering what the best way would be to fetch all 35,000 points in a fast way. I heard pagination could possibly resolve my issue, but I'm not sure how I would implement such pagination method within Graphene/Django. I know Relay provides a cursor-based pagination, but it limits the maximum amount of values per page to 100.
Any help would be appreciate, and please let me know if I need to provide more code for more clarification.
Thanks!
Related
Question
I have five models:
class Features(models.Model):
name = models.CharField(max_length=100, unique=True)
class Filter(models.Model):
name = models.CharField(max_length=100, unique=True)
class Model(models.Model):
name = models.CharField(max_length=200, unique=True)
class TrainSet(models.Model):
name = models.TextField(unique=True)
class Algorithm(models.Model):
feature = models.ManyToManyField(Features)
filter = models.ManyToManyField(Filter)
model = models.ForeignKey(Model, on_delete=models.DO_NOTHING, null=True)
train_set = models.ForeignKey(TrainSet, on_delete=models.DO_NOTHING, null=True)
I am doing some measurements that produce a calculated prediction based on features, filters, model and trainset. Those together form an algorithm
First four are being changed trough the development process and because of that I would like to save each measurement with algorithm version.
Problem
If I would not care for data redundancy and database table normalizations I could create a different yet same algorithm for measurements. But because i care for redundancy and normalization I would like to check if the algorithm with respective one-to-many and many-to-many already exists. If instance exists, use that instance, otherwise create a new one.
after each measurement the server that calculates the result sends a JSON to my django server in following form:
{
"filters": ["filter1", "filter2"],
"model": "model-1",
"features":["feature1", "feature2"],
"trainset": "trainset1"
}
The problem is that i cannot find a way to check if an algorithm already exists with all its relations.
What is the simplest way to find out if algorithm already exists?
Update 1
I have tried Mohammad Ali's answer and it works partially.
When the algorithm table is empty and i use following data:
{
"filters": ["filter1", "filter2"],
"model": "model-1",
"features":["feature1", "feature2"],
"trainset": "trainset1"
}
it obviously finds zero rows.
I then update the tables with data and then make the same query. It will then find the row with upper data.
But then I make a new request with the following data (filters and features are different than before).
{
"filters": ["filter1", "filter3"],
"model": "model-1",
"features":["feature1", "feature3"],
"trainset": "trainset1"
}
I get the same algorithm as before. Why is that?
You can do it, with filter on Algorithm model, for example:
algorithmList = Algorithm.objects.filter(feature__name__in=['f1', 'f2'], filter__name__in=['fil1', 'fil2'], model__name__in=['m1', 'm2'], train_set__name__in=['t1', 't2'])
if algorithmList.count() == 0:
# do something
You add conditions by using filter multiple times. You can loop in to all your filters and features and add it to your query
query = Algorithm.objects.filter(train_set=train_set_obj, model=model_obj)
features = [feature_obj1,feature_obj2]
filters = [filter_obj1,filter_obj2]
for feature in features:
query = query.filter(feature=feature)
for filter in filters:
query = query.filter(filter=filter)
So, if query exists, that means Algorithm exist or else create a new one
I have the following models:
class Member(models.Model):
ref = models.CharField(max_length=200)
# some other stuff
def __str__(self):
return self.ref
class Feature(models.Model):
feature_id = models.BigIntegerField(default=0)
members = models.ManyToManyField(Member)
# some other stuff
A Member is basically just a pointer to a Feature. So let's say I have Features:
feature_id = 2, members = 1, 2
feature_id = 4
feature_id = 3
Then the members would be:
id = 1, ref = 4
id = 2, ref = 3
I want to find all of the Features which contain one or more Members from a list of "ok members." Currently my query looks like this:
# ndtmp is a query set of member-less Features which Members can point to
sids = [str(i) for i in list(ndtmp.values('feature_id'))]
# now make a query set that contains all rels and ways with at least one member with an id in sids
okmems = Member.objects.filter(ref__in=sids)
relsways = Feature.geoobjects.filter(members__in=okmems)
# now combine with nodes
op = relsways | ndtmp
This is enormously slow, and I'm not even sure if it's working. I've tried using print statements to debug, just to make sure anything is actually being parsed, and I get the following:
print(ndtmp.count())
>>> 12747
print(len(sids))
>>> 12747
print(okmems.count())
... and then the code just hangs for minutes, and eventually I quit it. I think that I just overcomplicated the query, but I'm not sure how best to simplify it. Should I:
Migrate Feature to use a CharField instead of a BigIntegerField? There is no real reason for me to use a BigIntegerField, I just did so because I was following a tutorial when I began this project. I tried a simple migration by just changing it in models.py and I got a "numeric" value in the column in PostgreSQL with format 'Decimal:( the id )', but there's probably some way around that that would force it to just shove the id into a string.
Use some feature of Many-To-Many Fields which I don't know abut to more efficiently check for matches
Calculate the bounding box of each Feature and store it in another column so that I don't have to do this calculation every time I query the database (so just the single fixed cost of calculation upon Migration + the cost of calculating whenever I add a new Feature or modify an existing one)?
Or something else? In case it helps, this is for a server-side script for an ongoing OpenStreetMap related project of mine, and you can see the work in progress here.
EDIT - I think a much faster way to get ndids is like this:
ndids = ndtmp.values_list('feature_id', flat=True)
This works, producing a non-empty set of ids.
Unfortunately, I am still at a loss as to how to get okmems. I tried:
okmems = Member.objects.filter(ref__in=str(ndids))
But it returns an empty query set. And I can confirm that the ref points are correct, via the following test:
Member.objects.values('ref')[:1]
>>> [{'ref': '2286047272'}]
Feature.objects.filter(feature_id='2286047272').values('feature_id')[:1]
>>> [{'feature_id': '2286047272'}]
You should take a look at annotate:
okmems = Member.objects.annotate(
feat_count=models.Count('feature')).filter(feat_count__gte=1)
relsways = Feature.geoobjects.filter(members__in=okmems)
Ultimately, I was wrong to set up the database using a numeric id in one table and a text-type id in the other. I am not very familiar with migrations yet, but as some point I'll have to take a deep dive into that world and figure out how to migrate my database to use numerics on both. For now, this works:
# ndtmp is a query set of member-less Features which Members can point to
# get the unique ids from ndtmp as strings
strids = ndtmp.extra({'feature_id_str':"CAST( \
feature_id AS VARCHAR)"}).order_by( \
'-feature_id_str').values_list('feature_id_str',flat=True).distinct()
# find all members whose ref values can be found in stride
okmems = Member.objects.filter(ref__in=strids)
# find all features containing one or more members in the accepted members list
relsways = Feature.geoobjects.filter(members__in=okmems)
# combine that with my existing list of allowed member-less features
op = relsways | ndtmp
# prove that this set is not empty
op.count()
# takes about 10 seconds
>>> 8997148 # looks like it worked!
Basically, I am making a query set of feature_ids (numerics) and casting it to be a query set of text-type (varchar) field values. I am then using values_list to make it only contain these string id values, and then I am finding all of the members whose ref ids are in that list of allowed Features. Now I know which members are allowed, so I can filter out all the Features which contain one or more members in that allowed list. Finally, I combine this query set of allowed Features which contain members with ndtmp, my original query set of allowed Features which do not contain members.
I want to achieve something like the map drag search on airbnb (https://www.airbnb.com/s/Paris--France?source=ds&page=1&s_tag=PNoY_mlz&allow_override%5B%5D=)
I am saving the data like this in datastore
user.lat = float(lat)
user.lon = float(lon)
user.geoLocation = ndb.GeoPt(float(lat),float(lon))
and whenever I drag & drop map or zoom in or zoom out I get following parameters in my controller
def get(self):
"""
This is an ajax function. It gets the place name, north_east, and south_west
coordinates. Then it fetch the results matching the search criteria and
create a result list. After that it returns the result in json format.
:return: result
"""
self.response.headers['Content-type'] = 'application/json'
results = []
north_east_latitude = float(self.request.get('nelat'))
north_east_longitude = float(self.request.get('nelon'))
south_west_latitude = float(self.request.get('swlat'))
south_west_longitude = float(self.request.get('swlon'))
points = Points.query(Points.lat<north_east_latitude,Points.lat>south_west_latitude)
for row in points:
if row.lon > north_east_longitude and row.lon < south_west_longitude:
listingdic = {'name': row.name, 'desc': row.description, 'contact': row.contact, 'lat': row.lat, 'lon': row.lon}
results.append(listingdic)
self.write(json.dumps({'listings':results}))
My model class is given below
class Points(ndb.Model):
name = ndb.StringProperty(required=True)
description = ndb.StringProperty(required=True)
contact = ndb.StringProperty(required=True)
lat = ndb.FloatProperty(required=True)
lon = ndb.FloatProperty(required=True)
geoLocation = ndb.GeoPtProperty()
I want to improve the query.
Thanks in advance.
No, you cannot improve the solution by checking all 4 conditions in the query because ndb queries do not support inequality filters on multiple properties. From NDB Queries (emphasis mine):
Limitations: The Datastore enforces some restrictions on queries.
Violating these will cause it to raise exceptions. For example,
combining too many filters, using inequalities for multiple
properties, or combining an inequality with a sort order on a
different property are all currently disallowed. Also filters
referencing multiple properties sometimes require secondary indexes to
be configured.
and
Note: As mentioned earlier, the Datastore rejects queries using inequality filtering on more than one property.
I've got three models: Address, Address_localisation (where address_id is ForeignKey) and Address_users (where address_id is ForeignKey again).
In first step I would like to get all the addresses with it's localisation.
I tried to use:
data = address.objects.select_related().all().annotate(
longitude=F('address_localisation__longitude'),
latitude=F('address_localisation__latitude')
)
but if in address_users i've got two (or more) users for one address, this gives me two (or more) rows with the same address (because of joining models).
So I would like to get Addresses and connected localisations only.
What i've tried:
data = address.objects.prefetch_related('address_localisation_set').all()
for e in data.all():
for ee in e.address_localisation_set.all():
e.longitude = ee.longitude
e.latitude = ee.latitude
data = list(data.values('id',
'longitude',
'latitude'
))
data = json.dumps(data)
data = json.loads(data)
return JsonResponse(data, safe = False)
But it leads me to an error:
"Cannot resolve keyword 'longitude' into field. Choices are: id.. (listed fileds from Address model)"
As far as I understand it's because in the main model I don't have longitude/latitude fields... but how should I add them?
I know I could iterate through address_localisation_set in template (i didn't tried that, but find solution on stackoverflow), but adding fields could be useful in another place of my sourcecode, so I would like to know how to do it.
Thank you in advance for your time
You really can't add fields to a model; but you can add any attribute to a model instance because that's how Python works - an object can be assigned attributes "on the fly"; but this is not really what you need in your scenario.
You need to build a custom data set and send it to your template, which is easily done in your view:
results = [] # This is what you will send to the template
for location in address.objects.select_related():
data = {} # an empty dictionary
localized_data = location.address_localisation_set.first()
data['lat'] = localized_data.latitude
data['lon'] = localized_data.longitude
data['id'] = location.id
results.append(data)
return JsonResponse(json.dumps(results), safe=False)
I am writing a simple Python web application that consists of several pages of business data formatted for the iPhone. I'm comfortable programming Python, but I'm not very familiar with Python "idiom," especially regarding classes and objects. Python's object oriented design differs somewhat from other languages I've worked with. So, even though my application is working, I'm curious whether there is a better way to accomplish my goals.
Specifics: How does one typically implement the request-transform-render database workflow in Python? Currently, I am using pyodbc to fetch data, copying the results into attributes on an object, performing some calculations and merges using a list of these objects, then rendering the output from the list of objects. (Sample code below, SQL queries redacted.) Is this sane? Is there a better way? Are there any specific "gotchas" I've stumbled into in my relative ignorance of Python? I'm particularly concerned about how I've implemented the list of rows using the empty "Record" class.
class Record(object):
pass
def calculate_pnl(records, node_prices):
for record in records:
try:
# fill RT and DA prices from the hash retrieved above
if hasattr(record, 'sink') and record.sink:
record.da = node_prices[record.sink][0] - node_prices[record.id][0]
record.rt = node_prices[record.sink][1] - node_prices[record.id][1]
else:
record.da = node_prices[record.id][0]
record.rt = node_prices[record.id][1]
# calculate dependent values: RT-DA and PNL
record.rtda = record.rt - record.da
record.pnl = record.rtda * record.mw
except:
print sys.exc_info()
def map_rows(cursor, mappings, callback=None):
records = []
for row in cursor:
record = Record()
for field, attr in mappings.iteritems():
setattr(record, attr, getattr(row, field, None))
if not callback or callback(record):
records.append(record)
return records
def get_positions(cursor):
# get the latest position time
cursor.execute("SELECT latest data time")
time = cursor.fetchone().time
hour = eelib.util.get_hour_ending(time)
# fetch the current positions
cursor.execute("SELECT stuff FROM atable", (hour))
# read the rows
nodes = {}
def record_callback(record):
if abs(record.mw) > 0:
if record.id: nodes[record.id] = None
return True
else:
return False
records = util.map_rows(cursor, {
'id': 'id',
'name': 'name',
'mw': 'mw'
}, record_callback)
# query prices
for node_id in nodes:
# RT price
row = cursor.execute("SELECT price WHERE ? ? ?", (node_id, time, time)).fetchone()
rt5 = row.lmp if row else None
# DA price
row = cursor.execute("SELECT price WHERE ? ? ?", (node_id, hour, hour)).fetchone()
da = row.da_lmp if row else None
# update the hash value
nodes[node_id] = (da, rt5)
# calculate the position pricing
calculate_pnl(records, nodes)
# sort
records.sort(key=lambda r: r.name)
# return the records
return records
The empty Record class and the free-floating function that (generally) applies to an individual Record is a hint that you haven't designed your class properly.
class Record( object ):
"""Assuming rtda and pnl must exist."""
def __init__( self ):
self.da= 0
self.rt= 0
self.rtda= 0 # or whatever
self.pnl= None #
self.sink = None # Not clear what this is
def setPnl( self, node_prices ):
# fill RT and DA prices from the hash retrieved above
# calculate dependent values: RT-DA and PNL
Now, your calculate_pnl( records, node_prices ) is simpler and uses the object properly.
def calculate_pnl( records, node_prices ):
for record in records:
record.setPnl( node_prices )
The point isn't to trivially refactor the code in small ways.
The point is this: A Class Encapsulates Responsibility.
Yes, an empty-looking class is usually a problem. It means the responsibilities are scattered somewhere else.
A similar analysis holds for the collection of records. This is more than a simple list, since the collection -- as a whole -- has operations it performs.
The "Request-Transform-Render" isn't quite right. You have a Model (the Record class). Instances of the Model get built (possibly because of a Request.) The Model objects are responsible for their own state transformations and updates. Perhaps they get displayed (or rendered) by some object that examines their state.
It's that "Transform" step that often violates good design by scattering responsibility all over the place. "Transform" is a hold-over from non-object design, where responsibility was a nebulous concept.
Have you considered using an ORM? SQLAlchemy is pretty good, and Elixir makes it beautiful. It can really reduce the ammount of boilerplate code needed to deal with databases. Also, a lot of the gotchas mentioned have already shown up and the SQLAlchemy developers dealt with them.
Depending on how much you want to do with the data you may not need to populate an intermediate object. The cursor's header data structure will let you get the column names - a bit of introspection will let you make a dictionary with col-name:value pairs for the row.
You can pass the dictionary to the % operator. The docs for the odbc module will explain how to get at the column metadata.
This snippet of code to shows the application of the % operator in this manner.
>>> a={'col1': 'foo', 'col2': 'bar', 'col3': 'wibble'}
>>> 'Col1=%(col1)s, Col2=%(col2)s, Col3=%(col3)s' % a
'Col1=foo, Col2=bar, Col3=wibble'
>>>
Using a ORM for an iPhone app might be a bad idea because of performance issues, you want your code to be as fast as possible. So you can't avoid boilerplate code. If you are considering a ORM, besides SQLAlchemy I'd recommend Storm.