Постгрес. Выборка N Случайных Записей

Во время работы над одним проектом возникла необходимость написать какую-то тестовую систему.

Задача была сформулирована примерно так:

  • из N записей в базе данных необходимо выбрать m (3-5) случайных строк в серии из k выборок (чаще всего k=2).

А теперь то же самое на человеческом языке: нужно дважды выбрать из таблицы 3-5 случайных записей.

Дубликатов быть не должно, выборка должна быть случайной.

Первое, что приходит на ум:

  
  
  
  
  
  
  
  
  
  
  
  
   

SELECT * FROM data_set WHERE id NOT IN (1,2,3,4, 5) ORDER BY random() LIMIT 5;

И это даже сработает. Вот только цена такого решения.

Поэтому мне пришлось использовать «высший разум» , который выпустил намек на решение .



WITH RECURSIVE r AS ( WITH b AS (SELECT min(id), max(id) FROM table1) ( SELECT id, min, max, array[]::integer[] AS a, 0 AS n FROM table1, b WHERE id > min + (max - min) * random() LIMIT 1 ) UNION ALL ( SELECT t.id, min, max, a || t.id, r.n + 1 AS n FROM table1 AS t, r WHERE t.id > min + (max - min) * random() AND t.id <> all(a) AND r.n + 1 < 10 LIMIT 1 ) ) SELECT t.id FROM table1 AS t, r WHERE r.id = t.id;

Но это решение.

«немного» непонятно.

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

Что.

Логика в общих чертах стала более-менее понятной: n раз мы выбираем по одной строке со случайным смещением от минимального значения ID (первичного ключа).

Таким образом, запрос затрагивает максимум N строк вместо полного перечисления содержимого таблицы.

Но «на бумаге все было гладко».

Когда я попытался использовать его «как есть» (с поправкой на имена таблиц/полей), всплыли «сюрпризы»:

  1. Массив в строке 4 создается пустым.

    Из-за этого в окончательной выборке могут быть дубликаты.

    Допустим, запрос в строках 4–7 возвращает id==5. После этого он обрабатывает запрос в СОЮЗ блок (строки 9-15) и на каком-то этапе возвращает тот же id==5, так как предыдущее значение id не было включено в массив " А " и проверка в строке 13 "t.id <> все(а)» прошло успешно;

  2. Количество выходных значений было не более заданного (п.

    14).

    Но легко меньше, чем это.

    Даже если это количество было гарантировано в таблице.

    Иногда возвращалась пустая выборка (количество значений «0»).

С первой «особенностью» справиться оказалось относительно легко — достаточно было немного изменить строку с инициализацией массива.

Как это:

SELECT id, min, max, array[]::integer[] || id AS a, 0 AS n

Но второй момент заставил меня задуматься дважды.

Подвох был обнаружен в самом «сердце» алгоритма — выборке записи из диапазона.

Действительно, рекурсивный запрос пытается выбрать строку, для которой условие будет выполнено:

id > min + (max - min) * random()

Но в случае, когда случайный() возвращает «1», это условие преобразуется в:

id > max

Естественно, в этом случае запрос ничего не найдет и перестанет работать.

И если это произойдет при первом проходе запроса, то «выход» будет пустым.

Даже если в базе данных явно есть необходимые записи.

Первым моим побуждением было немного изменить условие и довести его примерно до такого:

id >= min + (max - min) * random()

Это, так сказать, решение позволило нам получить в выводе хотя бы одну строку.

Но это вовсе не гарантировало, что в результате будет получено заданное количество строк.

Мне пришлось думать дальше.

После долгих раздумий, многих попыток и литров стимулятор был рожден это вариант: запрашиваемый код

WITH RECURSIVE r AS ( WITH b AS ( SELECT min(t.id), ( SELECT t.id FROM table1 AS t ORDER BY t.id DESC LIMIT 1 OFFSET 9 ) max FROM table1 AS t ) ( SELECT id, min, max, array[]::integer[] || id AS a, 0 AS n FROM table1 AS t, b WHERE id >= min + ((max - min) * random())::int LIMIT 1 ) UNION ALL ( SELECT t.id, min, max, a || t.id, r.n + 1 AS n FROM {table} AS t, r WHERE t.id >= min + ((max - min) * random())::int AND t.id <> all(a) AND r.n + 1 < 10 LIMIT 1 ) ) SELECT t.* FROM table1 AS t, r WHERE r.id = t.id

Вся соль в строках 5-11. Те.

чтобы гарантировать, что на «выходе» будет N элементов, нужно исходить из наихудшего возможного сценария.

В данном случае это означает, что случайное N раз подряд вернет 1. Следовательно, вам необходимо знать/иметь N-1 значений до макс.

Как этого добиться в запросе? Альтернативный вариант — отсортировать все записи по идентификатору в порядке убывания и сдвинуть «вниз» на N строк.

А поскольку в строках 19 и 25 используется «> =", смещение можно указать на единицу меньше (N-1).

Это хорошо – основные трудности решены, а «мелочи» остались: запрос в нынешнем виде малопригоден.

Необходимо делать выбор с учетом определенных условий.

В простейшем случае исключить из выборки идентификаторы записей, выбранных на предыдущем этапе.

Кроме того, мы не можем исключить возможность использования каких-то дополнительных условий, налагаемых на строки таблицы (is_active = true, is_deleted=false, .

).

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

Что-то вроде следующего шаблона: код шаблона

