Найкращий спосіб вибору випадкових рядків PostgreSQL


345

Я хочу випадковий вибір рядків у PostgreSQL, я спробував це:

select * from table where random() < 0.01;

Але деякі інші рекомендують це:

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

У мене дуже велика таблиця з 500 мільйонами рядків, я хочу, щоб вона була швидкою.

Який підхід кращий? Які відмінності? Який найкращий спосіб вибрати випадкові рядки?


1
Привіт Джеку, дякую за вашу відповідь, час виконання відбувається повільніше, але я хотів би дізнатися, що є різним, якщо вони є ...
nanounanue

А-а-а ... ласкаво просимо. Отже, ви спробували зіставити різні підходи?

Є й набагато швидші способи. Все залежить від ваших вимог і з чим ви маєте працювати. Вам потрібно рівно 1000 рядів? Чи має таблиця числовий ідентифікатор? Без / мало / багато прогалин? Наскільки важлива швидкість? Скільки запитів за одиницю часу? Чи потребує кожного запиту інший набір чи вони можуть бути однаковими протягом певного часового відрізка?
Ервін Брандстетер

6
Перший варіант "(випадковий () <0,01)" є математично неправильним, оскільки ви не можете отримати рядків у відповідь, якщо жодне випадкове число не перевищує 0,01, це може статися в будь-якому випадку (хоча і менш вірогідне), незалежно від того, наскільки велика є таблиця або вище поріг. Другий варіант завжди правий
Герме

1
Якщо ви хочете вибрати тільки один рядок, побачити це питання: stackoverflow.com/q/5297396/247696
Флімм

Відповіді:


230

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

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

Запит нижче не потребує послідовного сканування великої таблиці, лише індексного сканування.

Спочатку отримайте оцінки основного запиту:

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

  • Приєднуйтесь до 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);

Ви навіть можете зробити це загальним для роботи для будь-якої таблиці: Візьміть назву стовпця ПК та таблиці як поліморфний тип та використовуйте EXECUTE… Але це виходить за межі цього питання. Подивитися:

Можлива альтернатива

Якби ваші вимоги дозволяють ідентичні набори для повторних дзвінків (а ми говоримо про повторні дзвінки), я б вважав матеріалізованим видом . Виконайте над запитом один раз і запишіть результат у таблицю. Користувачі отримують квазі випадковий вибір на світловій швидкості. Оновіть свій випадковий вибір через проміжки часу або події на ваш вибір.

Постгрес 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);

Див відповідь Евана для деталей.

Але це все ще не зовсім випадково.


Де визначена таблиця t ? Якщо це г замість т ?
Люк М

1
@LucM: Тут визначено:, JOIN bigtbl tщо скорочено JOIN bigtbl AS t. tце псевдонім таблиці для bigtbl. Її мета - скоротити синтаксис, але це не буде потрібно в цьому конкретному випадку. Я спростив запит у своїй відповіді та додав просту версію.
Ервін Брандстеттер

Яке призначення діапазону значень від generator_series (1,1100)?
Awesome-o

@ Awesome-o: Мета - отримати 1000 рядків, я починаю з додаткових 10%, щоб компенсувати кілька прогалин або (навряд чи можливо) дублювати випадкові числа ... пояснення є у моїй відповіді.
Ервін Брандстеттер

Ервіне, я розмістив варіант вашої "Можливої ​​альтернативи": stackoverflow.com/a/23634212/430128 . Були зацікавлені у ваших думках.
Раман

100

Ви можете вивчити та порівняти план виконання обох, скориставшись

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


1
Хороший момент щодо написання тимчасового файлу для здійснення замовлення. Це справді великий хіт. Я думаю, що ми могли б зробити, random() < 0.02а потім перетасувати цей список limit 1000! Сорт буде менш дорогим на кілька тисяч рядів (lol).
Дональд Майнер

"Вибрати * з таблиці, де випадковий () <0,05 межа 500;" є одним з найпростіших методів для postgresql. Ми використали це в одному з наших проектів, де нам потрібно було вибрати 5% результатів і не більше 500 рядків одночасно для обробки.
tgharold

Чому б у світі ви коли-небудь розглядали повне сканування O (n) для отримання зразка на 500-метровій таблиці рядків? Це смішно повільно на великих столах і зовсім непотрібне.
мафу

76

Порядок 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

1
select your_columns from your_table ORDER BY random() limit 1займіть ~ 2 хвилини для виконання на 45mil рядках
nguyên

чи є спосіб прискорити це?
CpILL

43

