Baris acak dari Linq ke Sql


112

Apa cara terbaik (dan tercepat) untuk mengambil baris acak menggunakan LINQ ke SQL ketika saya memiliki kondisi, misalnya beberapa bidang harus benar?


Anda memiliki dua opsi untuk pesanan Anda memeriksa kondisi sebenarnya. Jika kondisi sebenarnya terjadi pada sebagian besar item maka ambil item secara acak kemudian tes dan ulangi sementara salah. Jika jarang, biarkan database membatasi opsi ke kondisi sebenarnya dan kemudian ambil satu secara acak.
Rex Logan

1
Seperti banyak jawaban di situs ini - peringkat kedua jauh lebih baik daripada yang diterima.
nikib3ro

Jawaban:


169

Anda dapat melakukan ini di database, dengan menggunakan UDF palsu; di kelas parsial, tambahkan metode ke konteks data:

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

Kemudian hanya order by ctx.Random(); ini akan melakukan pengurutan acak di SQL-Server milik NEWID(). yaitu

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

Perhatikan bahwa ini hanya cocok untuk tabel ukuran kecil hingga menengah; untuk tabel yang besar, ini akan berdampak pada kinerja server, dan akan lebih efisien untuk menemukan jumlah baris ( Count), lalu memilih satu secara acak ( Skip/First).


untuk pendekatan hitungan:

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
Jika 30k setelah filter, saya akan mengatakan tidak: jangan gunakan pendekatan ini. Lakukan 2 perjalanan pulang pergi; 1 untuk mendapatkan Hitungan (), dan 1 untuk mendapatkan baris acak ...
Marc Gravell

1
Bagaimana jika Anda ingin lima (atau "x") baris acak? Apakah yang terbaik adalah melakukan enam perjalanan pulang pergi atau adakah cara yang nyaman untuk menerapkannya dalam prosedur yang tersimpan?
Neal Stublen

2
@Neal S .: urutan oleh ctx.Random () dapat dicampur dengan Take (5); tetapi jika Anda menggunakan pendekatan Count (), saya perkirakan 6 perjalanan pulang pergi adalah opsi yang paling sederhana.
Marc Gravell

1
jangan lupa untuk menambahkan referensi ke System.Data.Linq atau atribut System.Data.Linq.Mapping.Function tidak akan berfungsi.
Jaguir

8
Saya tahu ini sudah lama, tetapi jika Anda memilih banyak baris acak dari tabel besar, lihat ini: msdn.microsoft.com/en-us/library/cc441928.aspx Saya tidak tahu apakah ada LINQ yang setara.
jwd

60

Contoh lain untuk Entity Framework:

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

Ini tidak bekerja dengan LINQ ke SQL. Itu OrderByhanya dijatuhkan.


4
Sudahkah Anda membuat profil ini dan mengonfirmasi bahwa ini berfungsi? Dalam pengujian saya menggunakan LINQPad, urutan berdasarkan klausa dihapus.
Jim Wooley

Ini adalah solusi terbaik untuk masalah ini
reach4thelasers

8
Ini tidak berfungsi LINQ ke SQL ... mungkin berfungsi di Entity Framework 4 (tidak mengkonfirmasinya). Anda hanya dapat menggunakan .OrderBy dengan Guid jika Anda menyortir Daftar ... dengan DB itu tidak akan berhasil.
nikib3ro

2
Hanya untuk akhirnya memastikan bahwa ini berfungsi di EF4 - ini adalah opsi yang bagus dalam hal itu.
nikib3ro

1
Bisakah Anda mengedit jawaban Anda dan menjelaskan mengapa orderBy dengan Panduan baru berhasil? Jawaban bagus omong-omong :)
Jean-François Côté

32

EDIT: Saya baru saja menyadari ini LINQ ke SQL, bukan LINQ ke Objek. Gunakan kode Marc untuk mendapatkan database untuk melakukan ini untuk Anda. Saya meninggalkan jawaban ini di sini sebagai tempat menarik potensial untuk LINQ ke Objek.

Anehnya, Anda sebenarnya tidak perlu menghitungnya. Namun, Anda perlu mengambil setiap elemen kecuali Anda mendapatkan hitungannya.

