If a SELECT
returns a result
set you don’t expect, this does not always mean something is wrong with
the query itself. Perhaps you didn’t insert, update, or delete data that
you thought you had.
Before you investigate this possibility, you should faithfully carry
out the investigation in the previous section, where we discussed a badly
written SELECT
statement. Here I
examine the possibility that you have a good SELECT
that is returning the values you asked
for, and that the problem is your data itself. To make sure the problem is
in the data and not the SELECT
, try to
reduce it to a simple query on a single table. If the table is small, go
ahead and remove all the WHERE
clauses,
as well as any GROUP BY
clauses, and
examine the full data set with a brute-force SELECT * FROM
table_name
. For a larger table, judiciously use
WHERE
to cull the values you want to
examine, and consider COUNT(*)
if you
just want to make sure the number of rows matching the query is what you
expect.
Once you are sure the SELECT
works fine, this means the data is inconsistent and you need to find where
it is broken. There can be a lot of reasons: a badly applied backup, an
incorrect UPDATE
statement, or a slave
getting out of sync with the master, just to name the most common. In this
section, we’ll look at some examples where a bad DELETE
or UPDATE
isn’t revealed until a later SELECT
. In a later section, we’ll address those
puzzling cases where the problem turns up long after it was triggered, and
show you how to work backward to find the error. This section does not
deal directly with problems in transactions, which are discussed in Chapter 2. Here I show cases where data in the database is
stable, i.e., all transactions, if used, were completed. I will continue
using examples reduced down from real cases.
Let’s start from the best possible case, when data inconsistency was noticed right after the error was made. We will use the following initial data set:
mysql>CREATE TEMPORARY TABLE t1(f1 INT);
Query OK, 0 rows affected (0.01 sec) mysql>CREATE TEMPORARY TABLE t2(f2 INT);
Query OK, 0 rows affected (0.08 sec) mysql>INSERT INTO t1 VALUES(1);
Query OK, 1 row affected (0.01 sec) mysql>SELECT * FROM t1;
+------+ | f1 | +------+ | 1 | +------+ 1 row in set (0.00 sec)
In the application, the tables shown were temporary tables containing partial result sets selected from the main log table. This is a common technique frequently used in stored routines when only a small set of data from the main table is needed but the user doesn’t want to change the data in the main table or lock the table.
So in this example, after finishing with a result set, the user wanted to delete rows from both tables. It always looks amazing when you can do things in a single query. But real life can work out differently from your plans, and you can get unexpected results or side effects:
mysql> DELETE FROM t1, t2 USING t1, t2;
Query OK, 0 rows affected (0.00 sec)
If the user paid attention to the string printed in response to the
DELETE
, he would realize right away
that something had gone wrong. No rows were affected by the DELETE
, meaning that it did nothing. The output
from a statement is often not so obvious, however, and sometimes it is
never seen, because the SQL statement is run within a program or script
with no human being to watch over the results. In general, you should
always check information returned by a statement execution to learn how
many rows were affected and whether this value is same as you expect. In
an application, you must explicitly check information functions.
Let’s continue. If you run SELECT
immediately, you could be surprised, thinking that the query was incorrect
or even that the query cache had not been cleared:
mysql> SELECT * FROM t1;
+------+
| f1 |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
You can be sure this is not a cache or some other problem if you
convert the SELECT
to ask for the
number of rows. This easy example shows how we can use different ways to
query a table to be sure the data is consistent:
mysql> SELECT count(*) FROM t1;
+----------+
| count(*) |
+----------+
| 1 |
+----------+
1 row in set (0.00 sec)
COUNT(*)
still returns a positive
number here, which shows the table is not empty as desired. As an
attentive user would have seen, the DELETE
didn’t actually remove any rows. To find
out why, we can convert the DELETE
to
the corresponding SELECT
. This will
show us which rows satisfy the condition for the delete.
Although our simple example had no WHERE
clause, the technique is certainly useful
to check the impact of a WHERE
clause
in a delete or update. The rows returned by a SELECT
are the ones that DELETE
would delete or that UPDATE
would change:
mysql> SELECT * FROM t1, t2;
Empty set (0.00 sec)
Consistent with previous results, this returns an empty set. That’s
why no rows were removed! It still might not be clear why this happens,
but now that we have a SELECT
, we can
use familiar techniques from the previous section. For this case, our best
choice is to run SELECT
followed by
EXPLAIN
and analyze its output:
mysql>\W
Show warnings enabled. mysql>EXPLAIN EXTENDED SELECT * FROM t1, t2\G
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 type: system possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 1 filtered: 100.00 Extra: *************************** 2. row *************************** id: 2 select_type: SIMPLE SUBQUERY table: t2 type: system possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 0 filtered: 0.00 Extra: const row not found 2 rows in set, 1 warning (0.03 sec) Note (Code 1003): select '1' AS `f1`,'0' AS `f2` from `test`.`t1` join `test`.`t2`
The final note in the output shows that the query was modified
to an (inner) JOIN
, which can return
rows from each table only if there are matching rows in the other table.
For each row in table t1
, there should
be at least one row in table t2
with a
matching value in a matching row. In this case, because table t2
is empty, naturally the join returns an empty
set.
We just learned another important technique that helps us find out what is wrong with an
UPDATE
orDELETE
: convert it to aSELECT
with the sameJOIN
andWHERE
conditions. With aSELECT
, you can useEXPLAIN EXTENDED
[3] to get the actual execution plan, as well as to manipulate the result set without the risk of modifying the wrong rows.
Here’s a more complex example using UPDATE
. We will use the items
table again:
mysql>SELECT SUBSTR(description, 1, 20), additional IS NULL FROM items;
+----------------------------+--------------------+ | substr(description, 1, 20) | additional IS NULL | +----------------------------+--------------------+ | NULL | 1 | | NULL | 1 | | One who has TRIGGER | 1 | | mysql> revoke insert | 1 | | NULL | 0 | +----------------------------+--------------------+ 5 rows in set (0.00 sec) mysql>SELECT description IS NULL, additional IS NULL FROM items;
+---------------------+--------------------+ | description IS NULL | additional IS NULL | +---------------------+--------------------+ | 1 | 1 | | 1 | 1 | | 0 | 1 | | 0 | 1 | | 1 | 0 | +---------------------+--------------------+ 5 rows in set (0.00 sec)
The description
and additional
fields are of type TEXT
. In this example, we will use an erroneous
query that is supposed to replace NULL
values with more descriptive text (“no description” for one table and “no
additional comments” for the other):
mysql> UPDATE items SET description = 'no description' AND
additional = 'no additional comments' WHERE description IS NULL;
Query OK, 3 rows affected, 3 warnings (0.13 sec)
Rows matched: 3 Changed: 3 Warnings: 3
This query updates some data (“3 rows affected”), but let’s check whether we have the proper values in the table now:
mysql> SELECT SUBSTR(description, 1, 20), additional IS NULL FROM items;
+----------------------------+--------------------+
| substr(description, 1, 20) | additional IS NULL |
+----------------------------+--------------------+
| 0 | 1 |
| 0 | 1 |
| One who has TRIGGER | 1 |
| mysql> revoke insert | 1 |
| 0 | 0 |
+----------------------------+--------------------+
5 rows in set (0.09 sec)
As we see, three rows changed their values in the description
field, but 0 is different from the
“no description” string we thought we were setting. Furthermore, the
values in the additional
field have not
changed at all. To find out why this happened, we should check warnings.
Notice in these statements returned by the server that we see a warnings
count of three:
Query OK, 3 rows affected, 3 warnings (0.13 sec)
Rows matched: 3 Changed: 3 Warnings: 3
mysql> SHOW WARNINGS;
+---------+------+----------------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: 'no description' |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'no description' |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'no description' |
+---------+------+----------------------------------------------------+
3 rows in set (0.00 sec)
The message looks strange. Why does it complain about a DOUBLE
when description
and additional
are TEXT
fields, as the following queries
prove?
mysql>SHOW FIELDS FROM items LIKE 'description';
+-------------+------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+------+------+-----+---------+-------+ | description | text | YES | | NULL | | +-------------+------+------+-----+---------+-------+ 1 row in set (0.13 sec) mysql>SHOW FIELDS FROM items LIKE 'additional';
+------------+------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+------+------+-----+---------+-------+ | additional | text | YES | | NULL | | +------------+------+------+-----+---------+-------+ 1 row in set (0.13 sec)
We also want to know why we did not get any warning about the
additional
field, when it was not
changed at all.
Let’s split the query in chunks and examine what it going in each of them:
UPDATE items
This is a common start for an UPDATE
, and nothing’s wrong with it:
SET description = 'no description' AND additional = 'no additional comments'
That used a SET
statement.
Let’s examine what it is actually doing. What does the keyword AND
mean in this case? Let me add parentheses to
the query to underline operator
precedence:
SET description = ('no description' AND additional = 'no additional comments')
So actually, the statement calculates the expression:
'no description' and additional = 'no additional comments'
and then assigns it to description
. Evaluating the equal sign produces
a Boolean result as a LONGLONG
value.
To prove this, start the MySQL command line client with the --column-type-info
option and run the
SELECT
again:
$mysql --column-type-info
mysql>SELECT 'no description' AND additional = 'no additional comments' FROM items;
Field 1: `'no description' AND additional = 'no additional comments'` Catalog: `def` Database: `` Table: `` Org_table: `` Type: LONGLONG Collation: binary (63) Length: 1 Max_length: 1 Decimals: 0 Flags: BINARY NUM +------------------------------------------------------------+ | 'no description' AND additional = 'no additional comments' | +------------------------------------------------------------+ | 0 | | 0 | | 0 | | 0 | | 0 | +------------------------------------------------------------+ 5 rows in set, 5 warnings (0.09 sec)
We clearly see that the result of the expression is 0, which was
inserted into the description field.
And because our update to the additional
field got buried inside the absurd
expression, nothing was inserted into the field, nor did the server see
any reason to comment about the field.
Now you can rewrite the query without logic errors:
UPDATE items SET description = 'no description', additional = 'no additional comments' WHERE description IS NULL;
You can examine the WHERE
clause
if you want, but in this case it has no error.
This example shows the importance of return values and information about query execution. Let’s discuss them a bit more.
[3] Since version 5.6.3, you can use EXPLAIN
with UPDATE
and DELETE
as well, but converting such a
query into SELECT
still makes
sense because it is easier to examine an actual result set and
manipulate it than to just use EXPLAIN
. This is especially true for
complicated JOIN
s when EXPLAIN
shows it examined more rows than
were actually updated.
Get MySQL Troubleshooting now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.