Починаючи з PostgreSQL 9.5, є новий синтаксис, призначений для отримання випадкових елементів з таблиці:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

Цей приклад дасть вам 5% елементів з mytable.

Дивіться більше пояснень у цьому дописі в блозі: http://www.postgresql.org/docs/current/static/sql-select.html


3
Важливе зауваження в документах: "Метод SYSTEM робить вибірку на рівні блоку з кожним блоком, що має вказаний шанс бути вибраним; всі рядки у кожному вибраному блоці повертаються. Метод SYSTEM значно швидше, ніж метод BERNOULLI, коли невеликі відсоткові вибірки вказані, але він може повернути менш випадкову вибірку таблиці в результаті ефектів кластеризації. "
Тім

1
Чи можна вказати кількість рядків замість відсотка?
Флейм

4
Ви можете використовувати TABLESAMPLE SYSTEM_ROWS(400)для отримання вибірки з 400 випадкових рядків. Щоб використовувати цей оператор, потрібно ввімкнути вбудоване tsm_system_rowsрозширення .
Мікаель Ле Бейліф

27

Той, з ЗАМОВЛЕННЯМИ, буде повільнішим.

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). Однак реалізація перетасовки в SQL звучить як досить складна проблема.


3
Немає жодної причини, хоча ви не можете додати ліміт 1 до кінця першого прикладу. Єдина проблема полягає в тому, що є потенціал, що ви не отримаєте записів назад, тому вам доведеться врахувати це у своєму коді.
Вимовна

Проблема з Fisher-Yates полягає в тому, що вам потрібно мати весь набір даних у пам'яті, щоб вибрати з нього.
Неможливо

16

Ось рішення, яке працює для мене. Я думаю, це зрозуміти і виконати дуже просто.

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

6
Я думаю, що це рішення працює як таке, ORDER BY random()що працює, але може бути неефективним при роботі з великим столом.
Ань Чао

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

2
Я додав посилання на вашу додаткову відповідь, це помітне поліпшення порівняно з вбудованим SYSTEMметодом.
Ервін Брандстетер

Я просто відповів на питання тут (випадкова одиночна запис) , в протягом якого я виконав значний бенчмаркінг і тестування з tsm_system_rowsі tsm_system_timeрозширень. Наскільки я бачу, вони практично марні для нічого, але абсолютно мінімального підбору випадкових рядків. Буду вдячний, якщо ви можете швидко поглянути і прокоментувати обгрунтованість чи інше мого аналізу.
Vérace

6

Якщо ви хочете лише один рядок, ви можете використовувати обчислений offsetпохідний показник count.

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

2

Можлива варіація матеріалізованого виду "Можлива альтернатива", викладена Ервіном Брандстеттером .

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

Припустимо, що це таблиця введення:

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таблиці.

Для того щоб отримати (і «споживати») випадкові значення, використовувати оновлену-Повернувшись 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таблиці будуть додані до таблиці.


дуже цікаво. Чи буде це працювати, якщо мені потрібно не тільки вибрати, але й оновити, використовуючи select..для оновлення за допомогою pg_try_advisory_xact_lock? (тобто мені потрібно багато одночасних читань І пише)
Матьє

1

Один урок з мого досвіду:

offset floor(random() * N) limit 1не швидше, ніж order by random() limit 1.

Я думав, що offsetпідхід буде швидшим, тому що це має заощадити час на сортування в Postgres. Виявляється, це було не так.


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/a/7945274/1271094 .

На рівні програми вам потрібно виконати операцію ще раз, якщо n> кількість рядків або потрібно вибрати кілька рядків.


Мені це подобається, тому що він короткий і елегантний :) І я навіть знайшов спосіб його вдосконалити: ПОЯСНУЙТЕ АНАЛІЗ говорить мені, що так, індекс PKEY не буде використовуватися, тому що random () повертає подвійний, тоді як PKEY потребує BIGINT.
fxtentacle

виберіть * з YOUR_TABLE, де r> (виберіть (виберіть reltuples :: bigint AS оцінка з pg_class where oid = 'public.YOUR_TABLE' :: regclass) * random ()) :: BIGINT порядку по r asc limit (1);
fxtentacle

0

Я знаю, що я трохи спізнююся на вечірку, але я просто знайшов цей дивовижний інструмент під назвою pg_sample :

pg_sample - витягнути невеликий зразок набору даних із більшої бази даних PostgreSQL, зберігаючи референтну цілісність.

Я спробував це в базі даних 350M рядків, і це було дуже швидко, не знаю про випадковість .

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.