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

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

select * from table where random() < 0.01;

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

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

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

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

Ответ 1

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

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

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

  • Наконец обрежьте излишки 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)

Где n - это процент Руководство:

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

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

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

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

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

Связанные с:

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

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Смотрите Эван ответ для деталей.

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

Ответ 2

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

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 возвращает довольно много обращений.

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


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

Ответ 3

postgresql order by random(), выберите строки в произвольном порядке:

select your_columns from your_table ORDER BY random()

postgresql order by 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

Ответ 4

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

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

В этом примере вы получите 5% элементов из mytable.

Подробнее об этом в блоге: http://www.postgresql.org/docs/current/static/sql-select.html

Ответ 5

Тот, у кого 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 является то, что вы получите переменное количество выходных записей.


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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

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

Ответ 9

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

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

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

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

Заполните таблицу ID_VALUES по мере необходимости. Затем, как описано Erwin, создайте материализованное представление, которое рандомизирует таблицу 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 from 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, я считаю, что UPDATE-RETURNING должен выполняться очень быстро с небольшой нагрузкой. Он возвращает рандомизированные значения с одной обратной связью базы данных. Критерии для "подходящих" строк могут быть такими же сложными, как требуется. Новые строки могут быть добавлены в таблицу ID_VALUES в любое время, и они станут доступными для приложения, как только обновится материализованное представление (которое, вероятно, может быть запущено в нерабочее время). Создание и обновление материализованного представления будет медленным, но его нужно выполнить только при добавлении нового идентификатора в таблицу ID_VALUES.

Ответ 10

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

Для моей цели я хочу выбрать определенное число (около 10-20) случайных элементов "из прошлого месяца" (подмножество, возможно, 300-400 элементов - число, которое не будет увеличиваться с увеличением активности сервера). Решение Mickaël почти соответствует законопроекту, но кажется, что вы не можете использовать TABLESAMPLE после предложения WHERE.

Здесь решение, к которому я пришел:

Во-первых, я выполнил простой запрос:

SELECT id FROM table WHERE "timestamp" > now()::Date - 30

Как только результаты были возвращены моей программе, я выбрал случайную выборку идентификаторов. Тогда я просто бежал:

SELECT * FROM table WHERE id IN (1,2,3) (где (1,2,3) - мой случайный выбор).

Я понимаю, что это не строго решение PostgreSQL, но оно красивое и простое и, если учесть ограничения масштабирования, должно работать нормально. Надеюсь, он будет подходящим для кого-то в подобном положении.

Ответ 11

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

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

Выберите строки с 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);

Код не требует пояснений. Подзапрос в середине используется для быстрого подсчета количества строк таблицы из fooobar.com/questions/30742/....

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

Ответ 12

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

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

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

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

Ответ 13

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

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

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