Grouping similar rows together based on time
I would start with a query that finds the latest row in each group of similar rows that are less than 15 minutes apart, and picks the latest five of those:

SELECT DISTINCT model, model_id, timestamp 
FROM changes c1 WHERE (model, model_id) NOT IN
  (SELECT model, model_id FROM changes c2 
   WHERE c2.timestamp &gt; c1.timestamp 
     AND c2.timestamp &lt; c1.timestamp + INTERVAL 15 MINUTE)
ORDER BY timestamp DESC LIMIT 5;


To get a unique number for each group, you can use a variable:

SELECT model, model_id, timestamp, @num:=@num+1 grp 
FROM (&lt;query#1&gt;) last, (SELECT @num:=0) init;


Using this query as a derived table (subquery in FROM clause), you can find all rows that belong to these groups:

SELECT c3.*, groups.grp action_group 
FROM changes c3 JOIN (&lt;query#2&gt;) groups USING(model, model_id) 
WHERE c3.timestamp &lt;= groups.timestamp 
  AND groups.timestamp &lt; c3.timestamp + INTERVAL 15 MINUTE 
ORDER BY c3.timestamp DESC;


The full query then becomes:

SELECT c3.*, groups.grp action_group 
FROM changes c3 JOIN 
    (SELECT model, model_id, timestamp, @num:=@num+1 grp 
     FROM (SELECT DISTINCT model, model_id, timestamp 
           FROM changes c1 
           WHERE (model, model_id) NOT IN
               (SELECT model, model_id FROM changes c2 
                WHERE c2.timestamp &gt; c1.timestamp 
                AND c2.timestamp &lt; c1.timestamp + INTERVAL 15 MINUTE)
           ORDER BY timestamp DESC LIMIT 5) last, 
           (SELECT @num:=0) init
    ) groups USING(model, model_id) 
WHERE c3.timestamp &lt;= groups.timestamp 
  AND groups.timestamp &lt; c3.timestamp + INTERVAL 15 MINUTE 
ORDER BY c3.timestamp DESC;


If your changes table is very big, the above query might take some time.  Indexes on (model, model_id) and timestamp might help.  Also, it might be possible to rewrite the query to use JOIN instead of NOT IN.