Showing posts with label mysql. Show all posts
Showing posts with label mysql. Show all posts

Tuesday, March 23, 2010

The Dangers Of GroupBy

I had a complex join (three tables) with a group by and a where cause with three conditions. It seemed to be working, until I added more data; I eventually narrowed problem down to just a single table. Consider this query:

SELECT w.user_id, w.some_date, w.id
FROM mytable w
GROUP BY w.user_id


id is the primary key. That query returns all the data:

user_id some_date id
1 2010-02-01 1
5 2010-02-02 2
1 NULL 3
1 2010-03-22 4
1 2010-03-24 5


I'm only interested in the record with the latest date for each user, so I add a MAX on some_date and GROUP BY user_id:

SELECT w.user_id, MAX(w.some_date), w.id
FROM mytable w
GROUP BY w.user_id


That gives:

user_id MAX( w.some_date ) id
1 2010-03-24 1
5 2010-02-02 2


Hang on! The dates are right, but 2010-03-24 is from id=5, not id=1. The some_date column has been max-ed in isolation.


This issue is explained in an excellent two-part article: part1 part2

In fact it is so excellent I didn't fully understand it, and am going to re-read it three times a day until I do. I really need to understand it because, as the author shows, it is very easy to get correct answers from a query on your test data, pass all your unit tests, and deliver something that goes wrong when one more record is added.

My query is simpler than the one in his example, but after many tries I cannot find a solution. The "join(...)as d" syntax does not seem to work in MySQL or I am not using it correctly. It seems what I'm trying to do is very basic, so I do not understand why I cannot find more advice on the subject. I'm open to suggestions!

(For the moment I'm going to give up, do a simpler query and have PHP process the results to get the data I actually want.)

Thursday, August 20, 2009

Storing website translations in SQL

In php|a magazine, April 2009, there was an article called "Storing Multilingual Records in the MySQL Database". As the title says, it has some mysql-specific elements, but the concepts are quite general. The author introduced and compared three alternatives; as I have used a fourth way I thought I'd write about it here.

The situation was a product database, where the name, url and description have to be localized. But some entries will be untranslated and should fall back to using the default language entry.


1. Translations in a separate table


Here is the database schema:

product: product_id, group_id, price.
product_translation: product_id (links to product), language_id (links to language), name, url, description.
language: language_id, collation.

This requires an SQL join which is hard for mysql to execute efficiently. Fulltext indexes are also a problem, as the name and description fields contain text from different languages. The only good point of this approach is that it is easy to add a new language.


2. Data Copy


Here the group_id and price fields have been moved into the product_translation table. That makes the SQL queries a bit cleaner, as it saves one join, but doesn't really solve the other problems. And the data redundancy is an accident waiting to happen (I'm having a physical reaction just thinking about it).

3. Translation Directly In The Database


This is like 2 above, but each language gets its own field. For instance if we have three languages, English (en), German (de) and Japanese (ja) then it looks like:

product: product_id, language_id (links to language), name_en, url_en, description_en, name_de, url_de, description_de, name_ja, url_ja, description_ja, price, group_id.

The advantage this brings is using the default language is easy; you can either just always select the default language field, or use mysql's IFNULL function. E.g. either:
SELECT name_de,name_en WHERE product_id=123;
(and then check in your PHP script to see if name_de is blank, and if so use name_en instead)
Or:
SELECT IFNULL(name_de,name_en) as name WHERE product_id=123;

The other advantage is FULLTEXT indices work well now, as only one language is kept in each text column. Disadvantages are the space-wasting if actual translations are sparse, the work required to add a new language, and possibly hitting the mysql maximum row size (64K).

4. One table per language



This is the approach I've used in MLSN, among others:

product: product_id, group_id, price.
product_translation_en: product_id, name, url, description.
product_translation_de: product_id, name, url, description.
product_translation_ja: product_id, name, url, description.

Fulltext indices work well, joins are relatively easy, no data duplication. Adding a language feels cleaner than solution three above, as it doesn't require modifying an existing table, just adding a new one. If the site takes off then different languages can be split to different servers easily. And the language table can disappear as collation will now be part of the field definition when the table is created. (I think the same can be said of solution three, but the php|a article kept the language table in its schema - was that an oversight, or am I missing something?)

The dark side? Getting the default language requires two queries. Or would something like this (untested) SQL work?
SELECT ISNULL(de.name,en.name) FROM product_translation_de de,product_translation_en en WHERE de.product_id=123 AND en.product_id=123

Thoughts?



The 4th option works well for me, but I'd be interested to here arguments against it. Perhaps you use something different again?

Monday, June 2, 2008

Bidirectional unicode codes and Arabic

I have been importing Arabic data into MLSN (http://dcook.org/mlsn/) (incidentally, almost all data currently comes from the AWN project). We have a csv list of synonyms, and each synonym optionally has the Arabic root in square brackets. The whole list is an SQL string, surrounded by single quotes.

When I view in SciTE it looks exactly as I'd expect: the arabic is right-to-left, the square brackets are part of the flow, and then at the apostrophe we're back to left to right. (By the way, vi appearance is the same as SciTE)
But in gedit, firefox and open office writer, when the last synonym has a square bracket it gets jumbled up.

Here is how it looks in scite:


Here is how it looks in gedit:


So, I tried adding the unicode RLE (0x202B) character before the opening square bracket and before the closing square bracket. (Incidentally, in the tab-separated file this fixed the problem in all editors.)
In SciTE no change, which is good.
In gedit et al it now has the square brackets in the flow correctly, but it has been moved to the end of the line and the following SQL clauses are now right-to-left.
Putting a unicode LRE (0x202A) before the following comma did not help! (More precisely it moved the ",'awn');" part back into the left-to-right flow, but still left the Arabic string stranded on the right; but anyway the LRE on the comma causes MySQL problems, see below.)

Is this is a bug in all of gedit, firefox and open office? Could it be a linux or gnome bug and all those applications happen to use it, while SciTE/vi do not? Or am I doing something wrong? Any advice gratefully received!

Here is how it looks in gedit with the explicit RLE codes:



MySQL and Bidirectional Codes

It seems MySQL (after doing "SET NAMES UTF8;" of course), can cope with LRE/LRE inside a quoted string, but they cannot be in the SQL string itself. E.g. on a comma.

Firefox, IE6 and Bidirectional Codes

Without the RLE codes firefox messed up showing the square brackets in both normal display and in an edit form. With the RLE codes, just before each open and each close square bracket, it shows it correctly in normal display, but still gets it wrong in the HTML form input box.
(As an aside, IE6 on Windows XP is the opposite of firefox! The main table is (very) wrong but the edit box is correct!) (And as an aside to my aside, if there is one thing Windows does well it is i18n fonts: the Arabic looks beautiful.)

phpmyadmin has a textarea that shows it correctly (with firefox). They use an explicit dir="ltr" (?!). Using that did not work for me.

So, my solution was when the edit form is being used for Arabic is to dynamically set dir to "rtl", and "ltr" the rest of the time. And (for the IE users) also set dir="rtl" on the Arabic cells, and dir="ltr" on other cells, in the main table. There is no Arabic UI currently, but when there is the dir="rtl" will be set globally via a style sheet (which is why I set the default dir="ltr" explicitly on data cells for non-Arabic languages).

See it in action by doing a search on MLSN. Here is one example: http://two.dcook.org/software/mlsn/main.php?c=06car0
(mouseover the table cells, then clicking the cell will make the edit form active; compare Arabic with the other languages.)

I am open to suggestions for alternative solutions, but I believe this is the "proper" standards, cross-browser solution.