Pull to refresh

Comments 80

> Упрощенно проблемный запрос выглядел приблизительно так:

А можете привести не упрощённо, с куском реального кода и версиями клиента, драйвера и сервера?
хотя, нужна только версия драйвера и полный пример
так а собственно что непонятно?
rating = rating --5 например
Понятно всё. Сабмитить нужно, если проявляется в текущей dev-ветке.
Это всегда пожалуйста

avl@imac:~$ python
Python 2.7.1 (r271:86832, Jan 4 2011, 16:47:45)
[GCC 4.2.1 (Apple Inc. build 5664)] on darwin
Type «help», «copyright», «credits» or «license» for more information.
>>> import psycopg2
>>> psycopg2.__version__
'2.3.2 (dt dec pq3 ext)'

avl@imac:~$ psql --version
psql (PostgreSQL) 8.4.6

Выше приведенного примера вполне достаточно. На нем тот же эффект.
В 2.4.1a0 тоже присутствует. В DBAPI-2 вроде бы нет никаких указаний на тему явных разделителей, поэтому можно напрямую у разработчиков спросить, не является ли это фичей.
Вы тикет вот тут создавали?
нет. не создавал потому, что не уверен что это собственно баг
Чумовой пятничный пост! Спасибо :)
Почему вы не пользуетесь placeholderом '?'?
PostgreSQL здесь, кстати, совершенно не виноват.
Здесь никто не виноват. Топик не об этом. Он о том, что чудес не бывает и нужно внимательнее быть даже с очевидными, на первый взгляд, вещами
ну почему же, если я в дотнетном драйвере к postgre напишу «update pref_player set games=games-1, rating=rating-? where player_id=?» то он нормально прожуёт это. поскольку не занимается тупой конкатенацией строк.

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

Вы хотите сказать что дотнетовский драйвер превратит «rating=rating-?» при rating==-10 в «rating=rating+10» вместо позорного «rating=rating--10»? если да, то я могу только искренне порадоваться за прозорливость драйвера.
Это нормальное поведение, при чем тут прозорливость?
Детский сад какой то, я бы очень задумался о дальнейшем использовании этого драйвера, комьюнити то большое, неужели никто не сталкивался раньше?
Комьюнити пользуется более высокоуровневыми библиотеками типа SQLAlchemy, в которых данная проблема не проявляется в следствие строгого подхода к форматированию запросов при помощи явных разделителей и операторных скобок.
нормальные сервера поддерживают параметризацию на уровне протокола. не в курсе, как дела у postgresql, но в крайнем случае драйвер внутри себя можг бы заюзать переменные-курсоры
ох тыж… нормальные сервера, это какие? Oracle? MSSQL, прости госпади? Может MySQL? Чем это postgresql не нормальный?
это те, которые поддерживают параметризацию на уровне протокола
учтите, я не называл postgre ненормальным, и на 90% уверен что он это тоже умеет. просто я не могу утверждать этого, не зная его бинарного протокола.

зато я точно знаю что так делает оракл, фаербёрд и, прости хосспаде, мсскуэл.
Это нормальные драйвера используют нормальные плейсхолдеры. А не собирают «интеллектуально» значения в строку.
Все пракрасно с этим у Постгреса, это драйвер «насяльника, я тут минус полусиль, потом исё один минус полусиль, шайтанама, не знаю, чито делать… секаса не хочишь, насяльника?..»
значит в баню такой драйвер
UFO just landed and posted this here
Мда, жесть.
Я вот сколько смотрю, столько удивляюсь — ну почему люди до сих пор считают, что SQL-запрос — это строка?

Ведь на самом деле это команда+данные, практически везде существуют placeholders, через которые можно передавать команду отдельно, данные — отдельно. При этом данные автоматически приводятся к нужному типу, запрос автоматически защищается от любых SQL-injections, мы получаем автоматическую диагностику на несоответствие типа, и, как следствие, автоматически приучаемся писать правильнее, итд, итп.

