воскресенье, 31 января 2010 г.

Выбрать по одной строчке в каждой группе в MySQL

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

Например, есть такая таблица:

mysql> SELECT * from fruits;
+--------+------------+-------+
| type | variety | price |
+--------+------------+-------+
| apple | gala | 2.79 |
| apple | fuji | 0.24 |
| apple | limbertwig | 2.87 |
| orange | valencia | 3.59 |
| orange | navel | 9.36 |
| pear | bradford | 6.05 |
| pear | bartlett | 2.14 |
| cherry | bing | 2.55 |
| cherry | chelan | 6.33 |
+--------+------------+-------+
9 rows in set (0.00 sec)

Задача: получить информацию по самым дешевым сортам фруктов каждого типа. То есть, в итоге должно получится:

+--------+----------+-------+
| type | variety | price |
+--------+----------+-------+
| apple | fuji | 0.24 |
| orange | valencia | 3.59 |
| pear | bartlett | 2.14 |
| cherry | bing | 2.55 |
+--------+----------+-------+

Я раньше всегда решал подобные задачи с помощью JOIN и подзапроса:

SELECT f1.* FROM fruits AS f1
INNER JOIN (
SELECT type, MIN(price) AS price
FROM fruits
GROUP BY type
) AS f2 ON f2.type = f1.type AND f2.price = f1.price
GROUP BY type

GROUP BY type нужен, если необходимо, чтобы на каждый тип фрукта было только по одному сорту, даже если цены на некоторые сорта совпадут.

Но сегодня мне потребовалось сделать нечто вроде: выбери все самые дешевые разновидности каждого типа из тех, в которых встречается буква "a". Понятно, что запрос

SELECT f1.* FROM fruits AS f1
INNER JOIN (
SELECT type, MIN(price) AS price
FROM fruits
WHERE variety LIKE '%a%'
GROUP BY type
) AS f2 ON f2.type = f1.type AND f2.price = f1.price
GROUP BY type

будет работать правильно лишь до тех пор, пока не объявится несколько сортов одного фрукта по одной цене, но в названии одного из них будет буква "a", а в другом нет. В этом случае условие придется продублировать:

SELECT f1.* FROM fruits AS f1
INNER JOIN (
SELECT type, MIN(price) AS price
FROM fruits
WHERE variety LIKE '%a%'
GROUP BY type
) AS f2 ON f2.type = f1.type AND f2.price = f1.price
WHERE variety LIKE '%a%'
GROUP BY type

Теперь представьте, что условие не одно, а несколько, к тому же они зависят от пользовательского ввода и весь SQL должен генерироваться автоматически.

На этом месте я сказал себе: "Стоп, надо искать другой выход".

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

Затем наткнулся на переписку участника форума с дёрганым модератором. В конце концов участник форума сам нашел следующее решение (у него, конечно, не про фрукты):

SELECT f1.* FROM fruits AS f1
LEFT JOIN fruits AS f2 ON f2.type = f1.type AND f1.price > f2.price
WHERE f2.type IS NULL

Очень интересное решение, но такой JOIN устрашает при большом количестве строк — а это как раз мой случай. Но все равно, уже теплее.

В третий раз закинул он невод…

И первый же результат вывел меня на эту статью: How to select the first/least/max row per group in SQL. Я выбрал предпоследнее решение, без магии с FORCE INDEX. Оно выглядит так:

SET @type = NULL;
SELECT type, variety, price FROM (
SELECT (@num := IF(@type = type, @num+1, 1)) AS num, @type := type AS type, variety, price
FROM fruits
WHERE variety LIKE '%a%'
ORDER BY type, price
) AS t
WHERE num = 1;

Глобальные переменные в MySQL — сила, пусть даже это всего лишь уродский костыль.

суббота, 30 января 2010 г.

четверг, 28 января 2010 г.

Кури RFC, %username%!

Люблю я в своей работе использовать модуль Regexp::Common, созданный незабвенной Abigail!

