Лучший способ выбрать случайные строки PostgreSQL

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

select * from table where random() < 0.01;

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

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

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

какой подход лучше? В чем разница? Каков наилучший способ выбора случайных строк?

11 ответов


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

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

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

во-первых, получить оценки для основных запрос:

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 можно выбрать несколько раз случайно (хотя очень маловероятно с большим id-пространством), поэтому сгруппируйте сгенерированные числа (или используйте DISTINCT).

  • стать ids к большому столу. Это должно быть очень быстро с индексом.

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

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

вы можете упростить этот запрос. 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)

это очень быстро, но результат не совсем random. инструкции:

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

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

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

здесь n - это процент. Инструкция:

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

жирным выделено мной.

по теме:

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

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

посмотреть ответ Эвана для сведения.

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


вы можете изучить и сравнить план выполнения с помощью

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

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

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

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

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

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

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

и связанная статья depez, описывающая еще несколько подходов:


1 "большой", как в "полная таблица не будет вписываться в память".


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

select your_columns from your_table ORDER BY random()

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

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

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

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

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

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


С заказа будет медленнее.

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 звучит как довольно сложная задача.


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

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;

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

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

tsm_system_rows

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

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

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

CREATE EXTENSION tsm_system_rows;

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

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);

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

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));

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

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

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

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 on 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 to 1.

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


добавить столбец r С типом serial. Индекс r.

предположим, что у нас есть 200 000 строк, мы будем генерировать случайное число n, где 0 n

выбрать строки с r > n сортировать ASC и выберите самый маленький.

код:

select * from YOUR_TABLE 
where r > (
    select (
        select reltuples::bigint AS estimate
        from   pg_class
        where  oid = 'public.YOUR_TABLE'::regclass) * random()
    )
order by r asc limit(1);

код не требует пояснений. Подзапрос в середине используется для быстрой оценки количества строк таблицы из https://stackoverflow.com/a/7945274/1271094 .

на уровне приложения вам нужно выполнить инструкцию снова, если n > количество строк или необходимо выбрать несколько строк.


Я знаю, что немного опоздал на вечеринку, но я только что нашел этот удивительный инструмент под названием pg_sample:

pg_sample - извлечение небольшого образца набора данных из более крупной базы данных PostgreSQL при сохранении ссылочной целостности.

Я пробовал это с базой данных строк 350M, и это было очень быстро, не знаю о случайность.

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db