Но нет — «update $table set $field=$value where $where».
И перед всем этим
$value = urldecode(mysql_escape(encode_sql_wo_injections(megaescape(superdupermegafunction($_GET['value'])))
(названия функций не имеют отношения к реальным людям :))

Юзайте плейсхолдеры, они фидорулез.
В примере %s — плейсхолдер. Не путайте с deprecated string formatting syntax в питоне, который теперь замещён str.format().
В спецификации DBAPI2(в разделе «Module Interface») указаны возможные синтаксические конструкции для плейсхолдеров. Просто в самом постгресе плэйсхолдеры не такие. И psycopg использует не родной интерфейс для независимости.
Не путайте с deprecated string formatting syntax в питоне, который теперь замещён str.format().

К слову, весьма не везде. С тем же логгингом совершено непонятно что делать.
Может и плейсхолдер, для драйвера, однако к БД отсылается как обычная строка, что говорит о том что драйвер — гадость, не использует то, что должен.
'format' ANSI C printf format codes,
e.g. '...WHERE name=%s'
то есть таки %s — это строка. почему она не в "" закодирована была при форматировании?!
какой-то ужас в драйвере, получается, наверчен. откровенный.
Я предлагаю сначала ознакомиться со спецификацией не бегло, а вчитываясь в описание приведенных примеров. Конкретно в этом примере из PEP никакие кавычки не нужны. Он показывает, каким может быть маркер для плейсхолдера согласно стандарту DBAPI2 для python-библиотек, работающих с реляционными СУБД.
А затем сходить в документацию по драйверу:
initd.org/psycopg/docs/usage.html#passing-parameters-to-sql-queries
initd.org/psycopg/docs/usage.html#the-problem-with-the-query-parameters
и понять, почему это именно плейсхолдер, а не форматирование python-строки.
Синтаксис у них почти одинаковый. Способ передачи в интерфейс — разный.
Я посмотрел: psycopg2 собирает именно строку (и это кошмар), а нормальный placeholders не имеет.

А вот тот же py-postgresql умеет native placeholders. Понятно, что только за счёт cached prepare statements py-postgresql может быть до двух раз быстрее.
Нормальный плейсхолдер — это такой маркер, который заменяется пришедшими вместе с запросом параметром на стороне сервера. Принципиальной разницы в экранировании — никакой. Если вы мне объясните почему это именно «кошамар», буду благодарен.
Prepared Statements в psycopg2 нет. В Python 2.x нет py-postgresql.
Возможно, медленнее, тогда shame on python. Ищите более грамотный драйвер.

Кеширование prepared запросов + правильная реализация placeholders _обязаны_ давать существенный прирост производительности по сравнению с кустарщиной escapeленных строк.
за исключением запросов с «like ?»
но и в этом случае можно подсунуть правильный план выполнения вручную
То, что он его заменяет не передачей в sprintf а через геморрой не даёт никаких преимуществ над просто sprintf обработкой, так как на выходе всё равно получилась строка, которая поломала запрос в очень и очень далёкий от исходного.
Вы не любите кошек? Да вы просто не умеете их готовить.
Это относится либо к Вам, либо к тем, кто такой драйвер писал. :) Думаю, к авторам драйвера, ибо я, к стыду своему, в прошлый раз подумал, что вы через spritnf sql-запросы клепаете, а тут оказалось, что это просто эмфемизм такой был.

Честно говоря, разбираться в тонкостях реализации ненужного мне драйвера на языке, на котором я не программирую, мне леняво, но любой нормальный драйвер БД, который декларирует умение placeholders обязан доставлять данные в placeholders до самой БД. В противном случае это самописная функция пионера Васи, которая эмулирует placeholders.
Хорошо. Вы мне только объясните, в чём принципиальная разница между операцией экранирования (описанной в соответствие с правилами стандарта SQL или правилами Postgres) на стороне сервера и на стороне клиента?
в том, что на стороне сервера экранирования не происходит
в любой книжке про то, как устроен любой приличный sql-сервер. в главе про парсер запросов
скорее всего точно так же (он ведь приличный сервер, не так ли?).

отдельной строкой отправляется запрос, идёт в парсер (который, кстати, теперь может закэшировать результат парсинга и план выполнения).

и отдельными _бинарными_ данными — все параметры. соответственно не тратится всремя на их эскейп и санитизацию на клиенте и на декодирование обратно и парсинг на сервере.
Ок, это справедливо, с этим я согласен.
Только разбор выражения и вычленение маркеров там производится, проверка типов поля — тоже производится, и думаю, приведение массива байт к какому-то внутреннему представлению (также в бинарном) тоже происходит. Есть измерения, сколько времени уходит конкретно на парсинг текстового представления данных, если они передаются в hex-escape-последовательности (без учёта разбора всего выражения)?
очевидно, зависит от размера и типа данных.

