Laravel по-русски

Русское сообщество разработки на PHP-фреймворке Laravel.

Ты не вошёл. Вход тут.

#1 15.11.2016 15:52:31

respectpick
Откуда: Москва
Сообщений: 83

Как выбрать одно случайное значение

есть текстовый инпут в который вводится любой текст, далее строка разбивается посимвольно. каждый символ имеет свой ID. например при вводе в инпут текста "фыв!"

входной массив $array_symbols_id выглядит так

array:4 [
  0 => "21"
  1 => "28"
  2 => "3"
  3 => "33"
]

далее таким образом из БД выбираются нужные изображения - соотвествующие этим символам

        $array_images  = DB::table('photo')
            ->whereIn('photo_symbol_id', $array_symbols_id)
            ->lists('photo_src', 'photo_symbol_id');

допустим у меня есть символ ! (восклицательный знак). Я загружаю в таблицу фото и устанавливаю связь, что это фото связано с символом ! имеющим ID 33.

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

Вопрос - что дописать в запрос что бы выборка была рандомной?

версия Laravel 5.0.35

Изменено respectpick (15.11.2016 15:52:47)

Не в сети

#2 15.11.2016 16:08:12

Re: Как выбрать одно случайное значение

Если я правильно тебя понял, то ты можешь использовать inRandomOrder()->first() или, если тебе нужен `lists()`, то take(1):

$array_images  = DB::table('photo')
            ->whereIn('photo_symbol_id', $array_symbols_id)
            ->inRandomOrder()
            ->take(1)
            ->pluck('photo_src', 'photo_symbol_id');

И еще, не используй lists(), он был давно deprecated и его убрали в 5.3. Используй pluck(), поможет в будущем легче проапгрейдить проект.

inRandomOrder() - это "шорткат" для ->orderByRaw('RANDOM()')

Изменено AlexeyMezenin (15.11.2016 19:18:31)

Не в сети

#3 15.11.2016 16:15:35

respectpick
Откуда: Москва
Сообщений: 83

Re: Как выбрать одно случайное значение

AlexeyMezenin пишет:

Если я правильно тебя понял, то ты можешь использовать inRandomOrder()->first() или, если тебе нужен `lists()`, то take(1):

$array_images  = DB::table('photo')
            ->whereIn('photo_symbol_id', $array_symbols_id)
            ->inRandomOrder()
            ->take(1)
            ->pluck('photo_src', 'photo_symbol_id');

И еще, не используй lists(), он был давно deprecated и его убрали в 5.3. Используй pluck(), поможет в будущем легче проапгрейдить проект.

при вызове inRandomOrder() получаю Call to undefined method Illuminate\Database\Query\Builder::inRandomOrder()

и версия laravel у меня 5.0.35

Не в сети

#4 15.11.2016 16:20:29

Re: Как выбрать одно случайное значение

respectpick пишет:

при вызове inRandomOrder() получаю Call to undefined method Illuminate\Database\Query\Builder::inRandomOrder()

и версия laravel у меня 5.0.35

Да, эта функция в 5.2 и выше. Тогда через что-то вроде orderByRaw('RAND()')->take(1)  делать.

Не в сети

#5 15.11.2016 16:26:06

respectpick
Откуда: Москва
Сообщений: 83

Re: Как выбрать одно случайное значение

AlexeyMezenin пишет:
respectpick пишет:

при вызове inRandomOrder() получаю Call to undefined method Illuminate\Database\Query\Builder::inRandomOrder()

и версия laravel у меня 5.0.35

Да, эта функция в 5.2 и выше. Тогда через что-то вроде orderByRaw('RAND()')->take(1)  делать.

а как это присоединить к моему запросу?

Не в сети

#6 15.11.2016 16:29:38

respectpick
Откуда: Москва
Сообщений: 83

