A general single sql query - php

I have a table like this:
id | roll_no | name
---------------------
1 | 111 | Naveed
2 | 222 | Adil
3 | 333 | Ali
If I have data like this:
$fields = array( "id" , "roll_no" ) and $values = array( "1,111", "2,222" );
It means I have to write a sql query to get records from table where (id != 1 and roll_no != 111) and (id != 2 and roll_no != 222). It means 3rd record will be fetched.
If I have data like this:
$fields = array( "id" ) and $values = array( "2", "3" );
It means I have to write a sql query to get records from table where (id != 2) and (id != 3). It means 1st record will be fetched.
Q: How to write a general single query using php to get data from table using above two data arrays.
Thanks

select * from dummy where concat_ws (',', id, roll_no) not in ('1,111', '2,222')
Complete solution:
$tableName = "test";
$fields = array( "id" , "roll_no" );
$values = array( "1,111", "2,222" );
$fieldsStr = implode(',', $fields);
$valuesStr = implode("','", $values);
$sql = "SELECT *
FROM $tableName
WHERE concat_ws(',', $fieldsStr ) NOT IN ( '$valuesStr' )";

You will probably always have to explode the Array in PHP and pass the values as a string into the query (sprintf) so you probably can, and should, do all in PHP.
One thing that catches my eye is that you are always using ID's. Are the ID's a unique or primary field? If so just forget about the roll_no as your query will be faster using just ID's.

Complete solution with the help of accepted answer.
$tableName = "test";
$fields = array( "id" , "roll_no" );
$values = array( "1,111", "2,222" );
$fieldsStr = implode(',', $fields);
$valuesStr = implode("','", $values);
// Get all records from remote table
$sql = "SELECT * FROM $tableName WHERE concat_ws(',', $fieldsStr ) NOT IN ( '$valuesStr' )";

Related

Slow query with where on joined column

