Лучший способ выбрать случайные строки PostgreSQL - PullRequest
286 голосов
/ 30 декабря 2011

Я хочу случайный выбор строк в PostgreSQL, я попробовал это:

select * from table where random() < 0.01;

Но некоторые другие рекомендуют это:

select * from table order by random() limit 1000;

У меня очень большая таблица с 500 миллионами строк, я хочу, чтобы она была быстрой.

Какой подход лучше? Какие есть отличия? Каков наилучший способ выбора случайных строк?

Ответы [ 12 ]

197 голосов
/ 30 декабря 2011

С учетом ваших спецификаций (плюс дополнительная информация в комментариях),

  • У вас есть столбец числового идентификатора (целые числа) только с небольшим (или умеренно небольшим) пробелом.
  • Очевидно, нет или мало операций записи.
  • Ваш столбец идентификатора должен быть проиндексирован!Первичный ключ отлично работает.

В приведенном ниже запросе не требуется последовательное сканирование большой таблицы, только сканирование индекса.

Сначала получите оценки для основного запроса:

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

Единственная, возможно, дорогая часть - это count(*) (для огромных таблиц).Приведенные выше характеристики вам не нужны.Смета подойдет просто отлично, доступна почти бесплатно ( подробное объяснение здесь ):

SELECT reltuples AS ct FROM pg_class WHERE oid = 'schema_name.big'::regclass;

Пока ct не намного меньшечем id_span, запрос превзойдет другие подходы.

WITH params AS (
    SELECT 1       AS min_id           -- minimum id <= current min id
         , 5100000 AS id_span          -- rounded up. (max_id - min_id + buffer)
    )
SELECT *
FROM  (
    SELECT p.min_id + trunc(random() * p.id_span)::integer AS id
    FROM   params p
          ,generate_series(1, 1100) g  -- 1000 + buffer
    GROUP  BY 1                        -- trim duplicates
    ) r
JOIN   big USING (id)
LIMIT  1000;                           -- trim surplus
  • Генерация случайных чисел в пространстве id.У вас есть «несколько пробелов», поэтому добавьте 10% (достаточно, чтобы легко закрыть пробелы) к числу строк для извлечения.

  • Каждый id может быть выбран несколько раз случайно(хотя очень маловероятно с большим пространством идентификаторов), поэтому сгруппируйте сгенерированные числа (или используйте DISTINCT).

  • Присоедините id s к большой таблице.Это должно быть очень быстро при установленном индексе.

  • Наконец обрежьте излишки id s, которые не были использованы дупсами и пробелами.Каждая строка имеет абсолютно равный шанс для выбора.

Короткая версия

Вы можете упростить этот запрос.CTE в приведенном выше запросе только для образовательных целей:

SELECT *
FROM  (
    SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
    FROM   generate_series(1, 1100) g
    ) r
JOIN   big USING (id)
LIMIT  1000;

Уточнить с помощью rCTE

Особенно, если вы не уверены в пробелах и оценках.