Re: Как выбрать одно случайное значение

        $array_images  = DB::table('photo')
            ->whereIn('photo_symbol_id', $array_symbols_id)
            ->where('photo_moderation_id','2')
            ->orderByRaw('RAND()')
            ->lists('photo_src', 'photo_symbol_id');

вот так помогло. спасибо!

Не в сети

#7 15.11.2016 18:49:51

Re: Как выбрать одно случайное значение

  1. Тогда через что-то вроде orderByRaw('RAND()')->take(1) делать.

Если бы я был hzone, я бы сказал, что за такое увольняют.

Пока у тебя там 100 записей на все таблицы — это работает. А когда у тебя тысячи или миллионы (фотографий столько вполне может быть)… Что оно делает? Читает (full scan) всю таблицу от записи 0 до записи 1,000,000, что обычно приводит к созданию таблицы на диске в несколько гигов. И только потом возвращает, сколько там, одну запись?

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

Вместо этого в таблицу добавляется числовое поле, скажем random, и это поле cron’ом раз в час/сути/неделю обновляется на SET `random` = RAND(). Дальше любой запрос становится моментальным за счет PHPwhere('random'mt_rand()). Естественно, верхнюю границу для mt_rand/RAND нужно задать равным некоему числу, например, 10000 в зависимости от того, сколько обычно выбирается записей (сколько нужно записей в группе) и сколько их всего в таблице.

Другой вариант, без поля — это LIMIT RAND(), 1. Есть минусы, но я думаю даже в худшем случае (когда RAND попадает на COUNT — 1) он быстрее чем ORDER BY RAND за счёт отсутствия сортировки.

Не в сети

#8 15.11.2016 19:07:46

Re: Как выбрать одно случайное значение

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

А как тебе такое решение:

$total = $this->count();
$random = $mt_rand(1, $total) - 1;
$entity = $this->skip($random)->take(1)->first();

Не в сети

#9 15.11.2016 20:10:29

Re: Как выбрать одно случайное значение

  1. Proger_XP, при твоем решении в выборку не входят строки, добавленные за последний час/сутки/неделю.

Почему? Что мешает при вставке новой записи вставлять её со случайным значением в random?

  1. А как тебе такое решение:

Вариант со skip работает, но, как я написал, он медленнее:

  1. Скорость зависит от того, что выпало в виде $random, дальше — медленнее.
  2. Требуется 2 запроса (один для count), причём для MyISAM count подсчитывается моментально (число записей хранится в мета-данных каждой таблицы), а для InnoDB его вычисление занимает прилично времени (требует сканирования всех строк). MyISAM не следует использовать начиная с MySQL 5.6, так что недостаток существенный.

Плюс LIMIT — в том, что не нужно добавлять новое поле в таблицу, т.е. простота использования (условно «волшебная пуля» — заменил и работает). В общем-то других плюсов нет.

Не в сети

#10 15.11.2016 21:25:17

Re: Как выбрать одно случайное значение

Proger_XP пишет:

}%> Proger_XP, при твоем решении в выборку не входят строки, добавленные за последний час/сутки/неделю.
Почему? Что мешает при вставке новой записи вставлять её со случайным значением в random?

А как вставить здесь random? Все, что ты можешь здесь вставить - это количество строк в таблице (тот же count) плюс 1.

Вариант со skip работает, но, как я написал, он медленнее

"Медленнее" - не страшно, если там разница в пару мс. В среднем проекте полно вещей, которые несравнимо негативнее действуют на скорость. Вот твои слова про "создание таблицы на диске в несколько гигов", если это действительно так, это повод задуматься.

Плюс LIMIT - в том, что не нужно добавлять новое поле в таблицу, т.е. простота использования (условно "волшебная пуля" - заменил и работает). В общем-то других плюсов нет.