WITH RECURSIVE r AS ( WITH b AS ( SELECT min(t.{pk}), ( SELECT t.{pk} FROM {table} AS t WHERE {exclude} t.is_active ORDER BY t.{pk} DESC LIMIT 1 OFFSET {limit} - 1 ) max FROM {table} AS t WHERE {exclude} t.is_active ) ( SELECT t.{pk}, min, max, array[]::integer[] || t.{pk} AS a, 0 AS n FROM {table} AS t, b WHERE t.{pk} >= min + ((max - min) * random())::int AND {exclude} t.is_active LIMIT 1 ) UNION ALL ( SELECT t.{pk}, min, max, a || t.{pk}, r.n + 1 AS n FROM {table} AS t, r WHERE t.{pk} >= min + ((max - min) * random())::int AND t.{pk} <> all(a) AND r.n + 1 < {limit} AND {exclude} t.is_active LIMIT 1 ) ) SELECT {fields} FROM {table} AS t, r WHERE r.{pk} = t.{pk}

Здесь в фигурных скобках указаны именованные параметры, подлежащие замене:

  • {стол} — имя таблицы;
  • {пк} — имя поля PrimaryKey;
  • {поля} — список полей для выбора (также можно указать «*»);
  • {исключать} — условие (совокупность условий) исключения записей из выборки.

    Например, «t.id NOT IN (1,2,3,4)»;

  • {предел} — количество записей в итоговой выборке
И, наконец, последний в списке, но не менее важный вопрос: «стоит ли игра свеч»? Какая «профит» от этой умопомрачительной конструкции? Мы проверим.

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



DROP TABLE IF EXISTS ttbl; CREATE TABLE IF NOT EXISTS ttbl ( id serial NOT NULL, is_active BOOL NOT NULL DEFAULT true, CONSTRAINT ttbl_pkey PRIMARY KEY (id) ) WITH (OIDS=FALSE);

Теперь давайте наполним его данными.

При этом необходимо, чтобы значения id приходили не последовательно, а имели «дыры».

Те.

не «1, 2, 3, 4, 5.», а хотя бы «1, 4, 5, 8.».

Для этого набросаем простой скрипт. код сценария

import random import psycopg2 DB_TABLE = 'ttbl' PK_NAME = 'id' DATABASES = { 'NAME': 'test_db', 'USER': 'user_test', 'PASSWORD': 'R#Q9Jw*ZEKWOhBX+EP|3/xGkQn3', 'HOST': 'localhost', 'PORT': '', } TOTAL = 10000000 current = 0 step = 10000 dev = 8 while current <= TOTAL: data = set() for x in range(current, current+step, 10): data.add(x + int(random.random() * dev)) x = cur.execute( "INSERT INTO {t_table} VALUES {t_items};".

format( t_table=DB_TABLE, t_items='(' + '), ('.

join(map(str, data)) + ')' ) ) current += step print(x, current) cur.execute('COMMIT;')

Как видно из кода, каждый запрос вставляет сто записей.

Значения изменяются с шагом примерно «10».

Примерно, т.к.

каждое из значений может отклоняться на случайную величину в диапазоне 0-dev. Те.

при двух последовательных значениях x «340» и «350» любые значения из диапазона 340-348 и 350-358 (342, 347, 340.; 351, 355, 358.) могут вставить в таблицу.

Сумма в таблице получилась

select count(id) from ttbl;

1001000 записей Вполне приличная сумма.

Попробуем сделать выбор.

Понятно, что единичная проба не является показателем.

Поэтому мы выполним серию последовательных пусков.

Точнее, серия из 5 прогонов запросов каждого типа.

Сведем результаты в таблицу и посчитаем среднее значение.

Сначала простой запрос

select t.* from ttbl t where t.id not in (1, 3, 10, 89, 99, 22, 24, 25, 28, 30) AND t.is_active order by random() limit 5;

Полученные результаты:

Нет. время, мс
1 697
2 605
3 624
4 593
5 611
Как видите, среднее время выполнения запроса составляет около * 600 мс.

И вот – «монстр».

наблюдай за монстром

WITH RECURSIVE r AS ( WITH b AS ( SELECT min(t.id), ( SELECT t.id FROM ttbl AS t WHERE t.id NOT IN (1, 3, 10, 89, 99, 22, 24, 25, 28, 30) AND t.is_active ORDER BY t.id DESC LIMIT 1 OFFSET 5 - 1 ) max FROM ttbl AS t WHERE t.id NOT IN (1, 3, 10, 89, 99, 22, 24, 25, 28, 30) AND t.is_active ) ( SELECT id, min, max, array[]::integer[] || id AS a, 0 AS n FROM ttbl AS t, b WHERE id >= min + ((max - min) * random())::int AND t.id NOT IN (1, 3, 10, 89, 99, 22, 24, 25, 28, 30) AND t.is_active LIMIT 1 ) UNION ALL ( SELECT t.id, min, max, a || t.id, r.n + 1 AS n FROM ttbl AS t, r WHERE t.id > min + ((max - min) * random())::int AND t.id <> all( a ) AND r.n + 1 < 5 AND t.id NOT IN (1, 3, 10, 89, 99, 22, 24, 25, 28, 30) AND t.is_active LIMIT 1 ) ) SELECT t.* FROM ttbl AS t, r WHERE r.id = t.id

Полученные результаты:

Нет. время, мс
1 15
2 17
3 8
4 12
5 12
Среднее время ок.

* 15 мс.

В общей сложности разница составляет около полутора порядков (в 40-50 раз).

Оно того стоило.

Запросы проверялись, в том числе: и при «холодном» старте (после перезапуска машины/демона).

И хотя время выполнения в абсолютном выражении изменилось, соотношение осталось постоянным (насколько это возможно).

Те.

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



Примечания

*Примерно, поскольку точное значение не имеет значения из-за отклонений, вызванных кешем Postgres, влиянием операционной системы, оборудования, другого программного обеспечения и т. д. Теги: #postgres #случайная выборка #рекурсивные запросы #Высокая производительность #postgresql #программирование
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.