views:

758

answers:

6

I have a simple data model that includes

USERS: store basic information (key, name, phone # etc)

RELATIONS: describe, e.g. a friendship between two users (supplying a relationship_type + two user keys)

COMMENTS: posted by users (key, comment text, user_id)

I'm getting very poor performance, for instance, if I try to print the first names of all of a user's friends. Say the user has 500 friends: I can fetch the list of friend user_ids very easily in a single query. But then, to pull out first names, I have to do 500 back-and-forth trips to the Datastore, each of which seems to take on the order of 30 ms. If this were SQL, I'd just do a JOIN and get the answer out fast.

I understand there are rudimentary facilities for performing two-way joins across un-owned relations in a relaxed implementation of JDO (as described at http://gae-java-persistence.blogspot.com) but they sound experimental and non-standard (e.g. my code won't work in any other JDO implementation).

Worse yet, what if I want to pull out all the comments posted by a user's friends. Then I need to get from User --> Relation --> Comments, i.e. a three-way join, which isn't even supported experimentally. The overhead of 500 back-and-forths to get a friend list + another 500 trips to see if there are any comments from a user's friends is already enough to push runtime >30 seconds.

How do people deal with these problems in real-world datastore-backed JDO applications? (Or do they?)

Has anyone managed to extract satisfactory performance from JDO/Datastore in this kind of (very common) situation?

-Bosh

+2  A: 

First of all, for objects that are frequently accessed (like users), I rely on the memcache. This should speedup your application quite a bit.

If you have to go to the datastore, the right way to do this should be through getObjectsById(). Unfortunately, it looks like GAE doesn't optimize this call. However, a contains() query on keys is optimized to fetch all the objects in one trip to the datastore, so that's what you should use:

List myFriendKeys = fetchFriendKeys();
Query query = pm.newQuery(User.class, ":p.contains(key)");
query.execute(myFriendKeys);

You could also rely on the low-level API get() that accept multiple keys, or do like me and use objectify.

A totally different approach would be to use an equality filter on a list property. This will match if any item in the list matches. So if you have a friendOf list property in your user entity, you can issue a single Query friendOf == theUser. You might want to check this: http://www.scribd.com/doc/16952419/Building-scalable-complex-apps-on-App-Engine

Philippe Beaudoin
Unless AppEngine supports calls to contains() on Collection fields, it looks like you have the best solution.
Gunslinger47
A: 

Facebook has 28 Terabytes of memory cache... However, making 500 trips to memcached isn't very cheap either. It can't be used to store a gazillion pieces of small items. "Denomalization" is the key. Such applications do not need to support ad-hoc queries. Compute and store the results directly for the few supported queries.

in your case, you probably have just 1 type of query - return data of this, that and the others that should be displayed on a user page. You can precompute this big ball of mess, so later one query based on userId can fetch it all.

when userA makes a comment to userB, you retrieve userB's big ball of mess, insert userA's comment in it, and save it.

Of course, there are a lot of problems with this approach. For giant internet companies, they probably don't have a choice, generic query engines just don't cut it. But for others? Wouldn't you be happier if you can just use the good old RDBMS?

irreputable
A: 

Unfortunately Phillipe's suggestion

Query query = pm.newQuery(User.class, ":p.contains(key)");

is only optimized to make a single query when searching by primary key. Passing in a list of ten non-primary-key values, for instance, gives the following trace alt text

I'd like to be able to bulk-fetch comments, for example, from all a user's friends. If I do store a List on each user, this list can't be longer than 1000 elements long (if it's an indexed property of the user) as described at: http://code.google.com/appengine/docs/java/datastore/overview.html .

Seems increasingly like I'm using the wrong toolset here.

-B

Bosh
A: 

If it is a frequently used query, you can consider preparing indexes for the same. http://code.google.com/appengine/articles/index_building.html

Gaurav
A: 

You have to minimize DB reads. That must be a huge focus for any GAE project - anything else will cost you. To do that, pre-calculate as much as you can, especially oft-read information. To solve the issue of reading 500 friends' names, consider that you'll likely be changing the friend list far less than reading it, so on each change, store all names in a structure you can read with one get.

If you absolutely cannot then you have to tweak each case by hand, e.g. use the low-level API to do a batch get.

Also, rather optimize for speed and not data size. Use extra structures as indexes, save objects in multiple ways so you can read it as quickly as possible. Data is cheap, CPU time is not.

Richard Watson
A: 

The indexed property limit is now raised to 5000.

However you can go even higher than that by using the method described in http://www.scribd.com/doc/16952419/Building-scalable-complex-apps-on-App-Engine
Basically just have a bunch of child entities for the User called UserFriends, thus splitting the big list and raising the limit to n*5000, where n is the number of UserFriends entities.

Strom