Chaining SQL queries - mysql

Right now I'm running
SELECT formula('FOO') FROM table1
WHERE table1.foo = 'FOO' && table1.bar = 'BAR';
but I would like to run this not on the constant FOO but on each value from the query
SELECT foos FROM table2
WHERE table2.bar = 'BAR';
How can I do this?
Edit: Important change: added FOO to the arguments of function.
Illustration:
SELECT foo FROM table1 WHERE foo = 'FOO' && table1.bar = 'BAR';
gives a column with FOOa, FOOb, FOOc.
SELECT formula('FOO') FROM table1
WHERE table1.foo = 'FOO' && table1.bar = 'BAR';
gives a single entry, say sum(FOO) (actually much more complicated, but it uses aggregates at some point to combine the results).
I want some query which gives a column with sum(FOO1), sum(FOO2), ... where each FOOn is computed in like manner to FOO. But I'd like to do this with one query rather than n queries (because n may be large and in any case the particular values of FOOn vary from case to case).

Try this one:
SELECT formula FROM table1
WHERE table1.foo IN(SELECT foos FROM table2
WHERE table2.bar = 'BAR';
) AND table1.bar = 'BAR';

SELECT formula FROM table1 WHERE bar = 'BAR' AND foo IN (SELECT foos FROM table2 WHERE bar = 'BAR');
EDIT:
This isn't tested, but perhaps this will work?
SELECT formula(q1.foo) FROM table1 INNER JOIN (SELECT foo, bar FROM table2) q1 ON table1.foo = q1.foo WHERE table1.bar = 'BAR';

You will have to use dynamic SQL statements.
The way this works is that you just string the SQL statement together using parameters.
You will need to be careful though not to allow users to feed that data, because escaping cannot protect you against SQL-injection. You will need to check every column name against a whitelist.
Here's example code in a stored procedure.
The way this works, is that you have a (temporary) table with column names and the stored procedure builds this into a query:
dynamic /*holds variable parts of an SQL statement
-----------
id integer PK
column_name varchar(255)
operation ENUM('what','from','where','group by','having','order by','limit')
function_name varchar(255) /*function desc with a '#' placeholder where */
/* the column-name goes */
whitelist /*holds all allowed column names*/
-----------
id integer PK
allowed varchar(255) /*allowed column of table name*/
item ENUM('column','table')
Dynamic SQL stored procedure. Expects two tables: dynamic and whitelist to be prefilled.
DELIMITER $$
CREATE PROCEDURE dynamic_example;
BEGIN
DECLARE vwhat VARCHAR(65000);
DECLARE vfrom VARCHAR(65000);
DECLARE vwhere VARCHAR(65000);
DECLARE vQuery VARCHAR(65000);
SELECT group_concat(REPLACE(function_name,'#',column_name)) INTO vwhat
FROM dynamic
INNER JOIN whitelist wl ON (wl.allowed LIKE column_name
AND wl.item = 'column')
WHERE operation = 'what' AND
SELECT group_concat(REPLACE(function_name,'#',column_name)) INTO vfrom
FROM dynamic
INNER JOIN whitelist wl ON (wl.allowed LIKE column_name
AND wl.item = 'table')
WHERE operation = 'from';
SELECT group_concat(REPLACE(function_name,'#',column_name)) INTO vwhere
FROM dynamic
INNER JOIN whitelist wl ON (wl.allowed LIKE column_name
AND wl.item = 'column')
WHERE operation = 'where';
IF vwhat IS NULL THEN SET vwhat = ' * ';
IF vwhere IS NOT NULL THEN SET vwhere = CONCAT(' WHERE ',vwhere); END IF;
SET vQuery = CONCAT(' SELECT ',vwhat,' FROM ',vfrom,IFNULL(vwhere,''));
PREPARE dSQL FROM vQuery;
EXECUTE dSQL;
DEALLOCATE PREPARE dSQL;
END $$
DELIMITER ;

Try:
SELECT formula FROM table1, table2
WHERE table1.foo = table2.foo AND table1.bar = table2.bar;

Related

MySQL Use table name for function

