Pull to refresh

Comments 157

я, конечно, прошу прощения за возможную недалекость, но не является ли Ваш метод примером экономии на спичках?
Это интересный вопрос. Испытываю очень большой соблазн переадресовать его авторам руководств по prepared statements, упорно толкающим этот лозунг.

Увы, сам я тестов не делал, но есть подозрение, что это, все-таки, работает. К примеру, такая технология, как HandlerSocket как супер-скоростной способ доступа к БД позиционируется именно благодаря «изыманию» SQL из процесса. То есть, какие-то накладные расходы на парсинг и построение плана запроса всё-таки есть.

Если бы идея пошла, я бы обязательно сделал замеры производительности. Но поскольку пользоваться таким вариантом всё равно нельзя, то и тесты я счёл бесполезными.
HandlerSocket, насколько мне известно, уменьшает накладные расходы на веб сервере, а не на сервере базы данных. Добавить один или несколько веб серверов обычно никакой проблемы не составляет, так что всё равно экономия не настолько велика, чтобы был смысл серьёзно тратить на неё время.
Нет-нет, на веб-сервере и нет никаких расходов.
Суть HandlerSocket-а — это именно прямой интерфейс к innodb, минуя SQL. Вы его точно ни с чем не путаете?
Может и путаю :)
Не является. В моей практике на высоконагруженном проекте prepared запрос занимала иногда больше, иногда половину времени от самого запроса. В качестве СУБД был Postrges.
Ну, на мой взгляд, все-таки увеличение производительности есть, просто оно не значительное и не там, где бы вам хотелось. Вы рассматриваете подготовленные запросы практически как хранимые процедуры.
Если рассматривать что все-таки в рамках одного скрипта возможны одинаковые запросы. Например, если мы обновляем что-то на сайте и запускаем типовые INSERT/UPDATE.

PS. спасибо за статью
Решение простое — не инициализировать среду выполнения для каждого запроса, а оставлять её. Вариант — поднимаем полноценный http бэкенд на php, например на базе phpdaemon, фронтом к нему ставим nginx или apache, они же отдают статику. При запуске бэкенда сначала подготавливаем все выражения вместе со чтением конфигов и прочей инициализацией, а в основном цикле обработки запросов только биндим переменные. Можно ленивую подготовку использовать. Можно подготавливать так только выражения для обычных посетителей, а в админке использовать «одноразовые».
А это распространённое и коробочное решение, или пока что-то вроде наколенного? Интересуюсь для расширения кругозора.
Это конечно же не коробочное решение. Я так понимаю VolCh внес предложение как это можно реализовать и очень даже вероятно, что где-то такое делал у тебя. Но это точно не коробка.
Есть проект phpdaemon, где из коробки работает в том числе http-сервер, приложение свое естественно к нему прикручивать надо. На хабре о нём писали не раз. Некоторые фреймворки на нём запускаются без особых проблем.
Может я конечно безрук, но phpdaemon я запускал под маком целый день. Можно конечно сделать допуск на то что дело было под Mountain Lion, но все же, пока я собрал все зависимости я уже заранее ненавидел сам продукт.

Потом установленный по инструкции с сайта phpdaemon не запускался. Выяснилось что из своей /usr/ подпапки он не расползся по системе а искал свои конфиги в /etc/. Расположив их где надо, я запустил его. Увидел что он запустился. Но зайти на его «тестовую страничку» не смог. Вырубил по ctrl+c, потом еще отправил ему команду stop. Попробовал еще раз, чот не вышло. Вобщем через некоторео время обнаружил что в моей системе запущено с десяток php которые все жрут по 7% cpu, которые я еле переубивал. После чего выполнил торжественный rm -rf каталога с этой странной штукой.
Сам проект выглядит очень привлекательно, но явно еще сыроват. Я уж не говорю что версии некоторых pear и pecl-пакетов, на которые он опирается, ниже чем 0.1.
Про MacOS ничего сказать не могу, работал под ней ещё во времена первых Макинтошей.
офтоп моде он:
К спору о том говно ли PHP — в рельсах работа в режиме daemon является стандартом, и вопрос о том какое преимущество дает prepared statements даже не встает потому что это работает сразу из коробки ;)
Это не «оффтоп», а «д'Артаньян» моде.
Спасибо, интересная информация для пхешника
многие части бизнес логики переносится в бэкэнд.
это скоро станет устоявшейся практикой, как когда-то было новаторством использование nginx & php-fpm.
>> Но много ли вы знаете скриптов (написанных профессиональными программистами), которые выполняют кучу одинаковых запросов?

Не устаю повторять: каждому методу своя задача.

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

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

И речь идет совсем не о веб-сайтах и даже не о магазинах. В частности, у меня есть много автоматических ботов, которые занимаются перебором отчетов в компании, почту и выполняют прочие нудные задачи, заменяя собой добрый десяток сотрудников. Объем данных и задачи таковы, что в рамках одного запуска скрипта, который работает от 2 до 30 минут, встречаются тысячи однотипных вставок и апдейтов, между которыми идут промежуточные расчеты и уточнения.

Простейшим примером может быть пересборка поискового индекса.
С нетерпением жду от Вас статьи
«Отчёт о неудачной попытке получить заявленную эффективность от многоядерного процессора на примере многопоточного калькулятора»
Согласен, следовало более чётко оговорить, что речь про использование на фронтенде.
Просто руководства, типа процитированной мной в другом комментарии документации по PDO, не разделяют эти понятия.
Ну так надо же голову на плечах иметь, прежде чем применять что-либо :D
Этим и отличаются кодеры от программистов.
Ну во первых — использовать хранимки — давно известный кейс. В «нормальных» БД оптимизатор может еще копить статистику по их работе чтобы быстрее выбирать стратегии работы.
Во вторых prepared statements рулят только тогда когда они одинаковые, фактически это позволяет использовать query_cache для получения «заявленных» инструкций из переданного текста.
Поверить в эффективность очень просто — берем что-то одинаковое(новости, комменты, в общем селект должен различаться только на ID) и запускаем их получение в 100 потоков с или без PS.
Насколько я понимаю, хранимая процедура здесь не поможет.
Речь идёт о выполнении динамических запросов, например, тех же новостей по id.
Запрос внутри хранимой процедуры точно так же будет парситься или препарироваться каждый раз при вызове процедуры.

query_cache — это тоже совсем не то. Он работает на константные запросы, и не имеет отношения к PS.

Сами же по себе PS (в mysql) работают только в пределах одного и того же соединения, глобального кэша prepared запросов не существует. Отсюда в наших 100 потоках будут делаться все те же 100 prepare+ 100 execute vs. 100 parse. И последнее явно будет выигрывать, поскольку парсинг практически одинаковый, а запрос к базе только один.
Не совсем так.
Да — вам придется каждый раз делать prepare, вот только говорятся они обычно не на стороне клиента, а на сервере.
Именно в этом моменте QC увидит что ему на prepare(это по сути обычная команда) пришол «знакомый текст» и он резко поймет что с ним делать.
Разбор текста запроса, вычление таблиц, ключей, оптимизатор и другие работы(которые можно заметить в EXPLAIN например) — все это как бы «пролетает».
Это конечно зависит от весии или, назовем так, поставщика mysql
какая разница, кто сказал prepare?
Со слов Кости Осипова, который и принимал участие в разработке PS в Mysql, глобального кэша подготовленных запросов, о котором вы говорите (опознать по тексту) в mysql нет.
Если мне не изменяет память, то в MySQL prepared statements реализованы коряво, т.е. план запроса не кэшируется, а строится каждый раз заново при выполнении ps, т.ч. толку от таких ps в плане производительности не так уж и много.
Со стороны Java-разработчика… Выглядит странно, что в 2012 году люди до сих пор открывают новое соединение каждый раз когда надо соединиться с базой. Хорошо еще если база на локальном хосте…

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

