สุ่มแถวจาก Linq ถึง Sql


112

วิธีใดที่ดีที่สุด (และเร็วที่สุด) ในการดึงข้อมูลแถวสุ่มโดยใช้ Linq เป็น SQL เมื่อฉันมีเงื่อนไขเช่นบางฟิลด์ต้องเป็นจริง


คุณมีสองทางเลือกสำหรับคำสั่งซื้อที่คุณตรวจสอบเงื่อนไขที่แท้จริง หากเงื่อนไขที่แท้จริงเกิดขึ้นกับไอเท็มส่วนใหญ่ให้จับไอเทมแบบสุ่มจากนั้นทดสอบและทำซ้ำในขณะที่เป็นเท็จ หากหายากให้ฐานข้อมูล จำกัด ตัวเลือกให้เป็นเงื่อนไขที่แท้จริงจากนั้นจึงสุ่มเลือกตัวเลือก
Rex Logan

1
เช่นเดียวกับคำตอบมากมายในไซต์นี้คะแนนอันดับสองดีกว่าคำตอบที่ยอมรับมาก
nikib3ro

คำตอบ:


170

คุณสามารถทำได้ที่ฐานข้อมูลโดยใช้ UDF ปลอม ในคลาสบางส่วนเพิ่มวิธีการในบริบทข้อมูล:

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

จากนั้นเพียงorder by ctx.Random(); นี้จะทำสั่งสุ่มมารยาท SQL NEWID()เซิร์ฟเวอร์ของ กล่าวคือ

var cust = (from row in ctx.Customers
           where row.IsActive // your filter
           orderby ctx.Random()
           select row).FirstOrDefault();

โปรดทราบว่าสิ่งนี้เหมาะสำหรับตารางขนาดเล็กถึงขนาดกลางเท่านั้น สำหรับตารางขนาดใหญ่จะมีผลต่อประสิทธิภาพที่เซิร์ฟเวอร์และจะมีประสิทธิภาพมากขึ้นในการค้นหาจำนวนแถว ( Count) จากนั้นเลือกหนึ่งแถวแบบสุ่ม ( Skip/First)


สำหรับแนวทางการนับ:

var qry = from row in ctx.Customers
          where row.IsActive
          select row;

int count = qry.Count(); // 1st round-trip
int index = new Random().Next(count);

Customer cust = qry.Skip(index).FirstOrDefault(); // 2nd round-trip

3
ถ้าเป็น 30k หลังจากตัวกรองฉันจะบอกว่าไม่: อย่าใช้วิธีนี้ ทำไป - กลับ 2 รอบ; 1 เพื่อรับ Count () และ 1 เพื่อสุ่มแถว ...
Marc Gravell

1
จะเกิดอะไรขึ้นถ้าคุณต้องการสุ่มแถวห้า (หรือ "x")? ที่ดีที่สุดคือการเดินทางไปกลับหกครั้งหรือมีวิธีที่สะดวกในการนำไปใช้ในขั้นตอนการจัดเก็บหรือไม่?
Neal Stublen

2
@ นีลเอส: ลำดับโดย ctx.Random () สามารถผสมกับ Take (5); แต่ถ้าคุณใช้แนวทาง Count () ฉันคาดว่าการเดินทางไปกลับ 6 ครั้งเป็นตัวเลือกที่ง่ายที่สุด
Marc Gravell

1
อย่าลืมเพิ่มการอ้างอิงถึง System.Data.Linq หรือ System.Data.Linq.Mapping.Function แอตทริบิวต์จะไม่ทำงาน
จากัวร์

8
ฉันรู้ว่านี่เก่า แต่ถ้าคุณเลือกแถวสุ่มจำนวนมากจากตารางขนาดใหญ่ให้ดูที่นี่: msdn.microsoft.com/en-us/library/cc441928.aspx ฉันไม่รู้ว่ามีค่าเทียบเท่า LINQ หรือไม่
jwd

60

อีกตัวอย่างสำหรับ Entity Framework:

var customers = db.Customers
                  .Where(c => c.IsActive)
                  .OrderBy(c => Guid.NewGuid())
                  .FirstOrDefault();

สิ่งนี้ใช้ไม่ได้กับ LINQ เป็น SQL OrderByเป็นเพียงการลดลง