When we use a statement like select count(*) from TABLE, the function count() automatically knows which table it is counting. Is it possible to grab the table and use it in a user defined function.
drop function if exists related_count;
create function related_count(parent int(11)) returns int(11) deterministic
begin
declare count int(11) default 0;
set count=(select count(*) from TABLENAME where id=parent);
return count;
end;
So that I can use it like this:
select count(*),related_count(id) from TABLENAME
So that I can use the same function regardless of table instead of defining multiple functions because of multiple tables.
Is there a way to switch between select count(*) from TABLENAME1 where id=parent or select count(*) from TABLENAME2 where id=parent dependent on a variable related_count('TABLE1',id)
The comment above from #RajeevRanjan mentions using dynamic SQL. This won't work, but if it did it would look like this:
create function related_count(tablename varchar(64), parent int) returns int reads sql data
begin
declare count int default 0;
set #sql = concat('select count(*) into count from `', tablename, '` where id = ', parent);
prepare stmt from #sql;
execute stmt;
return count;
end
However, this is not allowed:
ERROR 1336 (0A000): Dynamic SQL is not allowed in stored function or trigger
The reason it doesn't work is that your stored function could be called by an expression in an SQL statement that is itself a dynamic SQL execution. I guess MySQL only allows one level "deep" of prepare/execute. You can't make a prepared query run another prepared query.
To do this, you'd have to hard-code each table name like the following:
create function related_count(tablename varchar(64), parent int) returns int reads sql data
begin
declare count int default null;
if tablename = 'foo' then set count = (select count(*) from foo where id = parent);
elseif tablename = 'bar' then set count = (select count(*) from bar where id = parent);
elseif tablename = 'baz' then set count = (select count(*) from baz where id = parent);
end if;
return count;
end
This also has an advantage that it isn't an SQL injection vulnerability, whereas the PREPARE/EXECUTE solution (if it had worked) would be.
PS: A function that reads from other tables is not deterministic.

How to use query string inside IN statement in MySQL stored procedure

