The key to the answer is that as a result, two types of questions arise: for each category - one question that must be limited in order to proceed from this category; and some remaining questions.
First, limited questions: we just select one entry from each category:
SELECT id, category_id, question_text, 1 AS constrained, max(random()) AS r FROM so_questions GROUP BY category_id
(This query is based on the function introduced in SQLite 3.7.11 (in jelly Bean or later): in the SELECT a, max(b) query SELECT a, max(b) value of a guaranteed to be obtained from the record with the maximum value of b .)
We should also get unlimited questions (filtering of duplicates that are already in a limited set will occur in the next step):
SELECT id, category_id, question_text, 0 AS constrained, random() AS r FROM so_questions
When we combine these two queries with UNION and then group by id , we have all the duplicates together. Choosing max(constrained) then ensures that for groups that have duplicates, only a limited question remains (while all other questions have only one entry for each group).
Finally, the ORDER BY guarantees that limited questions will be asked first, and then some random other questions:
SELECT *, max(constrained) FROM (SELECT id, category_id, question_text, 1 AS constrained, max(random()) AS r FROM so_questions GROUP BY category_id UNION ALL SELECT id, category_id, question_text, 0 AS constrained, random() AS r FROM so_questions) GROUP BY id ORDER BY constrained DESC, r LIMIT 5
For earlier versions of SQLite / Android, I did not find a solution without using a temporary table (because the subquery for a limited question should be used several times, but does not remain constant due to random() ):
BEGIN TRANSACTION; CREATE TEMPORARY TABLE constrained AS SELECT (SELECT id FROM so_questions WHERE category_id = cats.category_id ORDER BY random() LIMIT 1) AS id FROM (SELECT DISTINCT category_id FROM so_questions) AS cats; SELECT ids.id, category_id, question_text FROM (SELECT id FROM (SELECT id, 1 AS c FROM constrained UNION ALL SELECT id, 0 AS c FROM so_questions WHERE id NOT IN (SELECT id FROM constrained)) ORDER BY c DESC, random() LIMIT 5) AS ids JOIN so_questions ON ids.id = so_questions.id; DROP TABLE constrained; COMMIT TRANSACTION;