2

I need to select random questions per category

private int[] categoryId = {1, 2, 3, 4, 5, ...};
private int[] questionsPerCategory = {3, 1, 6, 11, 7, ...};

Before linq i achieved it by using

SELECT TOP (@questionsPerCategory) * From Questions WHERE CategoriesID = @categoryId  AND 
InTest ='1' ORDER BY NEWID()

Which also was not correct, since i had to call this for every categoryId.

How can i have the desired results with linq in a single query? All i need is fetch

  • 3 random questions, with categoryId = 1 and InTest = true,
  • 1 random question, with categoryId = 2 and InTest = true,
  • 6 random questions, with categoryId = 3 and InTest = true

and so on..

OrElse
  • 9,709
  • 39
  • 140
  • 253

4 Answers4

3

Maybe you want something like this, you do a group by then select how many you want from each category.

Edited: As pointed out by Enigmativity in the comments, Guid.NewGuid() shouldn't be used to for randomness only for uniqueness. To produce randomness you should consult this StackOverflow post.

Demo

using System;
using System.Linq;
using System.Collections.Generic;

public class Program
{
    private static int[] categoryIds = new int[] {1, 2, 3, 4, 5};
    private static int[] questionsPerCategory = {3, 1, 6, 11, 7};
    //Part of demo
    private static IEnumerable<QuestionVM> Questions = Enumerable.Range(0,100).Select(x=> new QuestionVM { Question = $"Question - {x}", CategoryId = (x % 5) + 1});


    public static void Main()
    {
        var questions = Questions.Where(x=> x.InTest).GroupBy(x=> x.CategoryId).SelectMany(x=> x.OrderBy(y=> Guid.NewGuid()).Take(GetQuestionTake(x.Key)));
        foreach(var question in questions)
            Console.WriteLine($"{question.Question} - CategoryId: {question.CategoryId}");  
    }

    ///Finds out how many questions it should take by doing a search and then picking the element in the same position
    private static int GetQuestionTake(int categoryId)
    {
        int element =  categoryIds.Select((x, i) => new { i, x }).FirstOrDefault(x => x.x == categoryId).i;
        return questionsPerCategory.ElementAtOrDefault(element);
    }
}


//Part of demo
public class QuestionVM
{
    public string Question {get;set;}
    public int CategoryId {get;set;}    
    public bool InTest {get;set;} = true;
}
Shoejep
  • 4,414
  • 4
  • 22
  • 26
  • It works as expected but unfortunately if i add .ToList() before .SelectMany i will end up getting all the questions and then call the GetQuestionTake method. Is that true or am i missing something? – OrElse Aug 11 '17 at 10:03
  • If you do a .ToList() it will pull out everything. The GetQuestionTake method gets called for each category id, if you don't like the GetQuestionTake method, I'd suggest turning categoryIds and questionsPerCategory into another type like a Dictionary. – Shoejep Aug 11 '17 at 10:14
  • Unfortunately if i do not call the .ToList() firstly, the GetQuestionTake will not be translated to SQL. By changing to a dictionary what will i gain? I do not understand that actually – OrElse Aug 11 '17 at 10:18
  • 1
    I'm afraid I don't have a database to test this on, but I was thinking something like this (it might not work), https://dotnetfiddle.net/DOXhh5 – Shoejep Aug 11 '17 at 10:36
  • Thank you truly for your effort. Unless i am missing something, the questions returned are always the same. It seems that the randomness in your example is generated while creating a new IEnumerable of Questions. I guess SortBy(random) should be called first, or something else – OrElse Aug 11 '17 at 11:08
  • Ahh okay, they appear "random" in the console, maybe something occurs during the database pulling out process, though you may be better off putting the order by before so that it only does it once like so, var questions = categoryQuestions.SelectMany(x=> Questions.Where(y => y.InTest).OrderBy(z => Guid.NewGuid()).GroupBy(y => x.Key).SelectMany(y => y.Where(z=> z.CategoryId == x.Key).Take(x.Value))); – Shoejep Aug 11 '17 at 11:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151723/discussion-between-orelse-and-shoejep). – OrElse Aug 11 '17 at 12:24
  • @Shoejep - Please don't use `Guid.NewGuid()` as a source of randomness. Guids are only guaranteed to be unique - you can't assume randomness. – Enigmativity Aug 17 '17 at 07:06
  • @Enigmativity Okay thanks, I'd say it depends on the application and doubt it would make much difference in this particular case, though you may want to voice your opinion [here](https://stackoverflow.com/questions/648196/random-row-from-linq-to-sql/648247#648247). – Shoejep Aug 17 '17 at 08:05
  • 1
    @Shoejep - No, it doesn't depend on the application. Guids could be produced in a sequence starting tomorrow if the .NET framework is updated. So long as they are globally unique that would be fine. If you want a random order then you should not choose a Guid. Now, the link you provided is a different thing entirely - if `NEWID` in a database is random then that's perfectly fine, but `Guid.NewGuid()` in .NET is not guaranteed to be random. – Enigmativity Aug 17 '17 at 10:12
  • @Enigmativity I apologise, I wasn't referring to the accepted answer on the link just the other 3+ answers that include Guid.NewGuid() – Shoejep Aug 17 '17 at 10:17
  • @Shoejep - Have a read of Eric Lippert's article - https://ericlippert.com/2012/04/30/guid-guide-part-two/. – Enigmativity Aug 17 '17 at 10:29
  • @Enigmativity Thanks, I realise now that they're not considered random but unique, though it would be nice if others knew this as well! – Shoejep Aug 17 '17 at 10:58
