mysql stored procedure - order by column index - mysql

I'm wrining a stored procedure in mysql and I want to use a parameter for the column index in the order-by-clause. I've tried the following:
CREATE PROCEDURE `testProc` (
IN $sortColNum INT
)
BEGIN
SELECT id, title, date, sticky, published, created, updated, content
FROM news
ORDER BY $sortColNum DESC;
END
The stored procedure doesn't throw an error, but the result is unsorted. When i use the column index as a parameter in a prepared statement, it works fine. Why doesn't it work in a stored procedure?

This won't work. Your $sortColNum is treated as a constant rather than a column reference.
You have two choices. One is to use a prepare statement. The other is to explicitly list the columns in a case statement:
order by (case $sortColNum
when 1 then id
when 2 then title
when 3 then date
when 4 then sticky
. . .
end)
This method has the downside that all values are converted to the same data type (presumably strings). You might want to do explicit conversion yourself, in the event that the conversion affects the sort order.

Try this one:
CREATE PROCEDURE `testProc`(IN sortColNum INT)
BEGIN
SET #query = CONCAT ('SELECT id, title, date, sticky, published, created, updated, content
FROM news ORDER BY (',sortColNum,') DESC');
PREPARE stmt FROM #query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END

Related

Using a Variable as a Table Name with CONCAT_WS in INSERT > SELECT

