-1

I have both side coma separated values like

 $ing=1,2,3,4,5,6

and the database has a table with values

 IDS    5,2,1,6,2,3,45 // in database

I know this is not a good practice but I had to do this to get the values in one query. i also have a separate table where the IDS are separate and corresponds to separate users like

  user 1    IDS 2 
  user 3    IDS 65 etc 

As of now I am using

 $conditions[] = "CONCAT(',', `rcring`, ',') REGEXP ',($ing),'";

It gives me good results but it gives me the values in which either of the $ing exists i want only the tables which has atleast all the $ing

Can you help me please I tried my work and I cant get a proper solution anywhere . thankx .

      //EDIT

i already have condition as an array

   if($ser !=''){
                   $conditions[] = "rc_ser='$ser'";
                }if($fridge == 1){
                    $ing="0|";
                    $ctch2fr='checked';
                    foreach($_SESSION['fridge'] as $item){
                    $ing=$ing.$item."|";}
                   $conditions[] = "CONCAT(',', `rcring`, ',') REGEXP ',($ing),'";
                }
                 if (count($conditions) > 0) {
                $sql= implode(' AND ', $conditions);
                }

Like this as an condition in array and i am calling it like this in query

select * ,(select count(rcid) from recipe where ($sql)) as total from recipe where ($sql)

i tried the answers below bu it gives me 0 result always when i print the query it shows me like this

 select *,(select count(rcid) from recipe where (CONCAT(',', `rcring`, ',') REGEXP ',(0),') ) as total from recipe where (CONCAT(',', `rcring`, ',') REGEXP ',(0),')
Amani
  • 281
  • 3
  • 15
  • 1. remove `[]` near `$conditions` 2. can you use `var_dump($conditions);` and tell me your results? – proofzy Nov 28 '18 at 13:37
  • 2
    Can you show an example of the tables and the data that you want as output? – David Lemon Nov 28 '18 at 15:42
  • Is storing a delimited list in a database column really that bad? **Yes, it is that bad** https://stackoverflow.com/a/3653574/2739274 – David Lemon Nov 29 '18 at 12:56
  • I doubt the truth of this statement "I know this is not a good practice but I had to do this to get the values in one query", could you expand? – Arth Nov 29 '18 at 14:22

3 Answers3

3

You seem to have a recipe table that contains comma separated list of ingredients:

5,2,1,6,2,3,45

And a list of given ingredients:

1,2,3,4,5,6

And you want to find recipes that could be prepared with the given ingredients (recipe ingredients is a subset of given ingredients). You need to write PHP code that builds the following query:

SELECT *
FROM recipe
WHERE (
    FIND_IN_SET('1', rcring) > 0 AND
    FIND_IN_SET('2', rcring) > 0 AND
    FIND_IN_SET('3', rcring) > 0 AND
    FIND_IN_SET('4', rcring) > 0 AND
    FIND_IN_SET('5', rcring) > 0 AND
    FIND_IN_SET('6', rcring) > 0
)

A rough outline of the PHP code based on your attempt (which you must convert to prepared statements):

