MySQL: how to include '\G' inside a procedure - mysql

I am creating a procedure in MySQL with the following codes:
delimiter #
drop procedure if exists a03_strings #
create procedure a03_strings ()
begin
select concat(cl_id, ' ',cl_name_last, ', ', cl_name_first) as Client
, group_concat(coalesce(concat(an_name, ' (', an_type, ')'), 'no animals')) as Animals
from vt_clients
left join vt_animals using (cl_id)
group by cl_id\G;
end;
#
I added \G at line 9 in order to get the following output:
So the output is not a table but rows.
When I test this just as select statements it worked with \G. However, when I include \G inside the procedure and it is giving me error saying that I cannot include a procedure inside another procedure.
Is there an alternative to achieve the output I need?
Thanks!

Short answer is no: '\G' is handled by the mysql client not the DBMS (where the stored procedure runs. But you can simply:
mysql> create procedure a03_strings ()
-> begin
-> select concat(cl_id, ' ',cl_name_last, ', ', cl_name_first) as Client
-> , group_concat(coalesce(concat(an_name, ' (', an_type, ')'), 'no animals')) as Animals
-> from vt_clients
-> left join vt_animals using (cl_id)
-> group by cl_id\G;
-> end;
-> #
...
mysql-> call a03strings() \G#
Or use this as your SELECT...
SELECT IF (rowselect='A',
CONCAT('Client: ', ilv.Client),
CONCAT('Animals', ilv.Animals)
FROM (
select concat(cl_id, ' ',cl_name_last, ', ', cl_name_first) as Client
, group_concat(coalesce(concat(an_name, ' (', an_type, ')'), 'no animals')) as Animals
from vt_clients
left join vt_animals using (cl_id)
group by cl_id
) ilv,
(SELECT 'A' AS rowselect UNION SELECT 'B') AS rowmultiplier

Related

MySQL GROUP_CONCAT 'Invalid use of Group function' error that DOESN'T involve an aggregate function

I have a MySQL stored procedure (shown below) that's supposed to build a list of IDs from a table of hierarchically related records. I'm having to re-engineer an older stored procedure to switch from using a simple CONCAT function to GROUP_CONCAT because the former couldn't handle the sizes of the lists being generated (i.e., the lists are going well over the 1024 character limit of the CONCAT function).
DELIMITER $$
CREATE PROCEDURE SPTest (top_id INT)
BEGIN
DECLARE ids_all TEXT;
SET SESSION group_concat_max_len = 1000000;
SET ids_all = top_id;
SET #str = GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR \', \') ',
'FROM tbl WHERE nha_id = ', top_id, ' INTO #ids_tmp' SEPARATOR '');
PREPARE stmt FROM #str;
EXECUTE stmt;
WHILE #ids_tmp != "" DO
SET ids_all = GROUP_CONCAT(ids_all, #ids_tmp SEPARATOR ', ');
SET #str = GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR \', \') ',
'FROM tbl WHERE nha_id IN (', #ids_tmp, ') INTO #ids_tmp' SEPARATOR '');
PREPARE stmt FROM #str;
EXECUTE stmt;
END WHILE;
SELECT ids_all AS ids;
END $$
DELIMITER ;
The problem is this routine is generating the following error when I try to call it (and sometimes it returns this error when I try to create the Stored Procedure):
ERROR 1111 (HY000): Invalid use of group function
When I manually create the same kinds of queries this code would build and run them at the command-line, they work perfectly fine -- no errors of any kind, and I get the results I expect. I've seen posts (both here on Stack Exchange and elsewhere) which say that MySQL doesn't support nested aggregate functions, but the concatenation here is just being done to strings. So I thought that maybe somehow it was seeing the GROUP_CONCAT in the string and burping because of that, so I tried putting "XXXX" in place of "GROUP_CONCAT" in the string and then using the REPLACE function to switch it back, but that made no difference. I also tried it with WHERE and HAVING for the criteria clause, but neither one worked. I've also done an extensive web search and was unable to find anything that was in any way helpful.
I don't know what else to try, mainly because I can't see what's wrong with the syntax here. Any help would be appreciated.
Update 1:
I have since tried a modified version of the script where the GROUP_CONCAT merges data from a subquery like this:
SET #qrystr = GROUP_CONCAT('SELECT GROUP_CONCAT(c SEPARATOR ", ") ',
'FROM (SELECT id AS c FROM tbl WHERE nha_id IN (', #ids_tmp,
') AS d INTO #ids_tmp' SEPARATOR '');
but that made no difference either.
You can't use GROUP_CONCAT() as a scalar function; it must be used in the context of a set of rows. Similarly, you can't use any other aggregate function without a table reference:
SET #x = MAX(<expr>); -- makes no sense
Ideally you should upgrade to MySQL 8.0 if you haven't already, and use a recursive CTE query instead:
WITH RECURSIVE hierarchy AS (
SELECT top_id AS id
UNION
SELECT tbl.id FROM tbl JOIN hierarchy ON tbl.nha_id = hierarchy.id
)
SELECT GROUP_CONCAT(id SEPARATOR ', ') AS ids_all FROM hierarchy;
That would eliminate the need for using loops or prepare/execute or temp variables.
Thanks to Bill Karwin's answer regarding the need to use SELECT with GROUP_CONCAT, I was able to see how my code needed to be changed. Rather than using a SET statement to assign the values to the variables, I need to use a SELECT ... INTO ... construct, as shown here:
DELIMITER $$
CREATE PROCEDURE ASSEMBLY_LIST_TEST (top_id INT)
BEGIN
DECLARE ids_all TEXT DEFAULT '';
SET SESSION group_concat_max_len = 1000000;
SET ids_all = top_id;
# Find the 1st-level children of 'top_id' to start building the list
SELECT GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR ", ") FROM tbl ',
'WHERE nha_id = ', top_id, ' INTO #ids_tmp' SEPARATOR '') INTO #qrystr;
PREPARE stmt FROM #qrystr;
EXECUTE stmt;
# Recursively find the children of each level of children of the previous loop & add to the list
WHILE #ids_tmp != '' DO
SELECT GROUP_CONCAT(ids_all, ', ', #ids_tmp SEPARATOR '') INTO ids_all;
SELECT GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR ", ") FROM tbl ',
'WHERE nha_id IN (', #ids_tmp, ') INTO #ids_tmp' SEPARATOR '') INTO #qrystr;
PREPARE stmt FROM #qrystr;
EXECUTE stmt;
END WHILE;
SELECT ids_all AS ids;
END $$
DELIMITER ;
This now produces exactly the result I was looking for. Of course, the recursive query approach is a much better solution for what I want this to do, but maybe my solution will help someone else.