Понятно, что это фишка архитектуры языка PHP, где нет нормального приложения, и все процессы живут отдельно… Видимо, простота работы решает.
Кооментарий, естественно, написан не для HolyWar. Сам имею около 10 сайтов на PHP. Работаю который год без проблем :)
В PHP есть поддержка «persistent connection», как раз без открытия нового соединения. На производительности работы с MySQL это не так сильно сказывается, коннект к MySQL это весьма быстрая операция.
Выглядит странно, что в 2012 году люди до сих пор открывают новое соединение каждый раз когда надо соединиться с базой.

Вы правда, что ли, в 2012 году не знаете про connection pool?
Про connection pool в PHP я узнал вот сейчас. В Java, понятно, я ими пользуюсь постоянно.
Потому что Joomla CMS, которую я немного копал однажды, все соединения были прямые, без пулинга.
Вот я и удивился. Вроде отраслевая промышленная CMS.
Вообще, в нормальном фреймворке разработчик коннекшн-пула просто не должен видеть.
А как он работает в PHP, если на каждый запрос скрипт отрабатывает с нуля и затем прибивается?
На уровне системного DAL.
В рамках одного запроса?
В рамках жизненного цикла сессии ОС.
Как это работает, если скрипт прибивается после обработки запроса?
Соединение открывается внутри контейнера, который управляет процессами php. Например php-fpm.
Пул находится не на уровне скрипта, а на уровне (того или иного) фреймворка и/или драйвера БД. На уровне скриптов пул делать бессмысленно.
Извиняюсь за нубские вопросы. По моему представлению, есть Apache, у него модуль mod_php. Точка входа в РНР-приложение — это конкретный файл скрипта. При получении запроса, грузится соответствующий файл скрипта, парсится и выполняется. Кому он там дальше отдаст управление — его дело. По завершению работы, apache/mod_php прибивает запущенный им процесс и дальше по новой. Где я не прав? php-fpm в расчёт не берём.
Грубо говоря, все зависит от реализации конкретного драйвера БД. Многие из них (я тут не говорю за PHP, реализаций все равно может быть миллион) реализуют пул на своем уровне. А поскольку это уже покинуло процесс апача/пхп, то жизненный цикл этого процесса никого не волнует.
Ясно. Спасибо! Необычное решение :))
Приведите, пожалуйста, примеры где средствами драйвера БД реализуется пул коннектов в пхп.
Специально же написано «я не говорю за PHP».
Как это оно может покинуть процесс апача/пхп? Драйвер БД по-вашему выполняется в отдельном процессе?
… хотя, конечно, создание процесса на обработку каждого запроса — это сильное решение, да.
Не создание, а fork. Юниксы же.

Но, да, мне тоже не нравится.
А что, апач нынче только под юниксами живет?
Так может под Win он просто медленнее и работает с большими затратами памяти. Как-минимум copy-on-write в fork дает преимущество.
Не могу сравнить адекватно, но вот сравнение мухи с попугаем (apache vs IIS) в плане системных вызовов.
Под виндой он использует т. н. «prefork» — фактически тот же пул процессов.

Речь выше о юниксоидном варианте. Да и на самом деле апач под виндой не живет ни у кого, из тех кому нужна производительность (уверен).
К PHP пул подготовленных запросов относится только при нестандартном применении. При стандартном (mod_php/php-fpm) процесс на каждый запрос хотя и не прибивается, но инициализируется заново и даже просто передать несериализуемые данные (ресурсы в терминах PHP или объекты где они инкапсулированы) между обработчиками двух запросами как минимум непростая задача, не говоря об их совместном использовании.
пожалуйста — не берём php-fpm, тогда берём mod_php
всё равно правило «контейнер, который управляет процессами php» сохраняется
=) ни разу ничего не писал на Java, но там не подсоеденяются к БД каждый раз при запуске приложения? В смысле, если я закрываю phpStorm (alt+f4) и запускаю его снова, он как-то будет пользуется предыдущим соединением? Мне любопытно как java хранит коннекты, если я постоянно закрываю/запускаю приложения. Это не троллинг, я всерьез любопытствую.
После перезапуска приложения к базе, конечно, все соединения идут заново.
Речь идет о пуле соединений к базе во время работы приложения. Пул шарится между множеством тредов. Каждый тред делает dataSource.getConnection() и connection.close(), и «думает», что соединение закрыто.
Сам же DataSource и Connection — это просто интерфейсы, а уж как они будет обрабатывать эти вызовы — сам разбирается. Обычно соединение возвращается в пул и ждет следующего вызова.
В PHP по сути таки и происходит, просто фактически приложение живёт миллисекунды и создавать пул смысла нет. php-приложения (вернее подавляющее их большинство) остались CGI-приложениями, просто оптимизировано время их запуска.
«создавать пул смысла нет»
Вот этой фразе я и удивлялся, говоря про 2012 год.
Пул создавать смысл есть, и очень ощутимый, может быть на уровне драйвера или на уровне среды исполнения php (как говорят выше, есть объекты persistent connection).
Реально, если в Java ты работаешь с базой без пула, другие разработчики просто покрутят у виска, это вопрос квалификации и понимания основ.
Даже в случае CGI приложения на Java есть смысл создавать пул? :)
Мне кажется, не стоит до такой уж степени троллить собеседника.
Он, кстати, единственный, кто внятно написал про организацию пула, в отличие о того же lair'а, у которого только невнятные пассы руками про «случаи бывают разные» и ничего конкретного.
Хотел бы троллить — выразил бы мысль как-то по другому и без смайлика :) Просто хотел уточнить тот момент, что к типичным приложениям на PHP нельзя подходить с теми же мерками, что и к приложениям на Java/Ruby/Python/C#/… Они по сути остаются CGI приложениями.
Если нужно много вставок делать, то почему бы не использовать подготовленный Multi-Insert запрос.

А по-поводу подготовки запросов можно вспомнить замечательный Gearman: добавляете рабочего который будет делать трудоемкие записи в базу и отправляете ему свои запросы.
Получается двойная выгода — и пользователь не тратит своё время (т.к. запрос уходит сразу в фон), так и появляется возможность держать подготовленные запросы в памяти (время жизни рабочего может быть достаточно долгим).
Вспоминается пословица про ежиков и кактус… ))
Не, ничего личного, но ей богу, забавно наблюдать, сколько велосипедов изобретено и сколько костылей настрогано вследствии принятия одного-единственного решения: «С чего бы начать изучать программирование? Наверное с PHP, не зря ж он такой популярный...»

Как бы там ни было, за статью автору респект. Prepared statements рулят. Ускорения sql там, в среднем, кот наплакал, но безопастность и надежность повышает сильно, поэтому их стоит использовать даже для единичных запросов.
Смешно сказать, но в самом сочном-модном ROR их тоже нет…
StackOverflow
По крайней мере до 3.1 (так написано в вопросе по линку выше)
Ниша ror недалеко отстоит от php. Никто в здравом уме трейд-ботов или анализаторов для данных с коллайдера на этих языках писать не будет )
И ruby язык сравнительно молодой, ему простительно, добавят как нибудь.

