выберите 30 случайных строк, где сумма = x

У меня есть таблица

items
id int unsigned auto_increment primary key,
name varchar(255)
price DECIMAL(6,2)

Я хочу получить по меньшей мере 30 случайных предметов из этой таблицы, где общая сумма цены равна 500, какой лучший подход для этого?

Я видел это решение, которое выглядит аналогичным образом. MySQL Выберите 3 случайные строки, где сумма трех строк меньше значения

И мне интересно, есть ли другие решения, которые проще реализовать и/или повысить эффективность

Ответы

Ответ 1

Существует решение, если ваш список продуктов удовлетворяет следующему допущению:

У вас есть продукты по всем ценам от 0,00 до 500,00. например. 0,01, 0,02 и т.д. до 499,99. или, может быть, 0,05, 0,10 и т.д. до 499,95.

Алгоритм основан на следующем:

В наборе n положительных чисел, суммирующих до S, по крайней мере один из них будет меньше S, деленный на n (S/n)

В этом случае этапы:

  1. Выберите продукт произвольно, где цена <500/30. Получите его цену, скажем X.
  2. Выберите продукт произвольно, где цена <(500 - X)/29. Получите его цену, предположите Y.
  3. Выберите продукт в случайном порядке, где цена <(500 - X - Y)/28.

Повторите это 29 раз и получите 29 продуктов. Для последнего продукта выберите тот, где цена = оставшаяся цена. (или цена <= оставшаяся цена и порядок по цене по убыванию, и, надеюсь, вы можете приблизиться достаточно).

Для элементов таблицы:

Получите случайную максимальную цену продукта:

CREATE PROCEDURE getRandomProduct (IN maxPrice INT, OUT productId INT, productPrice DECIMAL(8,2))
BEGIN
   DECLARE productId INT;
   SET productId = 0;
       SELECT id, price INTO productId, productPrice
       FROM items
       WHERE price < maxPrice
       ORDER BY RAND()
       LIMIT 1;
END

Получите 29 случайных продуктов:

CREATE PROCEDURE get29products(OUT str, OUT remainingPrice DECIMAL(8,2))
BEGIN
  DECLARE x INT;
  DECLARE id INT;
  DECLARE price DECIMAL(8,2);
  SET x = 30;
  SET str = '';
  SET remainingPrice = 500.00;

  REPEAT
    CALL getRandomProduct(remainingPrice/x, @id, @price);
    SET str = CONCAT(str,',', @id);
    SET x = x - 1;
    SET remainingPrice = remainingPrice - @price;
    UNTIL x <= 1
  END REPEAT;
END

Вызовите процедуру:

CALL 'get29products'(@p0, @p1); SELECT @p0 AS 'str', @p1 AS 'remainingPrice';

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

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

Обратите внимание, что дубликаты продуктов разрешены. Чтобы избежать дублирования, вы можете расширить getRandomProduct с дополнительным параметром IN уже найденных продуктов и добавить условие NOT IN, чтобы исключить их.

Обновление. Вы можете преодолеть вышеуказанное ограничение, чтобы всегда находить коллекции, сумма которых равна 500, используя процесс cron, как описано во втором разделе ниже.

2-й раздел: Использование процесса cron

Основываясь на предложении @Михаила Зуковского, вы могли бы

  • создать таблицу для хранения найденных коллекций
  • определить процесс cron, который выполняет указанный алгоритм несколько раз (пример 10 раз), например. каждые 5 мин.
  • если найдена коллекция, соответствующая сумме, добавьте ее в новую таблицу

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

Даже при скорости совпадения 20%, процесс cron, который запускает алгоритм 10 раз каждые 5 минут в течение 24 часов, может содержать более 500 коллекций.

По моему мнению, использование процесса cron имеет следующие преимущества и недостатки:

преимущества

  • найти точные соответствия
  • нет процесса по запросу клиента
  • даже с низкой степенью соответствия, вы можете найти несколько коллекций

недостатки

  • если данные о ценах обновляются часто, у вас могут быть непоследовательные результаты, возможно, использование процесса cron не будет работать.
  • должны отбрасывать или фильтровать старые коллекции
  • он, вероятно, не будет случайным для каждого клиента, так как другой клиент, вероятно, увидит одну и ту же коллекцию.

Ответ 2

