Jaki jest najlepszy sposób na uzyskanie losowego zamówienia?

27

Mam zapytanie, w którym chcę, aby wynikowe rekordy były uporządkowane losowo. Używa indeksu klastrowanego, więc jeśli go nie uwzględnię order by, prawdopodobnie zwróci rekordy w kolejności tego indeksu. Jak mogę zapewnić losową kolejność wierszy?

Rozumiem, że prawdopodobnie nie będzie to „prawdziwie” losowy, pseudolosowy jest wystarczający dla moich potrzeb.

goric
źródło

Odpowiedzi:

19

ORDER BY NEWID () posortuje rekordy losowo. Przykład tutaj

SELECT *
FROM Northwind..Orders 
ORDER BY NEWID()
Koczownik
źródło
7
ORDER BY NEWID () jest faktycznie losowy, ale nie statystycznie losowy. Jest niewielka różnica i przez większość czasu różnica nie ma znaczenia.
mrdenny 28.01.11
4
Z punktu widzenia wydajności jest to dość powolne - znaczącą poprawę można uzyskać, ORDER BY CHECKSUM (NEWID ())
Miles D
1
@mrdenny - Na czym opierasz się „nie statystycznie losowy”? Odpowiedź tutaj mówi, że kończy się CryptGenRandomna końcu. dba.stackexchange.com/a/208069/3690
Martin Smith
15

Pierwsza sugestia Pradeepa Adigi ORDER BY NEWID()jest w porządku i z tego powodu korzystałem w przeszłości.

Zachowaj ostrożność przy użyciu RAND()- w wielu kontekstach jest wykonywany tylko raz na instrukcję, więc nie ORDER BY RAND()przyniesie żadnego efektu (ponieważ otrzymujesz taki sam wynik z RAND () dla każdego wiersza).

Na przykład:

SELECT display_name, RAND() FROM tr_person

zwraca każde nazwisko z naszej tabeli osób i „losową” liczbę, która jest taka sama dla każdego wiersza. Liczba zmienia się przy każdym uruchomieniu zapytania, ale jest taka sama dla każdego wiersza za każdym razem.

Aby pokazać, że to samo ma miejsce w przypadku RAND()stosuje się w ORDER BYpunkcie, próbuję:

SELECT display_name FROM tr_person ORDER BY RAND(), display_name

Wyniki są nadal uporządkowane według nazwy wskazującej, że wcześniejsze pole sortowania (to, które ma być losowe), nie działa, więc przypuszczalnie zawsze ma tę samą wartość.

Porządkowanie według NEWID()działa, ponieważ jeśli NEWID () nie zawsze był ponownie oceniany, cel UUIDs zostałby zepsuty podczas wstawiania wielu nowych wierszy w jednej pozycji z unikatowymi identyfikatorami, ponieważ klucz:

SELECT display_name FROM tr_person ORDER BY NEWID()

nie zamówić nazw „losowo”.

Inne DBMS

Powyższe odnosi się do MSSQL (przynajmniej 2005 i 2008, i jeśli dobrze pamiętam również 2000). Funkcja zwracająca nowy UUID powinna być oceniana za każdym razem we wszystkich DBMS-ach NEWID () znajduje się w MSSQL, ale warto to zweryfikować w dokumentacji i / lub przez własne testy. Zachowanie innych funkcji o dowolnym wyniku, takich jak RAND (), jest bardziej prawdopodobne, że różnią się między DBMS, więc ponownie sprawdź dokumentację.

Widziałem też, że porządkowanie według wartości UUID jest ignorowane w niektórych kontekstach, ponieważ DB zakłada, że ​​typ nie ma znaczącego uporządkowania. Jeśli okaże się, że jest to przypadek jawnie rzutuj identyfikator UUID na typ łańcucha w klauzuli kolejności lub owiń wokół niego jakąś inną funkcję, jak CHECKSUM()w SQL Server (może występować niewielka różnica w wydajności, ponieważ kolejność zostanie wykonana na wartości 32-bitowe, a nie 128-bitowe, ale czy korzyść z tego przewyższy koszt uruchomienia CHECKSUM()według wartości najpierw, zostawię cię do przetestowania).

Dygresja

Jeśli chcesz dowolne, ale nieco powtarzalne porządkowanie, uporządkuj według względnie niekontrolowanego podzbioru danych w samych wierszach. Na przykład jedno lub drugie zwróci nazwy w dowolnej, ale powtarzalnej kolejności:

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)