Вот в golang сразу же в интерфейс DB заложили Prepare() — системный язык все-таки.
Про старичка perl уже и не упоминаю, там prepare испокон веков работает как положено.
И ruby язык сравнительно молодой, ему простительно


Если брать с википедии, то
PHP Появился в 1994
Ruby Появился в 1995

Почти одногодки.
Вы немножко лукавите, ибо рельсы 3.1 вышли почти год назад — 31 августа 2011. Это всё же уже не сочно-модная версия :)
Ну у меня решение было принято так: с чего бы начать изучать веб-программирование? О, на Си можно писать CGI! Мда… Можно, но муторно. Что ещё есть? PHP и Perl какие-то, надо глянуть оба, оба примерно одинаково популярны. Perl — ни фига не понятно с первого взгляда, а PHP почти как C, на нём и буду писать.
Чтобы начать писать программы на перле, мне было достаточно 2-х часового чтения доки по нему (на русском, правда, иначе понадобилось бы немного больше времени).

В то время набирал популярность php 3.x, я как-то взялся изучать и его, но забросил, поняв, что это недоделанная калька с перла, заточенная на выполнение внутри апача, которая ничего принципиально нового предложить мне не может, и в то же время заберет кучу времени и сил на борьбу с php-шными непоследовательными особенности (каша в наименовании функций и анекдоты про разницу '==' и '===' существовали уже тогда). А мрачный и у"№щный RE в php и вспоминать страшно…

Хотя против ветра по маленькому и не ходят (как свидетельствуют только что полученные минусы), но повторюсь — у php-шников карма такая — регулярно набивать шишки на обильно разбросанные повсюду грабли.
спасибо, Ваш комментарий расставил все точки над «i» в данной теме.
RE — это регулярки? Вы сейчас про перловские регулярки или про posix? Вам лично какие не нравятся?

А === Вам чем не угодило? Никогда не сталкивались с дувмя раными необходимостями: сравнивать приведённые значения и сравнивать типозависимо?

Понимаете, стебать безосновательно — это просто. Например, я могу сказать, что Ваш сайт для анализа производительности — это полная шляпа, т.к. создан в 2008 году, на два года позже firebug'а, который делает то же самое, но ещё и производительность на стороне клиента проверяет, при этом проверяет проихводительность для меня любимого, а не для какого-то непонятного амстердамского хостинга. Вы начнёте говорить о специфических ситуациях, когда Ваш сайт окажется полезнее firebug'а. И я сразу задам Вам вопрос — чем Вам не нравится наличие mod_php у apache (при наличии возможности использовать PHP в CGI режиме), предоставляющее множество специфических удобств?
Да, речь про регулярки, конечно. Лично мне не нравится php-шная имплементация. Если Вы знакомы с перловой реализацией, то должны понимать, что я имею ввиду. На всякий случай все же уточню — в перле регулярки сделаны гораздо елегантнее, чем в php, и пользоваться ими в перле — одно сплошное удовольствие.

Что касается site-perf — Вы должны помнить, что firebug 2008-го года и сегодняшний — это две большие разницы. В то время даже waterfall диаграммы загрузки не было. Сайт был создан исключительно по причине кривости существовавших на тот момент альтернатив, типа pingdom, результаты которых часто давали ложную картину (pingdom все грузил в один поток, как можно было адекватно оценить производительность загрузки страницы ?).
И многих, кстати, как раз и интересует, как оценивается производительность НЕ ТОЛЬКО со стороны «их любимых», firebug тут мало чем может помочь.

Ну и попытаюсь ответить на последний Ваш вопрос. Лично я совершенно ничего не имею против mod_php (всяко лучше, чем php-cgi). Данный режим часто предпочтительнее даже хваленого php-fpm, т.к. есть полный доступ к окружению апача (т.е. к GeoIP и прочим переменным), а также .htaccess и прочие вкусности. Хотя при очень больших нагрузках php-fpm все же предпочтительнее, апачу сложнее принять и обработать тысячи запросов в секунду к php, из-за оверхеда на блокировки.
Так вот, то, что Вы восприняли как стеб, это попытка показать другим людям, что есть множетсво и других инструментов, альтернативных PHP, которые, пусть и выглядят более страшными для изучения, но и возможности предлагают соответствующие. Затраты на борьбу с глюками неизбежны: и для Postgresql у меня есть пару неласковых слов, и в форум nginx отписано несколько злых багов… Но для PHP эти затраты просто неимоверны, по сравнению с другими языками.

Немного напоминает противостояние пользователей vim и notepad, не находите?
На данный момент PHP мой основной язык потому что, утрируя, notepad есть почти у всех.
Или вот, например, у Вас в профиле указан сайт:
wap-go.com

А в его заголовках почему-то ненавистные Вами:
Server:Apache/1.3.41 (Unix) mod_deflate/1.0.21 PHP/5.2.9

Хотя я Вас понимаю, ведь при попытке что-нибудь сделать сервер нам отдаёт 500-ю ошибку. Ну не получилось пока, тренируйтесь, у Вас получится.
О, спасибо за репорт, давно не пользовался им уже просто, делал когда-то для себя.
Пофиксил, кто бы мог подумать, оказывается во фре есть ограничение в кол-во хардлинков на один файл, 32к всего…

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

Спасибо за напутствие, пригодится, сотни Гбит/c действительно не шутки (я не про wap-go и не про site-perf ;) ).
И Вам взаимно желаю всяческих благ.

P.S. а php я, кстати, использую, очень добротный cli-шаблонизатор, знаете ли. Поддерживает циклы и прочие вкусности, рекомендую. А wap-go и site-perf работают оба на перле, хотя кому какая разница ;)
У PHP есть и PCRE (Perl Compatible Regular Expression), а не только Posix Regex (кстати тоже не made in PHP), о котором вы, видимо, говорите. Да и то последний уже depricated.
Оператор === появился только в четвёртой версии.
Не так давно прошёл по этому же пути. Также встретил проблему с mysql_change_user() в mysqli. Посмотрев исходники mysqli обнаружил, что если пересобрать это расширение с флагом MYSQLI_NO_CHANGE_USER_ON_PCONNECT данный функционал отключается, т.е. теоретически соединение не должно очищаться. Отключать флаг не пробовал, решил переписать решение на PDO. С драйвером из PDO всё заработало. Prepared statements остаются в соединении и ими можно пользоваться, как и ожидалось. Пока этот функционал находится на стадии тестирования и объективных тестов скорости ещё нет, но теоретически это должно работать быстрее постоянных разборов одних и тех же запросов.
Вот это да. До такого я не додумался. Спасибо за информацию!

А интерес к такому способу возник как у меня — чисто теоретически, или из практических соображений?
Раньше я считал, что раз у PHP есть возможность создавать постоянные соединения и держать их открытыми и есть функционал prepared statements то при использовании постоянных соединений драйвер должен сам эффективно использовать их. Принимал это как само-собой разумеющееся, т.к. не видил причины делать иначе. Однажды решил проверить, оказалось что prepared statements работают только внутри одного объекта. Для меня до сих пор является загадкой, почему разработчики сделали именно так. Решение довольно спорное. Ведь в web-приложениях повторяющиеся запросы между реквестами встречаются гораздо чаще чем много повторяющихся запросов внутри одного реквеста. После этого решил написать реализацию руками.
Я сначала пришёл к PDO как к DBAL. Уже потом (через пару часов после чтения документации и проведения тестов) вкусил все прелести в виде prepared statements и автоматическом экранировании.