и для блобов это не так много, как например для строк или больших чисел; поскольку в hex длина итоговой строки = длина hex представления / 2, а для других форматов приходится извращаться.

если вам надо изредка вставить немного данных — то тут больше важна не скорость, а принципиальное отсутствие _любого_ преобразования данных.
и мы можем быть уверены, что:
1. в запрос не попадёт инъекция или синтаксическая ошибка
2. наши данные не покоцаются при эскейпе-декодинге

(за исключением случая, когда ваш драйвер работает не в той кодировке, в которой сохранена текущая таблица, но вы же так не делаете да?)
Ok, начнем сначала.

Когда на Земле бегали динозаврики, а папоротник мешал работать солнечным батареям, SQL-запросы состояли только из текста (строчки). Чтобы сделать запрос, приходилось эту строчку формировать, спасаясь от SQL-injects.

Потом умные люди придумали placeholders. Это означает, что мы передаем в сервер запрос вида «UPDATE table_name SET field1=? WHERE fiedl2=?», плюс набор данных, два числа, например, 1 и 4. Этот набор данных (string и два integer) в неизменном виде доходят до сервера, и дальшу, до его персера запросов, и уже там, почти при начале поиска, подставляются как аргументы функции. Не добавляются в строчку запроса, а именно передаются уже при запросе как аргументы поиска и апдейта, прямо как были, в виде integer.

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

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

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

Там в любом случае должно быть приведение к внутреннему формату из бинарного представления, полученного от клиента. Например, как параметризованно передается intarray?
Разумеется, оно есть, тонкости мне неизвестны, в коде постгреса настолько не ковырялся.

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

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

Я нигде не написал, что преобразуется, я написал, что даже если оно так делает, все равно оно должно это делать быстрее, чем из строки, и что даже если и не быстрее, все равно мы получаем кучу плюсов.
вопрос был на пост выше ;)
На стороне клиента — мы предпринимаем нечеловеческие усилия, чтобы проверить, правильный ли у нас integer, не подсунули ли нам SQL-inject, преобразуем его в строку, потом передаем внутри query на сторону сервера, а потом оказывается, что «черт побери, integer может содержать минус».

Если мы используем placeholders, то значение integer передается в неизменном виде в сервер, где подставляется для использования после проверки. При этом:
— нам не надо ничего проверять и ни от чего защищаться (по крайней мере, фатальных последствий не будет)
— как следствие, код более читаем
— нам не надо думать «а как может собраться строчка и что за алиен оттуда выпарсится?»
— мы не теряем время на преобразование integer в строку и сервер не теряет время на преобразование его обратно
— все проверки идут на стороне сервера, а значит, это делается единым образом для всех платформ, языков и драйверов

Как-то так.
А вот если бы автор вместо rating=rating-%s написал бы хотя бы rating = rating - %s, то он бы не открыл для себя практическую пользу хорошего coding style-а.
всегда удивлялся, как люди из простой проблемы, могут написать пост о глобальном катаклизме
А дальше автор будет возмущаться что — в коде внезапно даст декремент?
О таких вещах обычно пишут не на хабр, а в багзиллу.
Всем кто так ратует за prepared statements стоит ознакомиться с тем как Postgre планирует запросы, а он в этом довольно хитёр и использует статистическую информацию о распределении значений в колонках таблиц, чтобы составить лучший план. При использовании prepared statements конкретные значения параметров недоступны и поэтому план может быть не лучшим, что в итоге будет приводить к худшей производительности, чем просто серия обычных строковых команд.

Кроме того, код должен достаточно низкоуровневым, чтобы использовать один подготовленный statement для нескольких похожих изменений.
А можно привести пример, когда prepared будут выполняться дольше? Может быть не какой-то конкретный пример, а пример в общем виде? Мне не очень понятно, что за статистическая информация может быть о распределении значений, а читать документацию некогда — ещё «Работа с Postgresql» А.Ю.Васильева не прочитана, хоть и распечатана и на столе лежит…
Простой пример — таблица с булевым полем, которое в 99% — true, а в 1% — false и есть индекс по этому полю (пример дурацкий, но и так бывает).