friends I have a stored procedure. which taking an input. But the input is a Query string. When I'm executing that string in IN statement I'm not getting anything.
My Stored Procedure is:
CREATE DEFINER=`root`#`localhost` PROCEDURE `SampleProcedure`(IN category VARCHAR(255)
IN location VARCHAR(255),
IN classification VARCHAR(255))
BEGIN
SELECT u1.firstname , u1.lastname, u1.avatar , s1.address ,c1.cityName
FROM user u1,serviceprovider s1, city c1
WHERE s1.userId=u1.id
AND c1.cityId=s1.city
AND s1.serviceProviderId
IN
(SELECT DISTINCT serviceprovider_cl AS serviceProviderId FROM db.serviceprovider_classification t1
INNER JOIN
db.locationid t2 ON t1.serviceprovider_cl=t2.serviceprovider_locationId
INNER JOIN
db.serviceprovider_category t3 ON t2.serviceprovider_location
INNER JOIN
db.serviceprovider_category t3 ON t2.serviceprovider_locationId=t3.serviceprovider_category
WHERE
t1.serviceproviderclassification_classification IN (classification)
AND
t2.location_serviceLocation IN (location)
AND
t3.category_serviceProviderCategory IN (category)
);
END
In category, classification and location. I'm getting another query in String. So to execute that string or How to convert it into query or how to use string as Query?
Thanks
for this you can use something called Prepared Statements, you can find more about that here...
So here is an SQL Fiddle where you can see how prepared statement works...
As you can see in this simple stored procedure it is not complicated that much. Basically there is three step to do this.
First create string which will be used in prepared statement. You do this to connect your query and query you will get as a string (IN category VARCHAR(255)) into one statement.
In my Fiddle:
SET #myString =
CONCAT('SELECT * FROM t2 WHERE t1_id IN (', category, ')');
That is the hardest part. Than you should perapare statement from that string
PREPARE statement FROM #myString;
End than execute the statement
EXECUTE statement;
When you call your procedure you pass your string which will be part of statement:
CALL SimpleProcedure('SELECT id FROM t1 WHERE val1 = "myVal2"');
And that's the logic you should apply on your problem.
That should look like this:
CREATE DEFINER=`root`#`localhost` PROCEDURE `SampleProcedure`(IN category VARCHAR(255))
BEGIN
SET #myString =
CONCAT('SELECT u1.firstname , u1.lastname, u1.avatar , s1.address ,c1.cityName
FROM user u1,serviceprovider s1, city c1
WHERE s1.userId=u1.id
AND c1.cityId=s1.city
AND s1.serviceProviderId
IN
(', category,
' INNER JOIN
db.serviceprovider_category t3 ON t2.serviceprovider_locationId=t3.serviceprovider_category
WHERE
t3.category_serviceProviderCategory IN (', category, '))');
PREPARE statement FROM #myString;
EXECUTE statement;
END
EDIT: note that between ' and INNER JOIN there is one blank space because CONCAT, without that, would connect last word from 'category' query and inner join and that will cause you problem and your query wont work!
GL!
P.S. Also i notice that you mix both syntax when JOIN table (old comma separated JOIN and the new way) which is not look nice, it would be good to correct that and use new INNER JOIN syntax like you do in your sub query...
New EDIT (based on question edit)
CREATE DEFINER=`root`#`localhost` PROCEDURE `SampleProcedure`(IN category VARCHAR(255)
IN location VARCHAR(255),
IN classification VARCHAR(255))
BEGIN
SET #myString =
CONCAT('SELECT u1.firstname , u1.lastname, u1.avatar , s1.address ,c1.cityName
FROM user u1,serviceprovider s1, city c1
WHERE s1.userId=u1.id
AND c1.cityId=s1.city
AND s1.serviceProviderId
IN
(SELECT DISTINCT serviceprovider_cl AS serviceProviderId FROM db.serviceprovider_classification t1
INNER JOIN
db.locationid t2 ON t1.serviceprovider_cl=t2.serviceprovider_locationId
INNER JOIN
db.serviceprovider_category t3 ON t2.serviceprovider_location
INNER JOIN
db.serviceprovider_category t3 ON t2.serviceprovider_locationId=t3.serviceprovider_category
WHERE
t1.serviceproviderclassification_classification IN (', classification, ')
AND
t2.location_serviceLocation IN (', location, ')
AND
t3.category_serviceProviderCategory IN (', category, '))');
PREPARE statement FROM #myString;
EXECUTE statement;
END
Subquery in IN statement is equal to JOIN with subquery's table. Why you need "exotic" syntax instead of simple join.

if condition inside a sql query

following is a part of my stored proceedure im using it to extract data from my db.
query
BEGIN
SET #sqlstring = CONCAT("SELECT b.ID, c.name, c.accountID,, b.total_logs, a.time_start, a.time_end ,COUNT(a.id) AS number_of_users
FROM ",logtable," a INNER JOIN users b on a.ID = b.ID INNER JOIN accounts c on b.accountID = c.accountID
GROUP BY ID;");
PREPARE stmt FROM #sqlstring;
EXECUTE stmt;
END
At times in the db, the logtable(table is passed in a variable like logtable_1, logtable_2 .... ) can be non existent, currently when the perticuler table is missing it crashes and throws an error because a.time_start, a.time_end cannot have values without the log table.
but what i want is just to assign NULL on values a.time_start, a.time_end without throwing an error,
So can any body tell is there a way i could modify this code like
BEGIN
if logtable exists
\\ the query
else
\\ the query
END
Find existence of the table by querying information_schema.tables. If it returns a count equals to 1 then you can proceed executing your query on the table. Otherwise go with your Else block.
Sample:
declare table_exists int default 0;
select count(1) into table_exists
from information_schema.tables
where table_schema='your_table_schema_name'
and table_name = 'your_table_name';
if table_exists then
-- do something
else
-- do something else
end if;

mysql return results from update

I want to select a bunch of rows from a mysql database and update the viewed attribute of those once selected (this is a kind of 'I have read these' flag).
Initially I did something like this:
update (
select a, b, c
from mytable
where viewed = '0'
)
set viewed = '1';
This selects the rows nicely and updates their viewed attribute as required. But it does not return the selected rows from the subquery.
Is there a clause I can add, or perhaps I need to store the subquery, etc...? I did consider a transaction but I ended up with the same problem. I have not tried a stored procedure...
Please can someone advise / point me in the right direction on how to do what I do above but in addition return the selected tables from the subquery?
Thanks in advance.
Update:
As pointed out by #Barmar, #a_horse_with_no_name, #fancyPants and #George Garchagudashvil...
In MySQL you have to use two statements to select and update, and not a nested statement as in my initial post, if you want to return the selected rows.
e.g.
begin;
select a, b, c
from mytable
where viewed = '0';
update mytable
set viewed = '1'
where viewed = '0';
commit;
thanks guys.
I would create a simple function:
DELIMITER $$
DROP FUNCTION IF EXISTS `mydb`.`updateMytable`$$
CREATE
/*[DEFINER = { user | CURRENT_USER }]*/
FUNCTION `mydb`.`updateMytable`() RETURNS TEXT
BEGIN
SET #updated := '';
UPDATE mytable
SET viewed = 1
WHERE viewed = 0
AND (
SELECT #updated := CONCAT_WS(',', #updated, id)
) != ''
;
RETURN TRIM(LEADING ',' FROM #updated);
END$$
DELIMITER ;
which updates tables and returns concatenated ids.
From php you call this:
SELECT mydb.updateMytable()
and you get ids in a stirng: 1,2,7,54,132 etc...
Update:
my function is returning string containing comma separated ids:
'1,5,7,52,...' these ids are only which would have been updated during the function call,
better php-mysql example would be (you may and would use PDO):
$query = "SELECT mydb.updateMytable()";
$res = mysql_query($query);
$arr = mysql_fetch_array($res);
$ids = explode(',', $arr[0]);
// now you can do whatever you want to do with ids
foreach ($ids as $id)
{
echo "Hoorah: updated $id\n";
}
also remember to change mydb and mytable according to your database names
Final
because you need more complex functionality, simply run two query:
First run:
SELECT a, b, c
FROM mytable
WHERE viewed = 0
Next run:
UPDATE mytable
SET viewed = 1
WHERE viewed = 0

How to check if an index exists on a table field in MySQL

How do I check if an index exists on a table field in MySQL?
I've needed to Google this multiple times, so I'm sharing my Q/A.
Use SHOW INDEX like so:
SHOW INDEX FROM [tablename]
Docs: https://dev.mysql.com/doc/refman/5.0/en/show-index.html
Try:
SELECT * FROM information_schema.statistics
WHERE table_schema = [DATABASE NAME]
AND table_name = [TABLE NAME] AND column_name = [COLUMN NAME]
It will tell you if there is an index of any kind on a certain column without the need to know the name given to the index. It will also work in a stored procedure (as opposed to show index)
show index from table_name where Column_name='column_name';
SHOW KEYS FROM tablename WHERE Key_name='unique key name'
will show if a unique key exists in the table.
Use the following statement:
SHOW INDEX FROM *your_table*
And then check the result for the fields: row["Table"], row["Key_name"]
Make sure you write "Key_name" correctly
To look at a table's layout from the CLI, you would use
desc mytable
or
show table mytable
Adding to what GK10 suggested:
Use the following statement: SHOW INDEX FROM your_table
And then check the result for the fields: row["Table"],
row["Key_name"]
Make sure you write "Key_name" correctly
One can take that and work it into PHP (or other language) wrapped around an sql statement to find the index columns. Basically you can pull in the result of SHOW INDEX FROM 'mytable' into PHP and then use the column 'Column_name' to get the index column.
Make your database connection string and do something like this:
$mysqli = mysqli_connect("localhost", "my_user", "my_password", "world");
$sql = "SHOW INDEX FROM 'mydatabase.mytable' WHERE Key_name = 'PRIMARY';" ;
$result = mysqli_query($mysqli, $sql);
while ($row = $result->fetch_assoc()) {
echo $rowVerbatimsSet["Column_name"];
}
Try to use this:
SELECT TRUE
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = "{DB_NAME}"
AND TABLE_NAME = "{DB_TABLE}"
AND COLUMN_NAME = "{DB_INDEXED_FIELD}";
You can use the following SQL to check whether the given column on table was indexed or not:
select a.table_schema, a.table_name, a.column_name, index_name
from information_schema.columns a
join information_schema.tables b on a.table_schema = b.table_schema and
a.table_name = b.table_name and
b.table_type = 'BASE TABLE'
left join (
select concat(x.name, '/', y.name) full_path_schema, y.name index_name
FROM information_schema.INNODB_SYS_TABLES as x
JOIN information_schema.INNODB_SYS_INDEXES as y on x.TABLE_ID = y.TABLE_ID
WHERE x.name = 'your_schema'
and y.name = 'your_column') d on concat(a.table_schema, '/', a.table_name, '/', a.column_name) = d.full_path_schema
where a.table_schema = 'your_schema'
and a.column_name = 'your_column'
order by a.table_schema, a.table_name;
Since the joins are against INNODB_SYS_*, the match indexes only came from the INNODB tables only.
If you need to check if a index for a column exists as a database function, you can use/adopt this code.
If you want to check if an index exists at all regardless of the position in a multi-column-index, then just delete the part AND SEQ_IN_INDEX = 1.
DELIMITER $$
CREATE FUNCTION `fct_check_if_index_for_column_exists_at_first_place`(
`IN_SCHEMA` VARCHAR(255),
`IN_TABLE` VARCHAR(255),
`IN_COLUMN` VARCHAR(255)
)
RETURNS tinyint(4)
LANGUAGE SQL
DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT 'Check if index exists at first place in sequence for a given column in a given table in a given schema. Returns -1 if schema does not exist. Returns -2 if table does not exist. Returns -3 if column does not exist. If index exists in first place it returns 1, otherwise 0.'
BEGIN
-- Check if index exists at first place in sequence for a given column in a given table in a given schema.
-- Returns -1 if schema does not exist.
-- Returns -2 if table does not exist.
-- Returns -3 if column does not exist.
-- If the index exists in first place it returns 1, otherwise 0.
-- Example call: SELECT fct_check_if_index_for_column_exists_at_first_place('schema_name', 'table_name', 'index_name');
-- check if schema exists
SELECT
COUNT(*) INTO #COUNT_EXISTS
FROM
INFORMATION_SCHEMA.SCHEMATA
WHERE
SCHEMA_NAME = IN_SCHEMA
;
IF #COUNT_EXISTS = 0 THEN
RETURN -1;
END IF;
-- check if table exists
SELECT
COUNT(*) INTO #COUNT_EXISTS
FROM
INFORMATION_SCHEMA.TABLES
WHERE
TABLE_SCHEMA = IN_SCHEMA
AND TABLE_NAME = IN_TABLE
;
IF #COUNT_EXISTS = 0 THEN
RETURN -2;
END IF;
-- check if column exists
SELECT
COUNT(*) INTO #COUNT_EXISTS
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = IN_SCHEMA
AND TABLE_NAME = IN_TABLE
AND COLUMN_NAME = IN_COLUMN
;
IF #COUNT_EXISTS = 0 THEN
RETURN -3;
END IF;
-- check if index exists at first place in sequence
SELECT
COUNT(*) INTO #COUNT_EXISTS
FROM
information_schema.statistics
WHERE
TABLE_SCHEMA = IN_SCHEMA
AND TABLE_NAME = IN_TABLE AND COLUMN_NAME = IN_COLUMN
AND SEQ_IN_INDEX = 1;
IF #COUNT_EXISTS > 0 THEN
RETURN 1;
ELSE
RETURN 0;
END IF;
END$$
DELIMITER ;
You can't run a specific show index query because it will throw an error if an index does not exist. Therefore, you have to grab all indexes into an array and loop through them if you want to avoid any SQL errors.
Heres how I do it. I grab all of the indexes from the table (in this case, leads) and then, in a foreach loop, check if the column name (in this case, province) exists or not.
$this->name = 'province';
$stm = $this->db->prepare('show index from `leads`');
$stm->execute();
$res = $stm->fetchAll();
$index_exists = false;
foreach ($res as $r) {
if ($r['Column_name'] == $this->name) {
$index_exists = true;
}
}
This way you can really narrow down the index attributes. Do a print_r of $res in order to see what you can work with.