Подходы у нас разные видимо. Для меня читаемость кода - главное. Именно поэтому я использую фреймворк/ORM (а это приличный оверхед в плане ресурсов) и не использую вещи, усложняющие понимание кода (здравствуй, KISS). И именно поэтому не люблю код ребят "а смотрите как я еще умею", "сегодня я изобрел новый велосипед" и "этот хак работает на 1.2 мс быстрее". Другими словами, делаешь поддерживаемое приложение и уже потом смотришь на скорость. Если скорость можно решить железом (CPU, память) - сейчас это несравнимо дешевле, чем оплата работы разработчика, который будет оптимизировать код под скорость. Если нет, то можно уже посмотреть на самые ресурсоемкие запросы и править их.

Не в сети

#11 15.11.2016 21:51:39

Re: Как выбрать одно случайное значение

Proger_XP пишет:

}%> Тогда через что-то вроде %%orderByRaw('RAND()')->take(1)%%  делать.
Если бы я был **hzone**, я бы сказал, что за такое увольняют.

ты счас обидешься, но ты дурной!

Не в сети

#12 15.11.2016 22:34:20

Re: Как выбрать одно случайное значение

  1. А как вставить здесь random? Все, что ты можешь здесь вставить — это количество строк в таблице (тот же count) плюс 1.

Не понял.

  1. Если речь о добавлении новой записи, то точно так же как ей выставляются обычные атрибуты выставляется и поле random = mt_rand().
  2. Если речь о выборе нескольких записей, а не одной, в SELECT, то кто мешает сделать take(3)? Либо несколько запросов по take(1) для равномерного распределения.
  1. «Медленнее» — не страшно, если там разница в пару мс.

Разница совершенно не в паре мс. Я не сторонник оптимизации просто ради оптимизации. Разница существенна для таблиц с тысячами записей, не говоря о большем числе. Зависит от самой таблицы (размера строки) и сервера (диски).

Вот я сейчас сделал простой запрос на не очень нагруженном сервере:

shmysql> select count(1) from files;
+----------+
| count(1) |
+----------+
|  5020651 |
+----------+
1 row in set (1.19 sec)

1.2 секунды только на то, чтобы вычислить число записей в таблице.

ORDER BY RAND():

shmysql> select * from files order by rand() limit 1;
...
1 row in set (21.73 sec)

LIMIT RAND():

shmysql> select * from files limit 138515, 1;
...
1 row in set (0.46 sec)

Поле random:

shmysql> select * from files where random = 1385 limit 1;
...
1 row in set (0.04 sec)

Разница между полем и ORDER BY более чем на 2 порядка и на порядок между полем и LIMIT.


Про fullscan:

shmysql> explain select * from files order by rand() limit 1;
+----+-------------+--------+------+---------------+------+---------+------+---------+---------------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows    | Extra                           |
+----+-------------+--------+------+---------------+------+---------+------+---------+---------------------------------+
|  1 | SIMPLE      | files  | ALL  | NULL          | NULL | NULL    | NULL | 5020651 | Using temporary; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+---------+---------------------------------+
1 row in set (0.06 sec)

EXPLAIN Output Format:

If you want to make your queries as fast as possible, look out for Extra column values of Using filesort and Using temporary

ALL

A full table scan is done for each combination of rows from the previous tables. This is normally not good if the table is the first table not marked const, and usually very bad in all other cases. Normally, you can avoid ALL by adding indexes that enable row retrieval from the table based on constant values or column values from earlier tables.

Using filesort (JSON property: using_filesort)

MySQL must do an extra pass to find out how to retrieve the rows in sorted order. The sort is done by going through all rows according to the join type and storing the sort key and pointer to the row for all rows that match the WHERE clause. The keys then are sorted and the rows are retrieved in sorted order.

Using temporary (JSON property: using_temporary_table)

To resolve the query, MySQL needs to create a temporary table to hold the result. This typically happens if the query contains GROUP BY and ORDER BY clauses that list columns differently.

Короче, ALL + Using temporary; Using filesort это самое плохое, что может быть у запроса, т.к. второе создаёт копию таблицы (в случае с ORDER BY RAND() копию всей таблицы), а первое и третье требует как минимум одного прохода по всей таблице от начала до конца.


  1. словами, делаешь поддерживаемое приложение и уже потом смотришь на скорость. Если скорость можно решить железом (CPU, память) — сейчас это несравнимо дешевле, чем оплата работы разработчика, который будет оптимизировать код под скорость.