I have a database that stores readings from various instruments. By default, a new table is created in the database for each month and the instrument readings are inserted into the table for that month. As an example, the table for January 2021 is called data_1_2021_01.
The table for February 2021 is called data_1_2021_02.
The following stored procedure creates a table and inserts data for a specific month from that month's table (in the example, data_1_2021_01) and a second table that contains instrument tag information. The procedure (shown below) returns the results I need as expected:
DELIMITER //
CREATE
PROCEDURE db.DATAPREP ()
BEGIN
DROP TABLE IF EXISTS db.DATA;
CREATE TABLE db.DATA (
ts DATETIME,
tagid INT(11) NOT NULL,
tagpath VARCHAR(255),
curvalue FLOAT,
t_stamp BIGINT(20) NOT NULL,
INDEX (tagid , tagpath)
);
INSERT INTO db.DATA (
ts,
tagid,
tagpath,
curvalue,
t_stamp)
SELECT
FROM_UNIXTIME(data_1_2021_01.t_stamp/1000),
data_1_2021_01.tagid,
te.tagpath,
Case WHEN data_1_2021_01.FloatValue IS Null then data_1_2021_01.intValue
else data_1_2021_01.floatvalue END,
data_1_2021_01.t_stamp
FROM data_1_2021_01, te
where data_1_2021_01.tagid=te.id;
END //
DELIMITER ;
Now I need to automate the process, so the stored procedure can be executed on a monthly basis and pull in the data from the current month's table without manually having to update the table name in the procedure. I've declared variables to get the table name, but I can't get the INSERT INTO > SELECT to work properly. If I use a CONCAT_WS to identify the table/column name in the SELECT statement, it tries to store the result as the data value instead of viewing it as the table/column to pull the data value from. I've tried using PREPARE, EXECUTE, DEALLOCATE, but I can't seem to get that to work, either. I'm just not familiar enough with how to make this work in MySQL, so any help is greatly appreciated. Here's the script (minus PREPARE, EXECUTE, DEALLOCATE statements):
DELIMITER //
CREATE
PROCEDURE db.DATAPREP ()
BEGIN
-- Set this month's table.
DECLARE CurrentMthTable varchar(16383);
-- Set the current year.
DECLARE CurrentYear char(4);
-- Set the current month.
DECLARE CurrentMth varchar(2);
-- GET the year for the current month table (%Y returns year as 4-digit value).
Set CurrentYear = DATE_FORMAT(curdate(), '%Y');
-- Get the current month (%m returns month as 2-digit value).
SET CurrentMth = DATE_FORMAT(curdate(), '%m');
-- The entire Table name
Set CurrentMthTable = CONCAT_WS("_", "data_1", CurrentYear, CurrentMth);
DROP TABLE IF EXISTS db.DATA;
CREATE TABLE db.DATA (
ts DATETIME,
tagid INT(11) NOT NULL,
tagpath VARCHAR(255),
curvalue FLOAT,
t_stamp BIGINT(20) NOT NULL
);
INSERT INTO db.DATA (
ts,
tagid,
tagpath,
curvalue,
t_stamp)
SELECT
FROM_UNIXTIME((CONCAT_WS(“”, CurrentMthTable, “.t_stamp”)/1000)),
CONCAT_WS("", CurrentMthTable, ".tagid"),
te.tagpath,
Case WHEN (CONCAT_WS("", CurrentMthTable, ".FloatValue")) IS Null then (CONCAT_WS("", CurrentMthTable, ".intValue")) else (CONCAT_WS("", CurrentMthTable, ".floatvalue")) END,
CONCAT_WS("", CurrentMthTable, ".t_stamp")
FROM CurrentMthTable, te
where (CONCAT_WS("", CurrentMthTable, ".tagid"))=te.id
));
END //
DELIMITER ;
For what it's worth, SQL cannot use substitution variables for the names of schema items like tables, columns, databases, indexes, stored procedures, and the like. It just can't. This isn't a MYSql limitiation only, it's true for all sorts of SQL database servers.
As you have discovered, you can do what you need by using MySQL "server side prepared statements." This is a way to use string processing to create your SQL statements, then execute them.
If you do this in your stored procedure you need to write SQL string processing code to create the text of your SQL. Something like this to copy last month's table into this month's, then erase the data?
DECLARE stmt VARCHAR(16000);
SELECT CONCAT_WS("",
"CREATE TABLE data_1_",
DATE_FORMAT(NOW(), '%Y_%c'),
" AS SELECT * FROM data_1_",
DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y_%c'), ';')
INTO stmt;
/* now stmt is CREATE TABLE data_1_2021_2 AS SELECT * FROM data_1_2021_1; */
PREPARE s1 FROM stmt;
EXECUTE s1;
DEALLOCATE PREPARE s1;
SELECT CONCAT_WS("",
"TRUNCATE TABLE data_1_",
DATE_FORMAT(NOW(), '%Y_%c'),';')
INTO stmt;
/* now stmt is TRUNCATE TABLE data_1_2021_2; */
PREPARE s1 FROM stmt;
EXECUTE s1;
DEALLOCATE PREPARE s1;
My meta-point: This is hard programming to get right, even for simple stuff. And it's hard enough to read that you'll have trouble reasoning about it when troubleshooting in the future.
And, for what it's worth, you'll be much better off using a single table for all your data from all your instruments rather than a table for every month. Seriously. Accessing records from a single large and properly indexed table puts about the same load on the database server, is simpler to program, and gracefully scales out from months to years to decades. A table for each month just isn't wise.

Mysql Stored Procedure: match the column rule to retrieve value

I have a stored procedure which function to retrieve the ColumnProperty based on the input parameter matching the ColumnRule.
For example,
I will execute the below to get the ColumnProperty as string(short). But the problem is when using WHERE to filter ColumnRule, the retrieved value is string and get unknown type at the end.
CALL `testing-spGetColumnType`('varchar(11)', #outputProperty);
select #outputProperty;
DELIMITER $$
CREATE PROCEDURE `testing-spGetColumnType`(IN pColumnType varchar(50),OUT pColumnProperty varchar(50))
BEGIN
SELECT ColumnProperty FROM model_column_type where pColumnType like replace(ColumnRule, 'variable', pColumnType) into pColumnProperty;
SELECT IFNULL(pColumnProperty,'unknown type') into pColumnProperty;
END$$
DELIMITER ;
The sample table:
when the condition in the WHERE clause is evaluated against the stringShort row, given 'varchar(11)' as the argument, it's equivalent to
WHERE 'varchar(11)'
LIKE '%varchar% and SUBSTRING_INDEX(SUBSTRING_INDEX(varchar(11),''('',-1),'')'',1) < 50'
and the result of the LIKE comparison will be FALSE.
The value of ColumnRule from that row
%varchar% and SUBSTRING_INDEX(SUBSTRING_INDEX(variable,'(',-1),')',1) < 50
is a value. It doesn't matter that it looks like SQL text. In the context of the SELECT statement, it is just a string of characters. It's a string value. It is not references to identifiers, or SQL functions, or boolean operators.
We would get the same result if the ColumnRule value was
%varchar% one two buckle my shoe
To get a string value to be seen as SQL text, we can use dynamically prepared SQL.
In the context of MySQL PROCEDURE, we can dynamically create SQL text and store it as a string, and then execute the string, something like this:
SET #foo = ' foo, bar' ;
SET #sql = CONCAT('SELECT ',#foo,' FROM mytab',' ORDER BY ',#foo);
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
A word of caution: if we dynamically create SQL text, and incorporate potentially unsafe values, we can open a gaping SQL Injection vulnerability, ala Little Bobby Tables https://xkcd.com/327/
MySQL Reference https://dev.mysql.com/doc/refman/8.0/en/sql-syntax-prepared-statements.html

MySQL create a dynamic table name in a Join statement

long time user, first time poster.
I have 2 tables;
a1_watchlists {id(PK),name,date}
a1_watchlist {id(PK),watchlists_id(FK(a1_watchlists.id)),company_name,asx_code,date}
I also have 2000 other tables that have been created with the name 'asx_'+[asx_code] (where asx_code is pulled from another table)
this table looks like;
asx_[asx_code] {date(PK),open,high,low,close,volume}
I want to select all from a1_watchlists and a1_watchlist and then select the latest date from the asx_[asx_code] table using the value from a1_watchlist.asx_code to generate the [asx_code] part of the table name.
The problem I have is that I want to use the value from a1_watchlist.asx_code as the table name prepending the string 'asx_' to this first.
Closest I have been able to get is;
DECLARE #TableName VARCHAR(100)
SELECT *
FROM a1_watchlist AS wl
JOIN a1_watchlists AS wls
ON wls.id = wl.watchlists_id
SET #TableName = 'asx_' + wl.asx_code
INNER JOIN (SELECT MAX(date),open,high,low,close,volume,amount_change,percent_change FROM #TableName)
This currently give the error:
1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DECLARE #TableName VARCHAR(100)
SELECT *
FROM a1_watchlist AS wl
' at line 1
The expected colums I need in the final result would be:
wl.id,wl.watchlists_id,wl.company_name,wl.asx_code,asx_[asx_code].date,asx_[asx_code].open,asx_[asx_code].high,asx_[asx_code].low,asx_[asx_code].close,asx_[asx_code].volume
Let me know if you require more information.
I'm not going to speak to what to do in the case where you have 2000+ tables that start with asx+ some code... (i live in a town with multiple bridges) or even whether what you're doing is the best way to get where you want to go. BUT, it does look like you're attempting to concatenate things together and create a dynamic statement. If that sounds right, then I'd recommend you look into prepared statements. Like the following. Hope this helps.
DELIMITER $$
DROP PROCEDURE IF EXISTS prRetrieveAllFromTable$$
CREATE PROCEDURE prRetrieveAllFromTable(tableName VARCHAR(64))
BEGIN
SET #s = CONCAT('SELECT * FROM ',tableName );
PREPARE stmt FROM #s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END$$
DELIMITER ;
CALL prRetrieveAllFromTable('calendar');
http://dev.mysql.com/doc/refman/5.5/en/sql-syntax-prepared-statements.html
How To have Dynamic SQL in MySQL Stored Procedure

Using "IN" Statement for Stored Procedure Parameter SQL [duplicate]

This question already has answers here:
Parameterize an SQL IN clause
(41 answers)
Closed 9 years ago.
I have a stored procedure with one input parameter called "#IDs". This gets populated from my application which will populate it in the following format: '2, 30, 105'. The number of values inside this parameter will differ of course (For example: sometimes #IDs will be '100, 2005, 2, 510') My stored procedure is very simple. I have a table called "Persons". I'm trying to write this query:
Select * From Persons Where P_Id in (#IDs)
P_ID is the primary key in my table. The error I get is 'Conversion failed when converting the varchar value '2, 3, 4' to data type int.' Any suggestions will be greatly appreciated. Thanks.
One way is use dynamic SQL. That is generate the SQL as a string and then execute it.
An easier way (perhaps) is to use like:
where concat(', ', #IDS, ', ') like concat('%, ', id, ', %')
Note that this puts the separator at the beginning and end of the expressions, so "10" won't match "11010".
you might need to do a prepared statement. The idea is to build the select sentence and then execute it. Here's an example on how to do it...
USE mydb;
DROP PROCEDURE IF EXISTS execSql;
DELIMITER //
CREATE PROCEDURE execSql (
IN sqlq VARCHAR(5000)
) COMMENT 'Executes the statement'
BEGIN
SET #sqlv=concat(concat('select abc from yourtable where abc in (',sqll),')');
PREPARE stmt FROM #sqlv;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
Just change the query for the one you want to execute.
1) Show your code.
2) You've probably tried to pass in all the values as one parameter. That doesn't work. You have to list them as separate parameters and then bind them as separate parameters.
Yes, this makes it hard to use stored procedures when the number of in parameters may change.

Limiting selected rows count with a stored procedure parameter in MySQL

I have a procedure SelectProc which contains a SELECT statement. I want to add a procedure param LimitRowsCount and use it as following:
CREATE PROCEDURE SelectProc (IN LimitRowsCount INTEGER UNSIGNED)
BEGIN
SELECT (...)
LIMIT LimitRowsCount;
END
but this approach doesn't work.
The SELECT itself contains nested subqueries so I can't create view from it. Is there a way more proper than dynamic SQL (prepared statements)?
CREATE PROCEDURE SelectProc (IN LimitRowsCount INT)
BEGIN
SET #LimitRowsCount1=LimitRowsCount;
PREPARE STMT FROM "SELECT (...) LIMIT ?";
EXECUTE STMT USING #LimitRowsCount1;
END
From the manual:
The LIMIT clause can be used to constrain the number of rows
returned by the SELECT statement. LIMIT takes one or two numeric
arguments, which must both be nonnegative integer constants
(except when using prepared statements).
MySQL Manual - 12.2.8. SELECT Syntax
So that's a no - you cannot.