Учитывая ваши спецификации (плюс дополнительная информация в комментариях),
- У вас есть столбец с числовым идентификатором (целые числа) с небольшим (или умеренно небольшим) пробелом.
- Очевидно, нет или мало операций записи.
- Ваш столбец 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
... Но это выходит за рамки этого вопроса. Видеть:
Возможная альтернатива
Если ваши требования позволяют идентичные наборы для повторных вызовов (и мы говорим о повторных вызовах), я бы рассмотрел материализованное представление . Выполните вышеуказанный запрос один раз и запишите результат в таблицу. Пользователи получают квази-случайный выбор со скоростью молнии. Обновите ваш случайный выбор через интервалы или события по вашему выбору.
Где 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);
Смотрите ответ Эвана для деталей.
Но это все еще не совсем случайно.