4
คุณได้ทำข้อมูลนี้และยืนยันว่าได้ผลหรือไม่? ในการทดสอบของฉันโดยใช้ LINQPad ลำดับตามข้อจะถูกทิ้ง
Jim Wooley

นี่เป็นทางออกที่ดีที่สุดสำหรับปัญหานี้
reach4thelasers

8
สิ่งนี้ใช้ไม่ได้ใน LINQ ถึง SQL ... อาจจะใช้ได้ใน Entity Framework 4 (ไม่ยืนยัน) คุณสามารถใช้ได้เฉพาะ. OrderBy กับ Guid ถ้าคุณกำลังเรียงลำดับรายการ ... ด้วย DB มันจะใช้ไม่ได้
nikib3ro

2
ในที่สุดเพื่อยืนยันว่าสิ่งนี้ใช้งานได้ใน EF4 ซึ่งเป็นตัวเลือกที่ยอดเยี่ยมในกรณีนั้น
nikib3ro

1
คุณช่วยแก้ไขคำตอบของคุณและอธิบายได้ไหมว่าทำไม orderBy กับ Guid ใหม่จึงทำเคล็ดลับ คำตอบที่ดี :)
Jean-FrançoisCôté

32

แก้ไข: ฉันเพิ่งสังเกตว่านี่คือ LINQ เป็น SQL ไม่ใช่ LINQ to Objects ใช้รหัสของ Marc เพื่อรับฐานข้อมูลเพื่อทำสิ่งนี้ให้คุณ ฉันทิ้งคำตอบนี้ไว้ที่นี่เพื่อเป็นจุดสนใจสำหรับ LINQ to Objects

น่าแปลกที่คุณไม่จำเป็นต้องได้รับการนับ อย่างไรก็ตามคุณต้องดึงทุกองค์ประกอบเว้นแต่คุณจะได้รับการนับ

สิ่งที่คุณทำได้คือเก็บแนวคิดเกี่ยวกับค่า "ปัจจุบัน" และจำนวนปัจจุบัน เมื่อคุณดึงค่าถัดไปให้ใช้ตัวเลขสุ่มและแทนที่ "ปัจจุบัน" ด้วย "ใหม่" ด้วยความน่าจะเป็น 1 / n โดยที่ n คือจำนวนนับ

ดังนั้นเมื่อคุณอ่านค่าแรกคุณจะต้องทำให้เป็นค่า "ปัจจุบัน" เสมอ เมื่อคุณอ่านค่าที่สองคุณอาจกำหนดให้เป็นค่าปัจจุบัน (ความน่าจะเป็น 1/2) เมื่อคุณอ่านค่าที่สามคุณอาจกำหนดให้เป็นค่าปัจจุบัน (ความน่าจะเป็น 1/3) เป็นต้นเมื่อข้อมูลหมดค่าปัจจุบันจะสุ่มจากค่าที่คุณอ่านทั้งหมดโดยมีความน่าจะเป็นสม่ำเสมอ

หากต้องการใช้เงื่อนไขนั้นให้ละเว้นสิ่งที่ไม่เป็นไปตามเงื่อนไข วิธีที่ง่ายที่สุดคือพิจารณาลำดับ "การจับคู่" ที่จะเริ่มต้นโดยใช้ประโยค Where ก่อน

นี่คือการใช้งานอย่างรวดเร็ว ฉันคิดว่ามันโอเค ...

public static T RandomElement<T>(this IEnumerable<T> source,
                                 Random rng)
{
    T current = default(T);
    int count = 0;
    foreach (T element in source)
    {
        count++;
        if (rng.Next(count) == 0)
        {
            current = element;
        }            
    }
    if (count == 0)
    {
        throw new InvalidOperationException("Sequence was empty");
    }
    return current;
}

4
FYI - ฉันทำการตรวจสอบอย่างรวดเร็วและฟังก์ชันนี้มีการแจกแจงความน่าจะเป็นที่สม่ำเสมอ (จำนวนที่เพิ่มขึ้นเป็นกลไกเดียวกับการสับเปลี่ยนของ Fisher-Yates ดังนั้นจึงดูสมเหตุสมผลที่ควรจะเป็น)
Greg Beech

@Greg: เจ๋งขอบคุณ มันดูโอเคสำหรับฉันด้วยการตรวจสอบอย่างรวดเร็ว แต่มันง่ายมากที่จะได้รับข้อผิดพลาดทีละรายการในโค้ดเช่นนี้ แทบไม่เกี่ยวข้องกับ LINQ กับ SQL แน่นอน แต่ก็มีประโยชน์
Jon Skeet