Arbitralne, ale powtarzalne porządki nie są często przydatne w aplikacjach, chociaż mogą być przydatne w testowaniu, jeśli chcesz przetestować jakiś kod na wynikach w różnych zamówieniach, ale chcesz móc powtarzać każde uruchomienie kilka razy w ten sam sposób (aby uzyskać średni czas wyniki z kilku przebiegów lub testowanie, czy poprawka dokonana w kodzie usuwa problem lub nieefektywność poprzednio zaznaczoną przez określony zestaw wyników wejściowych, lub po prostu do testowania, czy kod jest „stabilny”, czyli zwraca ten sam wynik za każdym razem jeśli wysłane te same dane w danej kolejności).

Tej sztuczki można także użyć do uzyskania bardziej dowolnych wyników z funkcji, które nie pozwalają na wywołania niedeterministyczne, takie jak NEWID () w ich ciele. Ponownie, nie jest to coś, co może być często przydatne w prawdziwym świecie, ale może się przydać, jeśli chcesz, aby funkcja zwróciła coś losowego, a „losowe ish” jest wystarczająco dobre (ale pamiętaj, aby pamiętać o regułach, które określają kiedy ewaluowane są funkcje zdefiniowane przez użytkownika, tj. zwykle tylko raz na wiersz lub wyniki mogą nie być zgodne z oczekiwaniami / wymaganiami).

Wydajność

Jak zauważa EBarr, mogą wystąpić problemy z wydajnością w każdym z powyższych. W przypadku więcej niż kilku wierszy masz prawie gwarancję, że dane wyjściowe są buforowane do tempdb, zanim żądana liczba wierszy zostanie odczytana w odpowiedniej kolejności, co oznacza, że ​​nawet jeśli szukasz pierwszej 10, możesz znaleźć pełny indeks skanowanie (lub, co gorsza, skanowanie tabeli) odbywa się wraz z ogromnym blokiem zapisu do tempdb. Dlatego niezwykle ważne może być, podobnie jak w przypadku większości rzeczy, porównywanie realistycznych danych przed użyciem ich w produkcji.

David Spillett
źródło
14

To stare pytanie, ale moim zdaniem brakuje jednego aspektu dyskusji - WYDAJNOŚĆ. ORDER BY NewId()to ogólna odpowiedź. Kiedy ktoś ochotę dostać za dodają, że naprawdę należy owinąć NewID()w CheckSum(), wiesz, do wykonania!

Problem z tą metodą polega na tym, że nadal masz zagwarantowane pełne skanowanie indeksu, a następnie kompletny rodzaj danych. Jeśli pracujesz z jakimkolwiek poważnym wolumenem danych, może to szybko stać się kosztowne. Spójrz na ten typowy plan wykonania i zwróć uwagę, że sortowanie zajmuje 96% twojego czasu ...

wprowadź opis zdjęcia tutaj

Aby dać ci wyobrażenie o tym, jak to się skaluje, podam dwa przykłady z bazy danych, z którą pracuję.

  • Tabela A - ma 50 000 wierszy na 2500 stronach danych. Losowe zapytanie generuje 145 odczytów w 42ms.
  • Tabela B - ma 1,2 miliona wierszy na 114 000 stronach danych. Uruchomienie Order By newid()tej tabeli generuje 53 700 odczytów i zajmuje 16 sekund.

Morał tej historii jest taki, że jeśli masz duże tabele (pomyśl miliardy wierszy) lub musisz często uruchamiać to zapytanie, newid()metoda się psuje. Więc co robić chłopiec?

Poznaj TABLESAMPLE ()

W SQL 2005 utworzono nową TABLESAMPLEfunkcję o nazwie . Widziałem tylko jeden artykuł omawiający jego użycie ... powinno być ich więcej. Dokumenty MSDN tutaj . Najpierw przykład:

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

Ideą próbki tabeli jest podanie przybliżonego rozmiaru podzbioru, o który prosisz. SQL numeruje każdą stronę danych i wybiera X procent tych stron. Rzeczywista liczba odzyskanych wierszy może się różnić w zależności od tego, co istnieje na wybranych stronach.

Jak mam z tego korzystać? Wybierz rozmiar podzestawu, który przekracza liczbę potrzebnych wierszy, a następnie dodaj Top(). Chodzi o to, że możesz sprawić, by twój gigantyczny stół wydawał się mniejszy przed kosztownym sortowaniem.

Osobiście używałem go, aby w efekcie ograniczyć rozmiar mojego stołu. Tak więc w tabeli milionów wierszy wykonanie top(20)...TABLESAMPLE(20 PERCENT)zapytania spada do 5600 odczytów w 1600ms. Istnieje również REPEATABLE()opcja, w której możesz przekazać „Ziarno” do wyboru strony. Powinno to doprowadzić do stabilnego doboru próbki.