WITH RECURSIVE random_pick AS (
   SELECT *
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   generate_series(1, 1030)  -- 1000 + few percent - adapt to your needs
      LIMIT  1030                      -- hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss

   UNION                               -- eliminate dupe
   SELECT b.*
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   random_pick r             -- plus 3 percent - adapt to your needs
      LIMIT  999                       -- less than 1000, hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

Мы можем работать с меньшим излишком в базовом запросе.Если пропусков слишком много и мы не находим достаточно строк в первой итерации, rCTE продолжает выполнять итерацию с рекурсивным членом.Нам все еще нужно относительно несколько пробелов в пространстве идентификаторов, иначе рекурсия может иссякнуть до того, как будет достигнут предел - или нам придется начинать с достаточно большого буфера, который не отвечает цели оптимизации производительности.

Дубликаты удаляются с помощью UNION в rCTE.

Внешний LIMIT останавливает CTE, как только у нас появляется достаточно строк.

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

Перенос в функцию

Для повторного использования с различными параметрами:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big AS
$func$
DECLARE
   _surplus  int := _limit * _gaps;
   _estimate int := (           -- get current estimate from system
      SELECT c.reltuples * _gaps
      FROM   pg_class c
      WHERE  c.oid = 'big'::regclass);
BEGIN

   RETURN QUERY
   WITH RECURSIVE random_pick AS (
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   generate_series(1, _surplus) g
         LIMIT  _surplus           -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses

      UNION                        -- eliminate dupes
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   random_pick        -- just to make it recursive
         LIMIT  _limit             -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses
   )
   SELECT *
   FROM   random_pick
   LIMIT  _limit;
END
$func$  LANGUAGE plpgsql VOLATILE ROWS 1000;

Вызов:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

Вы могли бы даже заставить этот универсал работать для любой таблицы: возьмите имя столбца PK и таблицы как полиморфный тип и используйте EXECUTE ... Но это выходит за рамки этого вопроса.См .:

Возможная альтернатива

ЕСЛИ ваштребования допускают идентичные наборы для повторных вызовов (а мы говорим о повторных вызовах). Я бы рассмотрел материализованное представление .Выполните вышеуказанный запрос один раз и запишите результат в таблицу.Пользователи получают квази-случайный выбор со скоростью молнии.Обновите ваш случайный выбор через интервалы или события по вашему выбору.

Postgres 9.5 вводит TABLESAMPLE SYSTEM (n)

Где n в процентах. Руководство:

Каждый из методов выборки BERNOULLI и SYSTEM принимает один аргумент, представляющий собой долю таблицы в выборке, выраженную в процентах от 0 до 100 .Этот аргумент может быть любым real -значным выражением.

Жирный акцент мой.Это очень быстро , но результат не совсем случайный .Руководство снова:

Метод SYSTEM значительно быстрее, чем метод BERNOULLI если указан небольшой процент выборки, но он может вернуть Менее случайная выборка таблицы в результате эффектов кластеризации.

Количество возвращаемых строк может сильно отличаться. Для нашего примера, чтобы получить примерно 1000 строк:

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

Связанный:

Или установите дополнительный модуль tsm_system_rows , чтобы получить точное количество запрошенных строк (если их достаточно) и учесть более удобный синтаксис:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Подробнее см. Ответ Эвана .

Но это все еще не совсем случайно.

88 голосов
/ 30 декабря 2011

Вы можете проверить и сравнить план выполнения обоих с помощью

EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;

Быстрый тест на большом столе 1 показывает, что ORDER BY сначала сортирует полную таблицу, а затем выбирает первые 1000 элементов. Сортировка большой таблицы не только читает эту таблицу, но также включает чтение и запись временных файлов. where random() < 0.1 сканирует всю таблицу только один раз.

Для больших таблиц это может быть не то, что вам нужно, так как даже одно полное сканирование таблицы может занять много времени.

Третье предложение будет

select * from table where random() < 0.01 limit 1000;

Эта операция останавливает сканирование таблицы, как только будет найдено 1000 строк, и поэтому возвращается быстрее. Конечно, это немного сбивает с толку случайность, но, возможно, это достаточно хорошо в вашем случае.

Редактировать: Помимо этих соображений, вы можете проверить уже заданные вопросы для этого. Использование запроса [postgresql] random возвращает довольно много обращений.

И связанная статья с изложением еще нескольких подходов:


1 «большой», как в «полная таблица не помещается в память».

69 голосов
/ 22 января 2013

порядок postgresql случайным образом (), выберите строки в случайном порядке:

select your_columns from your_table ORDER BY random()

порядок postgresql случайно () с отличным:

select * from 
  (select distinct your_columns from your_table) table_alias
ORDER BY random()

порядок postgresql по случайному пределу в одну строку:

select your_columns from your_table ORDER BY random() limit 1
35 голосов
/ 25 января 2016

Начиная с PostgreSQL 9.5, появился новый синтаксис, предназначенный для получения случайных элементов из таблицы:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

Этот пример даст вам 5% элементов из mytable.

Больше объяснений смотрите в этом посте: http://www.postgresql.org/docs/current/static/sql-select.html

26 голосов
/ 30 декабря 2011

Тот, у которого ORDER BY, будет медленнее.

select * from table where random() < 0.01; идет запись за записью и решает отфильтровать ее случайным образом или нет. Это будет O(N), потому что нужно проверять каждую запись только один раз.

select * from table order by random() limit 1000; собирается отсортировать всю таблицу, а затем выбрать первую 1000. Помимо магии вуду за кулисами, порядок составляет O(N * log N).

Недостатком random() < 0.01 является то, что вы получите переменное количество выходных записей.


Обратите внимание, что существует лучший способ перестановки набора данных, чем случайная сортировка: Перестановка Фишера-Йейтса , которая выполняется в O(N). Однако реализация shuffle в SQL кажется довольно сложной задачей.

14 голосов
/ 01 июня 2017

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

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;
9 голосов
/ 27 декабря 2016
select * from table order by random() limit 1000;

Если вы знаете, сколько строк вы хотите, проверьте модуль tsm_system_rows.

tsm_system_rows

предоставляет метод выборки из таблицы SYSTEM_ROWS, который можно использовать в предложении TABLESAMPLE команды SELECT.

Этот метод выборки из таблицы принимает один целочисленный аргумент, который является максимальным числом строк для чтения.Полученный образец всегда будет содержать ровно столько строк, если в таблице недостаточно строк, и в этом случае будет выбрана вся таблица. Как и встроенный метод выборки SYSTEM, SYSTEM_ROWS выполняет выборку на уровне блоков, так что выборка не является полностью случайной, но может подвергаться эффектам кластеризации, особенно если запрашивается только небольшое количество строк.

Сначала установите расширение

CREATE EXTENSION tsm_system_rows;

Затем ваш запрос,

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
6 голосов
/ 12 сентября 2015

Если вам нужна только одна строка, вы можете использовать вычисленную offset, полученную из count.

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
2 голосов
/ 13 мая 2014

Возможна вариация материализованного представления "Возможная альтернатива" , изложенная Эрвином Брандштеттером .

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

Предполагая, что это входная таблица:

id_values  id  |   used
           ----+--------
           1   |   FALSE
           2   |   FALSE
           3   |   FALSE
           4   |   FALSE
           5   |   FALSE
           ...

Заполните ID_VALUES стол по мере необходимости.Затем, как описано Эрвином, создайте материализованное представление, которое рандомизирует таблицу ID_VALUES один раз:

CREATE MATERIALIZED VIEW id_values_randomized AS
  SELECT id
  FROM id_values
  ORDER BY random();

Обратите внимание, что материализованное представление не содержит используемый столбец, потому что это быстро станет устаревшим.Дата.Кроме того, представление не должно содержать другие столбцы, которые могут находиться в таблице id_values.

Чтобы получить (и «потребить») случайные значения, используйте UPDATE-RETURNING для id_values, выбрав id_values с id_values_randomized с объединением и применением желаемых критериев для получения только соответствующих возможностей.Например:

UPDATE id_values
SET used = TRUE
WHERE id_values.id IN 
  (SELECT i.id
    FROM id_values_randomized r INNER JOIN id_values i ON i.id = r.id
    WHERE (NOT i.used)
    LIMIT 5)
RETURNING id;

Измените LIMIT при необходимости - если вам нужно только одно случайное значение за раз, измените LIMIT на 1.

с соответствующими индексамина id_values я считаю, что ОБНОВЛЕНИЕ ОБНОВЛЕНИЯ должно выполняться очень быстро с небольшой нагрузкой.Возвращает рандомизированные значения с одним обращением к базе данныхКритерии для «приемлемых» строк могут быть настолько сложными, насколько это необходимо.Новые строки могут быть добавлены в таблицу id_values в любое время, и они станут доступны приложению, как только обновится материализованное представление (которое, вероятно, может быть запущено в непиковое время).Создание и обновление материализованного представления будет медленным, но его нужно выполнять только при добавлении новых идентификаторов в таблицу id_values.

0 голосов
/ 19 июня 2019

Один урок из моего опыта:

offset floor(random() * N) limit 1 не быстрее, чем order by random() limit 1.

Я думал, что подход offset будет быстрее, потому что онСледует сэкономить время сортировки в Postgres.Оказывается, это не так.

...