MapReduce using Dapper

#Patterns

Posted by nick on September 02, 2019

Whilst working at Patient.Info, I was tasked with upgrading the forums to be more performant; The Forums hose thousands of page, hundreds of categories, and help people from all over the globe to overcome medical issues - and some just like to chat. It's a real community.

Anyways; from a performance perspective, the forums pages were loading on an average of 4-5seconds, SQL was sat at around 30% utilization, going to 60% with some load, and then occasionally it would fall over... not a good place to be.

To tackle this; I took a look at how Orchard Core was using YesSql. YesSql turns data in to "Json Documents" and stores everything in one column, it then turns the queryable data into a highly optimized index table. SQL is very fast when the data is indexed, and that index is not defragmented. Each table within YesSql is a clustered index.

This was great - let's do that!

But just using YesSQL was not possible due to time constraints; I needed a way to simulate this.. to do this, I created a map-reduce pattern in C# using Views.

To start with, you need a query to get you what you want... in this scenario, a discussion. I use a TVP to make the query slightly faster when I have multiple records;

var itemDataTable = new DataTable();
itemDataTable.Columns.Add("Id", typeof(long));

foreach (var queryId in queryIds) {
    itemDataTable.Rows.Add(queryId);
}

var results = _connection.Query<DiscussionSummary,
    DiscussionSummary>(@"
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

    SELECT
        d.Id,
        d.Content,
        d.HasImages,
        d.HasVideos
    FROM Discussions d
        INNER JOIN @discussionIdsTVP dtvp ON dtvp.Id = d.Id;",
    (d) => {
        d.SluggedTitle = d.Title.Slug();

        return d;
    },
    param: new { discussionIdsTVP = itemDataTable.AsTableValuedParameter("[dbo].[IdTVP]") });

return results
    .ToDictionary(k => queryKeyValues.First(x => x.Value == k.Id).Key, v => v);

So... I can now get a discussion by an Id; the next step is knowing what I want!? This is where the filter comes in, the reduce.

I want to get all Discussions archived since a specific date;

public IEnumerable<long> GetAllArchivedSince(DateTime lastUpdated) {
    return _connection.Query<long>(@"
        SELECT
            d.[Id]
            FROM Discussions d
            WHERE d.Archived = 1
              AND d.LastUpdated >= @lastUpdated
        ",
        new { lastUpdated });
}

Here you would place an index on the Archived and LastUpdated records.

So... you have your MapReduce pattern using Dapper; you reduce your records and get your map. Next, add caching :)