How to return top-N from each group with GROUP_CONCAT() #4961
githubmanticore
announced in
Blog
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Originally published on the Manticore Search website on September 4, 2026
How to return top-N from each group with GROUP_CONCAT()
A practical example of how to select several most recent items from each group in Manticore Search and combine them into a single string.
Imagine a customer support screen. An agent searches for refund-related events and, instead of a long log, wants a short summary: one row per user, the total number of matches, and the five most recent events. If more details are needed, the application can load them by ID.
Two obvious approaches don't quite give us what we need:
GROUP_CONCAT()collects all values in the group together,GROUP N BYreturns several rows per user.Starting with Manticore Search 28.6.6, values inside
GROUP_CONCAT()can be sorted and limited to the number you need:Let's see how it works. We'll use SphinxQL and an explicit
GROUP BY- the new form ofGROUP_CONCAT()doesn't work without it.Events we'll work with
Let's create an
activitytable where each document is a single event. The event text is stored inbody, the user inuser_id, and the timestamp inevent_ts.A search for
refundwill find six events for user 101, four for user 202, and two for user 303. We deliberately gave events 1006 and 1007 the same timestamp: later you will see why sorting also needsid.What's wrong with the old approaches
Let's start with regular
GROUP_CONCAT(). The response format is what we need - one row per user:But for user 101, the string will contain all six IDs, for example
1001,1003,1004,1005,1006,1007, while we only need the five most recent ones. Also, without internal sorting, the order of the values is not guaranteed.Another option is to ask
GROUP N BYto select the five most recent documents from each group:The oldest event for user 101 disappears, but each remaining document is returned as a separate row. So instead of three rows, we get eleven: 5 + 4 + 2. This is convenient when the client needs the documents themselves, but it doesn't work for our compact summary.
Combining only the five most recent IDs
Now let's combine both steps: sort the documents directly inside
GROUP_CONCAT()and limit the list to five values there as well.First,
MATCH('refund')selects refund events, thenGROUP BY user_idgroups them by user.COUNT(*)counts all matching events, whileGROUP_CONCAT()takes only the first five from each group after sorting.The second sort key -
id DESC- is especially important here. Events 1006 and 1007 have the sameevent_ts, so without it their relative order would be undefined. Sorting by ID ensures that event 1007 always comes first.Notice that the query has two
ORDER BYclauses. The one insideGROUP_CONCAT()determines the order of IDs in the string. The finalORDER BYsorts the completed rows: first by the number of matches, then byuser_id.The inner
LIMITdoes not changeCOUNT(*)or affect pagination of the overall result. So user 101 still has six matches even though only five IDs are shown next to it. That's exactly what we need for this summary.One more detail:
GROUP_CONCAT()always returns a string, even when it contains numeric IDs. If the API needs to return an array of numbers or objects with several fields, the string has to be parsed on the client side, or a different response format should be used.The query works the same way with distributed tables. Manticore collects candidates from all local and remote tables, then selects the overall top-N for each group.
If a comma doesn't work
By default, values are separated by commas. Sometimes a more readable string is useful - for example, showing the event type next to its ID. You can set a custom separator with
SEPARATOR:In this syntax,
SEPARATORcomes beforeLIMIT. Manticore doesn't escape anything or add quotes: the result is a regular string, not a JSON array.This works well for
event_typebecause those values are controlled by the application. Be more careful with arbitrary text: if the separator appears in the data itself, the result can no longer be parsed reliably. In that case, it's better to return separate rows or use a structured format.Where the new approach doesn't work
This form of
GROUP_CONCAT()has several limitations. It works only in SQL queries with an explicitGROUP BYand does not support:DISTINCT,OFFSET, or combining multiple expressions at once;JOIN,FACET, outerSELECTqueries, or table functions;Its alias cannot be used in
HAVINGor the finalORDER BY. The groups themselves can still be sorted by the grouping key and regular aggregates - for example, byuser_idandCOUNT(*), as in the query above.Memory usage is another consideration. For each such expression, Manticore stores a separate top-N for every group that remains in the result. The more groups there are, the higher
Nis, and the more expressions you use, the more memory is required. The size of the values and sort keys also affects memory usage, so it is best not to set an unnecessarily high limit "just in case".Full documentation for the new functionality is available here.
More examples
Support events are just one possible scenario. The new mode can be useful anywhere a document already contains a group key, the value you want to collect, and a field to sort by:
product_id, sorted bydisplay_order.assignee_id, sorted by a precomputedpriority.If you need a short list of IDs, names, or paths,
GROUP_CONCAT(... ORDER BY ... LIMIT N)now lets you get it in a single query. We hope you find this useful.All reactions