Rasgele satır seçmenin en iyi yolu PostgreSQL


345

PostgreSQL satır rastgele bir seçim istiyorum, bunu denedim:

select * from table where random() < 0.01;

Ama bazıları bunu tavsiye ediyor:

select * from table order by random() limit 1000;

500 Milyon sıralı çok büyük bir masam var, hızlı olmasını istiyorum.

Hangi yaklaşım daha iyi? Farklılıklar nedir? Rastgele satırları seçmenin en iyi yolu nedir?


1
Merhaba Jack, Cevabınız için teşekkürler, yürütme süresi sırayla daha yavaş, ama hangisinin farklı olduğunu bilmek istiyorum ...
nanounanue

Uhhh ... rica ederim. Farklı yaklaşımları karşılaştırmayı denediniz mi?

Çok daha hızlı yollar da var. Her şey gereksinimlerinize ve neyle çalışmanız gerektiğine bağlıdır. Tam 1000 satıra mı ihtiyacınız var? Tablonun sayısal bir kimliği var mı? Hiç / az / çok boşluk yok mu? Hız ne kadar önemli? Zaman birimi başına kaç istek? Her isteğin farklı bir kümeye ihtiyacı var mı yoksa belirli bir zaman dilimi için aynı olabilir mi?
Erwin Brandstetter

6
İlk seçenek "(random () <0.01)" matematiksel olarak yanlıştır, çünkü herhangi bir rasgele sayı 0.01'in altında değilse, herhangi bir durumda (daha az olasılıkla da olsa), tablo ne kadar büyük olursa olsun yanıt olarak hiçbir satır alamayabilirsiniz. veya daha yüksek bir eşik değer. İkinci seçenek her zaman haklıdır
Herme

1
Yalnızca bir satır seçmek istiyorsanız, bu soruya bakın: stackoverflow.com/q/5297396/247696
Flimm

Yanıtlar:


230

Spesifikasyonlarınız verildiğinde (artı yorumlarda ek bilgi),

  • Çok az (veya orta derecede az) boşluk içeren bir sayısal kimlik sütununuz (tamsayı sayılar) var.
  • Açıkçası hiç veya az sayıda yazma işlemi.
  • Kimlik sütununuz dizine eklenmelidir! Birincil anahtar güzel hizmet eder.

Aşağıdaki sorgu büyük tablonun sıralı olarak taranmasını gerektirmez, sadece bir indeks taraması gerektirir.

İlk olarak, ana sorgu için tahminler alın:

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

Muhtemelen pahalı kısmı count(*)(büyük tablolar için) 'dir. Yukarıdaki özellikler göz önüne alındığında, buna ihtiyacınız yoktur. Bir tahmin iyi olur, neredeyse ücretsiz olarak kullanılabilir ( ayrıntılı açıklama burada ):

SELECT reltuples AS ct FROM pg_class WHERE oid = 'schema_name.big'::regclass;

Çok küçük ctolmadığı sürece , sorgu diğer yaklaşımlardan daha iyi performans gösterecektir. id_span

WITH params AS (
    SELECT 1       AS min_id           -- minimum id <= current min id
         , 5100000 AS id_span          -- rounded up. (max_id - min_id + buffer)
    )
SELECT *
FROM  (
    SELECT p.min_id + trunc(random() * p.id_span)::integer AS id
    FROM   params p
          ,generate_series(1, 1100) g  -- 1000 + buffer
    GROUP  BY 1                        -- trim duplicates
    ) r
JOIN   big USING (id)
LIMIT  1000;                           -- trim surplus
  • idUzayda rastgele sayılar üretin . "Birkaç boşluk" var, bu yüzden alınacak satır sayısına% 10 (boşlukları kolayca kaplayacak kadar) ekleyin.

  • Her biri idşans eseri birden çok kez seçilebilir (ancak büyük bir kimlik alanına sahip olma ihtimali düşüktür), bu nedenle üretilen numaraları gruplandırın (veya kullanın DISTINCT).

  • idBüyük masaya s katılın . Bu, dizin hazır olduğunda çok hızlı olmalıdır.

  • Son idolarak çiftler ve boşluklar tarafından yenilmeyen fazlaları düzeltin . Her sıranın seçilme şansı tamamen eşittir .

Kısa versiyon

Şunları yapabilirsiniz basitleştirmek bu sorguyu. Yukarıdaki sorgudaki CTE sadece eğitim amaçlıdır:

SELECT *
FROM  (
    SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
    FROM   generate_series(1, 1100) g
    ) r