А ЗРЯ :-E

$ perl -MRegexp::Common -le 'print "www.610.ru" =~ /^$RE{net}{domain}{-nospace}$/ ? 1 : 0'
0

Это обыкновенный промышленный саботаж. И главное, ведь не подкопаешься: всё в соответствии с RFC.

Кстати, для тех, кто не знает, що таке {-nospace}, подывытыся:

$ perl -MRegexp::Common -le 'print " " =~ /^$RE{net}{domain}$/ ? 1 : 0'
1

Призываю создать в культуре Perl новое движение — RFC-нацизм, Abigail назначить его бессменным лидером, модуль переименовать в Regexp::Uncommon и использовать для пыток инакомыслящих.

суббота, 23 января 2010 г.

Слив защитан

Скриншот сайта РТР с надписью «Ваш голос защитан»

Госканал. Очаг культуры, блядь.

пятница, 15 января 2010 г.

Wiki-движок на PHP (велосипед № 2)

В далеком 2008-м году я наткнулся в Сети на wiki-движок, написанный на JavaScript. Мне так это понравилось, что я решил его улучшить. Перевел на ООП-парадигму, выделил в отдельный модуль, привел в соответствие со спецификацией Creole 1.0. В результате получилась вот такая вещь. Однако на этом я не остановился.

В этом месте необходимо сделать небольшое лирическое отступление. По странным законам русской души я всегда испытывал непреодолимое влечение к халяве. В частности, к бесплатным хостингам. Учитывая, что года с 2000-го у меня стабильно был root хотя бы на одном серваке, такое влечение действительно можно назвать странным (не говоря уже о том, что с деньгами у меня проблем не было). Но на халяву и уксус сладок. Поэтому шведский стол русским противопоказан по медицинским соображениям. Короче, раз халявный хостинг есть, его грех не взять (и побольше!) Что с ним потом делать — это уже другой вопрос.

В 2009-м году как на грех мне попался на глаза бесплатный хостинг 110MB. Замечательность этого хостинга в том, что абсолютно на халяву можно получить PHP и SQLite! (Второе есть, но они это не афишируют — возможно, забыли отключить.) К сожалению, недавно они стали добавлять ссылку на свой сайт в конец страницы, но это дело легко поправимо с помощью <div style="display: none;"> (при этом никакие Terms of Service не нарушаются — они явно были рассчитаны не на русских :-) Что же сделать на бесплатном хостинге, на котором нет FTP, но зато есть PHP и RDBMS? Конечно, wiki. Я вооружился напильником и за день-другой перепилил JavaScript в PHP. И вот результат перед вами.

Скачать wiki-движок на PHP можно с Gitorious — лучшего в мире хостинга для свободного программного обеспечения. Лучшего не потому, что у него больше всего наворотов, а потому, что его исходники свободно распространяются по лицензии GNU Affero GPL. В мире есть еще один source hosting с открытым кодом — launchpad, но там Bazaar вместо Git.

Еще пару слов стоит сказать о парсере, который заложен в основу wiki-движка. С его помощью можно разобрать текст на языке (в хорошем смысле), слова которого описываются регулярным выражением (тоже в хорошем смысле). Однако благодаря такой штуке, как fallback, парсер получается гораздо более мощным, чем можно от него интуитивно ожидать. Chris Purcell назвал этот парсер RLRD (regular language recursive descent) — но сколько я ни гуглил, так и не разобрался, сам он этот принцип придумал или где-то подглядел. Короче, советую посмотреть и разобраться. Идея того стоит. Сердце алгоритма — метод apply класса creole_rule, всего 60 строк кода (на Perl было бы и того меньше).

Этот движок я впоследствии использовал на сайте детского городского лагеря, организованного моей женой, когда мне надоело вручную редактировать страницы, а учить жену HTML-разметке я не решился. На то, чтобы прикрутить его к сайту, потребовался один вечер. В общем, хороший получился велосипед, годный.