Самый близкий ответ, который я могу предоставить, - это

set @cnt = 0;
set @cursum = 0;
set @cntchanged = 0;
set @uqid = 1;
set @maxsumid = 1;
set @maxsum = 0;
select 
    t.id,
    t.name,
    t.cnt
from (
    select 
        id + 0 * if(@cnt = 30, (if(@cursum > @maxsum, (@maxsum := @cursum) + (@maxsumid := @uqid), 0)) + (@cnt := 0) + (@cursum := 0) + (@uqid := @uqid + 1), 0) id, 
        name,  
        @uqid uniq_id,
        @cursum := if(@cursum + price <= 500, @cursum + price + 0 * (@cntchanged := 1) + 0 * (@cnt := @cnt + 1), @cursum + 0 * (@cntchanged := 0)) as cursum, if(@cntchanged, @cnt, 0) as cnt  
    from (select id, name, price from items order by rand() limit 10000) as orig
) as t

where t.cnt > 0 and t.uniq_id = @maxsumid
;

Итак, как это работает? Сначала мы выбираем 10k случайно упорядоченных строк из элементов. После этого мы суммируем цены на предметы до тех пор, пока не получим 30 предметов с суммой менее 500. Когда мы найдем 30 пунктов, мы повторим процесс, пока не пройдем все 10k выбранных предметов. Найдя эти 30 предметов, мы сохраняем максимальную найденную сумму. Поэтому в конце мы выбираем 30 предметов с наибольшей суммой (что означает ближайший к цели 500). Не уверен, что то, что вы изначально хотели, но найти точную сумму 500 потребовало бы слишком много усилий на стороне БД.

Ответ 3

Если вы хотите быть эффективными, перестаньте тратить свое время и пойдите для возможного согласия. Создайте консольный скрипт, который делает то, что вы хотите выполнить любым необходимым образом, затем запустите этот скрипт в CRON или с каким-либо программным обеспечением планирования раз в то время.

Имея 100, 1000 посетителей, вы хотите, чтобы ваш запрос выполнялся каждый раз? Это время и ресурсы. Случайно упорядоченные запросы также не могут быть кэшированы СУБД. Идите для возможной согласованности: создайте таблицу для хранения записей и очистите их каждый раз, заблокируйте для записи, а затем загрузите новый набор каждые 5 минут.

По крайней мере, так я делаю это в сильно загруженных приложениях. В коде это вопрос простого запроса SELECT.

Ответ 4

В зависимости от средней цены и распределения цены вы можете попробовать что-то вроде этого:

  1. Случайно выберите несколько элементов меньше, чем вы хотите (например, 25). Повторите попытку, пока их общая сумма не станет меньше x.

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

Ответ 5

  1. сначала выберите все значения, где sum = 500
  2. использовать mysql_query

затем выполните следующий код

