Выборка случайной взвешенной записи из БД
Задача
Имеется таблица с некоторыми записями (пусть это будут баннеры, к примеру). Нужно выбрать из этой таблицы случайную запись.
И всё бы хорошо, но каждая запись имеет свой вес (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 возвращает массив со значениями.
Комментарии:
Как удалить комментарий?:)
В выводе не учитываеются переносы строк, я написал про свой метод в ЖЖ: http://kelionj.livejournal.com/2199.html
Теперь учитывается.
Делал однажды "мозги" для RC-машинки. Суть автоматизации была в таблице, содержащей вероятности перехода из предыдущего состояния в следующее. Чтобы машина, двигаясь вперед, не начала движение назад, например. Поскольку микроконтроллеры снабжаются малым количеством памяти, вариант с "длинным массивом" отпал сразу. Сделал почти так же как и у вас - методом "отрезков". Известна сумма всех "вероятностей" - 100. Выбрав произвольное число от 1 до 100, а потом прибавляя ко временной переменной значения всех отрезков и контролируя знак разности переменных можно найти нужный "отрезок".
Добавление комментария
|
или |





google@ kelionweb • 3.Дек.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, причем их будет несколько, массив будет намнооого больше. Решения проблемы я сходу не придумал