четверг, 14 января 2010 г.

Ёбаный стыд

Нередко реклама на уютненьком доставляет больше самой статьи.

Ёбаный стыд

вторник, 12 января 2010 г.

Новогодняя сказка от TELE2

1 копейка за минуту. 300 минут в подарок!

Иными словами, 3 рубля в подарок. Я в экстазе.

понедельник, 11 января 2010 г.

95% разработчиков не умеют использовать POST

Она проклинает тебя

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

Каждый посетитель, который, немного погуляв по сайту, пару раз нажмёт кнопку Back (а таких, согласно Якобу Нильсену, будет немало; а некоторые, больные интернет-зависимостью, — вроде меня — еще любят кнопку Refresh: а вдруг за пару минут появилось чего новенького?) — каждый такой посетитель внезапно получит в морду кирпичом с надписью на нем вроде: «Вы попали на эту страницу нажатием простой с виду кнопки, которая вызывает очень много чёрной магии. Вы намереваетесь и дальше испытывать терпение могущественного джинна?» (А больные интернет-зависимостью — вроде меня — после второго кирпича могут очень сильно расстроиться.)

Надеюсь, моя мысль ясна. Итак, сделайте это правильно: в ответ на POST надо отдать перенаправление со статусом 303 See Other на страницу с результатами выполнения запроса.

И да, POST используется для таких вещей, как добавление или изменение данных. Это означает, что в обработчике данных, переданных методом POST, мы делаем запись данных в базу, а потом перенаправляем на страницу, которая сама выгребет данные из базы (или если вам важна производительность — из кеша) и отобразит результат модификации данных.

«А как же поиск?» — спросят меня 95% разработчиков. «Поиск, — отвечу я, — делается методом GET». И не трогайте меня, я при исполнении служебных обязанностей.

При написании этой статьи ни один опенсорс не пострадал.

пятница, 8 января 2010 г.

Template::Object (часть 2)

Продолжение рассказа о винтажном велосипеде. Начало тут.

Извините, что затянул с продолжением. Хотелось написать что-то грандиозное (а именно, CRUD), но из-за лени ничего не вышло (может, еще допишу). Так что, к сожалению, вторая часть Марлезонского балета будет немного смазанной :-( В этой статье я представляю обещанный пример классов, производных от Template::Object. Это три класса Template::Input, Template::Input::Text и Template::Input::Select. На этот раз никакого POD'а я не писал, так что пример использования смотрите по unit-тестам.

Сырцы качать отсюда.

Disclaimer. В предыдущей статье я упомянул, что CTPP является правильным шаблонизатором. К сожалению, вынужден опровергнуть своё заявление: в CTPP есть циклы и условные операторы. Моё заблуждение вызвано тем, что в одном хорошем докладе на Highload++ прозвучала фраза про CTPP и про способ его правильного использования в компании докладчика (жаль, не могу вспомнить, чей был доклад). То есть, они просто игнорировали все управляющие конструкции и использовали CTPP для описания голых блоков, а всю логику описывали в представлении.

Update: По наводке Анатолия Шарифулина был вычислен докладчик — Андрей Шетухин и компания — Рамблер (а именно, Рамблер-Почта).

Update #2. В общем, фишка Template::Object в том, что я бы по аналогии с ORM назвал object-template mapping. Создаешь шаблон-объект, инициализируешь его данными в том виде, в котором их тебе удобно передавать, и всё: объект сам знает, что с этими данными делать дальше.

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

Гибкость: из нескольких кубиков можно собрать кубик Рубика — композиция, из одного кубика можно сделать Borg cube — наследование.

Шаблоны — одно из тех мест, где код практически не используется повторно. Template::Object способен решить эту проблему.

А unit-тесты? Удобно ли вам писать unit-тесты на логику, зашитую в ваши шаблоны? Как часто вы забиваете на тесты по этой причине?

И да, мощность этого метода особенно заметна при использовании convention over configuration.