IS !value a shortcut for IS NOT NULL MySQL? - mysql

When playing with MySQL I noticed that if I run
SELECT * FROM table WHERE !value;
returns the same thing as
SELECT * FROM table WHERE value IS NOT NULL;
The table is "employees" and the column is "MiddleInitial" (strings).
So I get all employees who's MiddleInitial is not null.
Is this a proper shorthand or just a coincidence? I am wondering if this is a safe way to write? I cannot seem to find any information on this.
I was expecting
SELECT * FROM table WHERE !value;
to return all null values. Oddly enough
SELECT * FROM table WHERE value;
returns nothing.

No, it's not.
It might seem like it because it's doing type coercion to force whatever is in the column into a boolean value. NULL values will coerce to false when forced as a boolean predicate, and most non-null column values will coerce to true. But some column values (that are not null) will also coerce to false.
You can see examples here:
https://dbfiddle.uk/ABLUgLex
Notice the last example is missing the 0 row. Also notice the one before that does not include the null row, which leads me to suspect your server might have an option set for non-standard null handling.
Here's a few more samples:
https://dbfiddle.uk/BNxiujKt
Notice the treatment of the '1' row.

In SQL, NULL is not the same as false.
Negating NULL is not true, it's still NULL.
mysql> select null;
+------+
| NULL |
+------+
| NULL |
+------+
mysql> select !(null);
+---------+
| !(null) |
+---------+
| NULL |
+---------+
Think of NULL as the value "unknown." If some piece of information is unknown, how can its opposite be known? It can't — the opposite is also unknown, because we don't know what we started with.
When used in a WHERE clause condition, NULL acts more or less like false because neither are strictly true. Only rows where the conditions are true become part of the result set of the query.
There are other values that act like false in MySQL:
mysql> select 1 where '';
Empty set (0.01 sec)
mysql> select 1 where 0;
Empty set (0.01 sec)
MySQL is a bit nonstandard because the boolean values true and false are literally the same as the integer values 1 and 0 respectively (this is not the way booleans are implemented in most other brands of SQL database).
These values are not NULL, so they can be negated and you can treat their opposites as true.
mysql> select 1 where !0;
+---+
| 1 |
+---+
| 1 |
+---+
mysql> select 1 where !'';
+---+
| 1 |
+---+
| 1 |
+---+
As the comment above said, the ! operator is deprecated in MySQL 8.0. It's not standard SQL, and using it makes your code less clear than if you use more explicit language like IS NOT NULL or <>.

Related

Can't detect null value from JSON_EXTRACT

