Certain things in MySQL really really gets to me...
MySQL has a weird non-standard way of quoting literals
For example, if you named a table "order", you need to quote it because order is also a reserved word in SQL:
Postgresql: select * from "order" MySQL: select * from `order`
Triggers are useless
Triggers are a controversial feature that many people prefer not to use, but they can be useful for validation. But not if you run an older version of MySQL, and they may turn out to be useless even in the newer versions. I am told this issue has been fixed in the latest versions, but we often have to deploy on older versions so I include this little gripe. Although you can "make things happen" upon an insert, update or delete, you cannot reject the data by raising some kind of error. The only way to do this is to do something illegal in your trigger to effectively crash the transaction, and then the error message does not match the crime at all.
Procedural support is limited
Its hard to write stored procedures or triggers (and therefore useful aggregate functions) for MySQL, because the languages at your disposal are so limited. Postgresql does spoil you with a plethora of choices, but even just one slightly-more-powerful-than-SQL yet slightly-higher-level than C language would help a lot.
It has no sequences
Yes, it has auto_increment. Yes, you can implement your own makeshift sequences, but then you have to use locking to ensure mutual exclusion to make sure two clients do not get the same sequence value.
Default values for a column must be a constant
When I define a column in a table, I can specify a default for the column. In Postgresql this can be any function, and in the normal auto-increment use case you use a function to get the next available value from a sequence. In MySQL, there are two special cases: NOW and auto_increment. All other default values must be constants.
GROUP BY allows selection of columns not in the GROUP BY clause
The documentation calls this a feature. MySQL will pick a representative value from the group if you don't tell it how to group a selected column. This allows people to write queries that appear to work, and they might even rely on the selected representative value without realising that there is no explicit way to know how it was picked. If you explicitly use an aggregate function in your where clause, you know exactly what to expect. Explicit is better than implicit. And besides, these queries are incompatible with other databases and hinders portability.
Strings sometimes evaluate to zero
This is where things become zopeish. On a recent project using ore.alchemist I spent an hour trying to figure out why acquisition does not work and I end up with a ghost object from the database instead of a template somewhere higher up. It turns out that when I asked for context/index_html, and due to an omission in my code that did not check for numeric keys, MySQL was asked to find a user with USERID='index_html'. Because USERID is an integer column, MySQL correctly raised a warning (which disappeared among many other lines in the log file), assumed that what I really meant is USERID=0, and returned that row instead. Obvious errors like this should not pass with just a warning but with a very loud and clear error.
It is not as fast as you might expect
I don't know if this might be limited to older versions or to the open source versions, but during development we noticed that MySQL only uses one core on the CPU. Its faster, but only when it runs on a single-core single-cpu system using the MyISAM engine. On a recent project, I found that Postgresql can easily outperform MySQL (with InnoDB) on a more powerful machine.