@JonSkeet สวัสดีคุณช่วยตรวจสอบสิ่งนี้ได้ไหมและแจ้งให้ฉันทราบว่าฉันขาดอะไรไป
shaijut

@TylerLaing: ไม่ไม่ได้หมายความว่าจะหยุดพัก ในการย้ำแรกcurrentจะเสมอถูกตั้งค่าเป็นองค์ประกอบแรก ในการทำซ้ำครั้งที่สองมีการเปลี่ยนแปลง 50% ซึ่งจะถูกตั้งค่าเป็นองค์ประกอบที่สอง ในการทำซ้ำครั้งที่สามมีโอกาส 33% ที่จะถูกตั้งค่าเป็นองค์ประกอบที่สาม การเพิ่มคำสั่งแบ่งจะหมายความว่าคุณจะออกหลังจากอ่านองค์ประกอบแรกเสมอทำให้ไม่สุ่มเลย
Jon Skeet

@JonSkeet Doh! ฉันอ่านการใช้การนับของคุณผิด (เช่นคิดว่านี่คือสไตล์ฟิชเชอร์ - เยตส์ที่มีช่วงสุ่มเช่นพรรณี) แต่ในการเลือกองค์ประกอบแรกใน Fisher-Yates คือการเลือกองค์ประกอบใด ๆ อย่างเป็นธรรม อย่างไรก็ตามต้องทราบจำนวนองค์ประกอบทั้งหมด ตอนนี้ฉันเห็นว่าโซลูชันของคุณเรียบร้อยสำหรับ IEnumerable โดยที่ไม่ทราบจำนวนรวมและไม่จำเป็นต้องวนซ้ำในแหล่งที่มาทั้งหมดเพื่อรับการนับจากนั้นทำซ้ำอีกครั้งกับดัชนีที่สุ่มเลือก แต่วิธีนี้แก้ได้ในครั้งเดียวตามที่คุณระบุ: "ต้องดึงทุกองค์ประกอบเว้นแต่คุณจะได้รับการนับ"
Tyler Laing

19

วิธีหนึ่งในการบรรลุอย่างมีประสิทธิภาพคือการเพิ่มคอลัมน์ให้กับข้อมูลของคุณShuffleที่เติมด้วย int แบบสุ่ม (เมื่อสร้างแต่ละระเบียน)

แบบสอบถามบางส่วนเพื่อเข้าถึงตารางตามลำดับแบบสุ่มคือ ...

Random random = new Random();
int seed = random.Next();
result = result.OrderBy(s => (~(s.Shuffle & seed)) & (s.Shuffle | seed)); // ^ seed);

นี่เป็นการดำเนินการ XOR ในฐานข้อมูลและสั่งการตามผลลัพธ์ของ XOR นั้น

ข้อดี: -

  1. มีประสิทธิภาพ: SQL จัดการการสั่งซื้อโดยไม่จำเป็นต้องดึงข้อมูลทั้งตาราง
  2. ทำซ้ำได้: (เหมาะสำหรับการทดสอบ) - สามารถใช้เมล็ดพันธุ์แบบสุ่มเดียวกันเพื่อสร้างลำดับแบบสุ่มเดียวกัน

นี่เป็นวิธีที่ระบบอัตโนมัติในบ้านของฉันใช้ในการสุ่มเพลย์ลิสต์ มันเลือกเมล็ดพันธุ์ใหม่ในแต่ละวันโดยให้ลำดับที่สม่ำเสมอในระหว่างวัน (ช่วยให้สามารถหยุดชั่วคราว / เล่นต่อได้ง่าย) แต่ดูใหม่ในแต่ละเพลย์ลิสต์ในแต่ละวันใหม่


ผลของการสุ่มจะเป็นอย่างไรถ้าแทนที่จะเพิ่มฟิลด์ int แบบสุ่มคุณเพิ่งใช้ฟิลด์ข้อมูลประจำตัวที่เพิ่มขึ้นอัตโนมัติที่มีอยู่ (เมล็ดพันธุ์จะยังคงเป็นแบบสุ่ม) ด้วย - ค่าเมล็ดพันธุ์ที่มี a สูงสุดเท่ากับจำนวนระเบียนในตารางเพียงพอหรือไม่หรือควรสูงกว่านี้
ไบรอัน