Если мы делаем выборку где это поле false, то быстрее будет вытащить по индексу. Если вытаскиваем по true, то быстрее просто сделать полный перебор, отбросив встречающиеся по ходу false (так как последовательное чтение сильно быстрее, чем случайное по индексу).

Если оптимизатор будет заранее знать что мы ищем, то он сможет сгенерировать правильный план. Если нет, то будет как получится — или происк по true будет выполняться не оптимально, либо по false.
А вам следует понимать, что это сильно зависит как от данных, так и от характера запросов. Как правило, в типичной OLTP системе большинство запросов вида «select/update entity where id = ?», распределение этих id равномерное, и выигрыша от литералов нет, а выигрыш от prepared statements наоборот огромен. Бывают запросы, выигрывающие от литералов, но их не так много, при правильном дизайне.
UFO just landed and posted this here
Мне кажется или с тем же MySQL возможно аналогичные грабли?
вот так проблема не решиться( это кусок, если бы писала его моя команда )?

sUpdate = """
update
pref_player
set
games= ( games — 1 )
, rating= rating — %(rating)s
where
player_id= %(player_id)s
"""

cursor.execute( sUpdate, {'rating': rating, 'player_id': player_id} )

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

Не решится. хоть rating-%(rating)s, хоть rating-%s никакой роли не играет. Решится она либо пробелом справа от минуса, либо взятием в скобки. Способ задания параметров абсолютно не важен
Да, давно работаю с psycopg2 и всегда считал, что использую настоящие плейсхолдеры и все делаю правильно. И в самой документации на их правильное использование делается большой акцент. И вот тебе на.
Создал тикет.
Меняем статус игры, откатываем денормализационные данные со статистикой игроков, инвалидируем оперативные кеши, затрагивающие эти данные, и дело в шляпе.
Черт, не туда ткнул.
Меняем статус игры, откатываем денормализационные данные со статистикой игроков, инвалидируем оперативные кеши, затрагивающие эти данные, и дело в шляпе.

Есть мнение, что если таким образом отменяются результаты игры — значит что-то в корне неправильно с дизайном базы.
Ибо, например, статистика игрока — всегда должна быть функцией от сыгранных игр. Так же, как и рейтинг игрока. И, соотвественно, должна вычисляться на ходу (через функцию, либо view, либо materialized view, если рассчеты сложные). Тогда никогда не будет нарушена нормализация базы, и не нужно будет делать такие шаманства, как описывал автор. Нужно будет всего-лишь изменить статус игры.
Есть мнение, что мир не бинарен. Ситуация. Четверо игроков сыграли партию в преферанс. Им насчитала рейтинг и прочую статистику функция от сыгранных игр. Далее один из игроков заподозрил что остальные играли нечисто. Попросту говоря «на лапу», втроем против него одного. Он стандартной процедурой отправил партию на арбитраж, предоставив в качестве доказательства протокол партии (опять же стандартной процедурой). Арбитры просмотрели протокол и пришли у выводу что мошенничество имело место быть. Дальше вопросы с моими ответами:

  1. Нужно ли обнулить рейтинги и статистику набранные в этой партии? Да, Нужно
  2. Нужно ли эту партию вообще удалить из общей истории игр? Нет, не нужно. Партия была сыграна и протокол должен быть доступен
  3. Есть ли проблемы с дизайном базы? Нет. С дизайном базы все в порядке

  4. Ваши ответы на вопросы 1,2,3?

1. Рейтинги и статистика должны обнулиться сами. Ровно в тот самый момент, когда игра будет помечена как недействительная.
2. Нет, игру удалять конечно же не нужно. Нужно пометить её как недействительную.
3. Да, если приходиться делать запросы вида «update pref_player set games=games-1, rating=rating-%s where player_id=%s», (rating, player_id)" — значит дизайн базы неправильный. У игрока вообще не должно быть полей rating и games. Ибо эти поля легко вычисляются из истории сыгранных им игр. Если это вдруг очень тяжелые расчеты — можно всегда хранить их в materialized view. Но никак не в виде атрибутов игрока. Ибо это может приводить к потере целостности базы. Что мы и наблюдаем, кстати.
Ключевое слово «денормализация». Поверьте на слово, materialized view в данном случае не спасают.
Sign up to leave a comment.

Articles