วิธีที่ดีที่สุดในการรับคำสั่งซื้อแบบสุ่มคืออะไร


27

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

ฉันเข้าใจว่ามันอาจจะไม่สุ่ม "จริง" หลอกหลอกก็ดีพอสำหรับความต้องการของฉัน

คำตอบ:


19

สั่งซื้อโดย NEWID () จะเรียงลำดับระเบียนแบบสุ่ม ตัวอย่างที่นี่

SELECT *
FROM Northwind..Orders 
ORDER BY NEWID()

7
ORDER BY NEWID () เป็นแบบสุ่มอย่างมีประสิทธิภาพ แต่ไม่สุ่มแบบสถิติ มีความแตกต่างเล็กน้อยและส่วนใหญ่ความแตกต่างไม่สำคัญ
mrdenny

4
จากมุมมองประสิทธิภาพนี่ค่อนข้างช้า - คุณสามารถได้รับการปรับปรุงที่สำคัญโดย ORDER BY CHECKSUM (NEWID ())
Miles D

1
@mrdenny - คุณใช้ "ไม่สุ่มทางสถิติ" บนอะไร คำตอบที่นี่บอกว่ามันจบลงด้วยการใช้CryptGenRandomในที่สุด dba.stackexchange.com/a/208069/3690
Martin Smith

15

คำแนะนำแรกของ Pradeep Adiga ORDER BY NEWID()นั้นใช้ได้และบางสิ่งบางอย่างที่ฉันเคยใช้ในอดีตด้วยเหตุผลนี้

ระวังการใช้RAND()- ในบริบทจำนวนมากมันจะถูกดำเนินการเพียงครั้งเดียวต่อคำสั่งดังนั้นORDER BY RAND()จะไม่มีผลกระทบ (ในขณะที่คุณได้รับผลลัพธ์เดียวกันจาก RAND () สำหรับแต่ละแถว)

ตัวอย่างเช่น

SELECT display_name, RAND() FROM tr_person

ส่งกลับแต่ละชื่อจากตารางบุคคลของเราและหมายเลข "สุ่ม" ซึ่งจะเหมือนกันสำหรับแต่ละแถว จำนวนจะแตกต่างกันไปในแต่ละครั้งที่คุณเรียกใช้แบบสอบถาม แต่จะเหมือนกันสำหรับแต่ละแถวในแต่ละครั้ง

เพื่อแสดงให้เห็นว่าเป็นกรณีเดียวกันกับที่RAND()ใช้ในORDER BYประโยคฉันลอง:

SELECT display_name FROM tr_person ORDER BY RAND(), display_name

ผลลัพธ์ยังคงจัดเรียงตามชื่อที่ระบุว่าฟิลด์เรียงลำดับก่อนหน้า (อันที่คาดว่าจะสุ่ม) ไม่มีผลดังนั้นสันนิษฐานว่ามีค่าเดียวกันเสมอ

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

SELECT display_name FROM tr_person ORDER BY NEWID()

จะเรียงลำดับชื่อ "สุ่ม"

DBMS อื่น ๆ

ข้างต้นเป็นจริงสำหรับ MSSQL (2005 และ 2008 อย่างน้อยและถ้าฉันจำได้อย่างถูกต้อง 2,000 เช่นกัน) ฟังก์ชันที่ส่งคืน UUID ใหม่ควรได้รับการประเมินทุกครั้งใน DBMSs NEWID ทั้งหมด () อยู่ภายใต้ MSSQL แต่มันก็คุ้มค่าที่จะตรวจสอบเรื่องนี้ในเอกสารประกอบและ / หรือโดยการทดสอบของคุณเอง พฤติกรรมของฟังก์ชั่นผลลัพธ์อื่น ๆ โดยพลการเช่น RAND () มีแนวโน้มที่จะแตกต่างกันระหว่าง DBMS ดังนั้นตรวจสอบเอกสารอีกครั้ง

