views:

66

answers:

4

Hi. I have 3 tables:

recipe_category

cid | category
 1  | desserts
 2  | cakes
 3  | biscuits

recipe_name

id | recipe_name       | cid | iid
 1 | black forest cake | 1,2 | 1,2,3,4
 2 | angel cake        | 2   | 1,2,4
 3 | melting moments   | 3   | 2,5
 4 | croquembouche     | 1,3 | 1,5

ingredients

iid | ingredient_name
 1  | self-raising flour
 2  | milk
 3  | chocolate
 4  | baking powder
 5  | plain flour

I am able to query the DB using cid to pull certain recipes, ie. desserts:

SELECT * FROM recipe_name WHERE cid='1'

But then how do I create a list of ingredients like the below where ingredients are listed with <br>?

Black Forest Cake:
Self-Raising Flour
Milk
Chocolate
Baking Powder

I'm new to this, so please forgive any stupid questions!

+1  A: 

First off you should be using an intersect table. In this table you will have a list of keys for recipe_names and a key for each of the ingredients. So for black forest cake you will have 4 rows in the intersect table. This allows you to build queries much quicker and easier. You don't have to mess around with key arrays.

The table may look like this for black forest cake:

recipe_name_id  ingredients_id
1                 1
1                 2
1                 3
1                 4

A query would look like this:

SELECT ingredient_name
  FROM recipe_name
     , ingredients
     , recipe_ingredients --intersect table
 WHERE recipe_name.id=1
   AND recipe_name.id = recipe_ingredients.recipe_name_id
   AND ingredients.id = recipe_ingredients.ingredients_id
northpole
A: 

The fact that you're asking about <br>tells me that you want to display these in a web page. My advice is to separate querying for the data from displaying it.

Since a recipe can have many ingredients, and an ingredient can appear in many recipes, you'll need a many to many join table between recipe and ingredient. It'll have two columns - one for the recipe primary key and another for the ingredient primary key.

Your comma-separated values breaks normalization rules. You'll want to know what those are.

duffymo
+4  A: 

Storing multivalued attributes in a single comma separated field is almost always a bad idea. It makes everything very difficult to query.

Instead, you may want to consider refactoring your schema, using two new intersection tables.

These two tables remain as they are (just changed name of recipe_category to categories in order not to clash with the intersection table):

CREATE TABLE categories (
   cid int NOT NULL PRIMARY KEY,
   category_name varchar(50)
) ENGINE=INNODB;

CREATE TABLE ingredients (
   iid int NOT NULL PRIMARY KEY,
   ingredient_name varchar(50)
) ENGINE=INNODB;

Modify the recipe_name table as follows, removing the cid and iid fields:

CREATE TABLE recipe_name (
    id int NOT NULL PRIMARY KEY,
    recipe_name varchar(50)
) ENGINE=INNODB;

Then you can define your multivalued relationships using the following two intersection tables:

CREATE TABLE recipe_ingredients (
    recipe_id int NOT NULL,
    ingredient_id int NOT NULL,
    PRIMARY KEY (recipe_id, ingredient_id),
    FOREIGN KEY (recipe_id) REFERENCES recipe_name (id),
    FOREIGN KEY (ingredient_id) REFERENCES ingredients (iid)
) ENGINE=INNODB;

CREATE TABLE recipe_categories (
    recipe_id int NOT NULL,
    category_id int NOT NULL,
    PRIMARY KEY (recipe_id, category_id),
    FOREIGN KEY (recipe_id) REFERENCES recipe_name (id),
    FOREIGN KEY (category_id) REFERENCES categories (cid)
) ENGINE=INNODB;

Now let's populate these tables with your example data:

INSERT INTO categories VALUES (1, 'desserts');
INSERT INTO categories VALUES (2, 'cakes');
INSERT INTO categories VALUES (3, 'biscuits');

INSERT INTO ingredients VALUES(1, 'self-raising flour');
INSERT INTO ingredients VALUES(2, 'milk');
INSERT INTO ingredients VALUES(3, 'chocolate');
INSERT INTO ingredients VALUES(4, 'baking powder');
INSERT INTO ingredients VALUES(5, 'plain flour');

INSERT INTO recipe_name VALUES(1, 'black forest cake');
INSERT INTO recipe_name VALUES(2, 'angel cake');
INSERT INTO recipe_name VALUES(3, 'melting moments');
INSERT INTO recipe_name VALUES(4, 'croquembouche'); 

To define the relationships between the recipes and their ingredients and categories, you would need to fill up the intersection tables as follows:

INSERT INTO recipe_categories VALUES (1, 1);
INSERT INTO recipe_categories VALUES (1, 2);
INSERT INTO recipe_categories VALUES (2, 2);
INSERT INTO recipe_categories VALUES (3, 3);
INSERT INTO recipe_categories VALUES (4, 1);
INSERT INTO recipe_categories VALUES (4, 3);

