Який найкращий спосіб отримати випадкове замовлення?


27

У мене є запит, де я хочу, щоб отримані записи були упорядковані випадковим чином. Він використовує кластерний індекс, тому, якщо я не включаю, order byвін, ймовірно, повертає записи в порядку цього індексу. Як я можу забезпечити випадковий порядок рядків?

Я розумію, що це, ймовірно, не буде "по-справжньому" випадковим, псевдовипадковий досить хороший для моїх потреб.

Відповіді:


19

ЗАМОВИТИ NEWID () буде сортувати записи випадковим чином. Приклад тут

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

7
ЗАМОВЛЕННЯ NEWID () є фактично випадковим, але не статистично випадковим. Існує невелика різниця, і більшість часу різниця не має значення.
mrdenny

4
З точки зору продуктивності, це відбувається досить повільно - ви можете досягти значного покращення, ЗАМОВИТИ ЧЕКСУМ (NEWID ())
Миль D

1
@mrdenny - На чому ви базуєте "не статистично випадкові"? Відповідь тут говорить, що в кінці кінців використовується CryptGenRandom. dba.stackexchange.com/a/208069/3690
Мартін Сміт

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 буде порушена при введенні багатьох нових рядків в один документ з унікальними ідентифікаторами, як вони вводять, так:

SELECT display_name FROM tr_person ORDER BY NEWID()

робить замовлення імен «випадкові».

Інші СУБД

Сказане стосується MSSQL (принаймні, 2005 та 2008 рр., І якщо я добре пам’ятаю 2000 р.). Функція, що повертає новий UUID, повинна оцінюватися кожного разу, коли всі СУБД NEWID () знаходиться під MSSQL, але варто перевірити це в документації та / або власними тестами. Поведінка інших функцій довільного результату, таких як RAND (), швидше відрізняється між СУБД, тому ще раз перевірте документацію.

Також я бачив, що впорядкування за значеннями 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 () у своєму тілі. Знову ж таки, це не те, що може бути корисним у реальному світі, але може стати в нагоді, якщо ви хочете, щоб функція повертала щось випадкове, а "випадковий результат" є досить хорошим (але будьте уважні, пам'ятайте правила, які визначають коли оцінюються визначені користувачем функції, тобто, як правило, один раз у ряд, або результати можуть бути не такими, які ви очікуєте / вимагаєте).

Продуктивність

Як зазначає EBarr, з будь-яким із перерахованих вище можуть виникнути проблеми з ефективністю. Більше декількох рядків вам майже гарантовано бачити результат, виведений на tempdb, до того, як запитане число рядків буде прочитано в правильному порядку, а це означає, що навіть якщо ви шукаєте першу десятку, ви можете знайти повний індекс сканування (або ще гірше, сканування таблиці) відбувається разом з величезним блоком запису в tempdb. Для цього може бути життєво важливим, як і для більшості речей, орієнтуватися на реалістичні дані, перш ніж використовувати їх у виробництві.


14

Це давнє запитання, але один аспект дискусії відсутній, на мою думку - ДІЯЛЬНІСТЬ. ORDER BY NewId()є загальною відповіддю. Коли фантазії Хто - то отримує в них додати , що ви повинні дійсно обернути NewID()в CheckSum(), ви знаєте, для виконання!

Проблема цього методу полягає в тому, що ви все одно гарантуєте повне сканування індексів, а потім повний вид даних. Якщо ви працювали з будь-яким серйозним обсягом даних, це може швидко стати дорогим. Подивіться на цей типовий план виконання та зверніть увагу, як сортування займає 96% вашого часу ...

введіть тут опис зображення

Щоб зрозуміти, як це масштабує, я наведу два приклади з бази даних, з якою я працюю.

  • ТаблицяA - має 50 000 рядків на 2500 сторінках даних. Випадковий запит генерує 145 зчитування за 42 мс.
  • Таблиця 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(). Ідея полягає в тому, що ви можете зробити ваш знаменитий стіл меншим перед дорогим сортом.

Особисто я використовую це, щоб фактично обмежити розмір своєї таблиці. Отже, на цій мільйонній таблиці рядків виконання top(20)...TABLESAMPLE(20 PERCENT)запиту падає до 5600 зчитувань за 1600 мс. Також є REPEATABLE()варіант, де ви можете передати "Насіння" для вибору сторінки. Це повинно призвести до стабільного відбору вибірки.

У будь-якому випадку, просто думав, що це слід додати до дискусії. Сподіваюся, це комусь допоможе.


Було б непогано мати змогу написати масштабований запит у випадковому порядку, який не тільки збільшує масштаби, але працює з невеликими наборами даних. Це здається, що вам потрібно вручну перемикатися між наявними та відсутніми TABLESAMPLE()на основі того, скільки у вас є даних. Я не думаю, що TABLESAMPLE(x ROWS)це навіть забезпечить повернення принаймні x рядків, оскільки в документації написано: "Дійсна кількість повернутих рядків може суттєво відрізнятися. Якщо ви вказали невелику кількість, наприклад, 5, ви не зможете отримати результати у вибірці. "- значить, ROWSсинтаксис все-таки є просто замаскованим PERCENTвсередині?
бінкі

Звичайно, автомагія приємна. На практиці я рідко бачив шкалу таблиці 5 рядів до мільйонів рядків без попереднього повідомлення. TABLESAMPLE (), здається, базує вибір кількості сторінок у таблиці, тому заданий розмір рядка впливає на те, що повертається. Суть вибірки таблиці, принаймні, як я бачу, полягає в тому, щоб ви отримали хороший підмножина, з якої ви можете вибрати - на зразок похідної таблиці.
EBarr

3

Багато таблиць мають відносно щільний (мало пропущених значень) індексований числовий стовпчик ІД.

Це дозволяє нам визначити діапазон існуючих значень та вибрати рядки, використовуючи випадково згенеровані значення ідентифікаторів у цьому діапазоні. Це найкраще працює, коли кількість рядків, що повертаються, відносно невелика, а діапазон значень ID густо заселений (тому шанс генерувати відсутнє значення досить малий).

Для ілюстрації, наступний код вибирає 100 чітких випадкових користувачів із таблиці переповнення стека користувачів, яка містить 8,123,937 рядків.

Перший крок - визначення діапазону значень ідентифікатора, ефективна операція завдяки індексу:

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
Таблиця "Файл роботи". Підрахунок сканування 0, логічне зчитування 0, фізичне зчитування 0, читання вперед-0

 Часи виконання SQL Server:
   Час процесора = 0 мс, минулий час = 9 мс.

Спробуйте його в Провіднику даних стека обміну.


0

Як я пояснював у цій статті , для переміщення набору результатів SQL вам потрібно використовувати виклик функції, що залежить від бази даних.

Зауважте, що сортування великого набору результатів за допомогою функції RANDOM може виявитися дуже повільним, тому переконайтеся, що ви робите це на невеликих наборах результатів.

Якщо вам доведеться перетасувати великий набір результатів і обмежити його згодом, то краще використовувати SQL Server TABLESAMPLEна SQL Server замість випадкової функції у пункті ЗАМОВЛЕННЯ ДО.

Отже, якщо у нас є така таблиця баз даних:

введіть тут опис зображення

І наступні рядки в 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.