นอกจากนี้ฉันได้เห็นการสั่งซื้อโดยค่า UUID ที่ถูกละเว้นในบริบทบางอย่างเนื่องจากฐานข้อมูลสมมติว่าประเภทนั้นไม่มีการเรียงลำดับที่มีความหมาย หากคุณพบว่าสิ่งนี้เป็นกรณีดังกล่าวอย่างชัดเจนโยน UUID กับชนิดสตริงในส่วนคำสั่งหรือห่อฟังก์ชั่นอื่น ๆ รอบ ๆ เช่นCHECKSUM()ใน SQL Server (อาจมีความแตกต่างของประสิทธิภาพเล็กน้อยจากนี้เช่นกันจะทำการสั่งซื้อ ค่า 32- บิตไม่ใช่ 128- บิตแม้ว่าประโยชน์ที่ได้นั้นจะมากกว่าค่าใช้จ่ายในการรันCHECKSUM()ต่อค่าแรกหรือไม่ก่อนอื่นฉันจะให้คุณทดสอบ)

หมายเหตุด้านข้าง

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

SELECT display_name FROM tr_person ORDER BY CHECKSUM(display_name), display_name -- order by the checksum of some of the row's data
SELECT display_name FROM tr_person ORDER BY SUBSTRING(display_name, LEN(display_name)/2, 128) -- order by part of the name field, but not in any an obviously recognisable order)

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

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

ประสิทธิภาพ

ตามที่ EBarr ชี้ให้เห็นอาจมีปัญหาเรื่องประสิทธิภาพกับข้อใดข้อหนึ่งข้างต้น สำหรับแถวมากกว่าสองสามแถวคุณเกือบจะรับประกันว่าจะเห็นเอาต์พุตสพูลออกไปยัง tempdb ก่อนที่จะอ่านจำนวนแถวที่ร้องขอตามลำดับที่ถูกต้องซึ่งหมายความว่าแม้ว่าคุณกำลังมองหา 10 อันดับแรกคุณอาจพบดัชนีเต็ม การสแกน (หรือแย่กว่านั้นคือการสแกนตาราง) เกิดขึ้นพร้อมกับบล็อกจำนวนมากในการเขียนไปยัง tempdb ดังนั้นมันจึงมีความสำคัญอย่างยิ่งเช่นเดียวกับสิ่งต่างๆส่วนใหญ่ในการเปรียบเทียบข้อมูลจริงก่อนที่จะใช้ในการผลิต


14

นี่เป็นคำถามเก่า แต่การอภิปรายด้านหนึ่งขาดหายไปในความคิดของฉัน - PERFORMANCE ORDER BY NewId()คือคำตอบทั่วไป เมื่อมีคนได้รับของพวกเขาเพิ่มแฟนซีที่คุณควรห่อNewID()ในCheckSum()คุณรู้ว่าสำหรับการทำงาน!

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

ป้อนคำอธิบายรูปภาพที่นี่

เพื่อให้คุณเข้าใจว่าเครื่องชั่งนี้จะให้สองตัวอย่างจากฐานข้อมูลที่ฉันทำงานด้วย

  • TableA - มี 50,000 แถวในหน้าข้อมูล 2,500 หน้า แบบสอบถามแบบสุ่มสร้าง 145 อ่านใน 42ms
  • ตาราง B - มี 1.2 ล้านแถวในหน้าข้อมูล 114,000 หน้า การวิ่งOrder By newid()บนโต๊ะนี้สร้างการอ่าน 53,700 ครั้งและใช้เวลา 16 วินาที

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

พบกับ TABLESAMPLE ()

ใน SQL 2005 ความสามารถใหม่ที่เรียกว่าTABLESAMPLEถูกสร้างขึ้น ฉันเคยเห็นบทความหนึ่งที่กล่าวถึงการใช้งาน ... ควรมีมากกว่านั้น MSDN เอกสารที่นี่ ตัวอย่างแรก:

SELECT Top (20) *
FROM Northwind..Orders TABLESAMPLE(20 PERCENT)
ORDER BY NEWID()

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

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

โดยส่วนตัวแล้วฉันใช้มันเพื่อ จำกัด ขนาดของตาราง ดังนั้นในตารางแถวล้านที่ทำtop(20)...TABLESAMPLE(20 PERCENT)แบบสอบถามลดลงถึง 5600 อ่านใน 1600ms นอกจากนี้ยังมีREPEATABLE()ตัวเลือกที่คุณสามารถผ่าน "Seed" สำหรับการเลือกหน้า สิ่งนี้จะส่งผลให้มีการเลือกตัวอย่างที่เสถียร