Посмотри на это с другой стороны. Когда ты разрабатываешь систему с нуля — цена оптимизации одна. Когда ты в процессе поддержки приложения обнаруживаешь бутылочное горлышко — цена оптимизации выше на порядки, т.к. уже есть данные, их нужно мигрировать, есть риски из-за кривого переноса, нужно делать перенос без простоя и так далее.

При подходе «щас работает, а дальше посмотрим» будет момент, когда система неожиданно выросла (ведь хорошо же, нет?), и над тобой нависает вопрос как это всё оптимизировать, надо ли или может «свалить по тихому». И где-то в сознании скребёт мысль, что ведь зря не заложил лишнего поля в таблицу, проблем бы не было.

Повторюсь, речь не о паре мс.

Можно не верить мне, но набери в Google «mysql random rows» и посмотри, что пишут.

Что касается читаемости, то если использование поля random в коде так режет глаз, то почему не завести отдельный метод в модели с красивым названием? Фреймворк/ORM должны помогать писать и работающий, и относительно быстрый (хотя бы «не медленный») код, и не надо оправдывать каждую запятую читаемостью, которая к тому же субъективная и зависит от личного опыта.

Не в сети

#13 16.11.2016 09:40:24

Re: Как выбрать одно случайное значение

Если речь о добавлении новой записи, то точно так же как ей выставляются обычные атрибуты выставляется и поле random = mt_rand().

random должен быть integer и быть не больше, чем общее количество строк в таблице, так? При добавлении новой строки ты не можешь вставить рандомное число, только "количество строк + 1". Правильно?

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

При подходе «щас работает, а дальше посмотрим»

Нет, подход совсем совсем совсем не такой. Подход - сделать поддерживаемое приложение, приоритет на читаемость кода. На мой взгляд, разработка 99% приложений должна иметь такой подход. Написал, видишь, что один из запросов занимает больше секунды - разбираешься, исправляешь. На выходе у тебя поддерживаемое приложение. Если же изначально приоритет ставить на скорость, жертвуя читаемостью, то на выходе будет более быстрое приложение с плохо читаемым или нечитаемым кодом. Но, в большинстве случаев, скорость не так важна (я сейчас не говорю о random в больших таблицах), а гораздо важнее возможность относительно недорого расширить и масштабировать приложение.

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

Конечно хорошо, если приложение и читаемое и быстрое, но на практике чем-то жертвовать все-равно приходится. Мы используем фреймворк, OOP/MVC/ORM в ущерб скорости, иначе бы мы писали приложения на процедурном PHP с сырыми запросами.

Не в сети

#14 16.11.2016 10:06:04

respectpick
Откуда: Москва
Сообщений: 83

Re: Как выбрать одно случайное значение

Proger_XP пишет:

}%> Тогда через что-то вроде %%orderByRaw('RAND()')->take(1)%%  делать.
Если бы я был **hzone**, я бы сказал, что за такое увольняют.

Пока у тебя там 100 записей на все таблицы - это работает. А когда у тебя тысячи или миллионы (фотографий столько вполне может быть)... Что оно делает? Читает (full scan) всю таблицу от записи 0 до записи 1,000,000, что обычно приводит к созданию таблицы на диске в несколько гигов. И только потом возвращает, сколько там, одну запись?

эм...но у меня же записи отбираются по условию

whereIn('photo_symbol_id', $array_symbols_id)

и уже из них делается рандом

Не в сети

#15 16.11.2016 11:05:04

Re: Как выбрать одно случайное значение

  1. random должен быть integer и быть не больше, чем общее количество строк в таблице, так? При добавлении новой строки ты не можешь вставить рандомное число, только «количество строк + 1». Правильно?

