DocumentDB Stored Procedures and User Defined Functions

Ok, time for confession. I’m not a fan of stored procedures (SProcs) within databases. I try not to be a hammer coder (one tool/language for all problems) but it’s more the fact I personally find SQL SProcs to be harder to read/develop and debug. Give me a nice C# source base any day. Now, I DO realise the proposed benefits of SProcs, do all data calculations on the server BEFORE they get returned top the application. Fewer bytes on the wire, quicker to transmit etc. BUT… when the DB and the application are co-located (both within the same Azure location) do we REALLY need to worry about data transfer between DB and app? Well, maybe. Depending if we’re talking about HUGE volumes of traffic or not. For now, I’m assuming not.

I’m not the NSA…

(or am I?) Smile with tongue out

Once I learned that DocumentDB was also introducing SProcs, I was VERY concerned that again I would get involved with a source base that has a huge volume of SProcs that would be hard to debug/deploy.

Remember, I’m highly bias AGAINST SProcs, but all my measuring/testing I’ll be doing for this blog post will be as unbiased as possible.

The simple scenario I’m looking at is searching a collection of documents for a particular term within a particular property (just to keep it easy). Each of these properties consist of 100 randomly selected words.

All of these tests are based on the compute and docdb being co-located in the same geo region.

So firstly, what the SProcs look like?

function ( searchTerm) {
    var context = getContext();
    var collection = context.getCollection();
    var response = context.getResponse();

    var returnMessages = [];

    var lowerSearchTerm;
    if (searchTerm) {
        lowerSearchTerm = searchTerm.toLowerCase();
    }
    
    GetDocuments(callback);

    function GetDocuments(callback) {
        var qry = 'SELECT c.Message FROM root c'
        var done = collection.queryDocuments(collection.getSelfLink(), qry, { pageSize: 1000 }, callback);
    }

    function callback(err, documents, responseOptions) {
        var messages = documents;

        for (var i = 0; i < messages.length; i++) {
            var message = messages[i];
            if ( message.Message && message.Message.toLowerCase().indexOf(lowerSearchTerm) > -1) {
                returnMessages.push( message);
            }
        }
          
        response.setBody(JSON.stringify(messageThreads));
    }
}

This is fairly straightforward and simple. Get all documents, throw to the callback search all the messages/documents by converting to lowercase then perform an indexOf. Simple and straight forward.

Now, my initial test data consisted of 1000 documents, 10 of which had my magical search term. The results were:

394ms
127ms
148ms
143ms
117ms

The initial query ALWAYS was far longer… assuming something is warming up/compiling/caching etc, but I thought I’d include it in the results anyway.

Ok, so 1000 docs, searching for my term, about 117-148ms for the most part. Cool, I can live with that.

Now, what about User Defined Functions? Now firstly, in case you don’t know what UDF’s are, they’re basically a snippet of Javascript which performs some functionality on a single record (to my knowledge). This Javascript can be called by using the SQL syntax when querying DocumentDB. In my case I needed to write a small UDF to search substrings within the Message property, so in this case the Javascript was:

function(input, searchTerm) {
    return input.Message.toLowerCase().indexOf( searchTerm ) > -1;
}

There are 2 ways to add UDF’s and SProcs, just as an example the way I initially added the above UDF was through code (as opposed to using a tool such as the very useful DocumentDB Studio).

private void SetupUDF(DocumentCollection collection)
{

    UserDefinedFunction function = new UserDefinedFunction()
    {
        Id = "SubStringSearch",
        Body = @"function(input, searchTerm) 
            {
                return input.toLowerCase().indexOf( searchTerm ) > -1;
            }"
    };

    var t = DocClient.CreateUserDefinedFunctionAsync(collection.SelfLink, function);
    t.Wait();
}

Once SetupUDF is called, then we’re able to use the function “SubStringSearch” via the SQL syntax.

var queryString = "SELECT r.Message FROM root r WHERE SubStringSearch( r, 'mysearchterm')";      
var results = DocClient.CreateDocumentQuery<MessageEntity>(collection.SelfLink, queryString);

Hey presto… we now have substring searching available via the SQL Syntax (of course when the DocumentDB team add a “like” type of operator, then this will not be needed). So, how did it perform?

I really had high hopes for this in comparison to the Stored Procedure. My understanding is that the SProc and UDF are “compiled” in some fashion behind the scenes and aren’t interpreted at query time. I also thought that since the UDF is called within a SQL statement which is completely run on the DocumentDB storage servers then the performance would be comparable to the SProc. I was wrong. Really wrong.

The results for the same set of documents were:

527ms
476ms
485ms
464ms
425ms

That’s 3-4 times worse than the SProc. Not what I hoped nor wanted. I’ve double checked the code, but alas the code is so small that I doubt even *I* could mess it up. I think.

So what about larger data sets? Instead of searching 1000 documents for a term that only appears in 10, what about 7500? (or more precisely 7505 since that when I got bored waiting for the random doc generator to finish)

It’s worse.

The SProc got:

420ms
101ms
139ms
106ms
137ms

Which is comparable to the results it previously got. But the UDF seems to scale linearly…  it got:

3132ms
3163ms
3259ms
15369ms
17832ms

Ignoring those last 2 entries for a moment, it looks like if I increase the document collection by 7.5 times (1000 to 7505) then my times also appear to increase by a similar factor. This was definitely unexpected.

Now, those last 2 entries are interesting. I’ve only shown one of my test runs here, but with virtually every test run performed I’d end up with one super large query time. This was due to a RequestRateTooLargeException being thrown and the LINQ provider retrying the request. Why would the UDF method be getting this and it appears that the SProc does not, even though the SProc does execute the query: “select c.Message from root c”  (ie get EVERY document)

So it looks like UDFs are slower and do not scale. Also one fact I discovered is that you can only call a single UDF per SQL query, but I’m guessing this is just an artificial limitation the DocumentDB team has enforced until the technology becomes more mature.

It is a disappointment that UDFs are not as quick (or even comparable) to the SProcs but I’m not giving up hope yet. If SProcs can be that quick, then (to my simplistic mind) I can’t see why UDF’s couldn’t be nearly as quick in the future.

As a closing note, while trawling through the fiddler traces when performing the tests I discovered some scary facts that relate to UDFs and the linear performance. When I executed the SProcs for testing I was getting Request Charges of:

628.82
627.18
629.06
617.53
629.18

 

But for the UDF approach the Request Charges were:

6911.24
6926.76
6913
7047.82
6903.06

I have not investigated further on the charges, but is certainly on my to-do list.

Conclusions to all of this? As much as I dislike SProcs in general (and business logic being able to creep into the datastore layer) I think I’ll have to continue using them.

DocumentDB is still my favourite storage option for my various projects (more features than Azure Table Storage but not as huge/crazy as Azure Database). It has its limitations, but the service/platform is young.

I’m definitely going to be re-running my tests to keep an eye on the UDF performance. One day, maybe UDF will be good enough that I can say goodbye to SProcs forever (cue party music…   fade out)

Advertisements

2 thoughts on “DocumentDB Stored Procedures and User Defined Functions

  1. Great post. Had a document validation idea that involved bringing in data from an outside api. After naively going for $.getJSON (i know… worth a try, but that didn’t work) I tried plain javascript and got a <> error. So external requests are not supported? got a comment on that?

    • Honestly no idea about that. Unsure if SProcs can go and call to external functions (I’d doubt it to be honest, but that’s just an opinion). If I see anything about it, I’ll write it up here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s