การเลือกแถวสุ่มอย่างรวดเร็วใน Postgres


98

ฉันมีตารางใน postgres ที่มีสองสามล้านแถว ฉันได้ตรวจสอบทางอินเทอร์เน็ตและพบสิ่งต่อไปนี้

SELECT myid FROM mytable ORDER BY RANDOM() LIMIT 1;

มันใช้งานได้ แต่ช้ามาก ... มีวิธีอื่นในการสร้างแบบสอบถามหรือวิธีโดยตรงในการเลือกแถวสุ่มโดยไม่ต้องอ่านตารางทั้งหมด? โดยวิธีการที่ 'myid' เป็นจำนวนเต็ม แต่สามารถเป็นฟิลด์ว่างได้


1
หากคุณต้องการเลือกหลายแถวแบบสุ่มโปรดดูคำถามนี้: stackoverflow.com/q/8674718/247696
Flimm

คำตอบ:


99

คุณอาจต้องการทดลองOFFSETเช่นเดียวกับใน

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

เป็นจำนวนแถวในN mytableคุณอาจต้องทำก่อนSELECT COUNT(*)เพื่อหาค่าของN.

อัปเดต (โดย Antony Hatchkins)

คุณต้องใช้floorที่นี่:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

พิจารณาตาราง 2 แถว random()*Nสร้าง0 <= x < 2และเช่นSELECT myid FROM mytable OFFSET 1.7 LIMIT 1;ส่งคืน 0 แถวเนื่องจากการปัดเศษโดยปริยายเป็น int ที่ใกล้ที่สุด


ควรใช้ N น้อยกว่าSELECT COUNT(*)? ฉันหมายถึงไม่ใช้ค่าทั้งหมดในตาราง แต่เป็นเพียงส่วนหนึ่งเท่านั้น?
ฮวน

@Juan ขึ้นอยู่กับความต้องการของคุณ
NPE

การใช้EXPLAIN SELECT ...ค่าที่แตกต่างกันของ N ให้ต้นทุนเท่ากันสำหรับแบบสอบถามดังนั้นฉันคิดว่าควรใช้ค่าสูงสุดของ N
Juan

3
ดูข้อบกพร่องในคำตอบของฉันด้านล่าง
Antony Hatchkins

2
สิ่งนี้มีข้อผิดพลาดปิดโดยหนึ่ง จะไม่ส่งคืนแถวแรกและจะสร้างข้อผิดพลาด 1 / COUNT (*) เนื่องจากจะพยายามส่งคืนแถวหลังแถวสุดท้าย
เอียน

62

PostgreSQL 9.5 นำเสนอแนวทางใหม่สำหรับการเลือกตัวอย่างที่เร็วขึ้นมาก: TABLESAMPLE

ไวยากรณ์คือ

SELECT * FROM my_table TABLESAMPLE BERNOULLI(percentage);
SELECT * FROM my_table TABLESAMPLE SYSTEM(percentage);

นี่ไม่ใช่วิธีแก้ปัญหาที่ดีที่สุดหากคุณต้องการเลือกเพียงแถวเดียวเพราะคุณต้องรู้ COUNT ของตารางเพื่อคำนวณเปอร์เซ็นต์ที่แน่นอน

เพื่อหลีกเลี่ยง COUNT ที่ช้าและใช้ TABLESAMPLE แบบเร็วสำหรับตารางตั้งแต่ 1 แถวไปจนถึงหลายพันล้านแถวคุณสามารถทำได้:

 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.000001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.00001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.0001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.001) LIMIT 1;
 ...

สิ่งนี้อาจดูไม่หรูหรานัก แต่อาจเร็วกว่าคำตอบอื่น ๆ

ในการตัดสินใจว่าคุณต้องการใช้ BERNULLI oder SYSTEM หรือไม่อ่านเกี่ยวกับความแตกต่างได้ที่http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/


2
คำตอบนี้เร็วและง่ายกว่าคำตอบอื่น ๆ มากข้อนี้ควรอยู่ด้านบนสุด
Hayden Schiff

1
ทำไมคุณไม่สามารถใช้แบบสอบถามย่อยเพื่อรับการนับได้ SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;เหรอ?
machineghost

2
@machineghost "เพื่อหลีกเลี่ยง COUNT ที่ช้า ... " ... หากข้อมูลของคุณมีขนาดเล็กมากจนคุณสามารถนับได้ในเวลาอันสมควรไปเลย! :-)
alfonx

2
@machineghost ใช้SELECT reltuples FROM pg_class WHERE relname = 'my_table'สำหรับการประมาณจำนวน
Hynek -Pichi- Vychodil