Apa yang dapat Anda lakukan adalah mempertahankan gagasan tentang nilai "saat ini" dan jumlah saat ini. Saat Anda mengambil nilai berikutnya, ambil nomor acak dan ganti "saat ini" dengan "baru" dengan probabilitas 1 / n di mana n adalah hitungannya.

Jadi, saat Anda membaca nilai pertama, Anda selalu menjadikannya sebagai nilai "saat ini". Saat Anda membaca nilai kedua, Anda mungkin membuat nilai saat ini (probabilitas 1/2). Ketika Anda membaca nilai ketiga, Anda mungkin membuat nilai saat ini (probabilitas 1/3) dll. Ketika Anda kehabisan data, nilai saat ini adalah nilai acak dari semua yang Anda baca, dengan probabilitas seragam.

Untuk menerapkannya dengan syarat, abaikan saja apa pun yang tidak memenuhi syarat. Cara termudah untuk melakukannya adalah dengan hanya mempertimbangkan urutan "pencocokan" untuk memulai, dengan menerapkan klausa Where terlebih dahulu.

Berikut implementasi cepatnya. Saya pikir tidak apa-apa ...

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 - Saya menjalankan pemeriksaan cepat dan fungsi ini memang memiliki distribusi probabilitas yang seragam (hitungan kenaikan pada dasarnya adalah mekanisme yang sama dengan pengocokan Fisher-Yates sehingga tampaknya masuk akal bahwa itu seharusnya).
Greg Beech

@ Greg: Keren, terima kasih. Tampaknya tidak masalah bagi saya dengan pemeriksaan cepat, tetapi sangat mudah untuk mendapatkan kesalahan dalam kode seperti ini. Sebenarnya tidak relevan dengan LINQ ke SQL, tapi tetap berguna.
Jon Skeet

@JonSkeet, hai, dapatkah Anda memeriksa ini dan memberi tahu saya apa yang saya lewatkan
shaijut

@TylerLaing: Tidak, tidak dimaksudkan untuk istirahat. Pada iterasi pertama, currentakan selalu disetel ke elemen pertama. Pada iterasi kedua, ada perubahan 50% yang akan disetel ke elemen kedua. Pada iterasi ketiga, ada 33% kemungkinan itu akan disetel ke elemen ketiga. Menambahkan pernyataan break berarti Anda akan selalu keluar setelah membaca elemen pertama, membuatnya tidak acak sama sekali.
Jon Skeet

@Jon Doh! Saya salah membaca penggunaan hitungan Anda (misalnya berpikir ini adalah gaya Fisher-Yates dengan rentang acak seperti ni). Tetapi untuk memilih elemen pertama di Fisher-Yates adalah dengan memilih salah satu elemen secara adil. Namun, itu membutuhkan mengetahui jumlah total elemen. Saya melihat sekarang bahwa solusi Anda rapi untuk IEnumerable yang jumlah totalnya tidak diketahui, dan tidak perlu mengulang seluruh sumber hanya untuk mendapatkan hitungan, untuk kemudian mengulang lagi ke beberapa indeks yang dipilih secara acak. Alih-alih ini menyelesaikan dalam satu lintasan, seperti yang Anda nyatakan: "perlu mengambil setiap elemen kecuali Anda mendapatkan hitungan".
Tyler Laing

19

Salah satu cara untuk mencapai secara efisien adalah dengan menambahkan kolom ke data Anda Shuffleyang diisi dengan int acak (karena setiap rekaman dibuat).

Permintaan parsial untuk mengakses tabel secara acak adalah ...

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

Ini melakukan operasi XOR dalam database dan perintah berdasarkan hasil XOR tersebut.

Keuntungan:-

  1. Efisien: SQL menangani pengurutan, tidak perlu mengambil seluruh tabel
  2. Berulang: (baik untuk pengujian) - dapat menggunakan benih acak yang sama untuk menghasilkan urutan acak yang sama

Ini adalah pendekatan yang digunakan oleh sistem otomasi rumah saya untuk mengacak daftar putar. Itu mengambil benih baru setiap hari memberikan urutan yang konsisten sepanjang hari (memungkinkan kemampuan jeda / melanjutkan yang mudah) tetapi tampilan baru di setiap daftar putar setiap hari baru.


