MySQL nested case procedure using concat - mysql

I'm doing a nested case. However, I am getting an error near this line:
else
select concat('The month parameter ', p_month, ' is invalid; cannot proceed.');
This is actually the else case for the most inner case. p_month is an IN parameter and also an integer. Could this be the error?
Any thoughts will be helpful. Thank you.
I played around with it a little bit more right now. So I decided to SELECT on the outside block. However, I know now I have an error for the Select statement in the inner block. How can I fix that? Thanks.
entire code:
Create procedure ExamFeesMonth(in p_cl_id int, in p_month int)
begin
declare count_cl_id int;
declare num_exam int;
declare count_exam int;
declare v_msg varchar(200);
-- check if p_cl_id is in vt_clients
select count(*) into count_cl_id from vt_clients where cl_id = p_cl_id;
-- count the number of exams that has happened in p_month of previous year
select count(*) into num_exam
from vt_clients cl
join vt_headers h on cl.cl_id = h.cl_id
join vt_details d on h.ex_id = d.ex_id
where cl.cl_id = p_cl_id
and month(ex_date) = p_month
and year(ex_date) = (year(current_date())-1)
;
select
-- first case block starts
case
-- client valid
when count_cl_id = 1 then
-- second case block starts
case
-- p_month valid
when p_month in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) then
-- third case block starts
case
-- existing exams
when count_exam >= 1 then
select concat( 'Client ', p_cl_id, ' has ', count(h.ex_id),
' exam(s) with total fees of ', sum(ex_fee),
' in ', year(current_date())-1, '-', p_month)
from vt_clients cl
join vt_exam_headers h on cl.an_id = h.an_id
join vt_exam_details d on h.ex_id = d.ex_id
where cl_id = p_cl_id
and year(ex_date) = (year(current_date())-1)
and month(ex_date) = p_month
;
-- no exams
when count_exam = 0 then
concat( 'No exams for client ', p_cl_id, ' in 2011-' , p_month, '.');
-- third case block ends
end
-- p_month invalid
else
concat('The month parameter ', p_month, ' is invalid; cannot proceed.');
-- second case block ends
end
-- client invalid
when count_cl_id = 0 then
concat('We have no client with id ', p_cl_id, '; cannot proceed.') ;
-- first case block ends
end case;
end;
#

I think the issue in END statements in the nested CASEs. You should use END CASE. CASE...END CASE
-- third case block ends
end case; -- <-------------here
-- p_month invalid
else
concat('The month parameter ', p_month, ' is invalid; cannot proceed.');
-- second case block ends
end case; -- <-------------and here

Related

Hidden syntax errors in MYSQL procedure

