views:

111

answers:

5

I have three tables:

  1. USER: user_id (pk); username
  2. FIELD: field_id (pk); name
  3. METADATA: metadata_id (pk); field_id (indx); user_id (indx); value

The reasoning for this is that the application allows custom fields to be created for each user. To display all the information for a user I am building a dynamic query (via PHP) which ends up looking like this:

SELECT
  u.username, m1.value AS m1value, m2.value AS m2value
FROM user AS u
LEFT JOIN metadata AS m1
  ON (u.user_id=m1.user_id AND m1.field_id=1)
LEFT JOIN metadata AS m2
  ON (u.user_id=m2.user_id AND m2.field_id=2)

This example has only 2 user metadata fields, but you get the idea what how this would look if there were a dozen fields.

Is there another, better way to write this query? I'm worried about the performance of this query as the users and metadata fields grow.

EDIT: I'd like to have one user per row in the returned results.

A: 

You will have to have two queries. One for user retrivial (SELECT * FROM users) and then another that will pull user's custom fields (SELECT * FROM fields WHERE users_id = user_id).

You could pull it off with one query in certain cases though ... tell us more on what exactly is the result you are going after.

Jan Hančič
+2  A: 

Why not just grab them all at once?

SELECT u.user_id,u.username, m.field_id,m.value FROM user u
LEFT JOIN metadata m
ON u.user_id=m.user_id 
WHERE 1 ORDER BY user_id

Or for a particular user:

SELECT u.user_id,u.username, m.field_id,m.value FROM user u
LEFT JOIN metadata m
ON u.user_id=m.user_id 
WHERE user_id = ? ORDER BY user_id

Beyond being indexed, make sure the user_id is exactly the same type and length between the two tables, or you still end up doing table scans.

What language is your server code?

A simple way to get one row per user (kinda) is in your loop returning the rows, check each user_id if it's the same as the last. If not, new row.

while ( $row = $sth->fetch_object() ) {
  $previous_user_id = '';
  if ( $row->user_id != $previous_user_id ) {
    # new row
  } else {
    # not new row
  }
  $previous_user_id = $row->user_id;
}
Daren Schwenke
This would work yes, but ideally I'd like one row returned per user.
Bart
PHP/MYSQL is the server code/db
Bart
Bart it might be faster to return them this way and then have the application pivot them. You also no longer need to dynamically build the query as well. Nor will you have to send code beforehand to figure out how many custom fields to add to your dynamic query.
HLGEM
After doing some tests both ways I have found that they actually perform nearly identically speed wise. This example of performing the pivot with PHP is a hair faster though.
Bart
A: 

You'd normally return one meta data element per row, as in:

SELECT u.username, mi.field_id, m1.value
FROM user AS u
LEFT JOIN metadata AS m1 ON u.user_id = m1.user_id

This should perform fine up to thousands of users.

Andomar
The target number of users I am planning to work with is 50,000. And I'm guessing there will be 6 or so metadata fields.
Bart
If you know there's six, why not move those metadata fields into the main user table? That will perform much better, and 6 fields is not much disk space, especially if they're empty. A varchar(4000) with an empty string takes up 2 bytes on disk.
Andomar
A: 

you might try something like...

SELECT
    u.username,
   (SELECT TOP 1 m.value
    FROM metadata m
    WHERE u.user_id=m.user_id AND m.field_id=1),
   (SELECT TOP 1 m.value
    FROM metadata m
    WHERE u.user_id=m.user_id AND m.field_id=2)
FROM user AS u

... but the performance will probably be similar (and may be worse) to what you have. Check to make sure both user_id and field_id are indexed if you are having performance issues.

Matthew Whited
Good call on the performance note. Thanks. I do have a unique composite key of user_id-field_id.
Bart
A: 

(As a commenter pointed out, this is not valid in MySQL, so, sorry. But in case you're interested:)

Do one JOIN to the metadata table as suggested above, and then use PIVOT to change your many rows per user into one row with many columns, one per field. I think this is valid in SQL Server 2005 and later.

Kevin Conner
unfortunately he is looking for MySQL not MS SQL
Matthew Whited