JOIN   big USING (id)
LIMIT  1000;

RCTE ile hassaslaştırın

Özellikle boşluklar ve tahminler hakkında o kadar emin değilseniz.

WITH RECURSIVE random_pick AS (
   SELECT *
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   generate_series(1, 1030)  -- 1000 + few percent - adapt to your needs
      LIMIT  1030                      -- hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss

   UNION                               -- eliminate dupe
   SELECT b.*
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   random_pick r             -- plus 3 percent - adapt to your needs
      LIMIT  999                       -- less than 1000, hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

Temel sorguda daha küçük bir artı ile çalışabiliriz . Çok fazla boşluk varsa, bu nedenle ilk yinelemede yeterli satır bulamazsak, rCTE özyinelemeli terimle yinelemeye devam eder. Kimlik alanında nispeten daha az boşluğa ihtiyacımız var veya sınıra ulaşılmadan tekrarlama kuru olabilir - ya da performansı optimize etme amacına meydan okuyan yeterince büyük bir tamponla başlamalıyız.

UNIONRCTE içindeki kopyalar elimine edilir .

Dış LIMITkısım, yeterli satırımız olduğunda CTE'yi durdurur.

Bu sorgu, mevcut dizini kullanmak, gerçekten rastgele satırlar oluşturmak ve sınırı karşılayana kadar (özyineleme kuru çalışmadığı sürece) durmamak için dikkatle tasarlanmıştır. Yeniden yazacaksanız burada birtakım tuzaklar var.

İşlevine sarın

Değişken parametrelerle tekrarlanan kullanım için:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big AS
$func$
DECLARE
   _surplus  int := _limit * _gaps;
   _estimate int := (           -- get current estimate from system
      SELECT c.reltuples * _gaps
      FROM   pg_class c
      WHERE  c.oid = 'big'::regclass);
BEGIN

   RETURN QUERY
   WITH RECURSIVE random_pick AS (
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   generate_series(1, _surplus) g
         LIMIT  _surplus           -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses

      UNION                        -- eliminate dupes
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   random_pick        -- just to make it recursive
         LIMIT  _limit             -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses
   )
   SELECT *
   FROM   random_pick
   LIMIT  _limit;
END
$func$  LANGUAGE plpgsql VOLATILE ROWS 1000;

Aramak:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

Bu jeneriki herhangi bir tablo için çalışacak şekilde bile yapabilirsiniz: PK sütununun ve tablonun adını polimorfik tip olarak kullanın ve kullanın EXECUTE... Ama bu, bu sorunun kapsamı dışında. Görmek:

Olası alternatif

Gereksinimleriniz tekrarlanan çağrılar için özdeş setlere izin veriyorsa (ve tekrarlanan çağrılardan bahsediyoruz) somutlaştırılmış bir görünüm düşünürüm . Yukarıdaki sorguyu bir kez yürütün ve sonucu bir tabloya yazın. Kullanıcılar aydınlatma hızında yarı rastgele bir seçim alır. Seçtiğiniz aralıklarla veya olaylarla rastgele seçiminizi yenileyin.

Postgres 9.5 tanıttı TABLESAMPLE SYSTEM (n)

nYüzde nerede . Kullanım kılavuzu:

BERNOULLIVe SYSTEMörnekleme yöntemleri her biri bir şekilde ifade edilen numuneye tablonun fraksiyon, tek bir bağımsız değişken kabul 0 ile 100 arasında yüzde . Bu argüman herhangi bir realdeğerli ifade olabilir.

Cesur vurgu benim. Bu var çok hızlı , ama sonuç tam olarak rastgele değil . Kılavuz tekrar:

SYSTEMYöntem daha hızlı önemli olan BERNOULLIküçük numune alma yüzdeleri belirtilen yöntem, ancak etkileri kümeleme sonucu tablo daha az rastgele örnek döndürebilir.

Döndürülen satır sayısı çılgınca değişebilir. Örneğimiz için, yaklaşık 1000 satır almak için :

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

İlişkili:

Veya ek modül yüklemek tsm_system_rows (yeterli olup olmadığını) tam olarak istenen satır sayısını almak ve daha uygun sözdizimi için izin vermek:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Ayrıntılar için Evan'ın cevabına bakınız.

Ama bu hala tam olarak rastgele değil.


T tablosu nerede tanımlanır ? T yerine r olmalı mı ?
Luc M

1
@LucM: Burada tanımlanmıştır: JOIN bigtbl tkısaltması JOIN bigtbl AS t. tBir olan tablo diğer adı için bigtbl. Amacı sözdizimini kısaltmaktır, ancak bu özel durumda gerekli değildir. Cevabımdaki sorguyu basitleştirdim ve basit bir sürüm ekledim.
Erwin Brandstetter

Create_series (1,1100) içindeki değer aralığının amacı nedir?
Awesome-o

@ Awesome-o: Amaç 1000 satır almak için, ben birkaç boşluk telafi etmek için ekstra% 10 ile başlamak veya (olası ama mümkün değil) yinelenen rasgele sayılar ... açıklama benim cevap.
Erwin Brandstetter

Erwin, "Olası alternatif" inizin bir varyasyonunu gönderdim: stackoverflow.com/a/23634212/430128 . Düşüncelerinizle ilgilenirsiniz.
Raman

100

Her ikisinin de uygulama planını kullanarak inceleyebilir ve karşılaştırabilirsiniz.

EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;

Büyük bir tablodaki 1 hızlı bir test , ORDER BYilkinin tüm tabloyu sıraladığını ve daha sonra ilk 1000 öğeyi seçtiğini gösterir . Büyük bir tabloyu sıralamak sadece o tabloyu okumakla kalmaz aynı zamanda geçici dosyaların okunmasını ve yazılmasını da içerir. where random() < 0.1Sadece bir kez tam tabloyu tarar.

Büyük tablolar için, tam bir tablo taraması bile uzun sürebileceğinden bu istediğiniz olmayabilir.

Üçüncü bir teklif

select * from table where random() < 0.01 limit 1000;

Bu, 1000 satır bulunur bulunmaz tablo taramasını durdurur ve bu nedenle daha erken döner. Tabii ki bu rastgele bir miktar bozulur, ancak belki de durumunuzda yeterince iyidir.

Düzenleme: Bu hususların yanı sıra, bunun için zaten sorulan soruları kontrol edebilirsiniz. Sorguyu kullanmak [postgresql] randombirkaç isabet döndürür.

Ve daha birçok yaklaşımı özetleyen bağlantılı bir depez makalesi:


1 "büyük", "tablonun tamamı belleğe sığmaz".


1
Sipariş vermek için geçici dosyayı yazmakla ilgili iyi bir nokta. Bu gerçekten büyük bir hit. Sanırım yapabiliriz random() < 0.02ve sonra bu listeyi karıştırabiliriz limit 1000! Birkaç bin sıra (lol) üzerinde daha az pahalı olacak.
Donald Miner

"Rasgele () <0.05 sınır 500; postgresql için daha kolay yöntemlerden biridir. Bunu, sonuçların% 5'ini ve her seferinde 500 satırdan fazlasını işlemememiz gereken projelerimizden birinde kullandık.
tgharold

Neden dünyada 500 metrelik bir satır tablosundan numune almak için O (n) tam taramasını düşünmelisiniz? Büyük masalarda gülünç yavaş ve tamamen gereksiz.
mafu

77

postgresql order by random (), satırları rastgele sırada seçin:

select your_columns from your_table ORDER BY random()

postgresql order by random () ile farklı:

select * from 
  (select distinct your_columns from your_table) table_alias
ORDER BY random()

postgresql sipariş rastgele bir satır sınırı:

select your_columns from your_table ORDER BY random() limit 1

1
select your_columns from your_table ORDER BY random() limit 145mil satır üzerinde yürütmek ~ 2 dakika
nguyên

Bunu hızlandırmanın bir yolu var mı?
CpILL

43

PostgreSQL 9.5 ile başlayarak, bir tablodan rastgele elemanlar almaya adanmış yeni bir sözdizimi var:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

Bu örnek size öğelerin% 5'ini verecektir mytable.

Bu blog gönderisiyle ilgili daha fazla açıklamaya bakın: http://www.postgresql.org/docs/current/static/sql-select.html


3
Dokümanlardan önemli bir not: "SYSTEM yöntemi, her bir blok seçilme şansına sahipken blok düzeyinde örnekleme yapar; seçilen her bloktaki tüm satırlar döndürülür. SYSTEM örnekleme, küçük örnekleme yüzdeleri olduğunda BERNOULLI yönteminden önemli ölçüde daha hızlıdır ancak kümeleme efektlerinin bir sonucu olarak tablonun daha az rasgele bir örneğini döndürebilir. "
Tim

1
Yüzde yerine satır sayısı belirtmenin bir yolu var mı?
Flimm

4
TABLESAMPLE SYSTEM_ROWS(400)400 rastgele satır örneği almak için kullanabilirsiniz . Bu ifadeyi kullanabilmek için yerleşik tsm_system_rowsuzantıyı etkinleştirmeniz gerekir .
Mickaël Le Baillif

27

SİPARİŞ BY ile daha yavaş olacak.

select * from table where random() < 0.01;kayda göre kayıt yapar ve rasgele filtrelemeye karar verir. Bunun O(N)nedeni her kaydın yalnızca bir kez kontrol edilmesi gerektiğidir.

select * from table order by random() limit 1000;tüm masayı sıralayacak, sonra ilk 1000'i seçecek O(N * log N).

Bunun dezavantajı, random() < 0.01değişken sayıda çıktı kaydı elde etmenizdir.


Bir veri kümesini rastgele sıralamak yerine karıştırmanın daha iyi bir yolu olduğunu unutmayın: Fisher-Yates Shuffle , içinde çalışır O(N). Ancak, shuffle'ı SQL'de uygulamak oldukça zor gibi görünüyor.


3
Yine de ilk örneğinizin sonuna Limit 1 ekleyemezsiniz. Tek sorun, hiçbir kayıt geri alma olasılığınızın olmamasıdır, bu yüzden bunu kodunuzda dikkate almanız gerekir.
Relequestual

Fisher-Yates ile ilgili sorun, seçim yapmak için tüm veri kümesini hafızaya almanız gerektiğidir. Çok büyük veri kümeleri için uygun değil :(
CpILL

16

İşte benim için çalışan bir karar. Sanırım anlamak ve yürütmek çok basit.

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;

6
Bence bu çözüm ORDER BY random()işe yarıyor ama büyük bir masa ile çalışırken etkili olmayabilir.
Anh Cao

15
select * from table order by random() limit 1000;

Kaç satır istediğinizi biliyorsanız, göz atın tsm_system_rows.

tsm_system_rows

modülü bir SELECT komutunun TABLESAMPLE yan tümcesinde kullanılabilen SYSTEM_ROWS tablo örnekleme yöntemini sağlar.

Bu tablo örnekleme yöntemi, okunacak maksimum satır sayısı olan tek bir tamsayı bağımsız değişkenini kabul eder. Ortaya çıkan örnek, tablo yeterli satır içermediği sürece her zaman tam olarak bu kadar satır içerecektir, bu durumda tüm tablo seçilir. Yerleşik SYSTEM örnekleme yöntemi gibi, SYSTEM_ROWS da blok düzeyinde örnekleme gerçekleştirir, böylece örnek tamamen rastgele değildir ancak özellikle az sayıda satır istendiğinde kümeleme etkilerine maruz kalabilir.

Önce uzantıyı yükleyin

CREATE EXTENSION tsm_system_rows;

Sonra sorgunuz,

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);

2
Eklenen cevabınıza bir bağlantı ekledim, bu yerleşik SYSTEMyöntem üzerinde dikkate değer bir gelişme .
Erwin Brandstetter

Ben sadece bir soru cevap var burada ben hatırı sayılır gerçekleştirilen bu süre boyunca (rastgele tek kayıt) karşılaştırma ve test ait tsm_system_rowsve tsm_system_timeuzantıları. Görebildiğim kadarıyla , rastgele satırların kesinlikle minimal seçimi dışında hiçbir şey için neredeyse işe yaramazlar . Hızlı bir şekilde bakıp geçerliliğimi veya analizimin başka türlü yorumunu yapabiliyorsanız minnettar olurum.
Vérace

6

Yalnızca bir satır istiyorsanız, offsettüretilmiş bir hesaplanmış kullanabilirsiniz count.

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));

