MySQL strange behavior when comparing comma-separated string with number - mysql

I am experiencing some weird behavior with MySQL. Basically I have a table like this:
ID string
1 14
2 10,14,25
Why does this query pull id 2?
SELECT * FROM exampletable where string = 10
Surely it should be looking for an exact match, because this only pulls id 1:
SELECT * FROM exampletable where string = 14
I am aware of FIND_IN_SET, I just find it odd that the first query even pulls anything. Its behaving like this query:
SELECT * FROM exampletable where string LIKE '10%'

When you compare a numeric and a string value, MySQL will attempt to convert the string to number and match. Number like strings are also parsed. This we have:
SELECT '10,14,25' = 1 -- 0
SELECT '10,14,25' = 10 -- 1
SELECT 'FOOBAR' = 1 -- 0
SELECT 'FOOBAR' = 0 -- 1
SELECT '123.456' = 123 -- 0
SELECT '123.456FOOBAR' = 123.456 -- 1
The behavior is documented here (in your example it is the last rule):
...
If one of the arguments is a decimal value, comparison depends on the
other argument. The arguments are compared as decimal values if the
other argument is a decimal or integer value, or as floating-point
values if the other argument is a floating-point value.
In all other cases, the arguments are compared as floating-point
(real) numbers.

Related

How to write sql query where the WHERE clause uses a substring casted to a long?

In a table called "accounts" there is an account id that is a 13 character long string, where the first 8 digits are the user id who owns that account. How do I query the database with an integer and check the first 8 characters only?
I was trying to do something like this:
SELECT * FROM networthr.accounts WHERE CAST(SUBSTRING(account_id, 0, 8) as long) = 1;
But it won't even let me run this query.
There are 2 problems with your query:
1) The 2nd argument of SUBSTRING() should be 1 (the index is 1 based not 0 based)
2) You should cast to the data type UNSIGNED
SELECT * FROM networthr.accounts WHERE CAST(SUBSTRING(account_id, 1, 8) as unsigned ) = 1;
This looks like a bad design. However - If the account_id is zero padded like "00000001ABCDE" and you have an index on it, an efficient way would be
SELECT *
FROM networthr.accounts
WHERE account_id LIKE CONCAT(LPAD(?, 8, 0), '%')
Replace ? with the user_id or use it as a prepared statement and bind user_id as parameter.
In case of user_id = 1 it's the same as
WHERE account_id LIKE '00000001%'
You can use implicit conversion:
WHERE LEFT(account_id, 8) + 0 = 1
However, you should really be comparing strings to string.

Mysql column = 1 and column like "1_" are equivalent?

I have two mysql query.
SELECT tanulok.nev, tanulok.osztaly, leadasok.idopont, leadasok.mennyiseg
FROM `leadasok`, tanulok
WHERE tanulok.tazon = leadasok.tanulo and osztaly LIKE "1_"
SELECT tanulok.nev, tanulok.osztaly, leadasok.idopont, leadasok.mennyiseg
FROM `leadasok`, tanulok
WHERE tanulok.tazon = leadasok.tanulo and osztaly = 1
They give the same result. Why?
column = 1 and column like "1_" are equivalent?
the values of the osztaly column are "1A", "1B", "2A" ...
Because in MySQL to make a comparison of string and number - the DB engine converts the string into a number. It starts on the left side of the string and takes all number characters and builds a number from it.
"1A" -> 1
"300miles" -> 300
"$3" -> 0 (because the string does NOT start with a number)
That is why the second query returns results as well.

select int column and compare it with Json array column

this is row in option column in table oc_cart
20,228,27,229
why no result found when value is 228 but result found when value is 20 like below :
select 1 from dual
where 228 in (select option as option from oc_cart)
and result found when I change value to 20 like
select 1 from dual
where 20 in (select option as option from oc_cart)
The option column data type is TEXT
In SQL, these two expressions are different:
WHERE 228 in ('20,228,27,229')
WHERE 228 in ('20','228','27','229')
The first example compares the integer 228 to a single string value, whose leading numeric characters can be converted to the integer 20. That's what happens. 228 is compared to 20, and fails.
The second example compares the integer 228 to a list of four values, each can be converted to different integers, and 228 matches the second integer 228.
Your subquery is returning a single string, not a list of values. If your oc_cart.option holds a single string, you can't use the IN( ) predicate in the way you're doing.
A workaround is this:
WHERE FIND_IN_SET(228, (SELECT option FROM oc_cart WHERE...))
But this is awkward. You really should not be storing strings of comma-separated numbers if you want to search for an individual number in the string. See my answer to Is storing a delimited list in a database column really that bad?

MySQL string cast to unsigned