select table and replace column fields by value from another table in mysql

I have been working in ms SQL from the last 3 year, I am new in MySQL, recently we have some MySQL requirement to select a table and replace column fields by value from another table.
I have converted working ms SQL query to MySQL. while executing MySQL query getting an exception. attached table description.here enter image description here
Please advise me on this.
Following are my tried queries
SET #v_Type := 'Account'; // where condition type =account
SET #select := 'SELECT';
SET #columns:= ( SELECT CONCAT('CONVERT(' , `ColDataType` , ', Col' , CONVERT(VARCHAR, `ColNum`) , ') AS [' , `ColName` , '],') FROM `Names` WHERE `TYPE` = #v_Type FOR XML PATH('') )
SET #where:='FROM `Data` WHERE [Type] = ''' + #v_Type + ''''
SET #statement = CONCAT(#select,#columns,#where);
PREPARE myStatement FROM #statement;
EXECUTE myStatement;
While executing I got the error like
Query: set #columns:= ( SELECT CONCAT('CONVERT(' , ColDataType , ', Col' , CONVERT(VARCHAR, ColNum) , ') AS [' , ColName , '],') ...
Error Code: 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 'VARCHAR, ColNum) , ') AS [' , ColName , '],') FROM `Name' at line 2
Thanks in advance.
SELECT CONCAT( 'SELECT ', GROUP_CONCAT(CONCAT('col', colnum, ' ', colname)),
' FROM data',
' WHERE type = ?')
INTO #sql
FROM names
WHERE type = #v_type
GROUP BY type;
PREPARE myStatement FROM #sql;
EXECUTE myStatement USING #v_type;
DROP PREPARE myStatement;
fiddle

MySQL Unknown column in 'order clause'