2

Erwin Brandstetter tarafından özetlenen "Olası alternatif" görünümünün bir varyasyonu mümkündür.

Örneğin, döndürülen rasgele değerlerde yineleme istemediğinizi varsayalım. Bu nedenle, (rasgele olmayan) değer kümenizi içeren birincil tabloda bir boolean değeri ayarlamanız gerekir.

Bunun giriş tablosu olduğunu varsayarsak:

id_values  id  |   used
           ----+--------
           1   |   FALSE
           2   |   FALSE
           3   |   FALSE
           4   |   FALSE
           5   |   FALSE
           ...

Doldur ID_VALUESgerektiği gibi tablo. Daha sonra, Erwin tarafından açıklandığı gibi, ID_VALUEStabloyu bir kez rasgele ayıran materyalize bir görünüm oluşturun :

CREATE MATERIALIZED VIEW id_values_randomized AS
  SELECT id
  FROM id_values
  ORDER BY random();

Materyalize edilmiş görünümün kullanılan sütunu içermediğini unutmayın, çünkü bu hızla güncelliğini yitirir. Görünümün id_valuestabloda bulunabilecek başka sütunları da içermesi gerekmez .

Rasgele değerler elde etmek (ve "tüketmek") için id_values, bir birleşimden seçim id_valuesyaparak id_values_randomizedve yalnızca ilgili olasılıkları elde etmek için istenen kriterleri uygulayarak bir GÜNCELLEME-DÖNÜŞ kullanın . Örneğin:

UPDATE id_values
SET used = TRUE
WHERE id_values.id IN 
  (SELECT i.id
    FROM id_values_randomized r INNER JOIN id_values i ON i.id = r.id
    WHERE (NOT i.used)
    LIMIT 5)
RETURNING id;

Değişim LIMITgerektiği gibi - sadece bir defada bir rasgele değer, değişim gerekiyorsa LIMITetmek 1.

Uygun indeksler açık olduğunda id_values, UPDATE-RETURNING'in çok az yük ile çok hızlı çalışması gerektiğine inanıyorum. Bir veritabanı gidiş dönüşüyle ​​rastgele değerler döndürür. "Uygun" satırlar için ölçütler gerektiği kadar karmaşık olabilir. Tabloya id_valuesherhangi bir zamanda yeni satırlar eklenebilir ve materyalize görünüm yenilendiğinde (muhtemelen yoğun olmayan bir zamanda çalıştırılabilecek) uygulamaya erişilebilecektir. Gerçekleştirilmiş görünümün oluşturulması ve yenilenmesi yavaş olacaktır, ancak yalnızca id_valuestabloya yeni kimlikler eklendiğinde yürütülmesi gerekir .


çok ilginç. Sadece seçmek değil, aynı zamanda pg_try_advisory_xact_lock ile güncellemek için select .. kullanarak güncelleme gerekiyorsa bu işe yarar mı? (yani birçok eşzamanlı okumaya ve yazmaya ihtiyacım var)
Mathieu

