Каков наилучший способ получить случайный заказ?

27

У меня есть запрос, где я хочу, чтобы результирующие записи были упорядочены случайным образом. Он использует кластерный индекс, поэтому, если я не включу, order byон, скорее всего, будет возвращать записи в порядке этого индекса. Как я могу обеспечить случайный порядок строк?

Я понимаю, что он, скорее всего, не будет «по-настоящему» случайным, псевдослучайный достаточно хорош для моих нужд.

goric
источник

Ответы:

19

ORDER BY NEWID () отсортирует записи случайным образом. Пример здесь

SELECT *
FROM Northwind..Orders 
ORDER BY NEWID()
кочевник
источник
7
ORDER BY NEWID () эффективно случайный, но не статистически случайный. Есть небольшая разница, и в большинстве случаев разница не имеет значения.
Мрденный
4
С точки зрения производительности, это довольно медленно - вы можете получить значительное улучшение, если закажите ORDER BY CHECKSUM (NEWID ())
Miles D
1
@mrdenny - На чем основано «не статистически случайное»? Ответ здесь говорит, что в конечном итоге он будет использован CryptGenRandom. dba.stackexchange.com/a/208069/3690
Мартин Смит
15

Первое предложение Прадипа Адиги 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 (), в своем теле. Опять же, это не то, что может быть часто полезно в реальном мире, но может пригодиться, если вы хотите, чтобы функция возвращала что-то случайное, а «random-ish» достаточно хорошо (но будьте осторожны, чтобы запомнить правила, которые определяют когда пользовательские функции оцениваются, т. е. обычно только один раз на строку, или ваши результаты могут не соответствовать вашим ожиданиям).

Спектакль

Как указывает EBarr, с любым из вышеперечисленных могут быть проблемы с производительностью. Для более чем нескольких строк вы почти гарантированы, что вывод буферизуется в базу данных tempdb до того, как запрошенное количество строк будет прочитано в правильном порядке, что означает, что даже если вы ищете топ-10, вы можете найти полный индекс сканирование (или, что еще хуже, сканирование таблицы) происходит вместе с огромным блоком записи в базу данных tempdb. Поэтому может быть жизненно важно, как и в большинстве случаев, сравнить с реалистичными данными, прежде чем использовать их в производстве.

Дэвид Спиллетт
источник
14

Это старый вопрос, но один аспект обсуждения, на мой взгляд, отсутствует - ЭФФЕКТИВНОСТЬ. ORDER BY NewId()это общий ответ. Когда фантазии Кто - то получает в них добавить , что вы должны действительно обернуть NewID()в CheckSum(), вы знаете, для исполнения!

Проблема этого метода заключается в том, что вам по-прежнему гарантируется полное сканирование индекса, а затем полная сортировка данных. Если вы работали с любым серьезным объемом данных, это может быстро стать дорогим. Посмотрите на этот типичный план выполнения и обратите внимание, что сортировка занимает 96% вашего времени ...

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

Чтобы дать вам представление о том, как это масштабируется, я приведу два примера из базы данных, с которой я работаю.

  • Таблица A - содержит 50000 строк на 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(). Идея заключается в том что вы можете сделать ваш Ginormous таблица выглядит меньше до к дорогому рода.

Лично я использовал это, чтобы фактически ограничить размер моего стола. Таким образом, выполнение этой таблицы строк с миллионами top(20)...TABLESAMPLE(20 PERCENT)запросов сокращается до 5600 операций чтения за 1600 мс. Существует также REPEATABLE()опция, где вы можете передать «Семя» для выбора страницы. Это должно привести к стабильному выбору образца.

Во всяком случае, просто подумал, что это должно быть добавлено к обсуждению. Надеюсь, это кому-нибудь поможет.

EBarr
источник
Было бы неплохо написать масштабируемый запрос случайного порядка, который не только масштабируется, но и работает с небольшими наборами данных. Похоже, вы должны вручную переключаться между наличием и отсутствием в TABLESAMPLE()зависимости от того, сколько данных у вас есть. Я не думаю, что это TABLESAMPLE(x ROWS)могло бы гарантировать, что хотя бы x строки будут возвращены, потому что документация гласит: «Фактическое количество возвращаемых строк может значительно различаться. Если вы укажете небольшое число, например 5, вы можете не получить результаты в образце ». - значит, ROWSсинтаксис все еще PERCENTвнутри маскируется ?
Бинки
Конечно, авто-магия это хорошо. На практике я редко видел масштаб таблицы из 5 строк до миллионов строк без предварительного уведомления. TABLESAMPLE (), кажется, основывает выбор количества страниц в таблице, поэтому заданный размер строки влияет на то, что возвращается. Цель примера таблицы, по крайней мере, на мой взгляд, состоит в том, чтобы дать вам хороший поднабор, из которого вы можете выбрать - что-то вроде производной таблицы.
EBarr
3

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

Это позволяет нам определять диапазон существующих значений и выбирать строки, используя случайно сгенерированные значения идентификаторов в этом диапазоне. Это работает лучше всего, когда количество возвращаемых строк относительно невелико, а диапазон значений идентификаторов густо заполнен (поэтому вероятность создания пропущенного значения достаточно мала).

Чтобы проиллюстрировать это, следующий код выбирает 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 мс.

Попробуйте это в Stack Exchange Data Explorer.

Пол Уайт говорит, что GoFundMonica
источник
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.

Влад Михалча
источник