I'm new to MYSQL and this is my first time creating a procedure so bear with me. I'm trying to loop through information_scheme.tables and populate a new table with info taken from some of the tables found there. I cannot save the procedure in workbench because of 'syntax errors'. I cannot figure out what those errors are however.
CREATE PROCEDURE `populate` ()
BEGIN
DECLARE thisDay varchar(100);
DECLARE i int;
DECLARE n int;
DECLARE cur CURSOR FOR SELECT table_name FROM (information_schema.tables);
SET i=0;
SELECT COUNT(table_name) FROM information_schema.tables INTO n;
OPEN cur;
getDay: LOOP
IF cur LIKE 'transactions_20%' THEN
FETCH cur INTO thisDay;
INSERT INTO days_totals
(date,num_of_ppl,punches,admission_total,pass_total,misc_total,food_total,drink_total,grand_total)
VALUES(
(SELECT SUBSTRING(thisDay,14,23)),
(SELECT COUNT(amount) FROM thisDay WHERE name = 'adult admission' OR name = 'punch a pass' OR name = 'child admission'),
(SELECT COUNT(amount) FROM thisDay WHERE name = 'punch a pass'),
(SELECT SUM(total) FROM thisDay WHERE name = 'adult admission' OR name = 'child admission'),
(SELECT SUM(total) FROM thisDay WHERE name = 'ten visit pass'),
(SELECT SUM(total) FROM thisDay WHERE type = 'misc' AND name != 'adult admission' AND name != 'child admission' AND name != 'ten visit pass'),
(SELECT SUM(total) FROM thisDay WHERE type = 'food'),
(SELECT SUM(total) FROM thisDay WHERE type = 'drink'),
(SELECT SUM(total) FROM thisDay WHERE type = 'misc' OR type = 'food' OR type = 'drink')
);
SET i = i + 1;
ELSE IF
i < n AND cur NOT LIKE 'transactions_20%' THEN SET i = i + 1;
ELSE
LEAVE getDay;
END IF;
END LOOP getDay;
CLOSE cur;
END
There are numerous problems with your procedure code.
You need to set the DELIMITER, even when using MySQL Workbench. See https://dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html
You can't use a variable in place of the table name. To make the table name dynamic, you must format the table name into an SQL query string, and then use PREPARE. See https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
You are comparing the cursor to a string, but you should compare the value fetched by the cursor.
You don't need a condition anyway, because you should just use a WHERE clause in your query against information_schema.tables.
Your cursor has no way of continuing. You don't declare a continue handler, and you just LEAVE the loop during the first iteration. Read examples more carefully: https://dev.mysql.com/doc/refman/8.0/en/cursors.html
Your i and n variables appear to have no purpose. You count up, but the value you count is not used.
I made the following version and tested it on MySQL 8.0. I used MySQL Workbench to create the procedure.
DELIMITER $$
CREATE PROCEDURE test.`populate`()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE thisDay varchar(100);
DECLARE cur CURSOR FOR SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'transactions_20%';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
getDay: LOOP
FETCH cur INTO thisDay;
IF done THEN
LEAVE getDay;
END IF;
SET #sql = CONCAT(
'SELECT
COUNT(CASE WHEN name IN (''adult admission'', ''punch a pass'', ''child admission'') THEN amount END),
COUNT(CASE WHEN name IN (''punch a pass'') THEN amount END),
SUM(CASE WHEN name IN (''adult admission'', ''child admission'') THEN total END),
SUM(CASE WHEN name IN (''ten visit pass'') THEN total END),
SUM(CASE WHEN name IN (''ten visit pass'') THEN total END),
SUM(CASE WHEN type = ''misc'' AND name NOT IN (''adult admission'', ''child admission'', ''ten visit pass'') THEN total END),
SUM(CASE WHEN type = ''food'' THEN total END),
SUM(CASE WHEN type IN (''misc'', ''food'', ''drink'') THEN total END)
FROM `', thisDay, '`
INTO #v_num_of_ppl, #v_punches, #v_admission_total, #v_pass_total, #v_misc_total, #v_food_total, #v_drink_total, #v_grand_total'
);
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO days_totals
SET date = SUBSTRING(thisDay, 14, 23),
num_of_ppl=COALESCE(#v_num_of_ppl, 0),
punches=COALESCE(#v_punches, 0),
admission_total=COALESCE(#v_admission_total, 0),
pass_total=COALESCE(#v_pass_total, 0),
misc_total=COALESCE(#v_misc_total, 0),
food_total=COALESCE(#v_food_total, 0),
drink_total=COALESCE(#v_drink_total, 0),
grand_total=COALESCE(#v_grand_total, 0);
END LOOP;
CLOSE cur;
END
By using CASE expressions inside the aggregation, this collects all the subtotals in one pass of reading the transactions table.
Frankly, I would not use a stored procedure at all. This task would be easier in virtually any other application programming language. There is no advantage to using a stored procedure in this case, and it's a language you are not accustomed to.

The group by clause is not working when used with sp and passed as a parameter

I have created a stored procedure for the purpose of optimization. Below is the actual code.
DELIMITER $$
CREATE PROCEDURE getImpressions (
IN aff_id BIGINT(20),
IN funn_id BIGINT(20),
IN grpBy TEXT,
IN odrBy TEXT
)
BEGIN
SELECT
affiliate_id,
funnel_id,
COUNT(DISTINCT (decimal_ip)) as no_of_records,
HOUR(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), 'UTC', 'US/Eastern')) AS hour,
CEIL(HOUR(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), 'UTC', 'US/Eastern'))/2) AS hoursby2,
DATE(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), 'UTC', 'US/Eastern')) AS date,
WEEK(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), 'UTC', 'US/Eastern')) AS week,
CEIL(WEEK(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), 'UTC', 'US/Eastern'))/2) AS weeksby2,
MONTH(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), 'UTC', 'US/Eastern')) AS month,
YEAR(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), 'UTC', 'US/Eastern')) AS year
FROM gs_aff_analytics
WHERE affiliate_id = aff_id AND funnel_id = funn_id
GROUP BY grpBy
ORDER BY odrBy
;
END$$
DELIMITER ;
#calling the sp
CALL getImpressions(36, 2, 'date', 'date');
Here everything is working fine. But the grpBy value which I'm passing through parameter is not working. This means the sp just not taking it even if I'm passing it. But as soon as I recreate the sp and explicitly write the group by clause as GROUP BY date (hard coding the group by) it start working properly.
In the context of the SQL statement, grpBy is a scalar value. The behavior is similar to including a literal value in place of grpBu
If we modify the SQL statement to replace grpBy with a literal value, e.g.
GROUP BY 'some_literal_string_value'
we would get an equivalent result.
grpBy is not seen as a column name. It's not seen as a SQL expression that references any identifier. (This isn't specific to the GROUP BY clause, this rule applies everywhere in the SQL statement.)
To get expressions/identifiers dynamically included in the SQL text, we would need to use create a string containing the SQL text, and then execute with dynamic SQL. Note that this approach can open up a huge SQL injection vulnerability...
SET #sql = CONCAT('SELECT ... GROUP BY ',grpBy,' ... ');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
Here is the working solution
DELIMITER $$
CREATE PROCEDURE getImpressions (
IN typ TEXT,
IN aff_id BIGINT(20),
IN funn_id BIGINT(20),
IN fromTimestamp BIGINT(20),
IN toTimestamp BIGINT(20),
IN grpBy TEXT,
IN odrBy TEXT
)
BEGIN
DECLARE strQuery text;
DECLARE stmtp text;
IF(typ = 'all')
THEN SET #strQuery = 'COUNT(1) AS no_of_records,';
END IF;
IF(typ = 'unique')
THEN SET #strQuery = 'COUNT(DISTINCT (decimal_ip)) AS no_of_records,';
END IF;
SET #strQuery = CONCAT('
SELECT ',
#strQuery,
'
HOUR(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), \'UTC\', \'US/Eastern\')) AS hour,
CEIL(HOUR(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), \'UTC\', \'US/Eastern\'))/2) AS hoursby2,
DATE(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), \'UTC\', \'US/Eastern\')) AS date,
WEEK(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), \'UTC\', \'US/Eastern\')) AS week,
CEIL(WEEK(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), \'UTC\', \'US/Eastern\'))/2) AS weeksby2,
MONTH(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), \'UTC\', \'US/Eastern\')) AS month,
YEAR(CONVERT_TZ(FROM_UNIXTIME(created_timestamp), \'UTC\', \'US/Eastern\')) AS year,
created_timestamp
FROM gs_aff_analytics
WHERE affiliate_id = ', aff_id, ' AND funnel_id = ', funn_id);
IF(fromTimestamp IS NOT NULL)
THEN SET #strQuery = CONCAT(#strQuery, ' AND created_timestamp BETWEEN ', fromTimestamp);
END IF;
IF(toTimestamp IS NOT NULL)
THEN SET #strQuery = CONCAT(#strQuery, ' AND created_timestamp <= ', toTimestamp);
END IF;
IF (grpBy IS NOT NULL)
THEN SET #strQuery = CONCAT(#strQuery, ' GROUP BY ', grpBy);
END IF;
IF (odrBy IS NOT NULL)
THEN SET #strQuery = CONCAT(#strQuery, ' ORDER BY ', odrBy);
END IF;
PREPARE stmtp FROM #strQuery;
EXECUTE stmtp;
END$$
DELIMITER ;
#calling the sp
CALL getAllImpressions('all', 36, 2, null, null, 'week', 'week');
CALL getAllImpressions('unique', 36, 2, 0, 1600000000, 'week', 'week');

MYSQL: ERROR 1241 (21000): Operand should contain 1 column(s)

this should be working but I get an error somehow. I want to get the credits column from the table and multiply it by 100. The problem is to get the number of credits for a given student id and year and get the total payment. Assuming each credit is $100.
delimiter //
create function fTest (stuYear varchar(4), stuID varchar(4))
returns varchar(200)
begin
declare msg varchar(200) default '';
if (stuYear = '' or stuYear is null) then
select 'Please input a valid year' into msg;
elseif (stuID = '' or stuID is null) then
select 'Please input a student id' into msg;
else
begin
if (msg = '' or msg is null) then
select ('No result found for student ID: ', stuID, ' at year: ', stuYear) into msg;
select (credits * 100) into msg from Students_Courses natural join Courses where sid=stuID and year=stuYear group by credits;
return msg ;
end if;
end ;
end if;
end ;
//
delimiter ;
This is incorrect:
select ('No result found for student ID: ', stuID, ' at year: ', stuYear)
A select statement can contain multiple columns, but you shouldn't enclose them in parentheses. Also, that would return multiple values, which you can't select into a single message.
So, I guess you wanted to concat the values into a single value. You can do that using the concat function, like so:
select concat('No result found for student ID: ', stuID, ' at year: ', stuYear)
Btw, a normal assignment using the concat function should also work in a trigger:
SET msg = concat('No result found for student ID: ', stuID, ' at year: ', stuYear);
See MySQL concat function
PS: In the next statement, you also got parentheses: (credits * 100)
In this case it accidentally will work, because it's a single expression. They don't have any functional value, though, and might as well be removed.

Split address column in streetname and number using SELECT statement

I have a mysql table with an address column.
Now I need to SELECT the streetname and number separately.
Address
Wallstreet 20
New Yorkavenue 30
New London Street 40
Needs to be:
Street: Number:
Wallstreet 20
New Yorkavenue 30
New London Street 40
Any ideas?
Thanks in advance!
If you assume that the number is the final "word" and separated by a space:
select replace(address, substring_index(address, ' ', -1), '') as street,
substring_index(address, ' ', -1) as number
I happen to think that those two assumptions are very big assumptions, meaning that this might not work on all your rows.
For Mysql probably you could create a MYSQL SUBSTRING_INDEX to separate the fields if the numbers are only in the address number and the address has no numbers.
Example
SELECT
REPLACE(address, SUBSTRING_INDEX(address, ' ', -1), '') as ADDRESS,
SUBSTRING_INDEX(address, ' ', -1) as NUMBER
FROM
ADDRESSES
it's not a really good method in performance and probably clould be done with other ways but if the schema is allways like the example it could works
Also probably is better in performance to do it on client side in the language that fetch the data.
You could use some string functions, like SUBSTRING_INDEX and LEFT.
Getting the Number is easy:
SELECT
SUBSTRING_INDEX(Street, ' ', -1)
(yes, it's not actually a number, but I suppose it's the last part of the string after the last space, it can also be a string as 20/C).
Getting the street name is a little more tricky:
SELECT
LEFT(Street,
CHAR_LENGTH(Street)
-CHAR_LENGTH(SUBSTRING_INDEX(Street, ' ', -1))
-1
) AS street_name,
SUBSTRING_INDEX(Street, ' ', -1) AS street_number
FROM
tablename
I'm may be some what late, but issues are still the same nower days :-)
The question was, how to split/select a streetname (Straßenname) and the number (Hausnummer) out of a adress string?
Therefor I created a function, to implement German DIN 5008:
DROP FUNCTION IF EXISTS HAUSNUMMER_DIN_5008;
delimiter //
CREATE DEFINER=`vlw`#`%` FUNCTION `HAUSNUMMER_DIN_5008`(
oldStreet VARCHAR(255), formating BOOL
) RETURNS varchar(16) CHARSET utf8
BEGIN
SET #oldString := oldStreet;
SET #newString := "";
tokenLoop: LOOP
END LOOP tokenLoop;
-- are there no figures at the beginning of street name?
IF NOT #oldString REGEXP '^[1-9]' THEN
-- must be a word, to jump over
SET #splitPoint := LOCATE(" ", #oldString);
SET #oldString := SUBSTRING(#oldString, #splitPoint+1);
ELSE
-- Okay, we found the first figure
-- Are there any chars inside the string including "."
IF #oldString REGEXP '[a-z,A-Z,.,--,/," "]' THEN
-- Are there any char directly behind a figure
IF #oldString REGEXP '[0-9][a-z,A-Z,.,--,/," "]' THEN
-- now we have to check step by step
SET #i := 1;
tokenPos: LOOP
-- jump over the first figures
IF NOT SUBSTRING(#oldString, #i, 1) REGEXP '[0-9]' THEN
-- this is the first non figure
IF formating THEN
IF SUBSTRING(#oldString, #i, 1) REGEXP '[a-z,A-Z]' THEN
-- If a char is directly written after figures, then add a blank between
SET #oldString := CONCAT(SUBSTRING(#oldString,1,#i-1)," ",SUBSTRING(#oldString,#i));
LEAVE tokenPos;
ELSE
IF SUBSTRING(#oldString, #i, 1) = "." THEN
-- this must be part of the street name, so we will loop some what
LEAVE tokenPos;
END IF;
-- SET #newString := concat(">xx>",REPLACE(#oldString," ",""));
SET #newString := REPLACE(#oldString," ","");
LEAVE tokenLoop;
END IF;
LEAVE tokenPos;
ELSE
IF SUBSTRING(#oldString, #i, 1) = "." THEN
-- this must be part of the street name, so we will loop some what
LEAVE tokenPos;
ELSE
-- the street number is found
SET #newString := #oldString;
LEAVE tokenLoop;
END IF;
END IF;
END IF;
SET #i := #i+1;
END LOOP tokenPos;
END IF;
SET #splitPoint := LOCATE(" ", #oldString);
IF SUBSTRING(#oldString, #splitPoint+1) REGEXP '[1-9]' THEN
-- we have to split one more word
SET #oldString := SUBSTRING(#oldString, #splitPoint+1);
ELSE
SET #newString := #oldString;
LEAVE tokenLoop;
END IF;
ELSE
IF formating AND #oldString REGEXP '[//][1-9]' THEN
SET #i := LOCATE(#oldString,"//")+4;
SET #oldString := CONCAT(SUBSTRING(#oldString,1,#i)," ",SUBSTRING(#oldString,#i+1));
ELSEIF formating THEN
SET #oldString := REPLACE(#oldString," ","");
END IF;
SET #newString := #oldString;
LEAVE tokenLoop;
END IF;
END IF;
RETURN #newString;
END //
delimiter ;
There is an additional BOOLEAN parameter, to reformat the number part of the address against DIN 5008. But the reformating part isn't finaly done yet.
Now we can test it with some examples:
select HAUSNUMMER_DIN_5008("Mörikestr. 28/3",TRUE);
select HAUSNUMMER_DIN_5008("Nettelbeckstraße 6 a",TRUE);
select HAUSNUMMER_DIN_5008("Auf dem Brande 19a",TRUE); ==>> "19 a"
select HAUSNUMMER_DIN_5008("Auf dem Brande 19a",FALSE); ==>> "19a"
select HAUSNUMMER_DIN_5008("Anger 1-3",TRUE);
select HAUSNUMMER_DIN_5008("Straße des 17. Juni 12-16",TRUE);
select HAUSNUMMER_DIN_5008("L11 2",TRUE); -- z.B in Mannheim
select HAUSNUMMER_DIN_5008("111. 2",TRUE);
select HAUSNUMMER_DIN_5008("Züricher Straße 17// 28",,TRUE);
-- Some special formating tests
SELECT HAUSNUMMER_DIN_5008("Mörikestr. 28 / 3",TRUE);
SELECT HAUSNUMMER_DIN_5008("8 - 6",TRUE);
SELECT HAUSNUMMER_DIN_5008("Straße des 17. Juni 12 - 16",TRUE);
SELECT HAUSNUMMER_DIN_5008("Straße des 17. Juni 12- 16",TRUE);
SELECT HAUSNUMMER_DIN_5008("Straße des 17. Juni 12 -16",FALSE);
-- Next one is not DIN 5008, but was a loop issue inside function
SELECT HAUSNUMMER_DIN_5008("8 /App.6",FALSE);
SELECT HAUSNUMMER_DIN_5008("8 /App.6",TRUE);
SELECT HAUSNUMMER_DIN_5008("8/App.6",TRUE);
If you need only the street name, you have to use HAUSNUMMER_DIN_5008() with formating FALSE, otherwise you can't find the numberpart within your adress.
SET #address := "Auf dem Brande 19a";
SELECT SUBSTRING(#address, 1, LOCATE(HAUSNUMMER_DIN_5008(#address, FALSE),#address)-1);
SET #address := "Straße des 17. Juni 12-16";
SELECT SUBSTRING(#address, 1, LOCATE(HAUSNUMMER_DIN_5008(#address, FALSE),#address)-1);
That are my 5 cent
Christian Eickhoff
ERROR 1064 (42000) at line 5: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'END LOOP tokenLoop;
IF NOT #oldString REGEXP '^[1-9]' THEN
SET #sp' at line 9
For everyone coming from Google: As you can't trust humans, I chose this way:
SELECT
REGEXP_SUBSTR(address, '[a-z"äöüß"\-\. ]+') AS street ,
REGEXP_SUBSTR(address, '[0-9]+.*') AS number,
FROM
ADDRESSES
Because this will also work on:
Streetname 5
Streetname 5B
Streetname 5 B
Streetn.5B
Street-Name5
Streetname 5 (+trailing Space)
Street name5

T-SQL strip all non-alpha and non-numeric characters

Is there a smarter way to remove all special characters rather than having a series of about 15 nested replace statements?
The following works, but only handles three characters (ampersand, blank and period).
select CustomerID, CustomerName,
Replace(Replace(Replace(CustomerName,'&',''),' ',''),'.','') as CustomerNameStripped
from Customer
One flexible-ish way;
CREATE FUNCTION [dbo].[fnRemovePatternFromString](#BUFFER VARCHAR(MAX), #PATTERN VARCHAR(128)) RETURNS VARCHAR(MAX) AS
BEGIN
DECLARE #POS INT = PATINDEX(#PATTERN, #BUFFER)
WHILE #POS > 0 BEGIN
SET #BUFFER = STUFF(#BUFFER, #POS, 1, '')
SET #POS = PATINDEX(#PATTERN, #BUFFER)
END
RETURN #BUFFER
END
select dbo.fnRemovePatternFromString('cake & beer $3.99!?c', '%[$&.!?]%')
(No column name)
cake beer 399c
Create a function:
CREATE FUNCTION dbo.StripNonAlphaNumerics
(
#s VARCHAR(255)
)
RETURNS VARCHAR(255)
AS
BEGIN
DECLARE #p INT = 1, #n VARCHAR(255) = '';
WHILE #p <= LEN(#s)
BEGIN
IF SUBSTRING(#s, #p, 1) LIKE '[A-Za-z0-9]'
BEGIN
SET #n += SUBSTRING(#s, #p, 1);
END
SET #p += 1;
END
RETURN(#n);
END
GO
Then:
SELECT Result = dbo.StripNonAlphaNumerics
('My Customer''s dog & #1 friend are dope, yo!');
Results:
Result
------
MyCustomersdog1friendaredopeyo
To make it more flexible, you could pass in the pattern you want to allow:
CREATE FUNCTION dbo.StripNonAlphaNumerics
(
#s VARCHAR(255),
#pattern VARCHAR(255)
)
RETURNS VARCHAR(255)
AS
BEGIN
DECLARE #p INT = 1, #n VARCHAR(255) = '';
WHILE #p <= LEN(#s)
BEGIN
IF SUBSTRING(#s, #p, 1) LIKE #pattern
BEGIN
SET #n += SUBSTRING(#s, #p, 1);
END
SET #p += 1;
END
RETURN(#n);
END
GO
Then:
SELECT r = dbo.StripNonAlphaNumerics
('Bob''s dog & #1 friend are dope, yo!', '[A-Za-z0-9]');
Results:
r
------
Bobsdog1friendaredopeyo
I faced this problem several years ago, so I wrote a SQL function to do the trick. Here is the original article (was used to scrape text out of HTML). I have since updated the function, as follows:
IF (object_id('dbo.fn_CleanString') IS NOT NULL)
BEGIN
PRINT 'Dropping: dbo.fn_CleanString'
DROP function dbo.fn_CleanString
END
GO
PRINT 'Creating: dbo.fn_CleanString'
GO
CREATE FUNCTION dbo.fn_CleanString
(
#string varchar(8000)
)
returns varchar(8000)
AS
BEGIN
---------------------------------------------------------------------------------------------------
-- Title: CleanString
-- Date Created: March 26, 2011
-- Author: William McEvoy
--
-- Description: This function removes special ascii characters from a string.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
declare #char char(1),
#len int,
#count int,
#newstring varchar(8000),
#replacement char(1)
select #count = 1,
#len = 0,
#newstring = '',
#replacement = ' '
---------------------------------------------------------------------------------------------------
-- M A I N P R O C E S S I N G
---------------------------------------------------------------------------------------------------
-- Remove Backspace characters
select #string = replace(#string,char(8),#replacement)
-- Remove Tabs
select #string = replace(#string,char(9),#replacement)
-- Remove line feed
select #string = replace(#string,char(10),#replacement)
-- Remove carriage return
select #string = replace(#string,char(13),#replacement)
-- Condense multiple spaces into a single space
-- This works by changing all double spaces to be OX where O = a space, and X = a special character
-- then all occurrences of XO are changed to O,
-- then all occurrences of X are changed to nothing, leaving just the O which is actually a single space
select #string = replace(replace(replace(ltrim(rtrim(#string)),' ', ' ' + char(7)),char(7)+' ',''),char(7),'')
-- Parse each character, remove non alpha-numeric
select #len = len(#string)
WHILE (#count <= #len)
BEGIN
-- Examine the character
select #char = substring(#string,#count,1)
IF (#char like '[a-z]') or (#char like '[A-Z]') or (#char like '[0-9]')
select #newstring = #newstring + #char
ELSE
select #newstring = #newstring + #replacement
select #count = #count + 1
END
return #newstring
END
GO
IF (object_id('dbo.fn_CleanString') IS NOT NULL)
PRINT 'Function created.'
ELSE
PRINT 'Function NOT created.'
GO
I know this is an old thread, but still, might be handy for others.
Here's a quick and dirty (Which I've done inversely - stripping out non-numerics) - using a recursive CTE.
What makes this one nice for me is that it's an inline function - so gets around the nasty RBAR effect of the usual scalar and table-valued functions.
Adjust your filter as needs be to include or exclude whatever char types.
Create Function fncV1_iStripAlphasFromData (
#iString Varchar(max)
)
Returns
Table With Schemabinding
As
Return(
with RawData as
(
Select #iString as iString
)
,
Anchor as
(
Select Case(IsNumeric (substring(iString, 1, 1))) when 1 then substring(iString, 1, 1) else '' End as oString, 2 as CharPos from RawData
UNION ALL
Select a.oString + Case(IsNumeric (substring(#iString, a.CharPos, 1))) when 1 then substring(#iString, a.CharPos, 1) else '' End, a.CharPos + 1
from RawData r
Inner Join Anchor a on a.CharPos <= len(rtrim(ltrim(#iString)))
)
Select top 1 oString from Anchor order by CharPos Desc
)
Go
select * from dbo.fncV1_iStripAlphasFromData ('00000')
select * from dbo.fncV1_iStripAlphasFromData ('00A00')
select * from dbo.fncV1_iStripAlphasFromData ('12345ABC6789!&*0')
If you can use SQL CLR you can use .NET regular expressions for this.
There is a third party (free) package that includes this and more - SQL Sharp .