$conditions = [];
foreach($fridge_ingredients as $ingredient) {
    $conditions[] = sprintf("FIND_IN_SET('%d', rcring) > 0", $ingredient);
}
$query = sprintf("SELECT *
FROM recipe
WHERE (%s)", implode(" AND ", $conditions));

Having said that, the correct solution is to normalize your data. Here is an outline of the structure:

CREATE TABLE recipe (recipeid INT NOT NULL PRIMARY KEY, name VARCHAR(100));
CREATE TABLE ingredient (ingredientid INT NOT NULL PRIMARY KEY, name VARCHAR(100));
CREATE TABLE recipe_ingredient(recipeid INT NOT NULL,ingredientid INT NOT NULL, PRIMARY KEY(recipeid, ingredientid));

INSERT INTO recipe VALUES
(1, 'recipe 1'),
(2, 'recipe 2'),
(3, 'recipe 3'),
(4, 'recipe 4');
INSERT INTO ingredient VALUES
(1, 'ingredient 1'),
(2, 'ingredient 2'),
(3, 'ingredient 3'),
(4, 'ingredient 4');
INSERT INTO recipe_ingredient VALUES
(1, 1),
(2, 1), (2, 2),
(3, 1), (3, 2), (3, 3),
(4, 1), (4, 2), (4, 3), (4, 4);

The query:

SELECT *
FROM recipe
WHERE recipeid IN (
    SELECT recipeid
    FROM recipe_ingredient
    GROUP BY recipeid
    HAVING COUNT(CASE WHEN ingredientid IN (1, 2, 3) THEN 1 END) = COUNT(*)
)

And the result:

recipeid  |  name
----------+----------
1         |  recipe 1
2         |  recipe 2
3         |  recipe 3
Salman A
  • 262,204
  • 82
  • 430
  • 521
2

As far as I can see, the condition You specified matches only when rcring contains $ing exactly as substring, e.g. rcring = "5,2,1,6,2,3,45" wouldn't match $ing = "1,2,3,4,5,6" from your example. To match here, rcring should be stored as "1,2,3,4,5,45".

Simple sorting wouldn't help here, as $ing = "1,2,5,6" wouldn't match rcring = "1,2,3,4,5,6,45"

Simple way

Make $conditions[] contain a conjuntion of matches on distinct IDs, e.g.

<?
foreach (explode(",", $ing) as $key => $val) {
    $conditions[] = "CONCAT(',', `rcring`, ',') REGEXP ',($id),'"
}
?>

This will check what You want -- if rcing contains all IDs from $ing

However, this is quite slow for large (> 10^8 rows) tables. Such queries will work for O(n) time, checking all the rows in the table. For several gigabyte tables, it can take significantly more than a minute.

Optimization

Use Fulltext index searches. You need to store IDs separated by space, e.g. 1 2 3 4 5 45, to allow index treat it as words. After indexing, the code may be rewritten like following:

<?
    $plus_ing = "+1 +2 +3 +4 +5"
    $conditions[] = "MATCh(`rcring`) AGAINST($plus_ing)"
?>

Now that will work quite fast, and would skip the rows that don't match at all!

BE CAREFUL!

Always use PHP::PDO and pass IDs as parameters!

Hackers may abuse your system by making SQL injections with passing something dangerous instead of IDs, for example: 123");DROP DATABASE TEST; COMMIT;// This will turn $ings into smth like

"+1 +2 +123\");DROP DATABASE TEST; COMMIT;//"

and final query will look like

SELECT * FROM USERS WHERE MATCh(`rcring`) AGAINST(+1 +2 +123\");DROP DATABASE TEST; COMMIT;//");

The right way is to PDO prepared statements howto and to always construct queries that way!

Nikita Malyavin
  • 1,868
  • 1
  • 12
  • 10
  • it gives me 0 results always , never matches with anything even it should . – Amani Nov 29 '18 at 09:01
  • Your fulltexts index searching advice is rather inaccurate. The minimum length for a word is 4 characters by default, so only ids over 1000 will be indexed/matched against, I'm fairly sure shorter conditions are ignored. This default can be shortened but will affect all your fulltext indexes. Also a comma counts as a non-word character so there would be no need change from comma separated to space separated. – Arth Nov 29 '18 at 14:16
  • A fulltext index is not the right tool for this job – Arth Nov 29 '18 at 14:19
  • I prefer the FIND_IN_SET method from Salman, but for your simple way you could ditch your `REGEXP ...` in favour of the faster `LIKE CONCAT('%,', :i, ',%')` – Arth Nov 29 '18 at 14:26
  • @Arth that's not "rather inaccurate". It is a working and fast solution. Check the docs. There's `ft_min_word_len` variable to control word length. `LIKE...` way is overestimated. Anyway, it will still make O(n) reads – Nikita Malyavin Nov 30 '18 at 05:12
  • @NikitaMalyavin, well "You need to store IDs separated by space, e.g. 1 2 3 4 5 45, to allow index treat it as words" is just wrong. And yeah you can change the word length, but that is server wide, and you'll get strange results of you don't change it. Honestly I thought 'rather inaccurate' was being kind.. – Arth Nov 30 '18 at 09:19
  • @Arth nothing wrong there. It works exactly that way. You're right about second part however -- it's really server-wide. Sure salman-a's solution is an academically correct way of doing that, and I voted that, although, it also implies that the optimizer figured out the correct query plan. – Nikita Malyavin Nov 30 '18 at 18:21
1

You need to split the $ing and check all its parts in the where-clause:

<?php
$ings = explode(',',$ing);
$conditions = array();
foreach($ings as $i) {
    $conditions[] = "CONCAT(',', `rcring`, ',') REGEXP ',(".intval($i)."),'";
}
$where = implode(' AND ', $conditions);
/* Use $where in where clause in your sql query */
Adder
  • 5,708
  • 1
  • 28
  • 56
  • 1
    I prefer the FIND_IN_SET method from Salman, but you could ditch your `REGEXP ...` in favour of the faster `LIKE CONCAT('%,', :i, ',%')` – Arth Nov 29 '18 at 14:25