Точных цифр не помню, но на прогонке одинаковых тестовых запросов 2-й и следующие запросы отрабатывали сильно быстрее.

В общем, PDO — это удобно, быстро и безопасно.
Послушайте, вы точно поняли, о чём статья?
Во-первых, любые «следующие запросы отрабатывают сильно быстрее» из-за банального query cache.
Во-вторых, «одинаковых запросов» в типичном PHP скрипте не встречается вовсе. Повторяющиеся запросы бывают в мизерной доле случаев. То есть, никакого прироста скорости в реальной жизни native prepared statements не дают. Именно об этом написано в этой статье.
В-третьих, если говорить о PDO, то это недо-DAL. В нем нету методов, чтобы получить данные сразу из запроса. Нет плейсхолдера для оператора IN. Нельзя подготовить часть запроса.

Вы путаете понятия.
Безопасно — это плейсхолдер, не PDO. Реализуется хоть на mysql_*.
Быстро — это только так кажется. О том, как на самом деле сделать быстро — и написана эта статья. PDO из коробки этого не умеет.
Удобство — по сравнению с mysql_* — да, есть пара улучшений. Но по сравнению с нормальным DAL это такой же корявый велосипед.
Послушайте, я разве писал, что PDO — мана небесная?

Прироста производительно засчёт кеша не будет, потому что параметры разные. од одинаковыми запросами я имел ввиду именно структуру запроса, а не параметры. Так ведь мы именно о prepared statements в этом топике и говорим.

Одинаковые запросы с разными параметрами встречаются сплошь и рядом, например, если у Вас есть уровень кеширования и Вы избираете тактику чтения/изменения «один id — один запрос».

Экранирование в любом случае надо делать, поэтому экранирование в PDO — это удобно. Безопасно, потому что это изначально реализовано и в обход экранирования через PDO сделать не получится.

Недо-DBAL или до-DBAL — это зависит от того, какой уровень абстракции Вам нужен. Если есть желание, введите для себя термины DBLAL, DBMAL, DBHAL (low, medium, high).

Я ничего не имею против Ваших экспериментов с устаревшей библиотекой mysql. Вы написали, что с ней потерпели фиаско, я просто дополнил, чем можно воспользоваться. Так что Ваша реакция мне непонятна.
Вы реально не читали статью, которую взялись комментировать :)

Вообще-то, там написано всё наоборот: что с «устаревшей библиотекой mysql» всё прекрасно работает.
А фиаско я потерпел с новой и рекомендованной mysqli :)

Статья не о том, какую библиотеку использовать. И даже не о том, стоит ли использовать prepared statements. Она совсем, совсем о другом.
Честно говоря, статья вообще непонятно о чём.

Говорите о заявленной эффективности, а сами пытаетесь хакнуть prepared statements для использования в обход того, как это предписывается документацией.

На комментарии тех, кто нашёл эту самую заявленную эффективность смотрите снисходительно — «это только в 0.5 процентов случаев надо». Так, скажем, и создание memory-таблиц нужно далеко не всегда, что же, они не соответствуют заявленной эффективности?

Так скажите же, почему заголовок статьи называется не «проблемы persistent connection в MySQL», а называется так, как называется?

Далее, Вы пишите, что:
идея изначально противоречит идеологии постоянных соединений — «persistent connection служит только для экономии на коннекте, во всех остальных аспектах предоставляя клиенту абсолютно чистое соединение, во всём аналогичное новому»

А на самом деле:
First, when connecting, the function would first try to find a (persistent) link that's already open with the same host, username and password. If one is found, an identifier for it will be returned instead of opening a new connection.

Second, the connection to the SQL server will not be closed when the execution of the script ends. Instead, the link will remain open for future use ( mysql_close() will not close links established by mysql_pconnect()).

Т.е. никто Вам чистое соединение не обещал.

Далее Вы называете PDO кривым велосипедом, хотя как раз в нём всё реализовано так, как заявлено в prepared statements.

У Вас есть два выбора — либо делать так, как предписано документацией, либо создавать хранимые процедуры (задумайтесь над словом «хранимые»), либо мириться с теми проблемами, которые неизбежно возникают, когда Вы начинаете вольно относиться к документации.
А вы попробуйте прочесть её, всё-таки.
Можно не сейчас, можно утром, на свежую голову.

Я не смотрю снисходительно на тех, кто «нашёл эффективность в 0.5%». Я пытаюсь найти её в остальных 99.5.
В отличие от memory таблиц SQL запросы используются практически в любом скрипте. И используются неэффективно, подготавливаясь всё время заново.

Статья называется так, как называется, потому что в стандартном варианте prepared statements вообще нет возможности сделать prepare в одном скрипте, а execute — в другом. В принципе без вариантов. Независимо от типа соединения. Это всё написано в статье.

> На самом деле:

На самом деле не надо цитировать документацию по mysql, когда речь идёт о mysqli
То, что mysql мне гарантирует «нечистое» соединение — я, как бы, в курсе. Приведённый в статье код это подтверждает.
Но речь идёт о mysqli. Поэтому цитировать надо вот это: php.net/manual/ru/mysqlnd.persist.php

То есть именно только чистое соединение мне и обещают.
PDO, кстати, собирается на том же mysqlnd. И точно так же очищает соединение.

> Далее Вы называете PDO кривым велосипедом, хотя как раз в нём всё реализовано так, как заявлено в prepared statements.

Опять 25.
Во-первых, реализовано далеко не всё то, что нужно.
Во-вторых, между вызовами скриптов prepared statements не работают. выполнить один раз prepare и 100 — execute при условии, что эти вызовы приходятся на разные НТТР запросы — невозможно. А мой «велосипед» это может.
Угомонитесь уже. Пойдите поспите. Утром на свежую голову перечитайте то что тут понаписали, и попробуйте переосмыслить.
В стандартном варианте использование execute в другом скрипте и не предполагается. Вы же говорите о чём-то заявленном.

Ваш же «велосипед» работает не так, как заявлено.

У меня нет претензий к самой статье. У меня есть претензии к различию между тем, что в ней доносится, и тем, как она называеется. А доносится в ней, что попытка получить дополнительные бенефиты при недокументированном использовании prepared statements провалилась.

Насчёт документации по mysqli, там explicit написано:
As there is some overhead associated with the COM_CHANGE_USER call, it is possible to switch this off at compile time. Reusing a persistent connection will then generate a COM_PING (mysql_ping) call to simply test the connection is reusable.

Хотя я уже понял, что именно попытку сделать свой «велосипед» вне идеологии Вы и признали неудавшейся.

То, что я здесь написал, имеет абсолютно понятный смысл — эффективность prepared statements абсолютно соответствует заявленной.

Утром перечитайте свой пост и заголовок к нему и приведите их в соответствие.
И прочитайте уже о хранимых процедураъ. Не нужно отрицательно отзываться о сущностях, которые не умеют выполнять задачи других сущностей. Задачи, которые они изначально выполнять не должны.
О, кстати да, про них я забыл совсем.
Они здесь вообще не при делах.
Речь о выполнении динамических запросов, и хранимые процедуры мне здесь никак не помогут.
Запрос можно поместить в процедуру, но эффективнее он от этого не станет.
Вот как. Не станет? Какую же эффективность Вы хотели получить от prepared statements в этом случае?
EXECUTE stmt2 USING @a, @b

