views:

102

answers:

4

How can rows with Non-Ascii Characters be returned using SQL Server?
If you can show how to do it for one column would be great.

I am doing something like this now but it is not working

select *
from Staging.APARMRE1 AS ar
where ar.Line like '%[^!-~ ]%'

For extra credit, if it can span ALL VARCHAR columns in a Table would be outstanding!
In this solution, it would be nice to return three columns
* the identity field for that record. This will allow the whole record to be reviewed with another query.
* the field name
* the field with the invalid character

In this example I was not sure how to get an invalid character to show. Just pretend that Smith and Henry had invalid characters.

Id      |FieldName    |InvalidCharacter   |
--------|-------------|-------------------|
25      |LastName     |Smith              |
56      |FirstName    |Henry              |
100     |Address1     |123 Some street    |

Invalid characters would be outside the range of SPACE (Dec 32) through ~ (Dec 127)

Thanks in advance

+1  A: 

This script searches for non-ascii characters in one column. It generates a string of all valid characters, here code point 32 to 127. Then it searches for rows that don't match the list:

declare @str varchar(128)
declare @i int
set @str = ''
set @i = 32
while @i <= 127
    begin
    set @str = @str + '|' + char(@i)
    set @i = @i + 1
    end

select  col1
from    YourTable
where   col1 like '%[^' + @str + ']%' escape '|'
Andomar
This works with one minor change Varchar(128) needs to be bigger because 2 characters are being stored. I made it Varchar(200). It does take some time to run through my database. I am also suprised that a range cannot be used to simplified this process. i.e. like '%[^| -|~]%' escape '|' I tried to get a range working but it does not return the correct information.
Gerhard Weiss
I also changed 127 to 126. I did not want the DEL character.
Gerhard Weiss
+1  A: 

There is a user defined function available on the web 'Parse Alphanumeric'. Google UDF parse alphanumeric and you should find the code for it. This user defined function removes all characters that doesn't fit between 0-9, a-z, and A-Z.

Select * from Staging.APARMRE1 ar
where udf_parsealpha(ar.last_name) <> ar.last_name

That should bring back any records that have a last_name with invalid chars for you...though your bonus points question is a bit more of a challenge, but I think a case statement could handle it. This is a bit psuedo code, I'm not entirely sure if it'd work.

Select id, case when udf_parsealpha(ar.last_name) <> ar.last_name then 'last name'
when udf_parsealpha(ar.first_name) <> ar.first_name then 'first name'
when udf_parsealpha(ar.Address1) <> ar.last_name then 'Address1'
end, 
case when udf_parsealpha(ar.last_name) <> ar.last_name then ar.last_name
when udf_parsealpha(ar.first_name) <> ar.first_name then ar.first_name
when udf_parsealpha(ar.Address1) <> ar.last_name then ar.Address1
end
from Staging.APARMRE1 ar
where udf_parsealpha(ar.last_name) <> ar.last_name or
udf_parsealpha(ar.first_name) <> ar.first_name or
udf_parsealpha(ar.Address1) <> ar.last_name 

I wrote this in the forum post box...so I'm not quite sure if that'll function as is, but it should be close. I'm not quite sure how it will behave if a single record has two fields with invalid chars either.

As an alternative, you should be able to change the from clause away from a single table and into a subquery that looks something like:

select id,fieldname,value from (
Select id,'last_name' as 'fieldname', last_name as 'value'
from Staging.APARMRE1 ar
Union
Select id,'first_name' as 'fieldname', first_name as 'value'
from Staging.APARMRE1 ar
---(and repeat unions for each field)
)
where udf_parsealpha(value) <> value

Benefit here is for every column you'll only need to extend the union statement here, while you need to put that comparisson three times for every column in the case statement version of this script

M.E.
Comment on myself...the case statement version, I mentioned a single row having multiple columns with bad values. If both first_name and last_name had a bad value in it...I think the case statement will find the first_name portion and show it correctly, but would end there and not show the last_name value correctly. Probably not an optimal solution....the subquery version at the bottom of my post that unions all the tables values into id,columnname,value format appears to be much more functional and easier to follow
M.E.
+3  A: 