ตกลงนี่เป็นคำตอบที่ดีซึ่ง IMO ควรมีการโหวตเพิ่มขึ้น ฉันใช้สิ่งนี้ในแบบสอบถาม Entity Framework และตัวดำเนินการ bitwise-XOR ^ ดูเหมือนจะทำงานโดยตรงจึงทำให้เงื่อนไขสะอาดขึ้นเล็กน้อย: result = result.OrderBy(s => s.Shuffle ^ seed);(กล่าวคือไม่จำเป็นต้องใช้ XOR ผ่านตัวดำเนินการ ~, & และ |)
Steven Rands

7

ถ้าคุณต้องการรับเช่นvar count = 16สุ่มแถวจากตารางคุณสามารถเขียน

var rows = Table.OrderBy(t => Guid.NewGuid())
                        .Take(count);

ที่นี่ฉันใช้ EF และตารางเป็น Dbset


1

หากจุดประสงค์ในการสุ่มตัวอย่างแถวสุ่มฉันได้พูดคุยสั้น ๆที่นี่เกี่ยวกับแนวทางที่ดีจาก Larson et al. ทีมวิจัยของ Microsoft ซึ่งพวกเขาได้พัฒนากรอบการสุ่มตัวอย่างสำหรับ Sql Server โดยใช้มุมมองที่เป็นรูปธรรม มีลิงค์ไปยังกระดาษจริงด้วย


1
List<string> lst = new List<string>();
lst.Add("Apple"); 
lst.Add("Guva");
lst.Add("Graps"); 
lst.Add("PineApple");
lst.Add("Orange"); 
lst.Add("Mango");

var customers = lst.OrderBy(c => Guid.NewGuid()).FirstOrDefault();

คำอธิบาย: โดยการใส่ guid (ซึ่งเป็นแบบสุ่ม) คำสั่งที่มี orderby จะเป็นแบบสุ่ม


Guids ไม่ใช่ "สุ่ม" แต่ไม่ได้เรียงตามลำดับ มีความแตกต่าง ในทางปฏิบัติอาจไม่สำคัญสำหรับบางสิ่งที่เล็กน้อยเช่นนี้
Chris Marisic

0

มาที่นี่ด้วยความสงสัยว่าจะสุ่มหน้าจากจำนวนน้อย ๆ ได้อย่างไรดังนั้นผู้ใช้แต่ละคนจึงได้รับ 3 หน้าแบบสุ่มที่แตกต่างกัน

นี่เป็นวิธีแก้ปัญหาขั้นสุดท้ายของฉันโดยใช้ LINQ กับรายการเพจใน Sharepoint 2010 ใน Visual Basic ขออภัย: p

Dim Aleatorio As New Random()

Dim Paginas = From a As SPListItem In Sitio.RootWeb.Lists("Páginas") Order By Aleatorio.Next Take 3

น่าจะได้รับการทำโปรไฟล์ก่อนที่จะค้นหาผลลัพธ์จำนวนมาก แต่ก็เหมาะสำหรับวัตถุประสงค์ของฉัน


0

ฉันมีแบบสอบถามฟังก์ชันสุ่มเทียบกับDataTables:

var result = (from result in dt.AsEnumerable()
              order by Guid.NewGuid()
              select result).Take(3); 

0

ตัวอย่างด้านล่างนี้จะเรียกแหล่งที่มาเพื่อดึงจำนวนจากนั้นใช้นิพจน์การข้ามกับแหล่งที่มาโดยมีตัวเลขระหว่าง 0 ถึง n วิธีที่สองจะใช้คำสั่งโดยใช้วัตถุสุ่ม (ซึ่งจะสั่งทุกอย่างในหน่วยความจำ) และเลือกหมายเลขที่ส่งผ่านไปยังการเรียกวิธีการ

public static class IEnumerable
{
    static Random rng = new Random((int)DateTime.Now.Ticks);

    public static T RandomElement<T>(this IEnumerable<T> source)
    {
        T current = default(T);
        int c = source.Count();
        int r = rng.Next(c);
        current = source.Skip(r).First();
        return current;
    }

    public static IEnumerable<T> RandomElements<T>(this IEnumerable<T> source, int number)
    {
        return source.OrderBy(r => rng.Next()).Take(number);
    }
}