CALL sp_takes_string_returns_string(?)

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

В чём принципиальная разница, по-Вашему?
внутри sp_takes_string_returns_string() написано либо prepare, либо обычный запрос с подстановкой.
соответственно, либо выполняется prepare, либо парсится запрос.

проанализирована и сохранена процедура, а не запрос внутри неё.
хранимыми процедурами можно кэшировать только статические запросы, а это опять же — доли процента.
Почитал, уточнил. Хранимые процедуры не компилятся в MySQL, в отличие от MS SQL Server и Oracle. С другой стороны, судя из некоторых статей, откомпилированные процедуры какое-то время сохраняются в кеше, что уже выгодно их отличает от обычных запросов (т.к. кеш прекомпилированного запроса в этом случае не зависит от инлайн-параметров).

В любом случае, если бы статья звучала, как «попытка получить дополнительную эффективность от prepared statements в MySQL», я бы подписался под каждым словом. Сейчас, к сожалению, заголовок статьи всё-таки не совпадает с её содержимым.
Я понял, чем мы с Вами не совпали в терминах. Вы говорите о DAL (data access layer), я говорю о DBAL (database abstraction layer).

DAL — это ORM и т.п.

DBAL — это ODBC, PDO, DBA и т.п.
В-третьих, если говорить о PDO, то это недо-DAL. В нем нету методов, чтобы получить данные сразу из запроса. Нет плейсхолдера для оператора IN. Нельзя подготовить часть запроса.

Главный, по-моему, недостаток PDO как DBAL это то, что нельзя писать одинаковые запросы для разных RDBMS, нужно учитывать конкретный диалект SQL или брать «наиобольший общий делитель» от них всех.
С другой стороны, это можно считать как недостатком, так и сознательным шагом. Более высокоуровневые реализации (в том числе основанные на PDO) предлагают свои собственные языки запросов, транслируемые в нужный запрос, в зависимости от используемой базы данных. Такие, как DQL в Doctrine. Что сказывается на производительности.

Хотя, конечно, наличие такой возможности хотя бы в виде дополнительной фичи было бы крайне полезно.
Угу, какой-нибудь PDO::ATTR_ANSI_EMULATION не помешал бы.
Кстати, при наличии «автоматического экранирования» никакого прироста в скорости быть не может.
Или то — или другое.
Если термин «экранирование» используется по назначению.
Ваша фраза построена некорректно, т.к. мы говорим не об одном лишь автоматическом экранировании.

Я очень люблю приодить в пример аналогии. Так вот, сейчас Вы сказали «если авто тяжелее, то быстрее оно быть не может, или то, или другое». Я же утверждаю, что намного больше зависит от того, какие двигатели установлены в этих авто.

Экранирование определённого набора символов — задача, которой можно принебречь по сравнению с задачей семантического разбора запроса.
Я писал всего лишь о том, что вы не понимаете, как работают native prepared statements.
В случае их использования никакое «экранирование определённого набора символов» в принципе не нужно и поэтому библиотека его не делает.

И наоборот — если используется экранирование, то не используется механизм prepare/execute на сервере, и — следовательно — нет даже теоретического ускорения.
Насчёт первого я согласен — некорректно высказался. Тем не менее, я делал упор как раз на то, что Вам не нужно делать mysql_escape_string каждому внешнему параметру, как это нужно делать в случае использования mysql.

По второму, можно уточнить, если есть инлайн-параметры, то запрос не будет подготавливаться?

Имею ввиду следующее:
SELECT *
FROM `foo`
WHERE
    `id` = :id AND
    `status` = 'active'

Точно не помню, но разве не должен?
mysql_escape_string нужно делать каждому строковому значению (или дате), а не внешнему параметру. Для параметров типа имён таблиц или полей делать его не нужно, там применяется другие правила. А источник (внешний, внутренний или получено из самой БД) вообще значения не имеет.
mysql_escape_string нужно делать каждому параметру, в «чистоте» которого Вы не уверены. Именно это я называю «внешним» параметром. И не только строковому (как раз через внедрение одинарной кавычки в нестроковый параметр и проводятся большинство инъекций).

Источник имеет очень большое значение. Если Ваш параметр берётся, скажем, из константы Вашего класса, то смысла делать mysql_escape_string нет никакого. Только если в целях достижения единообразия или в случае повышенной паранойи.
Вы знаете, количество написанной вами ерунды в последних комментариях уже зашкаливает.

> как раз через внедрение одинарной кавычки в нестроковый параметр и проводятся большинство инъекций

Сюрприз! Для нестроковых параметров кавычка не нужна вовсе:
$_GET['id'] = "1; DROP TABLE users";
поможет вам mysql_escape_string от этой (и любой другой) инъекции?

> mysql_escape_string нужно делать каждому параметру, в «чистоте» которого Вы не уверены.

1. Эта функция ничего не чистит. Она всего лишь экранирует ограничители строк и пару служебных символов для удобства. Дело не в «чистоте», а в переменности. Если мы добавляем в запрос неизвестную строку, мы обязаны её экранировать, поскольку в ней могут встретиться нуждающиеся в экранировании символы. Безотносительно к любой «чистоте» или «загрязнённости».

2. про «любые параметры, не только строковые» я привёл пример выше.

3. «Именно это я называю «внешним» параметром» — слишком крутой поворот. Слово «внешний» имеет вполне определённый смысл. И не надо его подменять в стиле прапорщика из анекдота — «Лэтають, только нызенько-нызенько!»

4. И всё равно, даже ваши «сомнения в чистоте» — слишком нечёткий критерий. На нём нельзя строить защиту. Вы и плейсхолдеры предлагаете применять избирательно — только для тех данных, по поводу которых у вас «есть сомнения»? А если сомнений у [неопытного программиста] нет, а инъекция — есть?
Санирование данных должно идти на уровне их принятия в систему.

На уровне вставки параметров в запрос у Вас остаётся только экранирование. Это можно реализовать отдельно или, как я уже писал, взять PDO. Если Вы каким-то образом используете решения, позволяющие в одном запросе из PHP в базу данных передать несколько запросов, разделённых точкой с запятой — что ж, решайте и эту проблему. Но проблема специфическая и в целом, насколько я знаю, инъекция через неё в стандартных условиях не работает. Защищаться же от того, что будет работать только при специфических настройках (которые Вы в своей системе не предполагаете иметь) — создание бесполезной нагрузки.

поможет вам mysql_escape_string от этой (и любой другой) инъекции?

От некоторых поможет. Опять же, одинарная кавычка. Кстати, отличие этой инъекции от описанной Вами в том, что она замечательно работает даже с использованием библиотек mysql, mysqli и PDO при стандартных настройках. В отличие от вашей инъекции с точкой с запятой, которая в станартных условиях не работает.

1. Эта функция чистит то, что может чистить. Именно за счёт того, что экранирует служебные символы.

2. Экранирование не отменяет необходимости санирования и дополнительных проверок.

3. Скажите, какой для Вас смысл имеет слово «внешний»? Если для Вас данные, поступившие с клиента или стороннего сервиса — не внешние данные, то что же для Вас «внешние»?

