Выборка случайной взвешенной записи из БД

Косыгин Александр • 3 Декабря, 2009

Задача

Имеется таблица с некоторыми записями (пусть это будут баннеры, к примеру). Нужно выбрать из этой таблицы случайную запись.

И всё бы хорошо, но каждая запись имеет свой вес (weight) выраженный целым числом от 0 до 100. Чем больше этот вес, тем больше должна быть вероятность выборки данной записи. Случай weight=0 означает, что запись вообще не должна попадать в выборку.

Решение

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

В таблице заведём дополнительные поля left и right. left будет соответствовать левой координате отрезка на прямой, а right - соответственно правой. Пусть max равно максимальному значению right. Тогда мы можем взять случайную координату rand из промежутка [0,max). Отрезок который содержит в себе эту координату и будет искомым. При попадании rand на границу отрезка, правый отрезок отбрасывается.

Реализация

Я не буду расписывать по шагам как это сделать, а просто выкладываю работающие функции в том виде, в котором они лежат у меня в проекте. Знающие люди найдут в них всё, что надо.

Сохранение/изменение записи

$params - ассоциативный массив с сохраняемыми данными

function save($params){
 $id_bg = empty($params['id_bg']) ? 0 : intval($params['id_bg']);
 
 $params['bg_weight'] = (empty($params['bg_weight']) || $params['bg_weight']<0) ?
    0 : ($params['bg_weight']>100 ? 100 : $params['bg_weight']);
 // Проверим существование редактируемой записи
 if(!empty($id_bg) && isset($params['bg_weight'])){
   $res = mysql_query('SELECT bg_left,bg_weight FROM '.$this->bg_table.
     ' WHERE id_bg="'.$id_bg.'" LIMIT 1',$this->db->conn) or $this->db->Error();
   if($res){
     $sheet = mysql_fetch_assoc($res);
     mysql_free_result($res);
     $sheet['shift'] = intval($params['bg_weight'])-intval($sheet['bg_weight']);
     $params['bg_right'] = $sheet['bg_left']+$params['bg_weight'];
   }else{
     $id_bg = 0;
   }
 }
 
 // Если добавляем запись
 if(empty($id_bg)){
   $res = mysql_query('SELECT max(bg_right) FROM '.$this->bg_table,$this->db->conn)
      or $this->db->Error();
   if($res){
     $max = array_shift(mysql_fetch_assoc($res));
     mysql_free_result($res);
   }else{
     $max = 0;
   }
   $params['bg_left'] = $max;
   $params['bg_right'] = $max+intval($params['bg_weight']);
 }
 
 unset($params['id_bg']);
 $id_bg = $this->db->updateRecord($id_bg,'id_bg',$this->bg_table,$this->bg_params,$params);
 if(!empty($id_bg) && !empty($sheet['shift'])) 
   mysql_query('UPDATE '.$this->bg_table.' SET bg_left=bg_left+'.$sheet['shift'].
   ',bg_right=bg_right+'.$sheet['shift'].' WHERE bg_left>'.$sheet['bg_left'].
   ' OR (bg_left='.$sheet['bg_left'].' AND bg_right>bg_left AND id_bg<>'.$id_bg.')'
   ,$this->db->conn) or $this->db->Error();
 return $id_bg;
}

Удаление записи по id

function delete_by_id($id){
 if(empty($id) || !is_numeric($id)) return false;
 // Проверим существование удаляемой записи
 $res = mysql_query('SELECT bg_left,bg_weight FROM '.$this->bg_table.' WHERE id_bg="'.
           $id.'" LIMIT 1',$this->db->conn) or $this->db->Error();
 if(!$res) return false;
 $sheet = mysql_fetch_assoc($res);
 mysql_free_result($res);
 $shift = intval($sheet['bg_weight']);
 mysql_query('UPDATE '.$this->bg_table.' SET bg_left=bg_left-'.$shift.',bg_right=bg_right-'.
  $shift.' WHERE bg_left>'.$sheet['bg_left'],$this->db->conn) or $this->db->Error();
 return $this->db->Query('DELETE FROM '.$this->bg_table.' WHERE id_bg="'.$id.'"');
}

Выборка случайной записи

function get_random(){
 $res = mysql_query('SELECT max(bg_right) FROM '.$this->bg_table,$this->db->conn) 
     or $this->db->Error();
 if($res){
   $max = array_shift(mysql_fetch_assoc($res));
   mysql_free_result($res);
 }else{
   return false;
 }
 if($max==0) return false;
 $rand = rand(0,$max-1);
 return array_shift($this->db->Query('SELECT * FROM '.$this->bg_table.
            ' WHERE bg_left<='.$rand.' AND bg_right>'.$rand.' LIMIT 1'));
}

В функциях используются следующие не описанные в этой статье методы:

$this->db->UpdateRecord($id, $key_name, $table, $params, $values ) - обновление/создание записей в таблице.

$this->db->Query($query) - SQL запрос к БД. В случае SELECT возвращает массив со значениями.

Тэги: mysql, random

Share/Bookmark

Комментарии:

google@ kelionweb3.Дек.2009 • 10:12

Можно сделать намного проще, но с затратами на память (а если вспомнить теорию вероятностей, возможно сделать без них:)), создавая массив с элементами, которые повторяются "вес" раз.
У меня получилась следующая функция:
function getItem() {
//Выбираем вес и количество записей в таблице с таким весом
$res = mysql_query('SELECT weight, COUNT(*) count
FROM items
GROUP BY weight');
$values = array();
//Добавляем во временный массив вес count*weight раз
//Для увеличения вероятности выбора данного веса функцией rand()
while ($arr = mysql_fetch_assoc($res)) {
for ($i=0;$i<$arr['count']*$arr['weight'];$i++) {
$values[] = $arr['weight'];
}
}
//Выбираем случайный вес из сгенерированного массива
$weight = $values[rand(0,count($values)-1)];
unset($values);
//Выбираем случайную запись из таблицы с соответствующим весом
$res = mysql_query('SELECT * FROM items
WHERE weight = '.$weight.'
ORDER BY RAND()
LIMIT 0,1');
$arr = mysql_fetch_assoc($res);
return $weight;
}
Для тестов создал таблицу items:
+-----+--------+
| id | weight |
+-----+--------+
| 1 | 0 |
| 2 | 1 |
| 3 | 2 |
| 4 | 2 |
| 5 | 2 |
| 6 | 2 |
| 7 | 5 |
| 8 | 5 |
+-----+--------+
Для нее элемент с весом 5 должен быть выбран в 10 случаях из 19, (2 — 8 из 19, 1 — 1 из 10, 0 — 0 раз).
Для 19000 тестов получил результат:
5: 10065 из 19000
2: 7952 из 19000
1: 983 из 19000
Похоже на правду:)
В моем примере вспомогательный массив имел всего 19 элементов, а если в таблице будут веса порядка 100, причем их будет несколько, массив будет намнооого больше. Решения проблемы я сходу не придумал

google@ kelionweb3.Дек.2009 • 10:12

Как удалить комментарий?:)
В выводе не учитываеются переносы строк, я написал про свой метод в ЖЖ: http://kelionj.livejournal.com/2199.html

Косыгин Александр4.Дек.2009 • 07:12

Теперь учитывается.

livejournal@ k0ldbl00d29.Мар.2011 • 04:03

Делал однажды "мозги" для RC-машинки. Суть автоматизации была в таблице, содержащей вероятности перехода из предыдущего состояния в следующее. Чтобы машина, двигаясь вперед, не начала движение назад, например. Поскольку микроконтроллеры снабжаются малым количеством памяти, вариант с "длинным массивом" отпал сразу. Сделал почти так же как и у вас - методом "отрезков". Известна сумма всех "вероятностей" - 100. Выбрав произвольное число от 1 до 100, а потом прибавляя ко временной переменной значения всех отрезков и контролируя знак разности переменных можно найти нужный "отрезок".

Добавление комментария

Как вас зовут:
E-mail:

или

Комментарии перед публикацией просматриваются и одобряются (или не одобряются), спасибо за понимание.



Напишите нам сейчас

Как вас зовут

Ваши контакты

Ваше сообщение

обзоры сайтов (15), #404fest (11), конференции (11), создание сайтов (8), социальное продвижение (7), лекции (6), семинары (6), а люди не знают (5), полезное (5), маркетинг (5), umi (5), еда (5), технологии (5), дизайн (4), пятница (4), seo (3), иллюстрации (3), образование (2), почта (2), новый год (2), книги (2), Яндекс (2), манифест (1),  (1), эконки (1), mysql (1), картинки (1), алмаз (1), random (1), Встреча дизайнеров (1), вручение призов (1), конкурс (1), gmail (1), softool (1), mail.ru (1), продвижение сайтов (1), музыка (1), ортопринт (1), неожиданно (1), редкие минуты счастья (1), конкурсы и призы (1), сайты (1), tumblr (1), контекст (1), коммуникации (1), бесплатно (1), продвижение (1), семинар (1), picasa (1), faq (1), интервью (1), генерация (1), программирование (1), автомобили (1), копирайтинг (1), Яндекс фотки (1), Hyundai (1), реагенты (1), google (1), интернет в Самаре (1), обеды (1), art in samara (1), Илья негодует (1), ересь (1), реклама (1), дядюшка Гудвин рассказывает (1)

Москва:

(495) 797-26-42

Самара:

(846) 222-92-14
(846) 222-92-13

info@blackbox.ru