@ Hynek-Pichi-Vychodil อินพุตดีมาก! เพื่อให้แน่ใจว่าการประมาณค่าจะไม่ล้าสมัยจึงต้องมีการวิเคราะห์ VACUUM ANALYZEd เมื่อเร็ว ๆ นี้ .. แต่ฐานข้อมูลที่ดีควรได้รับการวิเคราะห์อย่างเหมาะสมอยู่ดี .. และทั้งหมดขึ้นอยู่กับกรณีการใช้งานที่เฉพาะเจาะจง โดยปกติโต๊ะขนาดใหญ่จะไม่โตเร็วขนาดนี้ ... ขอบคุณ!
alfonx

34

ฉันลองสิ่งนี้กับแบบสอบถามย่อยและใช้งานได้ดี Offset อย่างน้อยใน Postgresql v8.4.4 ก็ใช้ได้ดี

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;

ในความเป็นจริง v8.4 จำเป็นสำหรับการทำงานนี้ใช้ไม่ได้กับ <= 8.3
Antony Hatchkins

1
ดูข้อบกพร่องในคำตอบของฉันด้านล่าง
Antony Hatchkins

32

คุณต้องใช้floor:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

พิจารณาตาราง 2 แถว random()*Nสร้าง 0 <= x <2 และตัวอย่างSELECT myid FROM mytable OFFSET 1.7 LIMIT 1;ส่งคืน 0 แถวเนื่องจากการปัดเศษโดยปริยายเป็น int ที่ใกล้ที่สุด
Antony Hatchkins

น่าเสียดายที่สิ่งนี้ใช้ไม่ได้หากคุณต้องการใช้ LIMIT ที่สูงขึ้น ... ฉันต้องการได้รับ 3 รายการดังนั้นฉันจึงต้องใช้ไวยากรณ์ ORDER BY RANDOM ()
Alexis Wilke

1
การสืบค้นต่อเนื่องสามครั้งจะยังคงเร็วกว่าหนึ่งคำค้นหาorder by random()โดยประมาณ3*O(N) < O(NlogN)- ตัวเลข reallife จะแตกต่างกันเล็กน้อยเนื่องจากดัชนี
Antony Hatchkins

ปัญหาของฉันคือ 3 รายการต้องแตกต่างกันและ a WHERE myid NOT IN (1st-myid)และใช้WHERE myid NOT IN (1st-myid, 2nd-myid)ไม่ได้เนื่องจากการตัดสินใจของ OFFSET อืม ... ฉันเดาว่าฉันสามารถลด N ลง 1 และ 2 ใน SELECT ที่สองและสาม
Alexis Wilke

คุณหรือใครก็ได้ช่วยขยายคำตอบนี้พร้อมคำตอบว่าทำไมฉันถึงต้องใช้floor()? มีข้อได้เปรียบอะไรบ้าง?
ADTC

14

ตรวจสอบลิงค์นี้เพื่อดูตัวเลือกต่างๆ http://www.depesz.com/index.php/2007/09/16/my-thoughts-on-getting-random-row/

อัปเดต: (อ. Hatchkins)

สรุปบทความยาว (มาก) มีดังนี้

ผู้เขียนแสดงสี่แนวทาง:

1) ORDER BY random() LIMIT 1; - ช้า

2) ORDER BY id where id>=random()*N LIMIT 1- ไม่สม่ำเสมอหากมีช่องว่าง

3) คอลัมน์แบบสุ่ม - ต้องได้รับการอัปเดตเป็นระยะ ๆ

4) การรวมแบบสุ่มที่กำหนดเอง- วิธีการฉลาดแกมโกงอาจช้า: ต้องสร้างแบบสุ่ม () N ครั้ง

และแนะนำให้ปรับปรุงวิธี # 2 โดยใช้

5) ORDER BY id where id=random()*N LIMIT 1 ด้วยการร้องขอที่ตามมาหากผลลัพธ์ว่างเปล่า


ฉันสงสัยว่าทำไมพวกเขาถึงไม่ครอบคลุม OFFSET? การใช้ ORDER ไม่ได้เป็นเพียงคำถามเพื่อให้ได้แถวสุ่ม โชคดีที่ OFFSET ได้รับคำตอบอย่างดี
androidguy

4

วิธีที่ง่ายที่สุดและเร็วที่สุดในการดึงข้อมูลแถวสุ่มคือการใช้tsm_system_rowsส่วนขยาย:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

จากนั้นคุณสามารถเลือกจำนวนแถวที่คุณต้องการ:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

สามารถใช้ได้กับ PostgreSQL 9.5 และใหม่กว่า

ดู: https://www.postgresql.org/docs/current/static/tsm-system-rows.html


1
คำเตือนที่เป็นธรรมนี่ไม่ใช่การสุ่มอย่างสมบูรณ์ ในตารางขนาดเล็กฉันให้มันส่งคืนแถวแรกตามลำดับเสมอ
Ben Aubin

