The Query to Power It All

This article is only meant for major, major SQL geeks. You’ll be bored out of your mind if you’re not one; don’t tell me I didn’t warn you.

As you might have guessed, there’s one, only one query powering the basic cocktail builder functionality. It’s the query that answers the question “if I have ingredients A, B, and C, give me a list of all cocktails that I can make”. Also:

  • give me cocktails that I can almost make, ordered by the number of missing ingredients (i.e. those that I can make right away go first, those that are missing a single ingredient second, etc).
  • consider ingredient substitutions (a concept I refer to as “normalized ingredients”): if you have Sky Vodka, and the recipe calls for Stoli, you can make the cocktail just fine.
  • calculate cocktail ratings from the user feedback

As you might guess, the SQL is rather involved. Here’s the statement that gets executed when the user has two ingredients – items with ID’s 73 and 76:

SELECT 
    InnerCountsTable.CocktailID, 
    InnerCountsTable.CocktailName, 
    InnerCountsTable.CocktailShortName,     
    CountPresent, 
    CountMissing, 
    FORMAT(round((AVG(rating.Rating)*2))/2, 1) AS Rating, 
    (CountPresent - CountMissing) as Difference 
FROM 
    (SELECT 
        cocktail.ID as CocktailID, 
        cocktail.Name as CocktailName, 
        cocktail.ShortName as CocktailShortName, 
        cocktail.Instructions as CocktailInstructions,
        SUM(IF(userHas.NormalizedIngredientID IS NULL, 1, 0)) 
            as CountMissing, 
        SUM(IF(userHas.NormalizedIngredientID IS NULL, 0, 1))
            as CountPresent 
        FROM 
            mix INNER JOIN ingredient 
                ON mix.IngredientID = ingredient.ID 
            LEFT JOIN (
                SELECT NormalizedIngredientID 
                FROM ingredient 
                WHERE ID=76 
                UNION 
                SELECT NormalizedIngredientID 
                FROM ingredient 
                WHERE ID=73) AS userHas 
                ON userHas.NormalizedIngredientID =
                    ingredient.NormalizedIngredientID 
            INNER JOIN cocktail ON mix.CocktailID = cocktail.ID 
            WHERE NOT (cocktail.Status = 'Cut') 
            GROUP BY mix.CocktailID) AS InnerCountsTable 
LEFT JOIN rating 
    ON InnerCountsTable.CocktailID = rating.CocktailID 
WHERE CountPresent > 0 
GROUP BY InnerCountsTable.CocktailID 
ORDER BY 
    CountMissing ASC, 
    Difference DESC, 
    CountPresent DESC LIMIT 30

A weird part of the query is the “UNION” part inside (the one that’s creating the userHas part). Basically, I don’t believe there’s any other way to create an in-memory virtual table in MySQL to do a JOIN with. Anyone got better ideas?

Or, maybe you can offer a completely different approach that will work faster?

Cocktail Builder: JavaScript Alcoholic