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

211

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

select * from table where random() < 0.01;

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

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

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

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

  • 1
    Привет, Джек, спасибо за твой ответ, время выполнения медленнее, но я хотел бы знать, что отличается, если таковые имеются ...
  • 0
    Уххх ... пожалуйста. Итак, вы пытались сравнить различные подходы?
Показать ещё 5 комментариев
Теги:
performance
random

11 ответов

141
Лучший ответ

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

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

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

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

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

  • Присоедините 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 продолжает итерацию с помощью рекурсивного термина. Нам все еще требуется относительно небольшое количество пробелов в ID-пространстве, или рекурсия может затухать до достижения предела - или мы должны начать с достаточно большого буфера, который не соответствует цели оптимизации производительности.

Дубликаты устраняются 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)

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

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

И количество возвращаемых строк может сильно различаться. Для нашего примера, чтобы получить примерно 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);

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

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

  • 0
    Где определяется таблица т? Должно ли это г вместо т ?
  • 1
    @LucM: Здесь определено: JOIN bigtbl t , что сокращенно от JOIN bigtbl AS t . t это псевдоним таблицы для bigtbl . Его цель - сократить синтаксис, но в этом конкретном случае он не понадобится. Я упростил запрос в своем ответе и добавил простую версию.
Показать ещё 3 комментария
65

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

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 "большой", как в "полная таблица не помещается в память".

  • 1
    Хорошая мысль о написании временного файла для выполнения заказа. Это действительно большой успех. Я думаю, мы могли бы сделать random() < 0.02 и затем перетасовать этот список, а затем limit 1000 ! Сортировка будет дешевле на несколько тысяч рядов (смеется).
  • 0
    «Выбрать * из таблицы, где random () <0,05 предел 500;» это один из самых простых методов для postgresql. Мы использовали это в одном из наших проектов, где нам нужно было выбрать 5% результатов и не более 500 строк за раз для обработки.
54

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
18

Тот, у кого 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 звучит как вызов.

  • 0
    Получите объяснение :) Я увижу (только из любопытства), что рыбацкие йаты перемешивают ...
  • 2
    Нет никакой причины, по которой вы не можете добавить предел 1 в конец вашего первого примера. Единственная проблема в том, что существует вероятность того, что вы не получите никаких записей назад, поэтому вам придется учитывать это в своем коде.
16

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

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

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

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

  • 2
    Важное примечание из документа: «Метод SYSTEM выполняет выборку на уровне блоков, причем каждый блок имеет заданную вероятность выбора; возвращаются все строки в каждом выбранном блоке. Метод SYSTEM значительно быстрее, чем метод BERNOULLI, при небольших процентах выборки указаны, но он может вернуть менее случайную выборку таблицы из-за эффектов кластеризации. "
  • 0
    Есть ли способ указать количество строк вместо процента?
Показать ещё 2 комментария
3

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

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;
  • 0
    Вы какой-то гений человек!
  • 0
    Я думаю, что это решение работает как ORDER BY random() которое работает, но может быть неэффективным при работе с большой таблицей.
Показать ещё 1 комментарий
3

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

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
2
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);
  • 1
    Я добавил ссылку на ваш добавленный ответ, это заметное улучшение по сравнению со встроенным методом SYSTEM .
2

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

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

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

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.

  • 0
    очень интересно. Будет ли это работать, если мне нужно не только выбрать, но и обновить, используя select..for для обновления с pg_try_advisory_xact_lock? (т.е. мне нужно много одновременных операций чтения и записи)
  • 0
    Пожалуйста, объясните ваш downvote с комментарием.
1

После того как я получил вид в режиме мысли 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, но оно красивое и простое и, если учесть ограничения масштабирования, должно работать нормально. Надеюсь, он будет подходящим для кого-то в подобном положении.

0

Добавьте столбец с именем 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);

Код не требует пояснений. Подзапрос в середине используется для быстрого подсчета количества строк таблицы из https://stackoverflow.com/questions/7943233/fast-way-to-discover-the-row-count-of-a-table-in-postgresql.

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

  • 0
    Мне это нравится, потому что оно короткое и элегантное :) И я даже нашел способ улучшить его: EXPLAIN ANALYZE говорит мне, что подобным образом индекс PKEY не будет использоваться, потому что random () возвращает double, тогда как PKEY нужен BIGINT.
  • 0
    выберите * из YOUR_TABLE, где r> (выберите (выберите reltuples :: bigint AS, оцените из pg_class, где oid = 'public.YOUR_TABLE' :: regclass) * random ()) :: BIGINT порядок по r asc limit (1);

Ещё вопросы

Сообщество Overcoder
Наверх
Меню