3

Since Guid.NewGuid is not supported by LINQ to SQL, first you need to get access to NEWID function by using the trick from the accepted answer to Random row from Linq to Sql by adding the following to your context class:

partial class YourDataContext {
     [Function(Name="NEWID", IsComposable=true)] 
     public Guid Random() 
     { // to prove not used by our C# code... 
         throw new NotImplementedException(); 
     }
}

Then the query for single CategoryID and question count would be:

var query = db.Questions
    .Where(e => e.CategoriesID == categoryId[i] && e.InTest)
    .OrderBy(e => db.Random())
    .Take(questionsPerCategory[i])

To get the desired result for all category / question count pairs, you could build a UNION ALL SQL query by using Concat of the above single query for i = 0..N like this:

var query = categoryId.Zip(questionsPerCategory,
    (catId, questions) => db.Questions
        .Where(q => q.CategoriesID == catId && q.InTest)
        .OrderBy(q => db.Random())
        .Take(questions)
    ).Aggregate(Queryable.Concat)
    .ToList();

This should produce the desired result with single SQL query. Of course it's applicable if the count of the categoryId is relative small.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
1

I think you are looking for Take() method. You should also pass parameters to the method with category id and how many questions you want to receive. Pass those parameters from your arrays.

private IQuerable<Question> Method(int Id, int questionsCount)
{    
return Questions.Where(c=>c.CategoriesId==Id && c.InTest==1).Take(questionsCount).OrderBy(c=>c.NewId);
}
Crekate
  • 145
  • 1
  • 10
  • Operator '==' cannot be applied to operands of type 'int' and 'int[]' – OrElse Aug 11 '17 at 08:33
  • Checked it. It still complaints. Cannot convert from 'int' to 'System.Func' – OrElse Aug 11 '17 at 08:39
  • We have no idea what data types your properties are. Is InTest a bool? Remove the "==1" then. – Daniel Forslund Aug 11 '17 at 08:46
  • Ye my mistake iin Any() function. please check it now – Crekate Aug 11 '17 at 08:46
  • @DanielForslund as i posted the issue is c=>categoryId.Any(c.CategoriesId) not in the 'InTest' column. Becides that, i will remove the 'InTest' column from my query because the syntax is wrong? – OrElse Aug 11 '17 at 08:49
  • @Crekate Ok, now i have "cannot convert from 'int[]' to 'int'" in the .Take(questionsPerCategory) – OrElse Aug 11 '17 at 08:53
  • I also missed the error in Any() when duplicating the answer, please see the edited answer now. – Daniel Forslund Aug 11 '17 at 08:54
  • Is `questionsPerCategory` index based, so your intention is to select 3 questions from category 1, 1 question from category 2, 6 from category 3 and so forth? – Daniel Forslund Aug 11 '17 at 08:58
  • As I understand from your categoryId first item you would like to receive 3 questions. Is that correct? – Crekate Aug 11 '17 at 08:58
  • @Crekate Yes that it true – OrElse Aug 11 '17 at 08:59
  • @DanielForslund Yes, that is true – OrElse Aug 11 '17 at 09:00
  • Well, you should create a method where you can place parameters like Id and how many questions you want to receive and just pass it from your arrays – Crekate Aug 11 '17 at 09:04
  • @Crekate Thank you for your effort, but i have a feeling this one is gonna hurt. For each loop with "i" queries and all that just for one test? Or am i missing something? The categoryId's are not actually only five – OrElse Aug 11 '17 at 09:07
  • @OrElse Sorry but I cannot understand your comment. – Crekate Aug 11 '17 at 09:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151695/discussion-between-orelse-and-crekate). – OrElse Aug 11 '17 at 09:14
  • @OrElse There is no way to translate the the in-memory function (like IndexOf of the arrays and their values) to any acceptable SQL. Either in LINQ or SQL, you'll end up running multiple queries. – Daniel Forslund Aug 11 '17 at 09:16
  • @DanielForslund Thank you Daniel. I am not so sure about that, since linq provides the .Contains extention method. I guess i will have to open a boundy about this one. It really drives me nuts – OrElse Aug 11 '17 at 09:30
  • @OrElse Contains() only translates to SQL's IN(). What you're describing is a `SELECT TOP(n) FROM Questions`, per category and then you're faced with running multiple queries, one way or another. You can look into .LINQ's Union() to aggregate multiple queries into one result set, but that still doesn't alter the fact that for multiple SELECT's, you're issuing multiple queries to the database. – Daniel Forslund Aug 11 '17 at 11:31
0

A common way is to order by Guid.NewGuid(), so to extend Crekate's answer above.

.OrderBy(c=>Guid.NewGuid());
Daniel Forslund
  • 241
  • 1
  • 8
  • Thanks Daniel, the issue is Cannot convert from 'int' to 'System.Func in this one c=>categoryId.Any(c.CategoriesId) – OrElse Aug 11 '17 at 08:45