it-swarm.dev

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

Mam zapytanie, w którym chcę, aby wynikowe rekordy były uporządkowane losowo. Używa indeksu klastrowego, więc jeśli nie dołączę 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.

29
goric

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

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

To stare pytanie, ale moim zdaniem brakuje jednego aspektu dyskusji - WYDAJNOŚĆ. ORDER BY NewId() jest ogólną odpowiedzią. Kiedy ktoś ma ochotę, dodaje, że naprawdę powinieneś zawinąć NewID() w CheckSum(), wiesz, dla wydajności!

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

enter image description here

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() na 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, metoda newid() psuje się. Więc co robić chłopiec?

Poznaj TABLESAMPLE ()

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

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

Ideą próbki tabeli jest podanie w przybliżeniu żądanego rozmiaru podzbioru. 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 podzbioru, 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 wcześniej do kosztownego rodzaju.

Osobiście używałem go, aby w efekcie ograniczyć rozmiar mojego stołu. Tak więc na milionowej tabeli wierszy wykonującej top(20)...TABLESAMPLE(20 PERCENT) zapytanie spada do 5600 odczytów w 1600ms. Istnieje również opcja REPEATABLE(), w której można 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.

16
EBarr

Pierwsza sugestia Pradeep Adiga, ORDER BY NEWID(), jest w porządku i coś, z czego korzystałem w przeszłości z tego powodu.

Zachowaj ostrożność, używając Rand() - w wielu kontekstach jest wykonywana tylko raz na instrukcję, więc ORDER BY Rand() nie 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 za każdym razem jest taka sama dla każdego wiersza.

Aby pokazać, że tak samo jest w przypadku Rand() użytej w klauzuli ORDER BY, 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ść.

Kolejność według NEWID() jednak działa, ponieważ jeśli NEWID () nie był zawsze ponownie oceniany, cel UUID byłby zepsuty podczas wstawiania wielu nowe wiersze w jednym statusie z unikatowymi kluczami, więc:

SELECT display_name FROM tr_person ORDER BY NEWID()

porządkuje nazwy „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 sprawdzić w dokumentacji i/lub przez twoje 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 ciągu w klauzuli porządkowania lub zawiń wokół niego jakąś inną funkcję, taką jak CHECKSUM() w SQL Server (może występować niewielka różnica w wydajności od tego ponieważ porządkowanie zostanie wykonane na wartościach 32-bitowych, a nie na 128-bitowych, jednak czy korzyści z tego przeważają nad kosztem uruchomienia CHECKSUM() na wartość najpierw zostawię cię do przetestowania).

waga dodatkowa

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ć 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ą gdy funkcje zdefiniowane przez użytkownika są ewaluowane, 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.

16
David Spillett

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łniony (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;

Range query

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

random rows query

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, odczyt logiczny 1937, odczyt fizyczny 2, odczyt z wyprzedzeniem 408 
 Tabela „Tabela robocza”. Liczba skanów 0, odczyt logiczny 0, odczyt fizyczny 0, odczyt z wyprzedzeniem 0 
 Tabela „Plik roboczy”. Liczba skanów 0, odczyt logiczny 0, odczyt fizyczny 0, odczyt z wyprzedzeniem 0 
 
 Czasy wykonania programu SQL Server: 
 Czas procesora = 0 ms, czas, który upłynął = 9 ms. 

Wypróbuj w Eksploratorze danych wymiany stosu.

3
Paul White 9

Jak wyjaśniłem w ten artykuł , 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ć SQL Server TABLESAMPLE in SQL Server zamiast losowej funkcji w klauzuli ORDER BY.

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

enter image description here

I następujące wiersze w tabeli 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                      |

W SQL Server musisz użyć funkcji NEWID, 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 wywołaniu funkcji NEWID używanemu przez klauzulę ORDER BY.

0
Vlad Mihalcea