4. Я бы не стал испытывать излишния сомнения по отношению, скажем, к массиву описаний полей в Active record. Не стоит создавать излишнюю нагрузку из-за того, что не удаётся делать правильный код-ревью. Ведь в конце концов, неопытные программисты всегда могут взять файл настроек подключения и радостно побежать его использовать с чистыми mysql-функциями.

Не знаю, что там у меня зашкаливает. Вполне вероятно, Вы просто зациклены на попытке поиска ерунды в моих комментариях. Т.к. Вы пытаетесь видеть в моих «нужно» фразу «единственное, что нужно». Нет, нужно многое. Нужно проверять, что программисты используют только тот слой доступа к базе данных, который Вы считаете защищённым, нужно санировать данные на входе и экранировать их при вставке в запрос (если у Вас кастомная реализация плейсхолдеров, то в методе биндинга значений), нужно обеспечивать дополнительные проверки при нестандартных настройках.
Санирование данных должно идти на уровне их принятия в систему.

Вот у меня в ТЗ написано «Поле „описание“ допускает ввод произвольного текста длиной до 1000 символов». Что и где я должен делать, если пользователь ввёл хрестоматийное"'; DROP TABLE users;"? (считаем, что мультизапрос отработает). Имхо, только mysql_real_escape_string на этапе подготовки текста запроса. И это относится ко всем текстовым (и только текстовым) значения, в редких случаях к значениям даты/времени. А вот кавычку в числовом значение надо не экранировать, а или отбрасывать значение полностью, возвращая ошибку, или приводить его к числу.
А откуда Вы узнаете, что значение числовое?

Санирование — это первый этап. Далее, как я написал, экранирование на низком уровне (а именно, в DBAL). В запрос все значения вставляйте строками (в одинарных кавычках), MySQL это примет валидно и для числовых полей. Это в паре с экранированием поможет избежать инъекций типа 1; DROP и ' UNION Именно это я подразумевал под «все параметры».

Для LIMIT'ов и прочего нужно иметь отдельные сеттеры. Не надо собирать всё в кучу. Это совсем небезопасно. Там проверяйте is_numeric и ставьте резонные ограничения. Это же касается оффсета.

Для динамической подстановки имён полей нужно знать о схеме, о том, как проиндексирована таблица. Т.е. запросы типа «order by `$field`» или «where `$field` = ...» должны работать только, если $field содержится в массиве, описывающем проиндексированные поля таблицы. Иначе можете запустить full-scan чего-то очень большого.

Согласитесь, что имена полей тоже не подходят под Ваши «строки и даты», т.к. являются строками, но должны обслуживаться отдельно.

Все элементы запроса должны обслуживаться отдельно. Со стороны в запрос можно принимать только значения полей (санированные, экранированные и вставленные в запрос, как строки) и limit-offset.

Все действия с полями должны идти через маппинг. Т.к. использование имён полей напрямую из запроса не только небезопасно, но и раскрывает реализацию Вашей базы данных и создаёт высокую связанность компонентов от клиента (верхнего уровня) до базы дангых (нижнего уровня). Маппинг же поможет, во-первых, скрыть реализацию, во-вторых, ослабить связи, в-третьих, запретить использование непроиндексированных полей или других полей, которые Вы использовать в запросе не хотите.
Что за дурацкий вопрос откуда узнаете, что оно числовое?
Вы не знаете тип поля в которое собираетесь что-то записать?

И на кой черт что-то должно делать санирование и экранирование данных для запроса, кроме самой абстракции работы с БД?
Санирование — на тот чёрт, что кроме типов данных есть ограничения на значения данных, исходящих из бизнес-логики. Это же касается и лимитов, которые можно выставить в 10000000 (валидный integer) и потом полгода ждать ответа сервера. Но тут решать Вам, где какие лимиты нужны. Значит, санирование выше уровня DBAL.

Экранирование — а кто писал, что его должно делать что-то вне DBAL?

О том, что поле числовое Вы в нижнем уровне DBAL (при выполнении запроса) не знаете.

Стандартная архитектура:
DAL (ORM, менеджеры сущностей, коллекции, объекты и т.п.) -> DBAL (PDO, ODBC, что-то самописное и т.п.)

Так вот, схему базы данных знает DAL, слой доступа всего лишь обеспечивает асбтракцию над взаимодействием с конкретными DBMS.

И держите себя в руках.
Не выдавайте за стандарт свое решение.

Ограничения на значения исходя из бизнес-логики это одно.
А ограничения исходя из логики БД это совсем другое.

Это конечно можно совместить все в одном месте…
Но я бы не стал так делать. Причину, полагаю, придумаете сами.
Что значит откуда я знаю? Есть схема БД.

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

Проверка типа (если тип явно указан в запросе) — приятная правильная красивая плюшка, но на защите никак не сказывается, т.к. даже если Вы обработаете число, как строку, ничего страшного не произойдёт.
Я тут выступаю скорее со стороны клиента DBAL. А если говорить о разработке, то DBAL просто обязан знать тип поля к которому он обращается, просто потому что не все БД (по крайней мере в теории) корректно воспримут условие id = '1', если id числовое поле.
Вы, кстати, не понимаете смысла защиты от инъекций.
Защищаться надо не от какого-то конкретного синтаксиса, а от самой возможности инъекции.
Не нравится DROP TABLE — через UNION можно натворить дел не меньше.

И при этом защита получается совсем несложная. Главное — не следовать дурацким советам типа «эскейпить все входящие параметры».

Ну заканчивайте уже позориться, ей-богу.
Чем больше вы тут пишете, тем сильнее выставляете напоказ свои, гм… глубокие познания :)
Да, эскейпить нужно все параметры запроса, а в запросе их вставлять, как строку (в кавычках). Для лимитов и офсетов иметь свой сеттер и не позволять включать эти элементы в основное тело запроса. Вставлять пришедшие извне данные в качестве имён полей, таблиц и ключевых слов — запрещается.

Удачи в подстверждении заявленной эффективности sort() через замену его самописным пузырьком.
вы так и не поняли, о чём статья. и это реально жесть :)
я не собираюсь ничего заменять — я всего лишь хочу расширить.
Да понял я о чём статья, что Вы переживаете.

Статья о том, что Вы хотели немножко хакнуть prepared statements, чтобы они работали фактически на разных изолированных соединениях.

Я утверждаю, что слово «заявленная» в заголовке поста некорректное. Вы, как я понял, этого не отрицаете.

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

Ну да, параметрами запроса я считаю именно значения полей. Лимит и офсет можно сказать, с рождения привык добавлять отдельно, соответственно, применять совсем другую логику (is_numeric и разумные ограничения). На место полей, таблиц и служебных слов вообще никогда напрямую ничего не вставлял, т.к. изначально не улыбалось так сильно завязываться на схему БД на клиенте и на высоких уровнях.

Хотите, можете оскорблять меня дальше. Дело Ваше. Главное, что за всю мою жизнь на моих проектах инъекций не сделал никто (один проект, в который пришёл, от этого уберёг, переведя его на DBAL, основанный на PDO и реализованный так, как я написал выше).

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

Кавычка + юнион, считывание параметров доступа к базе данных из файла (функция была доступна), phpmyadmin, база данных wordpress'а, смена пароля администратора (стандартный PASSWORD()), загрузчик файлов, shell.