If I have a string that starts with a number, then contains non-numeric characters, casting this string to an integer in MySQL will cast the first part of the string, and give no indication that it ran into any problems! This is rather annoying.
For example:
SELECT CAST('123' AS UNSIGNED) AS WORKS,
CAST('123J45' AS UNSIGNED) AS SHOULDNT_WORK,
CAST('J123' AS UNSIGNED) AS DOESNT_WORK
returns:
+-------------+---------------+-------------+
| WORKS | SHOULDNT_WORK | DOESNT_WORK |
+-------------+---------------+-------------+
| 123 | 123 | 0 |
+-------------+---------------+-------------+
This doesn't make any sense to me, as clearly, 123J45 is not a number, and certainly does not equal 123. Here's my use case:
I have a field that contains (some malformed) zip codes. There may be mistypes, missing data, etc., and that's okay from my perspective. Because of another table storing Zip Codes as integers, when I join the tables, I need to cast the string Zip Codes to integers (I would have to pad with 0s if I was going the other way). However, if for some reason there's an entry that contains 6023JZ1, in no way would I want that to be interpreted as Zip Code 06023. I am much happier with 6023JZ1 getting mapped to NULL. Unfortunately, IF(CAST(zipcode AS UNSIGNED) <= 0, NULL, CAST(zipcode AS UNSIGNED)) doesn't work because of the problem discussed above.
How do I control for this?
Use a regular expression:
select (case when val rlike '[0-9][0-9][0-9][0-9][0-9]' then cast(val as unsigned)
end)
Many people consider it a nice feature that MySQL does not automatically produce an error when doing this conversion.
One options is to test for just digit characters 0 thru 9 for the entire length of the string:
zipstr REGEXP '^[0-9]+$'
Based on the result of that boolean, you could return the integer value, or a NULL.
SELECT IF(zipstr REGEXP '^[0-9]+$',zipstr+0,NULL) AS zipnum ...
(note: the addition of zero is an implicit conversion to numeric)
Another option is to do the conversion like you are doing, and cast the numeric value back to character, and compare to the original string, to return a boolean:
CAST( zipstr+0 AS CHAR) = zipstr
(note: this second approach does allow for a decimal point, e.g.
CAST( '123.4'+0 AS CHAR ) = '123.4' => 1
which may not be desirable if you are looking for just a valid integer

Comparing number in formatted string in MySQL?

I have a PolicyNo column in my table in MySQL with a format like this:
XXXX-000000
A four capital-case characters followed by a dash and a six digit number.
The six digit number is incremental, adding 1 for the next row, and the the four characters is always the same for all rows. The PolicyNo column is unique with a type of varchar(11).
If ordered, it will look like this:
XXXX-000001
XXXX-000002
XXXX-000003
...
Now I want to get all PolicyNo whose number is greater than a specified number.
For example: Retrieve all PolicyNo greater than 'XXXX-000100':
XXXX-000101
XXXX-000102
XXXX-000103
...
I test this query and it works fine, but I just didn't know if it is really safe to do such:
SELECT 'XXXX-000099' > 'XXXX-000098'
, 'XXXX-000099' > 'XXXX-000100'
, 'XXXX-000099' > 'XXXX-000101'
Result:
+-------------------------------+-------------------------------+-------------------------------+
| 'XXXX-000099' > 'XXXX-000098' | 'XXXX-000099' > 'XXXX-000100' | 'XXXX-000099' > 'XXXX-000101' |
+-------------------------------+-------------------------------+-------------------------------+
| 1 | 0 | 0 |
+-------------------------------+-------------------------------+-------------------------------+
Is there any other way to do this or is it already OK to use this?
Because your numbers are zero padded, as long as the four letter prefix is the same and always the same length, then this should work as MySQL will do a lexicographical comparison.
Note that one less 0 in the padding will cause this to fail:
SET #policy1 = 'XXXX-00099';
SET #policy2 = 'XXXX-000598';
SELECT #policy1, #policy2, #policy1 > #policy2 AS comparison;
=========================================
> 'XXXX-00099', 'XXXX-000598', 1
If you need to truly compare the numbers at the end, you will need to parse them out and cast them:
SET #policy1 = 'XXXX-00099';
SET #policy2 = 'XXXX-000598';
SELECT #policy1, #policy2,
CONVERT(SUBSTRING(#policy2, INSTR(#policy2, '-')+1), UNSIGNED) >
CONVERT(SUBSTRING(#policy2, INSTR(#policy2, '-')+1), UNSIGNED) AS comparison;
=========================================
> 'XXXX-00099', 'XXXX-000598', 0
You can also use SUBSTRING function provided by MySQL, like the following query.
SELECT count(*) FROM Table1 where substring(policyNo,6)>YOUR_RANGE;
here 6 is passed as the 6 digit number start from 6th position And if you do want to pass initial 4 charecter as well then you can use following query. Here second where clause will take intial 4 letters from the policyNo.
SELECT count(*) FROM Table1 where substring(policyNo,6)>YOUR_RANGE AND substring(policyNo,1,4) = 'ABCD'