PostgreSQL replay performance (Axon 2.4.5)

We had a change to our query model that required replaying around 100,000 of our events from our event log. Not a huge volume of data, but when we tried running the migration, we were dismayed to find that it was taking hours to run. The Java process was mostly idle but PostgreSQL was pegging a CPU.

The problem turned out to be that PostgreSQL was doing a poor job of planning the execution of the queries against the domainevententry table. For each batch of events, even though there was an appropriate index, it was doing a full table scan and a sort before returning any results, which took around 15 seconds. The default JDBC event store has a batch size of 100, so we would have ended up doing 1000 of these slow queries to finish the replay.

Our workaround: increase the batch size on the JDBC event store to 5000 during the replay. This doesn't cause PostgreSQL to execute the queries more efficiently, but it causes Axon to issue about 2% as many of them, so our replay finishes in minutes instead of hours.

Hopefully that workaround will be helpful to other PostgreSQL users.

Now for more details.

There are two issues here, a big PostgreSQL one and a small Axon one. We are filtering by aggregate type for our replay, so the generated SQL looks like

SELECT eventIdentifier, aggregateIdentifier, sequenceNumber, timeStamp, payloadType, payloadRevision, payload, metaData
FROM DomainEventEntry e
WHERE (
  (e.timeStamp > ?)
   OR (e.timeStamp = ? AND e.sequenceNumber > ?)
   OR (e.timeStamp = ? AND e.sequenceNumber = ? AND e.aggregateIdentifier > ?))
AND (type = ?)
ORDER BY e.timeStamp ASC, e.sequenceNumber ASC, e.aggregateIdentifier ASC;

There's an index on the timeStamp column which does end up getting used by this query when the starting timestamp is close to the end of the time range in the event log, but is ignored during the vast majority of the replay. However, if you remove the two OR clauses, it *does* use the index, and adding a LIMIT clause has no effect on the execution plan, so it doesn't seem to be purely a "cost of reading index is too high for the expected row count" situation. The query also gets fast if you transform it into three SELECTS with UNION ALL instead of one SELECT with OR. So PostgreSQL is definitely not doing the optimal thing here.

That said, Axon should ideally be including a LIMIT clause on that query to tell the database that it only plans to read a limited number of rows. That doesn't end up helping in this particular case, at least in my ad-hoc testing, but it seems worthwhile to give the DB the opportunity to optimize for returning the first N rows rather than the entire result set.

The query could also be made simpler by using a row value expression:

SELECT eventIdentifier, aggregateIdentifier, sequenceNumber, timeStamp, payloadType, payloadRevision, payload, metaData
FROM DomainEventEntry e
WHERE (e.timeStamp, e.sequenceNumber, e.aggregateIdentifier) > (?, ?, ?)
AND (type = ?)
ORDER BY e.timeStamp ASC, e.sequenceNumber ASC, e.aggregateIdentifier ASC
LIMIT ?;

Not sure how that'd affect compatibility with all the databases Axon supports, but row value expressions are valid SQL-92 so maybe it'd be okay. Although this is the same meaning as the OR version, PostgreSQL seems to be more willing to use an index for it. Not sure if there'd be a performance impact on other DBs but I suppose it's easy to imagine that it's more optimizer-friendly than the version with the OR clauses.

-Steve

Hi Steven,

the replay mechanism has been redesigned completely in Axon 3, mainly to remove dependency on the timestamp as the global sequence. In Axon 3, each event is given an (auto–increment) sequence number, which is used as primary key, and global sequence. Even though sequence numbers can still “overtake” eachother, they are much more reliable than the timestamps.

The LIMIT you mentioned is actually already on the query (using .setMaxResults() in JPA or setMaxRows() in JDBC).

You should be able to customize the queries performed by the Event Store, by providing another EventEntryStore implementation. However, I have tried the row value expressions, but I can’t get them to work in Hibernate/JPA.

Are you sure you have an index on the timestamp, sequenceNumber and aggregateIdentifier fields, in exactly that order?

Cheers,

Allard

Ah, my mistake on LIMIT; for some reason I thought the PostgreSQL JDBC driver was purely applying that on the client side by silently dropping any rows beyond the limit, but it does in fact pass the limit to the server. Sorry for the false alarm there.

But yes, I did make sure I had an index on those columns in that order. I even tried adding a four-column index on (type, timeStamp, sequenceNumber, aggregateIdentifier) since we’re filtering by exact match on type; that didn’t help either. Don’t know how to explain what the database is doing here.

Glad to hear this will be a complete nonissue in Axon 3! Hopefully by the time we have to to another one of these replays, we’ll have upgraded our application. In the meantime, the increased batch size has made things manageable.

BTW, here’s what I did that made me conclude row value expressions had different behavior than the OR clauses. This is with an index on (timeStamp, sequenceNumber, aggregateIdentifier).

explain analyze
select eventIdentifier, aggregateIdentifier, sequenceNumber, timeStamp, payloadType,
payloadRevision, payload, metaData
from DomainEventEntry e
WHERE ((e.timeStamp > ‘2016-11-19T20:34:22.315Z’)
OR (e.timeStamp = ‘2016-11-19T20:34:22.315Z’
AND e.sequenceNumber > 0)
OR (e.timeStamp = ‘2016-11-19T20:34:22.315Z’
AND e.sequenceNumber = 0
AND e.aggregateIdentifier > ‘dev:642e1953-2562-4768-80d9-0c3af9b0ff84’))
AND (type = ‘transAggPrototype’)
ORDER BY e.timeStamp ASC, e.sequenceNumber ASC, e.aggregateIdentifier ASC;

QUERY PLAN

I asked about this on the PostgreSQL mailing list and someone pointed out that if the query is reorganized a bit to put the ANDs above the ORs, the query planner can more easily detect that it should use the index without resorting to a row value expression.

SELECT ...
FROM DomainEventEntry e
WHERE (e.timeStamp >= ?
  AND (e.timeStamp >  ?
     OR (e.sequenceNumber >= ?
    AND (e.sequenceNumber >  ?
       OR (e.aggregateIdentifier > ?)))))
AND (type = ?)
ORDER BY e.timeStamp ASC, e.sequenceNumber ASC, e.aggregateIdentifier ASC;