try something like this:

DECLARE @YourTable table (PK int, col1 varchar(20), col2 varchar(20), col3 varchar(20))
INSERT @YourTable VALUES (1, 'ok','ok','ok')
INSERT @YourTable VALUES (2, 'BA'+char(182)+'D','ok','ok')
INSERT @YourTable VALUES (3, 'ok',char(182)+'BAD','ok')
INSERT @YourTable VALUES (4, 'ok','ok','B'+char(182)+'AD')
INSERT @YourTable VALUES (5, char(182)+'BAD','ok',char(182)+'BAD')
INSERT @YourTable VALUES (6, 'BAD'+char(182),'B'+char(182)+'AD','BAD'+char(182)+char(182)+char(182))

--if you have a Numbers table use that, other wise make one using a CTE
;WITH AllNumbers AS
(   SELECT 1 AS Number
    UNION ALL
    SELECT Number+1
        FROM AllNumbers
        WHERE Number<1000
)
SELECT 
    pk, 'Col1' BadValueColumn, CONVERT(varchar(20),col1) AS BadValue --make the XYZ in convert(varchar(XYZ), ...) the largest value of col1, col2, col3
    FROM @YourTable           y
        INNER JOIN AllNumbers n ON n.Number <= LEN(y.col1)
    WHERE ASCII(SUBSTRING(y.col1, n.Number, 1))<32 OR ASCII(SUBSTRING(y.col1, n.Number, 1))>127
UNION
SELECT 
    pk, 'Col2' BadValueColumn, CONVERT(varchar(20),col2) AS BadValue --make the XYZ in convert(varchar(XYZ), ...) the largest value of col1, col2, col3
    FROM @YourTable           y
        INNER JOIN AllNumbers n ON n.Number <= LEN(y.col2)
    WHERE ASCII(SUBSTRING(y.col2, n.Number, 1))<32 OR ASCII(SUBSTRING(y.col2, n.Number, 1))>127
UNION
SELECT 
    pk, 'Col3' BadValueColumn, CONVERT(varchar(20),col3) AS BadValue --make the XYZ in convert(varchar(XYZ), ...) the largest value of col1, col2, col3
    FROM @YourTable           y
        INNER JOIN AllNumbers n ON n.Number <= LEN(y.col3)
    WHERE ASCII(SUBSTRING(y.col3, n.Number, 1))<32 OR ASCII(SUBSTRING(y.col3, n.Number, 1))>127
order by 1
OPTION (MAXRECURSION 1000)

OUTPUT:

pk          BadValueColumn BadValue
----------- -------------- --------------------
2           Col1           BA¶D
3           Col2           ¶BAD
4           Col3           B¶AD
5           Col1           ¶BAD
5           Col3           ¶BAD
6           Col1           BAD¶
6           Col2           B¶AD
6           Col3           BAD¶¶¶

(8 row(s) affected)
KM
Interesting approach KM. For my own curiousity...can I ask why the line "OPTION (MAXRECURSION 1000) " at the end of your statement is needed and what it will do in this case?
M.E.
"OPTION (MAXRECURSION 1000)" is necessary for the CTE, which recursively builds a set of rows from 1 to 1000, the default value is 100 (I think) any nested recursion calls in a cte to exceed the default requires this option to be set. If you had a numbers table http://stackoverflow.com/q/1393951/65223 you would not need the CTE or this "OPTION (MAXRECURSION 1000)" line
KM
A: 

Here is a solution for the single column search using PATINDEX.
It also displays the StartPosition, InvalidCharacter and ASCII code.

select line,
  patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line) as [Position],
  substring(line,patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line),1) as [InvalidCharacter],
  ascii(substring(line,patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line),1)) as [ASCIICode]
from  staging.APARMRE1
where patindex('%[^ !-~]%' COLLATE Latin1_General_BIN,Line) >0
Gerhard Weiss