-4

I am creating a quiz and im using a random number from a range of 1 - 20 numbers ( Primary Keys)

Random r = new Random();
int rInt = r.Next(1, 9);

The numbers(primary keys) and then used for a query to select 5 random number but the problem is that I am getting repeated questions because the numbers repeat

 string SQL = "SELECT QuestionText,CorrectAnswer,WrongAnswer1,WrongAnswer2,WrongAnswer3 FROM Question Where QuestionID = " + rInt;

I have tried some methods to fix it but its not working and running out of ideas , anyone have any suggestions?

Mr.W
  • 3
  • 4
  • 2
    Random does not mean unique. Put the numbers 1-20 in a collection and shuffle it to use them in a random order. Feed your goggle the keyword *shuffle* - there are many, many good examples here – Ňɏssa Pøngjǣrdenlarp Dec 02 '20 at 00:53
  • 2
    `Enumerable.Range(1, 20).OrderBy(_ => r.Next()).Take(5).ToArray()` – Enigmativity Dec 02 '20 at 00:53
  • could you suggest on a way to put my number 1-20(primary keys) in a suggestion but by getting them directly from the database as the range of number can increase/ decrease if the user add or deletets questions therefore increasing or reducing the range – Mr.W Dec 02 '20 at 01:00
  • @Mr.W - What??? – Enigmativity Dec 02 '20 at 01:06
  • The range of numbers 1-20 are stored in my table(Question) in the column QuestionID , the user can increase the range e.g add 10 more question and now range is 1-30 , I cant think of a way to add those numbers to a list but having them change according to the users new range – Mr.W Dec 02 '20 at 01:23
  • @Mr.W - Why can't you just read them from the database? – Enigmativity Dec 02 '20 at 01:24

5 Answers5

3

Just ask the database for it:

string SQL = @"
SELECT TOP 5 QuestionText,CorrectAnswer,WrongAnswer1,WrongAnswer2,WrongAnswer3 
FROM Question 
ORDER BY NewID()";

If/when you outgrow this, there exists a more optimized solution as well:

string SQL = @"
WITH cte AS 
(
    SELECT TOP 5 QuestionId FROM Questions ORDER BY NEWID()
)
SELECT QuestionText,CorrectAnswer,WrongAnswer1,WrongAnswer2,WrongAnswer3
FROM cte c
JOIN Questions q 
  ON q.QuestionId = c.QuestionId 
";

The second query will perform much better (assuming QuestionId is your primary key) because it will only have to read the primary index (which will likely already be in memory), generate the Guids, pick the top 5 using the most efficient method, then look up those 5 records using the primary key.

The first query should work just fine for smaller number of questions, but I believe it may cause a table scan, and some pressure on tempdb, so if your questions are varchar(max) and get very long, or you have tens of thousands of questions with a very small tempdb with some versions of Sql Server, it may not perform great.

Robert McKee
  • 21,305
  • 1
  • 43
  • 57
0

Something like this might do the trick for you:

[ThreadStatic]
private static Random __random = null;

public int[] Get5RandomQuestions()
{
    __random = __random ?? new Random(Guid.NewGuid().GetHashCode()); // approx one in 850 chance of seed collision
    using (var context = new MyDBContext())
    {
        var questions = context.Questions.Select(x => x.Question_ID).ToArray();
        return questions.OrderBy(_ => __random.Next()).Take(5).ToArray();
    }
}
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
0

Another, server side approach:

private static Random _r = new Random();

...

var seed = _r.NextDouble();

using var context = new SomeContext();

var questions = context.Questions
    .OrderBy(p => SqlFunctions.Checksum(p.Id * seed))
    .Take(5);

Note : Checksum is not bullet proof, limitations apply. This approach should not be used to generate quiz questions in life or death situations.


As per request:

SqlFunctions.Checksum will essentially generate a hash and order by it

 CHECKSUM([Id] * <seed>) AS [C1], 
 ...
 ORDER BY [C1] ASC

CHECKSUM (Transact-SQL)

The CHECKSUM function returns the checksum value computed over a table row, or over an expression list. Use CHECKSUM to build hash indexes.

...

CHECKSUM computes a hash value, called the checksum, over its argument list. Use this hash value to build hash indexes. A hash index will result if the CHECKSUM function has column arguments, and an index is built over the computed CHECKSUM value. This can be used for equality searches over the columns.

Note, as mentioned before the Checksum is not bullet proof it returns an int (take it for what it is), however, the chances of a collision or duplicate is extremely small for smaller data sets when using it in this way with unique Id, it's also fairly performant.

So running this only a production database with 10 million records many times, there was no collisions.

In regards to speed, it can get the top 5 in 75ms, however it is slower when generated by EF

The cte solution tendered for NewId, is about 125 ms.

halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
-2

Try selecting all the questions from the db. Say you have them in a collection 'Question', you could then try Questions.OrderBy(y => Guid.NewGuid()).ToList()

danyel
  • 1
  • 2
  • Guids are not guaranteed to be random, just unique. Don't use them in circumstances where you need randomness. – Enigmativity Dec 02 '20 at 01:25
  • @Enigmativity While not completely random, I believe both .NET 4+ and SQL Server 2005+ generate V4 versions of UUID, which 122 of the 128 bits are random and the remaining bits are always the same making it random enough for sorting. – Robert McKee Dec 02 '20 at 02:42
  • @RobertMcKee - I agree that a particular implementation might have some degree of randomness, but that's only to provide a unique Guid - it's not about generating random values. Who knows what V5 will bring? – Enigmativity Dec 02 '20 at 02:56
-2

The Linq .Distinct() method is too nice to not use here

The easiest way I know of doing this would be like below, using a method to create an infinite stream of random numbers, which can then be nicely wrangled with Linq:

using System.Linq;

IEnumerable<int> GenRandomNumbers()
{
    var random = new Random();

    while (true)
    {
        yield return rand.Next(1, 20);
    }
}

var numbers = GenRandomNumbers()
    .Distinct()
    .Take(5)
    .ToArray();

Though it looks like the generator method will run for ever because of its closed loop, it will only run until it has generated 5 distinct numbers, because of how it yields.

Jason Holloway
  • 682
  • 5
  • 9
  • Except if you call `GenRandomNumbers().Distinct().Take(n)` where `n >= 20` (since `random.Next(1, 20)` produces values from `1 .. 19`). – Enigmativity Dec 02 '20 at 01:28
  • It's random.Next(minValue, maxValue) according to my IDE... (but you're absolutely correct about the potential deadlock – Jason Holloway Dec 02 '20 at 01:32
  • It's `random.Next(inclusiveMinValue, exclusiveMaxValue)`. – Enigmativity Dec 02 '20 at 01:34
  • You're not wrong – Jason Holloway Dec 02 '20 at 01:36
  • But the deadlock issue is only a problem if you have no way of enforcing the invariants protecting it from deadlock - if it was wrapped up in a conscientious method then this approach has got legs – Jason Holloway Dec 02 '20 at 01:37
  • I just scaled this up with `yield return random.Next(1, 20001);` and then asked for `GenRandomNumbers().Distinct().Take(20000)`. The number of calls to `random.Next` to get `20_000` numbers was `209_383`. It's not overly efficient. A simple Fisher-Yates shuffle is only `20_000` calls. – Enigmativity Dec 02 '20 at 01:43
  • This is also true, we should be generating as few random numbers as possible – Jason Holloway Dec 02 '20 at 01:48