1
ใช่สิ่งนี้ได้อธิบายไว้อย่างชัดเจนในเอกสารประกอบ (ลิงก์ด้านบน): «เช่นเดียวกับวิธีการสุ่มตัวอย่างของระบบในตัว SYSTEM_ROWS จะทำการสุ่มตัวอย่างระดับบล็อกเพื่อไม่ให้ตัวอย่างสุ่มสมบูรณ์ แต่อาจมีเอฟเฟกต์การจัดกลุ่มโดยเฉพาะอย่างยิ่งหากมีเพียงเล็กน้อย ขอจำนวนแถว ». หากคุณมีชุดข้อมูลขนาดเล็กORDER BY random() LIMIT 1;ก็ควรจะเร็วพอ
daamien

ผมเห็นว่า. แค่อยากจะบอกให้ชัดเจนกับทุกคนที่ไม่คลิกลิงก์หรือหากลิงก์นั้นตายในอนาคต
Ben Aubin

1
นอกจากนี้ควรสังเกตว่าวิธีนี้จะใช้ได้เฉพาะกับการเลือกแถวสุ่มจากตารางและการกรองจากนั้นเมื่อเทียบกับการเรียกใช้แบบสอบถามแล้วเลือกหนึ่งหรือบางระเบียนโดยสุ่ม
ชื่อ

3

TABLESAMPLEฉันได้มาด้วยวิธีการแก้ปัญหาอย่างรวดเร็วโดยไม่ต้อง เร็วกว่าOFFSET random()*N LIMIT 1. ไม่จำเป็นต้องมีการนับตาราง

ความคิดที่จะสร้างดัชนีการแสดงออกที่มีข้อมูลแบบสุ่ม md5(primary key)แต่คาดเดาได้เช่น

นี่คือการทดสอบด้วยข้อมูลตัวอย่าง 1M แถว:

create table randtest (id serial primary key, data int not null);

insert into randtest (data) select (random()*1000000)::int from generate_series(1,1000000);

create index randtest_md5_id_idx on randtest (md5(id::text));

explain analyze
select * from randtest where md5(id::text)>md5(random()::text)
order by md5(id::text) limit 1;

ผลลัพธ์:

 Limit  (cost=0.42..0.68 rows=1 width=8) (actual time=6.219..6.220 rows=1 loops=1)
   ->  Index Scan using randtest_md5_id_idx on randtest  (cost=0.42..84040.42 rows=333333 width=8) (actual time=6.217..6.217 rows=1 loops=1)
         Filter: (md5((id)::text) > md5((random())::text))
         Rows Removed by Filter: 1831
 Total runtime: 6.245 ms

บางครั้งการสืบค้นนี้อาจ (มีความน่าจะเป็นประมาณ 1 / Number_of_rows) ส่งคืน 0 แถวดังนั้นจึงจำเป็นต้องตรวจสอบและเรียกใช้ใหม่ ความน่าจะเป็นก็ไม่เหมือนกันทุกประการ - บางแถวมีความเป็นไปได้มากกว่าแถวอื่น ๆ

สำหรับการเปรียบเทียบ:

explain analyze SELECT id FROM randtest OFFSET random()*1000000 LIMIT 1;

ผลลัพธ์แตกต่างกันไปมาก แต่ก็ค่อนข้างแย่:

 Limit  (cost=1442.50..1442.51 rows=1 width=4) (actual time=179.183..179.184 rows=1 loops=1)
   ->  Seq Scan on randtest  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.016..134.835 rows=915702 loops=1)
 Total runtime: 179.211 ms
(3 rows)

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

น่าสนใจมากมันใช้งานได้ในกรณีการค้นหาที่คล้ายลอตเตอรีหรือไม่: แบบสอบถามต้องดูตั๋วที่มีทั้งหมดและสุ่มคืนตั๋วเพียงใบเดียวเท่านั้น ฉันสามารถใช้เทคนิคของคุณในแง่ร้ายได้หรือไม่ (เลือก ...
Mathieu

สำหรับทุกสิ่งที่เกี่ยวข้องกับลอตเตอรีคุณควรใช้การสุ่มตัวอย่างที่ปลอดภัยและมีความปลอดภัยในการเข้ารหัส - ตัวอย่างเช่นเลือกหมายเลขสุ่มระหว่าง 1 ถึง max (id) จนกว่าคุณจะพบ id ที่มีอยู่ วิธีการจากคำตอบนี้ไม่ยุติธรรมหรือปลอดภัย - มันรวดเร็ว ใช้ได้กับสิ่งต่างๆเช่น "สุ่ม 1% ของแถวเพื่อทดสอบบางสิ่งใน" หรือ "แสดงรายการสุ่ม 5 รายการ"
Tometzky
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.