W każdym razie pomyślałem, że należy to dodać do dyskusji. Mam nadzieję, że to komuś pomoże.

EBarr
źródło
Byłoby miło móc napisać skalowalne zapytanie losowe, które nie tylko skaluje, ale działa z małymi zestawami danych. Wygląda na to, że musisz ręcznie przełączać się między posiadaniem a nie posiadaniem TABLESAMPLE()na podstawie ilości posiadanych danych. Nie sądzę, TABLESAMPLE(x ROWS)że zapewni to nawet zwrot co najmniej x wierszy, ponieważ dokumentacja mówi „Rzeczywista liczba zwracanych wierszy może się znacznie różnić. Jeśli określisz małą liczbę, na przykład 5, możesz nie otrzymać wyników w próbce. ”- czy więc ROWSskładnia nadal jest po prostu zamaskowana PERCENT?
binki
Jasne, auto-magia jest fajna. W praktyce rzadko widuję 5-rzędową tabelę skalowaną do milionów wierszy bez uprzedzenia. TABLESAMPLE () wydaje się opierać wybór liczby stron w tabeli, więc podany rozmiar wiersza wpływa na to, co powróci. Próbka tabeli, przynajmniej tak, jak ją widzę, ma na celu dać ci dobry podzestaw, z którego możesz wybrać - coś w rodzaju tabeli pochodnej.
EBarr
3

Wiele tabel ma stosunkowo gęstą (kilka brakujących wartości) indeksowaną kolumnę z numerycznym identyfikatorem.

To pozwala nam określić zakres istniejących wartości i wybrać wiersze przy użyciu losowo generowanych wartości ID w tym zakresie. Działa to najlepiej, gdy liczba zwracanych wierszy jest stosunkowo niewielka, a zakres wartości ID jest gęsto zapełniany (więc szansa na wygenerowanie brakującej wartości jest wystarczająco mała).

Aby to zilustrować, poniższy kod wybiera 100 różnych losowych użytkowników z tabeli przepełnienia stosu użytkowników, która ma 8 123 937 wierszy.

Pierwszym krokiem jest określenie zakresu wartości ID, wydajna operacja dzięki indeksowi:

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;

Zapytanie o zakres

Plan odczytuje jeden wiersz z każdego końca indeksu.

Teraz generujemy 100 różnych losowych identyfikatorów w zakresie (z pasującymi wierszami w tabeli użytkowników) i zwracamy te wiersze:

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

zapytanie o losowe wiersze

Plan pokazuje, że w tym przypadku potrzebnych było 601 liczb losowych, aby znaleźć 100 pasujących wierszy. To jest dość szybkie:

Tabela „Użytkownicy”. Liczba skanów 1, logiczne odczyty 1937, fizyczne odczyty 2, odczyt z wyprzedzeniem 408
Tabela „Stół roboczy”. Liczba skanów 0, logiczne odczyty 0, fizyczne odczyty 0, odczyt z wyprzedzeniem 0
Tabela „Plik roboczy”. Liczba skanów 0, logiczne odczyty 0, fizyczne odczyty 0, odczyt z wyprzedzeniem 0

 Czasy wykonania programu SQL Server:
   Czas procesora = 0 ms, upływ czasu = 9 ms.

Wypróbuj go w Eksploratorze stosów wymiany danych.

Paul White mówi GoFundMonica
źródło
0

Jak wyjaśniłem w tym artykule , aby przetasować zestaw wyników SQL, musisz użyć wywołania funkcji specyficznej dla bazy danych.

Zauważ, że sortowanie dużego zestawu wyników za pomocą funkcji RANDOM może okazać się bardzo wolne, więc upewnij się, że robisz to na małych zestawach wyników.

Jeśli musisz przetasować duży zestaw wyników i ograniczyć go później, lepiej użyć programu SQL Server TABLESAMPLEw programie SQL Server zamiast funkcji losowej w klauzuli ORDER BY.

Zakładając, że mamy następującą tabelę bazy danych:

wprowadź opis zdjęcia tutaj

I następujące wiersze w songtabeli:

| 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                      |

W SQL Server musisz użyć NEWIDfunkcji, jak pokazano w poniższym przykładzie:

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

Podczas uruchamiania wspomnianego zapytania SQL na SQL Server otrzymamy następujący zestaw wyników:

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

Zauważ, że utwory są wyświetlane w kolejności losowej, dzięki NEWIDwywołaniu funkcji stosowanemu w klauzuli ORDER BY.

Vlad Mihalcea
źródło