Нет в защите от инъекций ничего сложного. Конечно, вместо того, чтобы городить огороды, мне стоило сразу всё расписать, а не выдавать информацию по частям. Особенно, с оппонентом, который жаждет подловить тебя на слове.
с лимитами и оффсетами копаться не нужно.
мы с вами тут не методики защиты запросов обсуждали, а вашу дурацкую фразу.

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

привет! :)
А расскажите про защиту limit'ов без выделения их обработки отдельно от обработки остальных параметров.

На самом деле, интересно. Я не представляю, как можно без парсинга запроса определить, что вот этот вот параметр — limit и он должен быть числом.
Значения плейсхолдеров-то как собираетесь обрабатывать? Неужели без экранирования?

Или Вы в плейсхолдер зашиваете информацию о типе данных?
Извините, но считаю фразу — «плейсхолдеры и белые списки» — дурацкой.
$query = 'SELECT * FROM `a` WHERE `id` = :id';
$query = str_replace(':id', '1; DROP', $query);

Плейсхолдер? Инъекция?
Из Вашей статьи по защите от инъекций:
SELECT * FROM table WHERE id >? LIMIT?

И пишите, что она абсолютно безопасна.

Во-первых, ничего не говорите о том, как потом эти плейсхолдеры обрабатывать. Всё, что пишите, это:
Из мануала:
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name LIKE ?");
$stmt->execute(array("%$_GET[name]%"));
$data = $stmt->fetchAll();


И мануала по чему? Какой библиотекой нужно пользоваться для обработки плейсхолдеров? Какая библиотека правильно их обрабатывает?

Похоже, Вы имели ввиду PDO. Ну так укажите явно — «я привожу в пример недо-DBAL».

Если говорите о каком-то самописном DBAL, давайте поговорим о биндинге значений и как Вы там не используете экранирование.

Насчёт плейсхолдера для лимита — это вызывает трудности, особенно у новичков. Из-за необходимости приведения к int'у и явного указания (не во всех версиях PHP) PDO::PARAM_INT. А если есть приведение к int'у, то какой смысл вообще использовать плейсхолдер для лимита? Разве что для переиспользования prepared statement, если это возможно. Говорите, с лимитами и офсетами не нужно отдельно заморачиваться?

Вся Ваша статья про защиту от инъекций, подтверждение моих слов — берите PDO и знайте схему своей базы данных.
А виноват, очень много говорите о том, как обрабатывать плейсхолдеры. Причём не без упомянутой выше и задевшей Вас до глубины души функции.

Уделю той Вашей статье побольше внимания — идея с типизированными плейсхолдерами заслуживает уважения. Причём очень изящно выбран сам формат плейсхолдеров.

Сам уже давно не использую низкоуровневые самописные решения, но для общего развития читать интересно.
Не надо передёргивать.
Если вы до сих пор не поняли, что меня задело, то вы не поняли вообще ни слова из всей ночной переписки.
> делать mysql_escape_string каждому внешнему параметру, как это нужно делать в случае использования mysql

Ох. Вам сюда. Специально для тех, кто пишет эту чудовищную фразу, я писал тот пост. Ни «внешние», ни «все», ни «параметры» не имеют к функции mysql_escape_string ни малейшего отношения. У неё очень узкая область применения, не имеющая никакого отношения к защите от инъекций.

Плюс к тому лично мне не надо делать mysql_escape_string руками даже в случае использования mysql. Потому что я никогда не использую сырые функции API в коде, а пишу над ними обёртку, которая реализует все нужные механизмы для работы с БД — в том числе и использование плейсхолдеров.

> если есть инлайн-параметры, то запрос не будет подготавливаться?

Речь не об инлайн-параметрах а об обработке плейсхолдеров.
:id либо подготавливается, либо экранируется. Одно из двух. Но не одновременно.
Вы берёте мою фразу, вырываете её из контекста и начинаете критиковать.

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

Тем не менее, сравнивая mysql и PDO — в PDO за меня всю функциональность плейсхолдеров делает библиотека.

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

Прошу прощения за создавшуюся путаницу в параметрах, которые получает скрипт, и параметрах prepared statements.
А насчёт плейсхолдеров, Вы хотите сказать, что Ваше самописное решение не делает экранирования параметров, которые у Вас биндятся к плейсхолдерам?

Далее, Вы пишете:
Ни «внешние», ни «все», ни «параметры» не имеют к функции mysql_escape_string ни малейшего отношения. У неё очень узкая область применения, не имеющая никакого отношения к защите от инъекций.

В то же время читаем сначала про mysql_real_escape_string:
This function must always (with few exceptions) be used to make data safe before sending a query to MySQL.

И далее про mysql_escape_string:
This function is identical to mysql_real_escape_string() except that mysql_real_escape_string() takes a connection handler and escapes the string according to the current character set.

Теперь про подготовку с экранированием:
:id либо подготавливается, либо экранируется. Одно из двух. Но не одновременно.

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

Ещё мне кажется, что Вы некорректно используете термин «плейсхолдер» для параметров prepared statement. Упоминание плейсхолдера здесь корректно только для описания самого запроса. При подготовке запроса никакие данные в плейсхолдер не подставляются, данные передаются в виде параметров к prepared statement. В общем, в этом основное отличие параметров prepared statement от плейсхолдеров, реализованных без использования prepared statement.
«Моё самописное», равно как и любое другое осмысленное решение (в частности, PDO) делает экранирование (в смысле добавления слешей к спецсимволам) только в строках, а не во «всех параметрах».
Именно поэтому функция quote() в PDO автоматически делает SQL строкой всё, что в неё попадает — добавляя кавычки по краям. Поскольку только в строках эскейпинг имеет смысл (и обязателен). Одно без другого — кавычки без эскейпинга или эскейпинг без кавычек — не имеет смысла.

> This function must always (with few exceptions) be used to make data safe before sending a query to MySQL.

К сожалению, эту фразу писал идиот.
Несмотря на все мои усилия, её не хотят убрать из документации, оправдывая это тем, что убирают всё расширение целиком.

Попробуйте применить к анализу проблемы не цитату из документации, а свой опыт и знания, если есть. Или хотя бы попробуйте выполнить пример запроса, который я дал выше, проэскейпив $_GET['id'].

> При подготовке запроса никакие данные в плейсхолдер не подставляются, данные передаются в виде параметров к prepared statement.

1. в этом случае никакого экранирования передаваемых данных не происходит.
2. В PDO это всего лишь один из двух вариантов. По умолчанию PDO подставляет данные прямо в запрос (экранируя при этом строки), а prepare не использует.
Поэтому я пишу, что или экранирование, или подстановка. Теперь понятно?
Честно говоря, при использовании библиотек mysql и mysqli все значения параметров в любом случае писал/передавал в кавычках. MySQL это воспринимает, как валидную ситуацию, что избавляет нас от кучи головной боли.

Если Вы ожидали получить «1», а получили «1; DROP ...», то в Вашем запросе это в любом случае будет строкой, а экранирование убережёт от инъекции терминального символа. Поэтому в моём случае значение плейсхолдера экранировалось и ставилось в кавычки. В этой ситуации и точка с запятой проблем не вызовут.