อย่างไรก็ตามเพิ่งคิดว่าควรเพิ่มการสนทนานี้ หวังว่าจะช่วยใครซักคน


มันจะเป็นการดีที่จะสามารถเขียนแบบสอบถามสุ่มเรียงลำดับที่ปรับขนาดได้ซึ่งไม่เพียง แต่ขยายขนาด แต่ทำงานกับชุดข้อมูลขนาดเล็ก ดูเหมือนว่าคุณจะต้องสลับระหว่างการมีและไม่มีTABLESAMPLE()ตามด้วยตนเองว่าคุณมีข้อมูลมากน้อยเพียงใด ฉันไม่คิดว่าTABLESAMPLE(x ROWS)จะให้แน่ใจว่ามีการส่งคืนแถวอย่างน้อย xเพราะเอกสารระบุว่า“ จำนวนแถวที่แท้จริงที่ส่งคืนอาจแตกต่างกันอย่างมีนัยสำคัญ หากคุณระบุจำนวนน้อยเช่น 5 คุณอาจไม่ได้รับผลลัพธ์ในตัวอย่าง” - ดังนั้นROWSไวยากรณ์จริง ๆ ยังคงเป็นเพียงการหลอกลวงPERCENTภายใน
binki

แน่นอนว่าเวทมนต์อัตโนมัตินั้นดี ในทางปฏิบัติฉันแทบไม่เคยเห็นมาตราส่วนของตาราง 5 แถวมาเป็นล้านแถวโดยไม่ต้องแจ้งให้ทราบล่วงหน้า TABLESAMPLE () ดูเหมือนว่าจะเลือกจำนวนฐานหน้าในตารางเป็นหลักดังนั้นขนาดของแถวที่กำหนดจะมีผลต่อสิ่งที่กลับมา อย่างน้อยที่สุดเท่าที่ฉันเห็นจุดตัวอย่างของตารางคือการให้ชุดย่อยที่ดีซึ่งคุณสามารถเลือกได้ - ประเภทของตารางที่ได้รับมา
EBarr

3

ตารางจำนวนมากมีคอลัมน์ ID ตัวเลขที่จัดทำดัชนีค่อนข้างหนาแน่น (ค่อนข้างขาดหายไป)

สิ่งนี้ช่วยให้เราสามารถกำหนดช่วงของค่าที่มีอยู่และเลือกแถวโดยใช้ค่า ID ที่สร้างแบบสุ่มในช่วงนั้น สิ่งนี้จะทำงานได้ดีที่สุดเมื่อจำนวนแถวที่จะส่งคืนมีขนาดค่อนข้างเล็กและช่วงของค่า ID จะได้รับการเติมให้หนาแน่น

เพื่อแสดงให้เห็นว่ารหัสต่อไปนี้เลือกผู้ใช้แบบสุ่ม 100 คนที่แตกต่างจากตาราง Stack Overflow ของผู้ใช้ซึ่งมี 8,123,937 แถว

ขั้นตอนแรกคือการกำหนดช่วงของค่า ID การดำเนินการที่มีประสิทธิภาพเนื่องจากดัชนี:

DECLARE 
    @MinID integer,
    @Range integer,
    @Rows bigint = 100;

--- Find the range of values
SELECT
    @MinID = MIN(U.Id),
    @Range = 1 + MAX(U.Id) - MIN(U.Id)
FROM dbo.Users AS U;

แบบสอบถามช่วง

แผนอ่านหนึ่งแถวจากแต่ละจุดสิ้นสุดของดัชนี

ตอนนี้เราสร้างรหัสสุ่ม 100 รหัสที่แตกต่างกันในช่วง (โดยมีแถวที่ตรงกันในตารางผู้ใช้) และส่งคืนแถวเหล่านั้น:

WITH Random (ID) AS
(
    -- Find @Rows distinct random user IDs that exist
    SELECT DISTINCT TOP (@Rows)
        Random.ID
    FROM dbo.Users AS U
    CROSS APPLY
    (
        -- Random ID
        VALUES (@MinID + (CONVERT(integer, CRYPT_GEN_RANDOM(4)) % @Range))
    ) AS Random (ID)
    WHERE EXISTS
    (
        SELECT 1
        FROM dbo.Users AS U2
            -- Ensure the row continues to exist
            WITH (REPEATABLEREAD)
        WHERE U2.Id = Random.ID
    )
)
SELECT
    U3.Id,
    U3.DisplayName,
    U3.CreationDate
FROM Random AS R
JOIN dbo.Users AS U3
    ON U3.Id = R.ID
-- QO model hint required to get a non-blocking flow distinct
OPTION (MAXDOP 1, USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

แบบสอบถามแถวสุ่ม

แผนแสดงให้เห็นว่าในกรณีนี้ต้องมีการสุ่มหมายเลข 601 เพื่อค้นหาแถวที่ตรงกัน 100 แถว มันค่อนข้างเร็ว:

ตาราง 'ผู้ใช้' จำนวนการสแกน 1, อ่านตรรกะ 1937, อ่านจริง 2, อ่านล่วงหน้าอ่าน 408
ตาราง 'โต๊ะทำงาน' สแกนนับ 0, อ่านโลจิคัล 0, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0
ตาราง 'Workfile' สแกนนับ 0, อ่านโลจิคัล 0, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0

 เวลาดำเนินการของ SQL Server:
   เวลา CPU = 0 ms, เวลาที่ผ่านไป = 9 ms

ลองใช้งานใน Stack Exchange Data Explorer


0

ดังที่ฉันอธิบายไว้ในบทความนี้เพื่อที่จะสลับชุดผลลัพธ์ SQL คุณต้องใช้การเรียกฟังก์ชันเฉพาะฐานข้อมูล

โปรดทราบว่าการเรียงชุดผลลัพธ์ขนาดใหญ่โดยใช้ฟังก์ชัน RANDOM อาจกลายเป็นช้ามากดังนั้นโปรดตรวจสอบให้แน่ใจว่าคุณได้ทำตามชุดผลลัพธ์ขนาดเล็กแล้ว

ถ้าคุณต้องสับเปลี่ยนชุดผลลัพธ์ขนาดใหญ่และ จำกัด ในภายหลังจากนั้นควรใช้ SQL Server TABLESAMPLEในSQL Serverแทนฟังก์ชันสุ่มในอนุประโยค ORDER BY

ดังนั้นสมมติว่าเรามีตารางฐานข้อมูลต่อไปนี้:

ป้อนคำอธิบายรูปภาพที่นี่

และแถวต่อไปนี้ในsongตาราง:

| id | artist                          | title                              |
|----|---------------------------------|------------------------------------|
| 1  | Miyagi & Эндшпиль ft. Рем Дигга | I Got Love                         |
| 2  | HAIM                            | Don't Save Me (Cyril Hahn Remix)   |
| 3  | 2Pac ft. DMX                    | Rise Of A Champion (GalilHD Remix) |
| 4  | Ed Sheeran & Passenger          | No Diggity (Kygo Remix)            |
| 5  | JP Cooper ft. Mali-Koa          | All This Love                      |

บน SQL Server คุณต้องใช้NEWIDฟังก์ชันดังแสดงในตัวอย่างต่อไปนี้:

SELECT
    CONCAT(CONCAT(artist, ' - '), title) AS song
FROM song
ORDER BY NEWID()

เมื่อเรียกใช้แบบสอบถาม SQL ดังกล่าวบน SQL Server เราจะได้รับชุดผลลัพธ์ต่อไปนี้:

| song                                              |
|---------------------------------------------------|
| Miyagi & Эндшпиль ft. Рем Дигга - I Got Love      |
| JP Cooper ft. Mali-Koa - All This Love            |
| HAIM - Don't Save Me (Cyril Hahn Remix)           |
| Ed Sheeran & Passenger - No Diggity (Kygo Remix)  |
| 2Pac ft. DMX - Rise Of A Champion (GalilHD Remix) |

ขอให้สังเกตว่าเพลงที่อยู่ในรายการแบบสุ่มขอบคุณการNEWIDเรียกใช้ฟังก์ชั่นที่ใช้โดยข้อ ORDER BY

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.