views:

177

answers:

6

Hi

This has been bugging me, I can't get my head around it. I will use the foodstuffs analogy to try and simplify my probelm.

1000 members of the public where asked to pick a variety from each of 13 categories of footstuff. These selections were then stored in a mysql database against their name.

e.g. billy mary etc. etc.

milk....semi. .skimmed...

bread...white...brown....

cheese..edam.....edam....

fruit...apple...orange...

veg....potato...sprout...

meat....beef.....beef....

sweet..bonbons..liquorice..

fish...trout....salmon...

crisp....s&v....plain....

biscuit..hovis..rich tea..

wine.....red.....red.....

beer....stella..carlsburg..

carb....coke.....pepsi....

One of those 1000 was then asked to select anywhere from zero to 13 of their selections via checkboxes.

By searching the database how many others selected the same varieties?

Display in a table showing all their names and what they selected for all 13 varieties.

Does that make sense? I hope so 'cause it's driving me mad.

Thanks in advance.

+1  A: 

partial answer:

  • compose a linear set of selections by sorting and concatenating
  • comparing then becomes a simple WHERE clause

so you would first do a calculation run, putting strings into some field like "milk|semi|skimmed" literally.

+2  A: 

Assuming you have a simple layout, then you would have something like this (I'll restrict myself to three of the categories):

PersonId  What_Milk  What_Bread  What_Cheese
       1  Semi       Wheat       Swiss
       2  Skimmed    Rolls       French
       3  Soy        Brown       Smelly
       4  Low Fat    Wheat       Swiss

If I understood correctly, your problem is this:

When the person 4 is asked to choose 0 .. 3 of her food items, she may select the "Bread" and the "Cheese" checkbox, which means the query should yield person 1 as a match. Right?

SELECT
  PersonId,
  What_Milk,
  What_Bread,
  What_Cheese
FROM
  FoodPreference
WHERE
  PersonId != ?
  AND What_Milk   = IFNULL(NULLIF(?, ''), What_Milk)
  AND What_Bread  = IFNULL(NULLIF(?, ''), What_Bread)
  AND What_Cheese = IFNULL(NULLIF(?, ''), What_Cheese)

Your checkbox values will later go where the question mark placeholders are. (I've replaced the CASE WHEN constructs that used to be here with IFNULL/NULLIF, that has the same effect but is friendlier for PHP prepared statements.)

If a checkbox has not been checked (thus fixing the value to something), the corresponding column is compared to itself. That means it's value does not influence the result. If the other columns match, the row will be selected.

That also means that if zero checkboxes are selected by the user, all rows will be returned. The more food items a person selects, the closer the match will be.

In PHP, I'd recommend you use mysqli_prepare() to create a prepared statement from the query string, and mysqli_stmt_bind_param() to bind actual values to the question mark placeholders. That is much safer than building the SQL string directly. The PHP documentation has a whole lot of info on mysqli, have a look at it.

Tomalak
Hi Tomalak. Yes, if person 4 on your example was asked to select and she chose checkboxes Bread and Cheese then everyone who selected the same as her for those two foodstuffs should be dispayed. As we need to dispay everything they chose the SELECT should be * not PersonId? And yes, the fewer.....
.....checkboxes that are ticked the more results. Ticking checkboxes narrows the search. I don't know this CASE WHEN term? Will look it up now.
Yes, select whatever columns you need, it was meant to be an example. Avoid the generic "SELECT *" in production code, its less efficient.
Tomalak
I endorse Tomalak's suggestion to use prepared statement parameters instead of interpolating PHP variables into SQL expressions.
Bill Karwin
OK. So, if the checkbox value goes where you have the ? what are the empty quotes doing?
The empty quotes are little trick to make NULLIF produce NULL if you don't supply a checkbox value, so IFNULL can replace it with the orignal column value. Please also read the mysql docs and try the expressions on their own to understand.
Tomalak
A: 
SELECT  
    PersonId,  
    milk,  
    bread,  
    cheese
FROM  FoodPreference
WHERE  PersonId != :chosen_person_id  
    AND $milk= CASE WHEN :isset($_POST["milk"]))> '' THEN :isset($_POST["milk"]))   ELSE milk END  
    AND $bread= CASE WHEN :isset($_POST["bread"]))> '' THEN :isset($_POST["bread"]))  ELSE bread END  
    AND $cheese= CASE WHEN :isset($_POST["cheese"]))> '' THEN :isset($_POST["cheese"])) ELSE cheese END