I have one procedure in my MySQL Database.
My procedure working fine. But currently I have to set order by tblUserKeyStatus.createdDate inside IF (opt=1) condition and when I set order by, It gives me the following error
Error Code: 1054. Unknown column 'tblUserKeyStatus.createdDate' in 'order clause
My Stored Procedure is given below
CREATE PROCEDURE proc_GetStatus(IN _userId varchar(64) , IN _qtr int, IN opt int)
BEGIN
IF (opt = 1) then
SELECT tblKeyStatus.*,'pending' AS `Status`,'' as scheduleDate,
' ' as doneDate,' ' as result FROM tblUserKeyStatus,tblKeyStatus WHERE tblKeyStatus.trimId not in
(SELECT trimId from tblUserKeyStatus WHERE userId=_userId)
union
SELECT tblKeyStatus.*,tblUserKeyStatus.`Status` AS `Status`,tblUserKeyStatus.scheduleDate,
tblUserKeyStatus.doneDate,tblUserKeyStatus.result FROM tblKeyStatus,tblUserKeyStatus WHERE
tblUserKeyStatus.trimId=tblKeyStatus.trimId and tblUserKeyStatus.userId=_userId
order by tblUserKeyStatus.createdDate;
Else
SELECT * from tblKeyStatus WHERE qtr=_qtr;
End if;
END
ORDER BY is applied to the whole query specified in the UNION operation, so that the entire result set returned by UNION is ordered.
Try to select createdDate field in both subqueries, so that it is accessible to ORDER BY:
SELECT tblUserKeyStatus.createdDate,
tblKeyStatus.*,'pending' AS `Status`,
'' as scheduleDate,
' ' as doneDate,
' ' as result
FROM tblUserKeyStatus, tblKeyStatus
WHERE tblKeyStatus.trimId not in (SELECT trimId
from tblUserKeyStatus
WHERE userId=_userId)
UNION
SELECT tblUserKeyStatus.createdDate,
tblKeyStatus.*,
tblUserKeyStatus.`Status` AS `Status`,
tblUserKeyStatus.scheduleDate,
tblUserKeyStatus.doneDate,tblUserKeyStatus.result
FROM tblKeyStatus,tblUserKeyStatus
WHERE tblUserKeyStatus.trimId=tblKeyStatus.trimId and
tblUserKeyStatus.userId=_userId
ORDER BY createdDate;
You also have to remove the tblUserKeyStatus name prefix.
Try having braces for select
CREATE PROCEDURE proc_GetStatus(IN _userId varchar(64) , IN _qtr int, IN opt int)
BEGIN
IF (opt = 1) then
SELECT tblKeyStatus.*,'pending' AS `Status`,'' as scheduleDate,
' ' as doneDate,' ' as result FROM tblUserKeyStatus,tblKeyStatus WHERE tblKeyStatus.trimId not in
((SELECT trimId from tblUserKeyStatus WHERE userId=_userId)
union
(SELECT tblKeyStatus.*,tblUserKeyStatus.`Status` AS `Status`,tblUserKeyStatus.scheduleDate,
tblUserKeyStatus.doneDate,tblUserKeyStatus.result FROM tblKeyStatus,tblUserKeyStatus WHERE
tblUserKeyStatus.trimId=tblKeyStatus.trimId and tblUserKeyStatus.userId=_userId
order by createdDate));
Else
SELECT * from tblKeyStatus WHERE qtr=_qtr;
End if;
END

MySQL UDF/Stored function not recognizing table name passed as an argument

The following is the stored function:
DELIMITER //
CREATE FUNCTION `calcMedian`(
`tbl` VARCHAR(64),
`clm` VARCHAR(64)
) RETURNS decimal(14,4)
BEGIN
SELECT AVG(middle_values) AS 'median'
INTO medRslt
FROM (
SELECT t1.clm AS 'middle_values'
FROM
(
SELECT #row:=#row+1 as `row`, table_column_name
FROM tbl, (SELECT #row:=0) AS r
ORDER BY clm
) AS t1,
(
SELECT COUNT(*) as 'count'
FROM tbl
) AS t2
WHERE t1.row >= t2.count/2 and t1.row <= ((t2.count/2) +1)) AS t3;
RETURN medRslt;
END//
DELIMITER ;
I then proceed to execute the following query:
USE ap2;
SELECT vendor_id, calcMedian('invoices', 'invoice_total')
FROM invoices i
WHERE vendor_id = 97
GROUP BY vendor_id;
I get the error message:
SQL Error (1146): Table 'ap2.tbl' doesn't exist *
I understand that the following may be better off as stored procedure/prepared statement rather than function. I just want to take things one step at a time now.
Also I made a different function to simply output the value stored in the variable 'tbl', and it displayed the correct table name (invoices in this case).
Identifiers in a SQL statement cannot be provided as values. Identifiers (table names, column names, function names, etc.) must be specified in the SQL text.
To get the value of the tbl variable (procedure argument) used as a table name within a SQL statement in the procedure, you can use dynamic SQL.
Set a variable to the SQL text, incorporate the string value, and then execute the string as a SQL statement. As an example:
SET #sql = CONCAT( 'SELECT AVG(middle_values) AS `median`'
, ' INTO medRslt'
, ' ... '
, tbl
, ' ... '
);
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Be aware that incorporating string values into the SQL text makes the procedure subject to SQL Injection vulnerabilities.
If I had to do this, I would reduce the potential for SQL Injection by verifying that tbl does not contain a backtick character, and enclose/escape the identifier in backticks, e.g.
CONCAT( ' ...' , '`' , tbl , '`' , ' ... ' );
^^^ ^^^

MySQL sorting table by column names