apa efeknya pada keacakan jika alih-alih menambahkan bidang int acak Anda hanya menggunakan bidang identitas auto-incrementing yang ada (benih jelas akan tetap acak)? juga - apakah nilai benih dengan maksimum sama dengan jumlah catatan dalam tabel memadai atau haruskah lebih tinggi?
Bryan

Setuju, ini adalah jawaban yang bagus dimana IMO seharusnya mendapatkan lebih banyak suara positif. Saya menggunakan ini dalam kueri Entity Framework, dan operator bitwise-XOR ^ tampaknya bekerja secara langsung sehingga membuat kondisi sedikit lebih bersih: result = result.OrderBy(s => s.Shuffle ^ seed);(mis. Tidak perlu mengimplementasikan XOR melalui operator ~, & dan |).
Steven Rands

7

jika Anda ingin mendapatkan misalnya var count = 16baris acak dari tabel, Anda dapat menulis

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

di sini saya menggunakan EF, dan Tabel adalah Dbset


1

Jika tujuan mendapatkan baris acak adalah pengambilan sampel, saya telah berbicara sangat singkat di sini tentang pendekatan yang bagus dari Larson et al., Tim Riset Microsoft di mana mereka telah mengembangkan kerangka kerja pengambilan sampel untuk Server Sql menggunakan tampilan terwujud. Ada juga link ke kertas yang sebenarnya.


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();

Penjelasan: Dengan memasukkan panduan (yang acak) urutan dengan orderby akan menjadi acak.


Panduan tidak "acak", tetapi tidak berurutan. Ada perbedaan. Dalam praktiknya mungkin tidak masalah untuk sesuatu yang sepele seperti ini.
Chris Marisic

0

Datang ke sini bertanya-tanya bagaimana cara mendapatkan beberapa halaman acak dari sejumlah kecil, sehingga setiap pengguna mendapatkan 3 halaman acak yang berbeda.

Ini adalah solusi terakhir saya, mengerjakan kueri dengan LINQ terhadap daftar halaman di Sharepoint 2010. Ada dalam Visual Basic, maaf: p

Dim Aleatorio As New Random()

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

Mungkin harus mendapatkan beberapa profil sebelum menanyakan sejumlah besar hasil, tetapi itu sempurna untuk tujuan saya


0

Saya memiliki kueri fungsi acak terhadap DataTables:

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

0

Contoh di bawah ini akan memanggil sumber untuk mengambil hitungan dan kemudian menerapkan ekspresi lewati pada sumber dengan angka antara 0 dan n. Metode kedua akan menerapkan urutan dengan menggunakan objek acak (yang akan mengurutkan semua yang ada di memori) dan memilih nomor yang diteruskan ke pemanggilan metode.

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);
    }
}

Beberapa penjelasan akan menyenangkan
Andrew Barber

Kode ini bukan threadsafe dan hanya dapat digunakan dalam kode utas tunggal (jadi bukan ASP.NET)
Chris Marisic

0

Saya menggunakan metode ini untuk mengambil berita acak dan berfungsi dengan baik;)

    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

Menggunakan LINQ ke SQL di LINQPad sebagai pernyataan C # terlihat

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

SQL yang dihasilkan adalah

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

0

Jika Anda menggunakan LINQPad , alihkan ke mode program C # dan lakukan dengan cara ini:

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

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

0
var cust = (from c in ctx.CUSTOMERs.ToList() select c).OrderBy(x => x.Guid.NewGuid()).Taket(2);

Pilih 2 baris acak


0

Untuk menambah solusi Marc Gravell. Jika Anda tidak bekerja dengan kelas datacontext itu sendiri (karena Anda mem-proxy-nya misalnya untuk memalsukan datacontext untuk tujuan pengujian), Anda tidak dapat menggunakan UDF yang ditentukan secara langsung: itu tidak akan dikompilasi ke SQL karena Anda tidak menggunakannya dalam subclass atau kelas parsial dari kelas konteks data nyata Anda.

Solusi untuk masalah ini adalah dengan membuat fungsi Acak di proxy Anda, memberinya makan dengan kueri yang ingin Anda acak:

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());
    }
}

Berikut adalah cara Anda menggunakannya dalam kode Anda:

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

Untuk menjadi lengkap, berikut adalah cara mengimplementasikannya di datacontext FAKE (yang digunakan dalam entitas memori):

public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
{
    return query.OrderBy(x => Guid.NewGuid());
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.