Нет, «количество строк + 1» — это как раз в случае с LIMIT, где либо нужен отдельный вызов COUNT, либо подставляешь заранее известное число записей, но жертвуешь последними добавлениями (если не обновлять это число при добавлении/удалении записей, что лишняя сложность).

Для поля random значением может быть любое число, никак не связанное с числом записей в таблице. Чем больше верхняя граница random, тем меньше будут группы (вплоть до верхней границы в count записей, где группы будут состоять из 0-1 записи), т.е. каждое значение random кластеризует записи.

Например, на сайте есть вывод фотографий в случайном порядке (сплошным потоком). Допустим, размер страницы не более 50 фото (т.е. за один запрос пользователь больше 50 фото не получит). В базе на данный момент 1 млн записей. Итого чтобы сделать случайный поток достаточно выставить поле random в случайное значение от 0 до count/perpage = 20000. Если ГПСЧ нормальный (не PHP rand()), то распределение должно быть равномерным и пользователь сможет запросить ≈20000 случайных страниц до того, как ему начнут попадать уже виденные результаты (до момента, когда cron перегенерирует random базы).

Если предел будет выше — группы будут меньше и запросы на страницы размером 50 записей будут выдавать меньше результатов и нужно будет делать второй запрос, чтобы «добить» её до лимита. Если ниже — будет меньше доступных страниц между перегенерацией random.

Если случайный поток к тому же фильтруется (как здесь, $array_symbols_id), то может быть смысл увеличить размер группы, чтобы требовалось меньше повторных запросов. Надо смотреть статистику по условиям, сколько в среднем уникальных записей на каждое.

  1. Но, в большинстве случаев, скорость не так важна (я сейчас не говорю о random в больших таблицах), а гораздо важнее возможность относительно недорого расширить и масштабировать приложение.

С этим соглашусь. Просто не надо игнорировать совсем уж очевидные бутылочные горлышки, потому как одно дело при возросшей нагрузке докупить железа и повысить производительность вего приложения в целом, а другое — докупить его только чтобы утранить одну-единственную проблему, потому что доработка уже слишком проблематична.


  1. эм…но у меня же записи отбираются по условию

Это просто уменьшает размер временной таблицы, но не отменяет fullscan для ORDER BY RAND и необходимости в отдельном COUNT для LIMIT RAND. Проблемность этого зависит от размера таблицы и сервера, т.е. если это high-end сервер — для него пересортировать пару сотен записей «в уме» раз плюнуть, но если это обычный VPS — то может быть задержка.

Не в сети

#16 16.11.2016 15:46:46

Re: Как выбрать одно случайное значение

Proger_XP пишет:

Нет, "количество строк + 1" - это как раз в случае с LIMIT, где либо нужен отдельный вызов COUNT, либо подставляешь заранее известное число записей, но жертвуешь последними добавлениями (если не обновлять это число при добавлении/удалении записей, что лишняя сложность).

Помоги тогда понять. Я вот так это понимаю. Есть данные (столбец ID - стоблец Sort):

1 - 5
4 - 2
5 - 1
6 - 3
10 - 4

Ты добавляешь поле с ID 6 и тебе нужно сюда сразу вставить число для Sort. Сюда ты можешь вставить только пару 11 - 6, иначе потом выборка по mt_rand не будет работать.

А чтобы достать данные, тебе нужен сначала count, чтобы посчитать количество строк и уже потом вызывать mt_rand() и дальше доставать строку.

Для поля random значением может быть любое число, никак не связанное с числом записей в таблице. Чем больше верхняя граница random, тем меньше будут группы (вплоть до верхней границы в count записей, где группы будут состоять из 0-1 записи), т.е. каждое значение random кластеризует записи.

А если строк в таблице 200, то и mt_rand() должен возвращать число от 1 до 200, иначе как? Если строк 200 и ты даешь ему число 250, которого нет в таблице, то он ничего не вернет.

Не в сети

Подвал раздела