คำอธิบายบางอย่างน่าจะดี
Andrew Barber

รหัสนี้ไม่ปลอดภัยสำหรับเธรดและสามารถใช้ได้เฉพาะในโค้ดเธรดเดียวเท่านั้น ( ไม่ใช่ ASP.NET)
Chris Marisic

0

ฉันใช้วิธีนี้เพื่อรับข่าวสารแบบสุ่มและทำงานได้ดี;)

    public string LoadRandomNews(int maxNews)
    {
        string temp = "";

        using (var db = new DataClassesDataContext())
        {
            var newsCount = (from p in db.Tbl_DynamicContents
                             where p.TimeFoPublish.Value.Date <= DateTime.Now
                             select p).Count();
            int i;
            if (newsCount < maxNews)
                i = newsCount;
            else i = maxNews;
            var r = new Random();
            var lastNumber = new List<int>();
            for (; i > 0; i--)
            {
                int currentNumber = r.Next(0, newsCount);
                if (!lastNumber.Contains(currentNumber))
                { lastNumber.Add(currentNumber); }
                else
                {
                    while (true)
                    {
                        currentNumber = r.Next(0, newsCount);
                        if (!lastNumber.Contains(currentNumber))
                        {
                            lastNumber.Add(currentNumber);
                            break;
                        }
                    }
                }
                if (currentNumber == newsCount)
                    currentNumber--;
                var news = (from p in db.Tbl_DynamicContents
                            orderby p.ID descending
                            where p.TimeFoPublish.Value.Date <= DateTime.Now
                            select p).Skip(currentNumber).Take(1).Single();
                temp +=
                    string.Format("<div class=\"divRandomNews\"><img src=\"files/1364193007_news.png\" class=\"randomNewsImg\" />" +
                                  "<a class=\"randomNews\" href=\"News.aspx?id={0}\" target=\"_blank\">{1}</a></div>",
                                  news.ID, news.Title);
            }
        }
        return temp;
    }

0

การใช้ LINQ เป็น SQL ใน LINQPad เป็นคำสั่ง C # มีลักษณะดังนี้

IEnumerable<Customer> customers = this.ExecuteQuery<Customer>(@"SELECT top 10 * from [Customers] order by newid()");
customers.Dump();

SQL ที่สร้างขึ้นคือ

SELECT top 10 * from [Customers] order by newid()

0

หากคุณใช้LINQPadให้เปลี่ยนเป็นโหมดโปรแกรม C #และดำเนินการดังนี้:

void Main()
{
    YourTable.OrderBy(v => Random()).FirstOrDefault.Dump();
}

[Function(Name = "NEWID", IsComposable = true)]
public Guid Random()
{
    throw new NotImplementedException();
}


0

เพื่อเพิ่มลงในโซลูชันของ Marc Gravell หากคุณไม่ได้ทำงานกับคลาส datacontext เอง (เนื่องจากคุณใช้พร็อกซีอย่างใดเช่นปลอมบริบทข้อมูลเพื่อวัตถุประสงค์ในการทดสอบ) คุณไม่สามารถใช้ UDF ที่กำหนดได้โดยตรง: จะไม่ถูกคอมไพล์ลงใน SQL เนื่องจากคุณไม่ได้ใช้งานใน a คลาสย่อยหรือคลาสบางส่วนของคลาสบริบทข้อมูลจริงของคุณ

วิธีแก้ปัญหาสำหรับปัญหานี้คือการสร้างฟังก์ชัน Randomize ในพร็อกซีของคุณโดยป้อนด้วยแบบสอบถามที่คุณต้องการสุ่ม:

public class DataContextProxy : IDataContext
{
    private readonly DataContext _context;

    public DataContextProxy(DataContext context)
    {
        _context = context;
    }

    // Snipped irrelevant code

    public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
    {
        return query.OrderBy(x => _context.Random());
    }
}

นี่คือวิธีที่คุณใช้ในรหัสของคุณ:

var query = _dc.Repository<SomeEntity>();
query = _dc.Randomize(query);

เพื่อให้เสร็จสมบูรณ์นี่คือวิธีการนำไปใช้ใน FAKE datacontext (ซึ่งใช้ในเอนทิตีหน่วยความจำ):

public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
{
    return query.OrderBy(x => Guid.NewGuid());
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.