I have a database that has an array of data stored in a JSON column. I need to find all values that have a null value at a particular position in the JSON array. While pulling out the data with JSON_EXTRACT seemed trivial, none of my comparisons to null have worked, all of them claiming the value is null.
Here is the example code that should work as far as I can tell:
SELECT JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') , (JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') is null)
FROM ate.readings_columns_new;
The first few rows of my results table look like this:
null | 0
"INTERNALTEMPERATURE" | 0
"INPUT_VOLTAGE" | 0
null | 0
null | 0
"AH1" | 0
I have tried every comparison I can think of, and they all result in a 0:
(JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') is null)
(JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') <=> null)
ISNULL(JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]'))
(JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') <=> 'null')
Is there some key to comparing null values pulled from a JSON_EXTRACT?
SELECT
JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]'),
(JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') = CAST('null' AS JSON))
FROM ate.readings_columns_new;
or
SELECT
JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]'),
(JSON_TYPE(JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]')) = 'NULL')
FROM ate.readings_columns_new;
See the docs for JSON_TYPE.
A bit of a belated answer but I just hit this problem and couldn't find anything reasonably documented. The solution I ended ended up using was the json_type function as 'abl' pointed out above.
The trick was to compare with the string 'NULL' not null or NULL.
As a test throw the following into a mysql prompt and play around with the values
(if using phpMyAdmin don't forget to check 'show this query here again' and 'retain query box' - the universe is frustrating enough without losing edits..)
set #a='{"a":3,"b":null}';
select if(json_type(json_extract(#a,'$.b')) = 'NULL',1,0);
I ended up with the following.
mysql> set #a='{"a":3,"b":null}';
Query OK, 0 rows affected (0.00 sec)
mysql> select if(json_type(json_extract(#a,'$.b')) = 'NULL',1,0);
+----------------------------------------------------+
| if(json_type(json_extract(#a,'$.b')) = 'NULL',1,0) |
+----------------------------------------------------+
| 1 |
+----------------------------------------------------+
1 row in set (0.00 sec)
mysql> set #a='{"a":3,"b":1}';
Query OK, 0 rows affected (0.00 sec)
mysql> select if(json_type(json_extract(#a,'$.b')) = 'NULL',1,0);
+----------------------------------------------------+
| if(json_type(json_extract(#a,'$.b')) = 'NULL',1,0) |
+----------------------------------------------------+
| 0 |
+----------------------------------------------------+
1 row in set (0.00 sec)
As the bare bones of a stored procedure - which is what I needed it for - using the 'if' statements rather than the if() function.
drop procedure if exists test;
delimiter $$
create procedure test(in x json)
begin
if json_type(json_extract(x,'$.b')) = 'NULL' then
select 1;
else
select 0;
end if;
end$$
delimiter;
mysql> call test('{"a":3,"b":1}');
+---+
| 0 |
+---+
| 0 |
+---+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
mysql> call test('{"a":3,"b":null}');
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Well I had a suspicion but I found a workaround that confirms that a JSON null value is not the same as a MySQL null value.
I tried various methods to get a similar null value but the only one that works is to extract a null JSON value from an array like the value I'm attempting to check against:
SELECT JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') , (JSON_EXTRACT(`COLUMNS_HEADERS`, '$[1]') = JSON_EXTRACT('[null]', '$[0]'))
FROM ate.readings_columns_new;
This seems like bad form, but was the only way I could get a value that evaluated as equal to the null values in my array.
Another trick is MySQL's NULLIF function
SELECT COLUMNS_HEADERS->>"$[1]", NULLIF(COLUMNS_HEADERS->>"$[1]",'null') IS NULL)
(I'm also using ->> which is an alias for JSON_UNQUOTE(JSON_EXTRACT())
That way querying a column containing {"id":1},{"id":2},{"id":null} & {"name":4} for the JSON path $.id will return 1,2,NULL,NULL instead of 1,2,null,NULL

MySQL where value NOT in SET datatype field

I should start by saying that I know how to check for the existence of a SET datatype, however, I'm now trying to negate the lookup, which doesn't seem to work, so I'm assuming I'm doing something stupid.
I've got this field:
| billing_payment_prefs | set('allow_admin','allow_trans','allow_supress','requires_nag') | YES | | NULL | |
And all records are currently NULL for said field. I have 3000+ records in this table, and running the following query:
mysql> select count(id) from customers where billing_payment_prefs not like '%allow_trans%';
+-----------+
| count(id) |
+-----------+
| 0 |
+-----------+
1 row in set (0.00 sec)
... I get 0 instead of the 3000 plus (which I'd expect, as they're all NULL). Now, I'm obviously unsure of how to look up a NOT LIKE against a SET field, but I had assumed (incorrectly, by the looks of things) that this would work, even though MYSQL DEV doesn't mention it.
Any help, is very much appreciated. Thanks.
The reason you are getting no records back is because MySQL (as well as most RDBMS) treat NULL as a special value. Comparing some text against NULL does not yield a true or false, but rather yields NULL, because comparing an unknown value against a known value yields an unknown result.
To make the logic in your query work, you can add a condition for NULL checks:
SELECT COUNT(id)
FROM customers
WHERE billing_payment_prefs NOT LIKE '%allow_trans%' OR
billing_payment_prefs IS NULL
You have to explicitly check for NULL values:
select count(id)
from customers
where billing_payment_prefs not like '%allow_trans%' or
billing_payment_prefs is null;
This holds for all data types, not only enumerations.

MySQL Null Safe Equals incorrect on Numeric types?

This one bit me today and it seems like a bug. It seems like when comparing the result of the LOCATE function in MySQL that it behaves totally different when using the not equals <> versus the null safe version <=> See the session below:
$ mysql --version
mysql Ver 14.14 Distrib 5.6.25, for osx10.8 (x86_64) using EditLine wrapper
mysql> SELECT LOCATE('A', "The Apple");
+--------------------------+
| LOCATE('A', "The Apple") |
+--------------------------+
| 5 |
+--------------------------+
1 row in set (0.00 sec)
mysql> SELECT LOCATE('A', "The Apple") <=> 0; -- WRONG!?
+--------------------------------+
| LOCATE('A', "The Apple") <=> 0 |
+--------------------------------+
| 0 |
+--------------------------------+
1 row in set (0.00 sec)
mysql> SELECT LOCATE('A', "The Apple") <> 0;
+-------------------------------+
| LOCATE('A', "The Apple") <> 0 |
+-------------------------------+
| 1 |
+-------------------------------+
1 row in set (0.00 sec)
The null safe version returns 0 (false) which seems odd to me. The LOCATE method's documentation doesn't specify what happens with nulls, but I tested it and it does return null for something like LOCATE('A', null) but the real issue seems to be comparing the integer that LOCATE returns to 0.
I checked the documentation on <=> but it just says:
NULL-safe equal. This operator performs an equality comparison like the = operator, but returns 1 rather than NULL if both operands are NULL, and 0 rather than NULL if one operand is NULL.
Which doesn't really tell me why 0 <=> 5 comes back 0 and 0 <> 5 comes back 1 The only thing I could think of was that MySQL says:
These operations work for both numbers and strings. Strings are automatically converted to numbers and numbers to strings as necessary.
And maybe there was an automatic cast, but that doesn't make any sense since doing the check directly (typing out 0 <=> 5) shows the same issue.
Does anyone know what's causing this? Or where in the documentation this is indicated? It seems like a pretty major issue if the null-safe equality operations return invalid results for as simple a comparison as 5 and 0.
The Null safe equality comparison <=> (spaceship) operator is an equality operator. It works like the = equality comparison operator, except with the bit about not returning NULL.
To get "inequality" out of that operator, you would need to negate it, for example
SELECT NOT ( 0 <=> 5 )

How to insert the default value in temporal tables in MySQL?

I want to create a temporal table from a SELECT statement in MySQL. It involves several JOINs, and it can produce NULL values that I want MySQL to take as zeroes. It sounds like an easy problem (simply default to zero), but MySQL (5.6.12) fails to elicit the default value.
For example, take the following two tables:
mysql> select * from TEST1;
+------+------+
| a | b |
+------+------+
| 1 | 2 |
| 4 | 25 |
+------+------+
2 rows in set (0.00 sec)
mysql> select * from TEST2;
+------+------+
| b | c |
+------+------+
| 2 | 100 |
| 3 | 100 |
+------+------+
2 rows in set (0.00 sec)
A left join gives:
mysql> select TEST1.*,c from TEST1 left join TEST2 on TEST1.b=TEST2.b;
+------+------+------+
| a | b | c |
+------+------+------+
| 1 | 2 | 100 |
| 4 | 25 | NULL |
+------+------+------+
2 rows in set (0.00 sec)
Now, if I want to save these values in a temporal table (changing NULL for zero), this is the code I would use:
mysql> create temporary table TEST_JOIN (a int, b int, c int default 0 not null)
select TEST1.*,c from TEST1 left join TEST2 on TEST1.b=TEST2.b;
ERROR 1048 (23000): Column 'c' cannot be null
What am I doing wrong? The worst part is that this code used to work before I did a system-wide upgrade (I don't remember which version of MySQL I had, but surely it was lower than my current 5.6). It used to produce the behavior I would expect: if it's NULL, use the default, not the frustrating error I'm getting now.
From the documentation of 5.6 (unchanged since 4.1):
Inserting NULL into a column that has been declared NOT NULL. For
multiple-row INSERT statements or INSERT INTO ... SELECT statements,
the column is set to the implicit default value for the column data
type. This is 0 for numeric types, the empty string ('') for string
types, and the “zero” value for date and time types. INSERT INTO ...
SELECT statements are handled the same way as multiple-row inserts
because the server does not examine the result set from the SELECT to
see whether it returns a single row. (For a single-row INSERT, no
warning occurs when NULL is inserted into a NOT NULL column. Instead,
the statement fails with an error.)
My current workaround is to store the NULL values in the temporal table, and then replace them by zeroes, but it seems rather cumbersome with many columns (and terribly inefficient). Is there a better way to do it?
BTW, I cannot simply ignore some columns in the query (as suggested for another question), because it's a multirow query.
IFNULL(`my_column`,0);
That would set NULLs to 0. Other values stay as is.
Just wrap your values/column names with IFNULL and it will convert them to whatever default value you put into the function. E.g. 0. Or "european swallow", or whatever you want.
Then you can keep strict mode on and still handle NULLs gracefully.

mysql, prepared statements, and automatic type conversion

I am getting different results performing the exact same query using regular statements and prepared statements, and I think it's a type conversion bug.
mysql> show columns from server where field = "vlan";
+-------------+--------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------+------+-----+---------+-------+
| vlan | int(5) | YES | MUL | NULL | |
+-------------+--------+------+-----+---------+-------+
mysql> select hostname from server where `vlan` = '184.182' limit 1;
Empty set (0.00 sec)
mysql> prepare stupid from "select hostname from server where `vlan` = ? limit 1";
Query OK, 0 rows affected (0.00 sec)
Statement prepared
mysql> set #vlan = '184.182';
Query OK, 0 rows affected (0.00 sec)
mysql> execute stupid using #vlan;
+-------------------+
| hostname |
+-------------------+
| web20.servers.com |
+-------------------+
1 row in set (0.00 sec)
the real value of vlan is 184
it looks like the way mysql is handling type conversions is different for prepared statements and regular statements? does that make sense? how do i fix this?
The expected data type of prepared statement parameters is determined upon statement preparation, and type conversion to that data type takes place prior to statement execution.
In your example, an integer parameter is expected; therefore the provided string is cast to an integer (184) before the statement is executed, and the comparison between the integer column vlan and the parameter is successful for the matching record.
The "regular" statement, by contrast, compares the integer column with a string; therefore the arguments are compared as floating point numbers, and no record has a matching vlan.
To avoid this situation, ensure that the data type cannot be determined upon preparation (or that the determined data type does not lose any information) - for example:
prepare not_so_stupid from
"select hostname from server where `vlan` = CAST(? AS CHAR) limit 1"
;