I tried to combine 2 queries into 1 by using JOINs
// Query 1
$entity = \DB::select("
SELECT memo_id
FROM entities
WHERE entity_type = 4
AND entity_id = '".$entity_nr."'
LIMIT 1
");
$memo = \DB::select("
SELECT memo
FROM memos
WHERE id = '".$entity[0]->memo_id."'
LIMIT 1
");
// Query 2
$memo = \DB::select('
SELECT memo
FROM memos
JOIN entities ON "memos"."id" = "entities"."memo_id"
WHERE "entities"."entity_type" = 4
AND "entities"."entity_id" IN (?)
LIMIT 1
', [$entity_nr]);
The 2 query's of Query 1 are done in less than a second. Query 2 takes several seconds. If i remove the where clause of Query 2 it executes fast. Using AND "entities"."entity_id" = ? did'nt helped as well.
Howto solve this?
UPDATE 2020-09-27
Refactoring my question. Hopes this makes it clearer. Expected output is to get the memo. There is always only 1 possible match. In the example code im not checking for existence of the result, because it doesn't really add something to the question.
Database which im using is "Actian Zen database".
Table structure
//////////////////////////
// Table: "entity_memo" //
//////////////////////////
CREATE TABLE "entity_memo" (
"entity_type" SMALLINT,
"entity_id" CHAR(15),
"memo_id" INTEGER
);
CREATE UNIQUE INDEX "key0" ON "entity_memo" ( "entity_type", "entity_id", "memo_id" );
CREATE INDEX "key1" ON "entity_memo" ( "memo_id", "entity_type", "entity_id" );
//////////////////////////
// Table: "memos" //
//////////////////////////
CREATE TABLE "memos" (
"id" INTEGER,
"memo" LVAR(32002)
);
CREATE UNIQUE INDEX "key0" ON "memos" ( "id" );
Option 1 (= fast, but 2 queries)
$entity_type = 1;
$entity_id = 'ABC123456';
$entity = \DB::select('
SELECT memo_id
FROM entity_memo
WHERE entity_type = ?
AND entity_id = ?
LIMIT 1
', [$entity_type, $entity_id]
);
$memo = \DB::select('
SELECT memo
FROM memos
WHERE id = ?
LIMIT 1
', [$entity[0]->memo_id]
);
return $memo[0]->memo;
Option 2 (= slow, but 1 query only)
$entity_type = 1;
$entity_id = 'ABC123456';
$memo = \DB::select('
SELECT memo
FROM memos
JOIN entity_memo ON "memos"."id" = "entity_memo"."memo_id"
WHERE "entity_memo"."entity_type" = ?
AND "entity_memo"."entity_id" = ?
LIMIT 1
', [$entity_type, $entity_id]
);
return $memo[0]->memo;
It will be look simpler, if you write it without JOINs.
SELECT ent.unique_id, mem.memo
FROM entities ent, memos mem
WHERE ent.unique_id = mem.unique_id
AND ent.entity_type = 4
AND ent.entity_id = '".$entity_nr."'
But the cause of query working slow is in tables itself. You should to add indexes to PK and FK columns for query to work faster.

DISTINCT Comma Separated SQL Table Rows

I have a b_topics table with tags column in multiple rows
id | tags
1 | Joshua, Janet, Hannah
2 | Glory, Jon, Celina, Johanna
3 | Bridge,Terry, Sterling
4 | Daniel, Florence, Joanne
I want to check for the related tags with the input Jo so i have the below sql select
$query = Jo;
$sql = mysql_query("SELECT DISTINCT tags FROM b_topics WHERE tags LIKE '%{$query}%'");
while ($row = mysql_fetch_array($sql)) {
$array[] = array ( 'label' => $row['tags'], 'value' => $row['tags'], );
}
echo json_encode ($array);
This is the output:
[{"label":"Joshua, Janet, Hannah","value":"Joshua, Janet, Hannah"},{"label":"Glory, Jon, Glory","value":"Glory, Jon, Glory"},{"label":"Daniel, Florence, Joanne","value":"Daniel, Florence, Joanne"}]
I want the matched words to be on foreach();
Expected output:
[{"label":"Joshua","value":"Joshua"},{"label":"Jon","value":"Jon"},{"label":"Johanna","value":"Johanna"},{"label":"Joanne","value":"Joanne"}]
Fix your data structure! You should not be storing multiple values in a string delimited list. This is simply not the right way to store the data. SQL has this great data structure for storing lists. It is not called "string". It is called "table". You want a table called TopicTags with one row per topic and per tag.
Although you can do nasty string functions to get what you want, this would be much simpler with the right data structure:
select topic_id, tag
from TopicTags tt
where tag like '%Jo%';
You can aggregate if you want the results in a particular format.
set your tables because when your query return a row then you can store that row not only a single tag.
$array[] = array ( 'label' => $row['tags'], 'value' => $row['tags'], );
where $row['tags']=Joshua, Janet, Hannah so its gives result like
'label' ="Joshua, Janet, Hannah"
$sql = "select * from test where tags like '%Jo%'";
$res = mysqli_query($con,$sql);
$string = '';
while($row=mysqli_fetch_array($res)){
$string .= ','.$row['tags'];
}
$array = explode(',', $string);
$search = preg_quote('Jo', '~'); // don't forget to quote input string!
$result = preg_grep('~' . $search . '~', $array);
print_r($result);

How to prevent mysql injection when using mysql IN clause without activeRecord in Yii?

I have an array with ids that I get from client. And I want use those ids in my sql query with IN clause. But this query goes on a table that has no model. So there is no active record (criteria) query possible.
** Table userTasks **
--------------------
| idUser | idTasks |
---------+----------
| 1 | 1 |
---------+----------
| 1 | 2 |
---------+----------
| 1 | 3 |
---------+----------
First approach does not work because params are always considered as strings. So :tasks is a string '1,2,3' instead of a comma separated list of ids:
$sql = 'SELECT COUNT(*) AS matches
FROM userTasks
WHERE idUser = :idUser
AND idTask IN (:tasks)';
$result = Yii::app()->db->createCommand($sql)
->queryRow(true,[
':idUser' => $idUser,
':tasks' => implode(',', $tasks)]); //$tasks is a simple array of ids [1,2,3]
So my workaround:
foreach($tasks as $task) //$tasks is a simple array of ids [1,2,3]
{
$inTasks[] = (int) $task;
}
$sql = 'SELECT COUNT(*) AS matches
FROM userTasks
WHERE idUser = :idUser
AND idTask IN (' . implode(',', $inTasks . ')';
$result = Yii::app()->db->createCommand($sql)
->queryRow(true,[':idUser' => $idUser]);
Having come across this problem a few times in my projects I have come-up with the following Yii work-around using CDbCriteria which is a little hacky, but gives the security of param count matching.
I would also use queryScalar() in this instance to get the result directly.
When applied to your example my code would be:
$idUser = 1;
$tasks = array(1,2,3);
$criteria = new CDbCriteria();
$criteria->addInCondition('idTask',$tasks);
$sql = '
SELECT COUNT(*) matches
FROM userTasks
WHERE idUser = :idUser
AND '.$criteria->condition;
$command = Yii::app()->db->createCommand($sql);
$command->bindValue('idUser',$idUser);
$command->bindValues($criteria->params);
$result = $command->queryScalar();
For preventing SQL injection in Yii IN clause we need to bind parameters in IN clause, Yii CDB criteria queries don't have this functionality in built. so you can use below code.
$products_ids = array(234,100,405,506);
map the array for binding
$in_query = implode(',', array_fill(0, count($products_ids), '?'));
Prepare the commadn object for select
$command = Yii::app()->db->createCommand()
->select('product_id, product_name, product_image, product_price')
->from('products')
->where('product_id IN(' . $in_query . ')');
bind the parameters
foreach ($products_ids as $k => $product_id){
$command->bindValue(($k+1),$product_id,PDO::PARAM_INT);
}
get the result
$products = $command->queryAll();

MySQL select unique values based on one or more possible clauses

So there is this database table:
and this array with selected options:
$options[1] = 1;
$options[2] = 5;
$options[3] = 3;
$options[4] = 2;
$options[5] = 1;
...
$options[x] = y;
Now, the aim is to fetch all item_ids, where if there is an option_id in its row from one of the options array's keys, the value must be the same as the value in the options array.
For example:
option 1 has selected value 1
option 2 has selected value 5
option 3 has selected value 4
option 4 has selected value 2
so we should select item_id 1 and other item_ids, where if option 1 -> option 1 = 1 AND if option 2 -> option 2 = 5 AND if option 3 -> option 3 = 4 AND ...
The item_ids will be used in IN() to select the items data from the items table.
The main point is that the user selects some options on a page, then the options are put into array, then I must find all items that comply with the selected options. In the table above we have the relation between the items and the options, and the option values per item which are predefined.
$where = array();
foreach($options as $key => $value) $where[] = 'option_id = ' . $key . ' AND option_value = ' . $value;
$sql = 'SELECT DISTINCT item_id
FROM table
WHERE (' . explode(') OR (', $where) . ')';
I might have completely misunderstood your question however.
I think this might do it for you. Haven't tested it at all and it'll probably blow up, but...
SELECT *
FROM items
WHERE (item_id in (
SELECT item_id
FROM optiontable
WHERE ((option_id = 1) and (option_value = 1)) or
((option_id = 2) and (option_value=5)) or
((option_id = 3) and (option_value=3)) or
((option_id = 4) and (option_value=2)) or
((option_id = 5) and (option_value=1))
GROUP BY CONCAT(option_id, ',', option_value)
HAVING COUNT(CONCAT(option_id, ',', option_value)) = 5
));
Basically, the inner query pulls out all the rows that match one of the member rows in your required options row. It does an artificial grouping/count on the paired option_id/option_value pairs and returns the item_ids of the rows where the number of opt_id/opt_val pairs add up to 5 rows.
You'd have to build such a query dynamically in the client, so that the number of 'where' clause entries matches the number in the having clause.
<?php
$options = array(
1 => 1,
2 => 5,
3 => 3,
4 => 2,
5 => 1
);
$cases = array();
foreach($options as $id => $value){
$cases[] = "WHEN $id THEN $value";
}
$query =
'SELECT item_id '.
'FROM your_table '.
'WHERE option_value = CASE option_id '.implode(' ', $cases).' '.
'GROUP BY item_id';
echo $query;
?>
Output (formatted by me):
SELECT item_id
FROM your_table
WHERE option_value = CASE option_id
WHEN 1 THEN 1
WHEN 2 THEN 5
WHEN 3 THEN 3
WHEN 4 THEN 2
WHEN 5 THEN 1
GROUP BY item_id
Test this query and let me know if it works the way you expect it to. :)
Update: Suggestion for your final query
$query =
'SELECT * FROM items '.
'WHERE id IN('.
'SELECT item_id '.
'FROM your_table '.
'WHERE option_value = CASE option_id '.implode(' ', $cases).' '.
'GROUP BY item_id)';

MySQL (exploding/matching array)

Question1:
MySQL table
id | array
1 | 1,2,3
2 | 2
3 | 2,3
4 | 4,5,6
$_GET['id'] = 2;
$a = mysql_query("SELECT * FROM `table` WHERE `array` ??? '$_GET[id]'");
In this step, I want to run through the entire array and see if it matches with the $_GET['id'], so it should output:
ids: 1,2,3
Question2:
MySQL table
id | array
1 | 4,5,6
2 | 3,4,7
$_GET['id'] = 4;
$a = mysql_query("SELECT * FROM `table` WHERE `array` ??? '$_GET[id]'");
In this step, I only want to match against the first element in the array, so it should output:
id: 4
I can only think of using PHP to do this, but I'd rather do all that just within the MySQL query, if that is even possible.
$a = mysql_query("SELECT * FROM `table`");
while($b = mysql_fetch_assoc($a))
{
$elements = explode(',', $b['array']);
foreach($elements as $element)
{
if($element == $_GET['id'])
{
echo $b['id'].'<br />';
}
}
}
or
$a = mysql_query("SELECT * FROM `table`");
while($b = mysql_fetch_assoc($a))
{
$array = $b['array'];
if(in_array($_GET['id'], $array))
{
echo $b['id'].'<br />';
}
}
that would look just awful.
That you can/should structure your database differently has already been mentioned (see http://en.wikipedia.org/wiki/Database_normalization). But....
See FIND_IN_SET()
mysql> SELECT FIND_IN_SET('b','a,b,c,d');
-> 2
e.g.
<?php
$mysql = init();
bar($mysql, 1);
bar($mysql, 2);
bar($mysql, 3);
bar($mysql, 4);
function bar($mysql, $x) {
$sql_x = mysql_real_escape_string($x, $mysql);
$result = mysql_query("SELECT id, foo FROM soTest WHERE FIND_IN_SET('$sql_x', foo)", $mysql) or die(mysql_error());
echo "$x:\n";
while( false!==($row=mysql_fetch_array($result, MYSQL_ASSOC)) ) {
echo $row['id'], ' ', $row['foo'], "\n";
}
echo "----\n";
}
function init() {
$mysql = mysql_connect('localhost', 'localonly', 'localonly') or die(mysql_error());
mysql_select_db('test', $mysql) or die(mysql_error());
mysql_query('CREATE TEMPORARY TABLE soTest (id int auto_increment, foo varchar(64), primary key(id))', $mysql) or die(__LINE__.' '.mysql_error());
mysql_query("INSERT INTO soTest (foo) VALUES ('1,2,3'), ('2,4'), ('3'), ('2,3'), ('1,2')", $mysql) or die(__LINE__.' '.mysql_error());
return $mysql;
}
prints
1:
1 1,2,3
5 1,2
----
2:
1 1,2,3
2 2,4
4 2,3
5 1,2
----
3:
1 1,2,3
3 3
4 2,3
----
4:
2 2,4
----
MySQL can't use indices to perform this search, i.e. the query results in a full table scan, see Optimizing Queries with EXPLAIN
edit:
For your second question you only have to change the WHERE-clause to
WHERE FIND_IN_SET('$sql_x', foo)=1
Your data structure in the DB is not optimal for querying the way you want it.
For the first question:
mysql_query("SELECT * FROM table WHERE array LIKE '%,$_GET[id],%' OR array LIKE '$_GET[id],%' OR array LIKE '%,$_GET[id]' OR array = '$_GET[id]'");
For the second:
mysql_query("SELECT id, SUBSTR(array, 1, POSITION(',' IN array) - 1) AS array FROM table WHERE array LIKE '$_GET[id],%' OR array = '$_GET[id]'");
As you can see, these queries aren't pretty, but they'll do what you want.
Untested, but you should be able to use:
Question 1:
SELECT * FROM table WHERE array REGEXP '(^|,)?(,|$)';
// Match either the start of the string, or a , then the query value, then either a , or the end of the string
Question 2:
SELECT * FROM table WHERE array REGEXP '^?(,|$)';
// Match the start of the string, then the query value, then either a , or the end of the string
Where ? is replaced with your $_GET value.
No idea on the performance of this.
I'd recommend you to bring your database to the first normal form, e. g.
CREATE TABLE t_master (
id INT PRIMARY KEY AUTO_INCREMENT
);
CREATE TABLE t_array (
id INT PRIMARY KEY AUTO_INCREMENT,
master_id INT NOT NULL,
value INT,
CONSTRAINT fk_array_master_id FOREIGN KEY (master_id) REFERENCES t_master (id)
);
Then you can find records in t_master that have a specific value with
$q = 'SELECT m.* ' .
'FROM t_master AS m INNER JOIN t_array AS a ON a.master_id = m.id ' .
"WHERE a.value = '" . mysql_real_escape_string($_GET['id'], $db) . "' " .
'GROUP BY m.id';
The most important advantage is that if you have a lot of values, you can add an index to find them much faster:
ALTER TABLE t_array ADD INDEX idx_value (value);
A less evident, but not the last advantage is that your queries become more logical and structured.
If you can't normalise your schema (which is the best option:
SELECT *
FROM table
WHERE ','+array+',' LIKE '%,$_GET[id],%'
But if you need to access the records by id, then you really should normalise
First One:
SELECT * FROM table WHERE array LIKE '$_GET[id],%' OR array LIKE '%,$_GET[id],%' OR array LIKE '%,$_GET[id]' OR array = '$_GET[id]
Second One:
SELECT * FROM table WHERE array LIKE '$_GET[id],%' OR array = '$_GET[id]
Explanation:
'$_GET[id],%' will match, if array is start with $_GET[id]
'%,$_GET[id],%' will match, if $_GET[id] is between any two of array items
'%,$_GET[id]' will match, if array is end with $_GET[id]
array = '$_GET[id]' match, if the array contains only one item equal to $_GET[id]

Categories