I have already built a table with field names in arbitrary order. I want those field names to be in alphabetical order so that I can use them in my dropdown list. Is it possible with a query?
Select columns from a specific table using INFORMATION_SCHEMA.COLUMNS and sort alphabetically with ORDER BY:
SELECT column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = '[schemaname]'
AND table_name = '[tablename]'
ORDER BY column_name
Note: The following code will alter the specified table and reorder the columns in alphabetical order
This should do the trick. It's a bit messy and lengthy, and you'll have to change the database name and table name, but for this one, the only requirement is that there is a database named "test" and that you are running these commands in it:
Let's create the tables we need:
-- CREATE TESTING TABLE IN A DATABASE NAMED "test"
DROP TABLE IF EXISTS alphabet;
CREATE TABLE alphabet (
d varchar(10) default 'dee' not null
, f varchar(21)
, e tinyint
, b int NOT NULL
, a varchar(1)
, c int default '3'
);
-- USE A COMMAND STORAGE TABLE
DROP TABLE IF EXISTS loadcommands;
CREATE TABLE loadcommands (
id INT NOT NULL AUTO_INCREMENT
, sqlcmd VARCHAR(1000)
, PRIMARY KEY (id)
);
Now let's create the two stored procedures required for this to work:
Separating them since one will be responsible for loading the commands, and including a cursor to immediately work with it isn't plausible (at least for me and my mysql version):
-- PROCEDURE TO LOAD COMMANDS FOR REORDERING
DELIMITER //
CREATE PROCEDURE reorder_loadcommands ()
BEGIN
DECLARE limitoffset INT;
SET #rank = 0;
SET #rankmain = 0;
SET #rankalter = 0;
SELECT COUNT(column_name) INTO limitoffset
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = 'test'
AND table_name = 'alphabet';
INSERT INTO loadcommands (sqlcmd)
SELECT CONCAT(t1.cmd, t2.position) AS commander FROM (
SELECT #rankalter:=#rankalter+1 AS rankalter, CONCAT('ALTER TABLE '
, table_name, ' '
, 'MODIFY COLUMN ', column_name, ' '
, column_type, ' '
, CASE
WHEN character_set_name IS NOT NULL
THEN CONCAT('CHARACTER SET ', character_set_name, ' COLLATE ', collation_name, ' ')
ELSE ' '
END
, CASE
WHEN is_nullable = 'NO' AND column_default IS NULL
THEN 'NOT NULL '
WHEN is_nullable = 'NO' AND column_default IS NOT NULL
THEN CONCAT('DEFAULT \'', column_default, '\' NOT NULL ')
WHEN is_nullable = 'YES' THEN 'DEFAULT NULL '
END
) AS cmd
, column_name AS columnname
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = 'test'
AND table_name = 'alphabet'
ORDER BY columnname
) t1
INNER JOIN (
SELECT #rankmain:=#rankmain+1 AS rownum, position FROM (
SELECT 0 AS rownum, 'FIRST' AS position
, '' AS columnname
UNION
SELECT #rank:=#rank+1 AS rownum, CONCAT('AFTER ', column_name) AS position
, column_name AS columnname
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = 'test'
AND table_name = 'alphabet'
ORDER BY columnname
LIMIT limitoffset
) inner_table
) t2 ON t1.rankalter = t2.rownum
;
END//
DELIMITER ;
If anyone thinks/sees that I'm missing to include any important column attributes in the ALTER command, please hesitate not and mention it! Now to the next procedure. This one just executes the commands following the order of column id from the loadcommands table. :
-- PROCEDURE TO RUN EACH REORDERING COMMAND
DELIMITER //
CREATE PROCEDURE reorder_executecommands ()
BEGIN
DECLARE sqlcommand VARCHAR(1000);
DECLARE isdone INT DEFAULT FALSE;
DECLARE reorderCursor CURSOR FOR
SELECT sqlcmd FROM loadcommands ORDER BY id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET isdone = TRUE;
OPEN reorderCursor;
read_loop:LOOP
FETCH reorderCursor INTO sqlcommand;
IF isdone THEN
LEAVE read_loop;
END IF;
SET #sqlcmd = sqlcommand;
PREPARE stmt FROM #sqlcmd;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP read_loop;
CLOSE reorderCursor;
END//
DELIMITER ;
The SQL is long, so if someone can point out ways (and has tested them) to make this shorter I'd gladly do it, but for now, this at least works on my end. I also didn't need to put dummy data in the alphabet table. Checking the results can be done using the SHOW... command.
The last part:
-- TO TEST; AFTER RUNNING DDL COMMANDS:
SHOW CREATE TABLE alphabet; -- SEE ORIGINAL ORDER
CALL reorder_loadcommands(); -- PREPARE COMMANDS
CALL reorder_executecommands(); -- RUN COMMANDS
SHOW CREATE TABLE alphabet; -- SEE NEW ORDER
Perhaps later on I could make reorder_loadcommands dynamic and accept table and schema parameters, but I guess this is all for now..