INSERT INTO recipe_ingredients VALUES (1, 1);
INSERT INTO recipe_ingredients VALUES (1, 2);
INSERT INTO recipe_ingredients VALUES (1, 3);
INSERT INTO recipe_ingredients VALUES (1, 4);
INSERT INTO recipe_ingredients VALUES (2, 1);
INSERT INTO recipe_ingredients VALUES (2, 2);
INSERT INTO recipe_ingredients VALUES (2, 3);
INSERT INTO recipe_ingredients VALUES (3, 2);
INSERT INTO recipe_ingredients VALUES (3, 5);
INSERT INTO recipe_ingredients VALUES (4, 1);
INSERT INTO recipe_ingredients VALUES (4, 5);

Finally, building your query will be as easy as this:

SELECT i.ingredient_name
FROM   recipe_ingredients ri
JOIN   ingredients i ON (i.iid = ri.ingredient_id)
WHERE  ri.recipe_id = (SELECT id 
                       FROM   recipe_name 
                       WHERE  recipe_name = 'Black Forest Cake');

Result:

+--------------------+
| ingredient_name    |
+--------------------+
| self-raising flour |
| milk               |
| chocolate          |
| baking powder      |
+--------------------+
4 rows in set (0.00 sec)

You may then want to format that result set (adding the <br>s) in your application code instead of in SQL.

However if you really wish to do that in SQL, then MySQL supports the handy GROUP_CONCAT() function, which can be used as follows:

SELECT GROUP_CONCAT(i.ingredient_name separator '<BR>') output
FROM   recipe_ingredients ri
JOIN   ingredients i ON (i.iid = ri.ingredient_id)
WHERE  ri.recipe_id = (SELECT id 
                       FROM   recipe_name 
                       WHERE  recipe_name = 'Black Forest Cake');

Result:

+----------------------------------------------------------+
| output                                                   |
+----------------------------------------------------------+
| self-raising flour<BR>milk<BR>chocolate<BR>baking powder |
+----------------------------------------------------------+
1 row in set (0.00 sec)

Dump that into HTML, and you're good to go!

Daniel Vassallo
Great! Thanks - I see (mostly) how this works. But I'm wanting to select recipes (and list their ingredients) by the category. Ie. I'd like to list all dessert recipes.So it would list this like: <h1>DESSERTS:</h1> <h2>Black Forest Cake:</h2> <p>Self-raising flour<br> Milk<br> Chocolate<br> Baking Powder</p> <h1>Croquembouche:</h2> <p>Self-raising flour<br> Plain flour</p>(sorry - can't seem to enter codes/breaks)
circey
@circey: I'm assuming you are calling your database queries from some server-side application (in php, asp, jsp, python, ruby, or whatever...) before you render the HTML? In general you would want to do such logic and formatting in application code. I'm not saying that it's impossible in SQL, but it gets pretty complicated. In general, SQL is not the right tool for that. The intended output of SQL is an unformatted resultset, and then it's something else's responsibility (php, asp, etc) to format it as required.
Daniel Vassallo
... In addition, there is no need to get your end result in just one query. You could first get all the categories: `SELECT * FROM categories`. Then iterate through the resultset, and get all the recipies of each category: `SELECT * FROM recipe_categories WHERE category_id = ?`. You can then iterate again through the list of recipes from which you can get the list of ingredients as suggested in my answer.
Daniel Vassallo
Thanks Daniel. Unfortunately, it's my responsibility to format (in php) the results. Like I said, I'm new to mysql and don't understand a lot of your second comment, but I'll keep working on it and hopefully get things sorted! Thanks again :-)
circey
+1  A: 

You definitely don't want to have a comma-separated list of foreign keys like you did with cid and iid in recipe_name. Instead, make tables that just handle those relationships:

recipe_name

id | recipe_name
 1 | black forest cake
 2 | angel cake
 3 | melting moments
 4 | croquembouche

recipe_categories

id | cid
 1 |  1
 1 |  2
 2 |  2
 3 |  3
 4 |  1
 4 |  3

recipe_ingredients

id | iid
 1 |  1
 1 |  2
 1 |  3
 1 |  4
 2 |  1
 2 |  2
 2 |  4
 3 |  2
 3 |  5
 4 |  1
 4 |  5

Now your query to select any desserts (your old one didn't work since it would miss rows with the cid 1,2, for example) looks like:

SELECT recipe_name.* FROM recipe_name, recipe_categories WHERE recipe_name.id = recipe_categories.id AND recipe_categories=cid = 1;

Once you've done that, you can write a query that joins the ingredients and recipe_ingredients tables to get you the results you want:

SELECT ingredients.* FROM ingredients, recipe_ingredients WHERE recipe_ingredients.id = 1 AND recipe_ingredients.iid = ingredients.iid;
Michael Mrozek