Am I on the right track?

Try to use the code format button to indent your code and make it more readable. I guess you are getting closer... somehow. I'll update my example a bit now that I know you use PHP.
Tomalak
A: 

Ok, I posted one response but Tomalak quite rightly pointed out a logical flaw. I'll try again:

An alternative to listing the categories as thirteen columns is to list them as thirteen rows in a table like the following:

CREATE TABLE FoodPreference (
  PersonID   INT NOT NULL REFERENCES People,
  FoodCat    VARCHAR(10) NOT NULL REFERENCES FoodCategories,
  FoodChoice VARCHAR(10) NOT NULL,
  PRIMARY KEY (PersonID, FoodCat)
);
INSERT INTO FoodPreference VALUES
  (123, 'bread', 'white'),
  (123, 'milk', 'skim'),
  (123, 'cheese', 'edam'), ...
  (321, 'bread', 'brown'),
  (321, 'milk', 'whole'),
  (321, 'cheese', 'edam'), ...

Then you can use a query like the following, matching any row from the chosen person (p1) to a row with the same food choice from another person (p2) and from there to all the other choices of that person (p3):

SELECT DISTINCT p3.*
FROM FoodPreference AS p1
  JOIN FoodPreference AS p2 
    ON (p1.FoodCat = p2.FoodCat AND p1.FoodChoice = p2.FoodChoice 
      AND p1.PersonID != p2.PersonID)
  JOIN FoodPreference AS p3 
    ON (p2.PersonID = p3.PersonID AND p2.FoodCat != p3.FoodCat)
WHERE p1.PersonID = {(int)$chosen_person_id}
  AND p1.FoodCat IN ('milk', 'bread', 'cheese');

The list 'milk', 'bread', 'cheese' in the WHERE clause is something you need to build in your PHP code, based on the $_POST variable in your application. If a checkbox is checked, include that food category in the list.