1

Deneyimlerimden bir ders:

offset floor(random() * N) limit 1daha hızlı değil order by random() limit 1.

offsetYaklaşımın daha hızlı olacağını düşündüm çünkü Postgres'te sıralama zamanından tasarruf etmeli. Öyle olmadığı ortaya çıktı.


0

rType ile adlandırılan bir sütun ekleyin serial. Dizin r.

200.000 satırımız olduğunu varsayalım, rastgele bir sayı üreteceğiz n, burada 0 n<<= 200, 000.

İle satırları seçin r > n, sıralayın ASCve en küçük olanı seçin.

Kod:

select * from YOUR_TABLE 
where r > (
    select (
        select reltuples::bigint AS estimate
        from   pg_class
        where  oid = 'public.YOUR_TABLE'::regclass) * random()
    )
order by r asc limit(1);

Kod açıklayıcıdır. Ortadaki alt sorgu, https://stackoverflow.com/a/7945274/1271094 adresinden tablo satırı sayısını hızlı bir şekilde tahmin etmek için kullanılır .

Uygulama düzeyinde, n> satır sayısı veya birden çok satır seçmeniz gerekiyorsa ifadeyi tekrar çalıştırmanız gerekir.


Bunu beğendim çünkü kısa ve zarif :) Ve bunu iyileştirmenin bir yolunu bile buldum: EXPLAIN ANALYZE bana bunun gibi bir PKEY dizininin kullanılmayacağını çünkü rasgele () bir çift döndürdüğünü, PKEY'in BÜYÜK bir ihtiyacı olduğunu söylüyor.
fxtentacle

YOUR_TABLE'dan * seçin (burada r> (select (reltuples :: bigint AS tahminini pg_class'dan seçin oid = 'public.YOUR_TABLE' :: regclass) * random ()) :: BIGINT siparişini r asc limitine (1);
fxtentacle

0

Partiye biraz geç kaldığımı biliyorum, ama pg_sample adlı bu harika aracı buldum :

pg_sample - referans bütünlüğünü korurken, daha büyük bir PostgreSQL veritabanından küçük, örnek bir veri kümesi ayıklayın.

Ben 350M satır veritabanı ile denedim ve gerçekten hızlı, rastgele hakkında bilmiyorum .

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.