Под экранированием всех внешних параметров я как раз и подразумевал, что вместо числа Вам могут захотеть подсунуть строку, которую Вы забудете проверить на входе в систему. Тем более, что из GET-а Вам в любом случае приходят строки. А не зная схему базы данных (что обычно бывает при использовании низкоуровневых DBAL), Вы никогда не узнаете, должен ли параметр быть числом или строкой. Всё, что Вам остаётся — экранирование. Насчёт того, что экранировать внутри нужно только date и string — это понятно. Но опять же, я говорю о более высокоуровневом подходе, а именно, о типах данных в базе данных, а не о типах данных переменных внутри PHP.
> при использовании библиотек mysql и mysqli все значения параметров в любом случае писал/передавал в кавычках.

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

во-вторых, «в любом случае» это всё равно не работает.
это не работает для параметров оператор LIMIT
это не работает для идентификаторов.

в-третьих, экранировать надо не только «внешние» строки, но и «внутренние». вообще любые.

так что ваше правило «эскейпить всё внешнее» не работает. А является, по факту, причиной большинства инъекций.

вам бы очень было полезно отбросить зазнайство, признать тот факт, что вы ничего не смыслите в составлении SQL запросов, а только повторяете несколько зазубренных правил, не понимая их смысла — и попробовать, всё-таки, разобраться в вопросе.
я понимаю, что это тяжело, особенно после рассказов о том, какой негодный материал ходит к вам на собеседование. но это будет сильно полезнее тех чудес изворотливости, которые вы сейчас демонстрируете.
Ну не доводите до абсурда. LIMIT — это не часть схемы, он в любом случае int. В самописном DBAL всегда сеттер для этих параметров выносил в отдельные методы. Захотел засетить LIMIT стандартным способом через плейсхолдер — получил ошибку в запросе. И это правильно.

Идентификаторы — это часть схемы. Не надо с ними работать, как с параметрами. Если Вы оголтело принимаете идентификаторы, то Вы можете получить фильтр по непроиндексированному полю таблицы с парой миллионов записей и В итоге получить по лицу.

Насчёт корелляции наличия оскорблений и отсутствия аргументов уже давно высказались некоторые мудрые люди.
вы очень длинно пишете :)
гораздо честнее и короче было бы написать
да, признаю: эскейпить все входящие параметры — дурацкая идея, с безопасностью ничего общая не имеющая
:)
Вы очень длинно пишете. Гораздо честнее и корояе было бы просто переименовать пост.

P.S. Удачи с самописным DBAL, написанным в обход PDO.
Чтобы Вам было неприятнее, скажу ещё раз: экранирование — это последний этап очистки параметров. И да, оно чистит.

magic_quotes_gpc был направлен на те же цели. Реализация — крайне бестолковая, при правильном подходе не нужная и сильно осложняющая жизнь.
So why did this feature exist? Simple, to help prevent SQL Injection. Today developers are better aware of security and end up using database specific escaping mechanisms and/or prepared statements instead of relying upon features like magical quotes.
Чтобы не забывать проверять на входе в систему нужно следовать одному правилу: во всех выражениях типа $sql .= " WHERE id = " . $id; $id должно или приводиться к целому, или экранироваться и квотироваться, то есть или $sql .= " WHERE id = " . (int)$id; или $sql .= " WHERE id = '" . mysql_real_escape_string($id) . "'";, даже если строчкой выше идёт строка $id = 1;
Первое — это если Вы знаете, какие именно поля у Вас численные.

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

> все прелести в виде prepared statements и автоматическом экранировании.

Что такое автоматическое экранирование?
Да, мне уже указали на моё некорректное высказывание. Я имею ввиду, что дополнительных действий в виде экранирования не требуется.
Ну, т.е. разобрались, что при использовании подготовленных выражений ничего и нигде не экранируется (если не используется эмуляция)?
А есть тесты производительности?
Немного не в тему, но скажу про то, с чем мы встретились при использовании prepared statements для работы с MSSQL. У нас система внутренняя система на PHP с достаточно большим количеством запросов к БД, некоторые из которых довольно тяжелые. Все запросы сделаны с использованием prepared statements.
Дак вот, однажды нам в голову пришла идея добавлять комментарии в SQL код, с данными пользователя, который запустил запрос(идентификатор, IP адрес), для того, чтобы было легче отслеживать кто тяжелые вопросы запускает. Технически это реализовать было несложно, и на тестовом сервере все работало прекрасно, однако когда выкатили на продакшен, это сразу же повесило всю систему. Оказалось, что MSSQL составляет заново планы для любого отличающегося запроса, даже если в нем отличается лишь комментарий, который по идее никак не влияет и должен вообще отбрасываться при выполнении запроса. Весьма странно поведение, но пришлось от идеи отказаться.
Не уверен, как бы вел себя MSSQL если бы не было prepared statements, возможно тоже определял бы, что запросы одинаковы и не стал бы строить план выполнения по новой, но возможно и prepared statements влияют.
Спасибо, наоборот — очень в тему!
Такие вот наблюдения из практического опыта очень ценны — можно будет понять что к чему, если mysql поведёт себя так же.
Если память не изменяет, в mysql кеш запросов производит побайтное сравнение. И отличие будет не только при разных комментах, но и разном числе пробелов.
Вроде хэш запросов сравнивается.
Не могу утверждать, но даже при сравнении хешей изменение байта будет фатальным для кеша плана выполнения.
Я просто уточнил, что сравнение не побайтовое самих запросов, а их хэшей. Всё-таки время поиска постоянно, а не зависит от длины запроса.
Если говорить о кеше запросов, то я опираюсь на следующие выборки:
Отсюда:
Queries must be absolutely the same. As no parsing is done before lookup queries are not normalized (would require parsing) before cache lookup, so they have to match byte by byte for cache hit to happen. This means if you would place dynamic comments in the query, have extra space or use different case – these would be different queries for query cache.

Отсюда:
… при получении запроса MySQL определяет, равны ли первые три символа запроса «SEL». Отсюда следуют два важных правила: 1) MySQL выполняет побайтовое сравнение, поэтому запросы, имеющие отличие хотя бы в одном символе будут рассматриваться как два разных запроса.

Не помню откуда я взял, что в качестве ключа в кэше используется хэш, но откуда-то взял. А первые три символа проверяются, чтобы вообще определить есть ли смысл искать в кеше запрос.
Вот потому я и писал выше, что это не истина в первой инстанции, а лишь ранее прочитанное в достаточно авторитетных для меня источниках. Если будет инфа в пользу сравнения хешей (которые, в прочем, не влияют на результат) — мой кругозор расширится.
В документации:
Queries must be exactly the same (byte for byte) to be seen as identical.
Ну, различие строки на байт изменит хэш.
Согласен. Просто насчёт хэша ничего не удалось найти. В принципе, вопрос-то интересный.
Из книги High Performance MySQL (Derek J. Balling, Jeremy Zawodny):

MySQL attempts to locate the results of any SELECT query in the query cache before bothering to analyze or execute it. It does this by hashing the query and using the hashed value to check for the results in the cache. MySQL uses the exact query text it receives, so the cache is sensitive to the most trivial variations.

As far as the cache is concerned, the query:

SELECT * FROM table1
is different from:

select * FROM table1
Значит не приснилось мне про хэши :) Спасибо.
По-моему логичное поведение. Так СУБД приходилось не только парсить запрос, но ещё перед этим канонизировать — а это уже почти тоже самое что и парсинг.
Ну в MariaDB (форке MySQL) есть возможность включить игнорирование комментариев. О полноценной канонизации речи вроде нет, простая регулярка.
Sign up to leave a comment.

Articles