<?php
$food_cat_array = array("'none'");
$legal_food_cats = array('milk'=>1, 'bread'=>1, 'cheese'=>1, ...);
foreach (array_intersect_key($_POST, $legal_food_cats) as $key => $checked) {
  if ($checked) {
    $food_cat_array[] = "'$key'"; 
  }
}
$in_predicate = join(',', $food_cat_array);
Bill Karwin
Hi Bill. Thank you so much, it looks like you have put a lot of thought into that answer. My table is unfortunately fixed allready. I could duplicate the data in another but really want to avoid that if possible. Can we work on Tomalak's answer. I am getting there with it. I've ditched the food.....
And am now using my actual values. Not sure if I'm comparing correctly. See my new code below. Getting some mysql errors. Not valid result resource etc.
A: 
$sql = mysql_query("        
    SELECT  
    *
    FROM  ".$prefix."_users
    WHERE  username !='$username'  
    AND req_country   = IFNULL(NULLIF($bcountry, ''), req_country)  
    AND req_region  = IFNULL(NULLIF($bregion, ''), req_region)  
    AND req_type = IFNULL(NULLIF($btype, ''), req_type)
    AND req_beds = IFNULL(NULLIF($bbeds, ''), req_beds)
    AND req_value = IFNULL(NULLIF($bvalue, ''), req_value)
    AND country   = IFNULL(NULLIF($scountry, ''), country)  
    AND region  = IFNULL(NULLIF($sregion, ''), region)  
    AND type = IFNULL(NULLIF($stype, ''), type)
    AND beds = IFNULL(NULLIF($sbeds, ''), beds)
    AND value = IFNULL(NULLIF($svalue, ''), value)
    AND pool = IFNULL(NULLIF($spool, ''), 'Yes')
    AND garage = IFNULL(NULLIF($sgarage, ''), >0) 
         AND disabled = IFNULL(NULLIF($sdisabled, ''), 'Yes')");
$num = mysql_num_rows($sql);
echo "Total matches ($num): <br><br>";
        while($row = mysql_fetch_array($sql)){...etc
Keep in mind that AND has higher precedence than OR. You probably should add parentheses around the nine garage terms.
Bill Karwin
it should be "req_country = IFNULL(NULLIF('$bcountry', ''), req_country)", and so on. Also all the "OR" conditions need to go into a single set of enclosing parentheses, as Bill said.
Tomalak
Thanks. I have edited the code, how does it look now? Pool and Disabled are held in the database as Yes and No. I only want these rows selected if the checkbox has been ticked and the value is Yes; so have I done that correct or should these be No? Also can I just say >0 for the garage?
No, you can't say >0 there, it's not valid syntax. Try replacing your garage term with: CASE IFNULL($sgarage, '') WHEN '' THEN garage IN (0,1,2,3,4,5,6,7,8,9) ELSE garage = $sgarage END
Bill Karwin
@Nigel: I think you need to spend more time learning SQL (and PHP). No offense, but you seen pretty... well... untaught on that. We've shown you what to do, you just need more time to connect the bits, I guess.
Tomalak
hehe...You are so right Tomalak. I haven't done any programming for 25 years when I could do anything in Sinclair ZX81 basic!! However, I am getting the hang of it. I figured this problem out, thanks for all your help and Bill, Cheers. Have a look at my code. It's works perfectly, any flaws guys?
A: 
case "display_results":

if ($bcountry = !isset($_POST["bcountry"])){
   $bcountry = "No";
   }else {
   $bcountry = "Yes";
   }
if ($bregion = !isset($_POST["bregion"])){
   $bregion = "No";
   }else {
   $bregion = "Yes";
   }
if ($btype = !isset($_POST["btype"])){
   $btype = "No";
   }else {
   $btype = "Yes";
   }
if ($bbeds = !isset($_POST["bbeds"])){
   $bbeds = "No";
   }else {
   $bbeds = "Yes";
   }
if ($bvalue = !isset($_POST["bvalue"])){
   $bvalue = "No";
   }else {
   $bvalue = "Yes";
   }
if ($scountry = !isset($_POST["scountry"])){
   $scountry = "No";
   }else {
   $scountry = "Yes";
   }
if ($sregion = !isset($_POST["sregion"])){
   $sregion = "No";
   }else {
   $sregion = "Yes";
   }
if ($stype = !isset($_POST["stype"])){
   $stype = "No";
   }else {
   $stype = "Yes";
   }
if ($sbeds = !isset($_POST["sbeds"])){
   $sbeds = "No";
   }else {
   $sbeds = "Yes";
   }
if ($svalue = !isset($_POST["svalue"])){
   $svalue = "No";
   }else {
   $svalue = "Yes";
   }
if ($spool = !isset($_POST["spool"])){
   $spool = "No";
   }else {
   $spool = "Yes";
   }
if ($sgarage = !isset($_POST["sgarage"])){
   $sgarage = "No";
   }else {
   $sgarage = "Yes";
   }
if ($sdisabled = !isset($_POST["sdisabled"])){
   $sdisabled = "No";
   }else {
   $sdisabled = "Yes";
   }

     $result = mysql_query("SELECT * FROM  ".$prefix."_users WHERE  username!='$username' 
AND (('$bcountry'='Yes' AND req_country= '$country') OR ('$bcountry'='No')) 
AND (('$bregion'='Yes' AND req_region= '$region') OR ('$bregion'='No'))
AND (('$btype'='Yes' AND req_type= '$type') OR ('$btype'='No'))
AND (('$bbeds'='Yes' AND req_beds= '$beds') OR ('$bbeds'='No'))
AND (('$bvalue'='Yes' AND req_value= '$value') OR ('$bvalue'='No'))
AND (('$scountry'='Yes' AND country= '$req_country') OR ('$scountry'='No'))
AND (('$sregion'='Yes' AND region= '$req_region') OR ('$sregion'='No'))
AND (('$stype'='Yes' AND type= '$req_type') OR ('$stype'='No'))
AND (('$sbeds'='Yes' AND beds= '$req_beds') OR ('$sbeds'='No'))
AND (('$svalue'='Yes' AND value= '$req_value') OR ('$svalue'='No'))
AND (('$spool'='Yes' AND pool= 'Yes') OR ('$spool'='No'))
AND (('$sgarage'='Yes' AND garage>=1 ) OR ('$sgarage'='No'))
AND (('$sdisabled'='Yes' AND disabled= 'Yes') OR ('$sdisabled'='No'))
")or die("MySQL ERROR: ".mysql_error());
$number = mysql_num_rows($result);
I would like the checkboxes to stay ticked once the search results have been displayed (so people can see what they selected). I have tried adding $ticked1, $ticked2 etc. to the checkbox options and then if ($bcountry=='Yes'){$ticked1='Checked'} etc. but it doesn't seem to work. Any help?