$arr = array();
$num = 0;
while($row = mysqli_fetch_array($result))
{
    array_push($arr,$row['id']);
}
$arr2= array();
while(count($arr2!=30)
{
    $cnt = random(0,count($arr));
    if(in_array($arr[$cnt],$arr2);
    {
        array_push($arr2,$arr[$cnt]);
    }
}
print_r($arr2);

здесь $ arr2 - необходимый массив

Ответ 6

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

SELECT 
    i1.id, 
    i2.id, 
    ..., 
    i30.id, 
    i1.price + i2.price + ... + i30.price
FROM items i1 
INNER JOIN items i2 ON i2.id NOT IN (i1.id)
...
INNER JOIN items i30 ON i30.id NOT IN (i1.id, i2.id, ..., i29.id)
ORDER BY ABS(x - (i1.price + i2.price + ... + i30.price))

Такая просьба может быть сгенерирована программой, чтобы избежать ошибок. Это почти шутка, потому что время O (n ^ 30) (общий https://en.wikipedia.org/wiki/Subset_sum_problem - NP полный, но если вы фиксируете размер подмножества, это не так.), но это возможно и может иметь смысл для предвычислений. Когда набор цен не изменится, используйте предварительно вычисленный набор цен и найдите случайные товары, которые имеют цены.

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

Я предлагаю другую возможность, без предположений от Jannes Botis. Принцип - это жадное "восхождение на холм" с некоторыми отступлениями, потому что жадный метод не подходит для каждой ситуации.

Прежде всего, резюме: возьмите в общей сложности 30 самых дешевых предметов, а затем проделайте как можно быстрее x (будьте жадными), заменив дешевые предметы дорогим; если вы занимаетесь х, то сделайте максимальный шаг назад и возобновите подъем, если вы не закончили или не устали.

И теперь детали (должны использовать PHP + MySQL, а не только MySQL):

Пусть N = 30

Шаг 1: инициализация

Сортировать позиции по возрастающей цене и выбрать первые N

  • Это общая цена x, вы закончили.
  • Если общая цена больше, чем x, сдайте: вы не можете получить общее количество равное x.
  • Else продолжают использовать N самых дешевых предметов.

С индексом B-дерева по ценам он должен быть быстрым

Шаг 2: подняться

Таким образом, x - total> 0, и мы хотим, чтобы разница была самой близкой к 0.

Выберите каждую пару элементов (с соединением), где:

  1. первый элемент i1 находится в N выбранных элементах
  2. второй элемент i2 не находится в N выбранных элементах,
  3. цена i1 больше, чем цена i2: p1 - p2> 0.
  4. (x - total) - (p1 - p2)> = 0

Закажите результат по возрастанию (x - total) - (p1 - p2).

  • Если нет соответствующей строки, есть два случая (так что, возможно, используйте два запроса, если вы разрешаете N расти):

    1. нет элементов, так что p1 - p2> 0: увеличьте N и добавьте элемент с самой низкой ценой. Если N == n, вы не можете достичь x, иначе перейдите к шагу 2.
    2. нет элементов, так что (x - total) - (p1 - p2)> = 0: вы достигнете предела x. Перейдите к шагу 3.
  • Else возьмите первую строку (ближайшую к пику) и замените i1 на i2 в элементах: новая сумма будет total - p1 + p2, а теперь x - total> = 0, и вы ближе к 0.

    • Если оно равно нулю, то мы закончили.
    • Повторите цикл до шага 2.

* Присоединение возьмет некоторые O (n): N элементов i1 * [(nN) элементов i2 минус единица с p2> p1] *

Шаг 3: отступление

Есть много способов отступить. Вот один.

  • Если вы только что отступили, сдайтесь: вы застряли.
  • Если вы уже отступили более чем n раз, или если вы достаточно близки к 0, вы можете отказаться. Это позволяет избежать бесконечных циклов.
  • Else: Удалите элемент с максимальной ценой в списке и замените его на элемент, которого нет в списке, с минимальной ценой (макс. И мин., Чтобы вы достаточно спустились). Затем обновите итоговое значение и вернитесь к шагу 2.

С индексом B-дерева по ценам он должен быть быстрым

Надеюсь, это ясно. Вы можете настроить его, чтобы решить, когда вы сделали достаточно, и использовать предварительно вычисленный набор из 30 предметов с общей ценой x. Я считаю, что временная сложность O (n) в среднем случае. Я сделал несколько тестов (python + sqlite) с 200 элементами, случайными ценами от 0 до 1000 и без отступления. На 1000 тестов 22 неудача достигает 5000 (0,44%), 708 успехов в 3 попытках, 139 успехов в 4 попытках, 126 успехов в 3 попытках, 4 успеха в 5 попытках и 1 успех в 1 попытке ("попытка" - это попробуйте набор элементов, отличный от 30 самых дешевых предметов: k try означает время запроса шага 2). Это будет зависеть от количества товаров, цен,...

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

Ответ 7

Если вы прочитали руководство по MySQL, вы, возможно, увидели ORDER BY RAND(), чтобы рандомизировать строки.

Этот пример работает отлично и быстро, если вы только, если скажете 1000 строк. Как только у вас будет 10000 строк, накладные расходы для сортировки строк становятся важными. Не забывайте: мы просто сортируем, чтобы выбросить почти все ряды.

Отличная почта, обрабатывающая несколько случаев, от простых до пробелов, до неравномерных с пробелами.

Вот как вы можете сделать это отлично:

SELECT id, name, price
 FROM 'items' AS i1 JOIN
    (SELECT CEIL(RAND() *
                 (SELECT MAX(id)
                    FROM 'items')) AS id) AS i2
 WHERE i1.id >= i2.id AND i1.price = 500
 ORDER BY i1.id ASC
LIMIT 30;