I have read in the PostgreSQL docs that without an ORDER statement, SELECT will return records in an unspecified order.
Recently on an interview, I was asked how to SELECT records in the order that they inserted without an PK or created_at or other field that can be used for order. The senior dev who interviewed me was insistent that without an ORDER statement the records will be returned in the order that they were inserted.
Is this true for PostgreSQL? Is it true for MySQL? Or any other RDBMS?
I can answer for MySQL. I don't know for PostgreSQL.
The default order is not the order of insertion, generally.
In the case of InnoDB, the default order depends on the order of the index read for the query. You can get this information from the EXPLAIN plan.
For MyISAM, it returns orders in the order they are read from the table. This might be the order of insertion, but MyISAM will reuse gaps after you delete records, so newer rows may be stored earlier.
None of this is guaranteed; it's just a side effect of the current implementation. MySQL could change the implementation in the next version, making the default order of result sets different, without violating any documented behavior.
So if you need the results in a specific order, you should use ORDER BY on your queries.
Following BK's answer, and by way of example...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table(id INT NOT NULL) ENGINE = MYISAM;
INSERT INTO my_table VALUES (1),(9),(5),(8),(7),(3),(2),(6);
DELETE FROM my_table WHERE id = 8;
INSERT INTO my_table VALUES (4),(8);
SELECT * FROM my_table;
+----+
| id |
+----+
| 1 |
| 9 |
| 5 |
| 4 | -- is this what
| 7 |
| 3 |
| 2 |
| 6 |
| 8 | -- we expect?
+----+
In the case of PostgreSQL, that is quite wrong.
If there are no deletes or updates, rows will be stored in the table in the order you insert them. And even though a sequential scan will usually return the rows in that order, that is not guaranteed: the synchronized sequential scan feature of PostgreSQL can have a sequential scan "piggy back" on an already executing one, so that rows are read starting somewhere in the middle of the table.
However, this ordering of the rows breaks down completely if you update or delete even a single row: the old version of the row will become obsolete, and (in the case of an UPDATE) the new version can end up somewhere entirely different in the table. The space for the old row version is eventually reclaimed by autovacuum and can be reused for a newly inserted row.
Without an ORDER BY clause, the database is free to return rows in any order. There is no guarantee that rows will be returned in the order they were inserted.
With MySQL (InnoDB), we observe that rows are typically returned in the order by an index used in the execution plan, or by the cluster key of a table.
It is not difficult to craft an example...
CREATE TABLE foo
( id INT NOT NULL
, val VARCHAR(10) NOT NULL DEFAULT ''
, UNIQUE KEY (id,val)
) ENGINE=InnoDB;
INSERT INTO foo (id, val) VALUES (7,'seven') ;
INSERT INTO foo (id, val) VALUES (4,'four') ;
SELECT id, val FROM foo ;
MySQL is free to return rows in any order, but in this case, we would typically observe that MySQL will access rows through the InnoDB cluster key.
id val
---- -----
4 four
7 seven
Not at all clear what point the interviewer was trying to make. If the interviewer is trying to sell the idea, given a requirement to return rows from a table in the order the rows were inserted, a query without an ORDER BY clause is ever the right solution, I'm not buying it.
We can craft examples where rows are returned in the order they were inserted, but that is a byproduct of the implementation, ... not guaranteed behavior, and we should never rely on that behavior to satisfy a specification.
Related
I know there are a lot related questions with many answers, but I have a bit of a more nuanced question. I have been doing reading on different insert techniques for mass records, but are there limits on how big a query insert can be? Can the same technique be used for REPLACE and INSERT ...ON DUPLICATE KEY UPDATE ... ? Is there a faster method?
Table:
+-----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+----------------+
| a | int(11) | NO | PRI | NULL | auto_increment |
| b | int(11) | YES | | NULL | |
| c | int(11) | YES | | NULL | |
#1
1) "INSERT INTO TABLE COLUMNS (a,b,c) values (1,2,3);"
2) "INSERT INTO TABLE COLUMNS (a,b,c) values (5,6,7);"
3) "INSERT INTO TABLE COLUMNS (a,b,c) values (8,9,10);"
...
10,000) "INSERT INTO TABLE COLUMNS (a,b,c) values (30001,30002,30003);"
or
#2 - should be faster, but is there a limit?
"INSERT INTO TABLE COLUMNS (a,b,c) values (1,2,3),(4,5,6),(8,9,10)....(30001,30002,30003)" ;
From a scripting perspective (PHP), using #2, is it better to loop through and queue up 100 entries (1000 times)...or a 1000 entries (100 times), or just all 10,000 at once? Could this be done with 100,000 entries?
Can the same be used with REPLACE:
"REPLACE INTO TABLE (a, b, c) VALUES(1,2,3),(4,5,6)(7,8,9),...(30001,30002,30003);"
Can it also be used with INSERT ON DUPLICATE?
INSERT INTO TABLE (a, b, c) VALUES(1,2,3),(4,5,6),(7,8,9),....(30001,30002,30003) ON DUPLICATE KEY UPDATE (b=2,c=3)(b=5,c=6),(b=8,c=9),....(b=30002,c=30003) ?
For any and all of the above (assuming the replace/on duplicate are valid), are there faster methods to achieve the inserts?
The length of any SQL statement is limited by a MySQL option called max_allowed_packet.
The syntax of INSERT allows you to add an unlimited number of tuples after the VALUES clause, but the total length of the statement from INSERT to the last tuple must still be no more than the number of bytes equal to max_allowed_packet.
Regardless of that, I have found that LOAD DATA INFILE is usually significantly faster than any INSERT syntax. It's so much faster, that you might even find it faster to write your tuples to a temporary CSV file and then use LOAD DATA INFILE on that CSV file.
You might like my presentation comparing different bulk-loading solutions in MySQL: Load Data Fast!
#1 (single-row inserts) -- Slow. A variant is INSERT IGNORE -- beware: it burns AUTO_INCREMENT ids.
#2 (batch insert) -- Faster than #1 by a factor of 10. But do the inserts in batches of no more than 1000. (After that, you are into "diminishing returns" and may conflict with other activities.
#3 REPLACE -- Bad. It is essentially a DELETE plus an INSERT. Once IODKU was added to MySQL, I don't think there is any use for REPLACE. All the old AUTO_INCREMENT ids will be tossed and new ones created.
#4 IODKU (Upsert) -- [If you need to test before Insert.] It can be batched, but not the way you presented it. (There is no need to repeat the b and c values.)
INSERT INTO (
INSERT INTO TABLE (a, b, c)
VALUES(1,2,3),(4,5,6),(7,8,9),....(30001,30002,30003)
ON DUPLICATE KEY UPDATE
b = VALUES(b),
c = VALUES(c);
Or, in MySQL 8.0, the last 2 lines are:
b = NEW.b,
c = NEW.c;
IODKU also burns ids.
MySQL LOAD DATA INFILE with ON DUPLICATE KEY UPDATE discusses a 2-step process of LOAD + IODKU. Depending on how complex the "updates" are, 2+ steps may be your best answer.
#5 LOAD DATA -- as Bill mentions, this is a good way if the data comes from a file. (I am dubious about its speed if you also have to write the data to a file first.) Be aware of the usefulness of #variables to make minor tweaks as you do the load. (Eg, STR_TO_DATE(..) to fix a DATE format.)
#6 INSERT ... SELECT ...; -- If the data is already in some other table(s), you may as well combine the Insert and Select. This works for IODKU, too.
As a side note, if you need to get AUTO_INCREMENT ids of each batched row, I recommend some variant on the following. It is aimed at batch-normalization of id-name pairs that might already exist in the mapping table. Normalization
I have a query that updates a field in a table using the primary key to locate the row. The table can contain many rows where the date/time field is initially NULL, and then is updated with a date/time stamp using NOW().
When I run the update statement on the table, I am getting a slow query log entry (3.38 seconds). The log indicates that 200,000 rows were examined. Why would that many rows be examined if I am using the PK to identify the row being updated?
Primary key is item_id and customer_id. I have verified the PRIMARY key is correct in the mySQL table structure.
UPDATE cust_item
SET status = 'approved',
lstupd_dtm = NOW()
WHERE customer_id = '7301'
AND item_id = '12498';
I wonder if it's a hardware issue.
While the changes I've mentioned in comments might help slightly, in truth, I cannot replicate this issue...
I have a data set of roughly 1m rows...:
CREATE TABLE cust_item
(customer_id INT NOT NULL
,item_id INT NOT NULL
,status VARCHAR(12) NULL
,PRIMARY KEY(customer_id,item_id)
);
-- INSERT some random rows...
SELECT COUNT(*)
, SUM(customer_id = 358) dense
, SUM(item_id=12498) sparse
FROM cust_item;
+----------+-------+--------+
| COUNT(*) | dense | sparse |
+----------+-------+--------+
| 1047720 | 104 | 8 |
+----------+-------+--------+
UPDATE cust_item
SET status = 'approved'
WHERE item_id = '12498'
AND customer_id = '358';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
How long does it take to select the record, without the update?
If select is fast then you need to look into things that can affect update/write speed.
too many indexes on the table, don't forget filtered indexes and indexed views
the index pages have 0 fill factor and need to split to accommodate the data change.
referential constraints with cascade
triggers
slow write speed at the storage level
If the select is slow
old/bad statistics on the index
extreme fragmentation
columnstore index with too many open rowgroups
If the select speed improves significantly after the first time, you may be having some cold buffer performance issues. That could point to storage I/O problems as well.
You may also be having concurrency issues caused by another process locking the table momentarily.
Finally, any chance the tool executing the query is returning a false duration? For example, SQL Server Management Studio can occasionally be slow to return a large resultset, even if the server handled it very quickly.
So i understand and found posts that indicates that it is not recommended to omit the order by clause in a SQL query when you are retrieving data from the DBMS.
Resources & Post consulted (will be updated):
SQL Server UNION - What is the default ORDER BY Behaviour
When no 'Order by' is specified, what order does a query choose for your record set?
https://dba.stackexchange.com/questions/6051/what-is-the-default-order-of-records-for-a-select-statement-in-mysql
Questions :
See logic of the question below if you want to know more.
My question is : under mysql with innoDB engine, does anyone know how the DBMS effectively gives us the results ?
I read that it is implementation dependent, ok, but is there a way to know it for my current implementation ?
Where is this defined exactly ?
Is it from MySQL, InnoDB , OS-Dependent ?
Isn't there some kind of list out there ?
Most importantly, if i omit the order by clause and get my result, i can't be sure that this code will still work with newer database versions and that the DBMS will never give me the same result, can i ?
Use case & Logic :
I'm currently writing a CRUD API, and i have table in my DB that doesn't contain an "id" field (there is a PK though), and so when i'm showing the results of that table without any research criteria, i don't really have a clue on what i should use to order the results. I mean, i could use the PK or any field that is never null, but it wouldn't make it relevant. So i was wondering, as my CRUD is supposed to work for any table and i don't want to solve this problem by adding an exception for this specific table, i could also simply omit the order by clause.
Final Note :
As i'm reading other posts, examples and code samples, i'm feeling like i want to go too far. I understand that it is common knowledge that it's just a bad practice to omit the Order By clause in a request and that there is no reliable default order clause, not to say that there is no order at all unless you specify it.
I'd just love to know where this is defined, and would love to learn how this works internally or at least where it's defined (DBMS / Storage Engine / OS-Dependant / Other / Multiple criteria). I think it would also benefit other people to know it, and to understand the inners mechanisms in place here.
Thanks for taking the time to read anyway ! Have a nice day.
Without a clear ORDER BY, current versions of InnoDB return rows in the order of the index it reads from. Which index varies, but it always reads from some index. Even reading from the "table" is really an index—it's the primary key index.
As in the comments above, there's no guarantee this will remain the same in the next version of InnoDB. You should treat it as a coincidental behavior, it is not documented and the makers of MySQL don't promise not to change it.
Even if their implementation doesn't change, reading in index order can cause some strange effects that you might not expect, and which won't give you query result sets that makes sense to you.
For example, the default index is the clustered index, PRIMARY. It means index order is the same as the order of values in the primary key (not the order in which you insert them).
mysql> create table mytable ( id int primary key, name varchar(20));
mysql> insert into mytable values (3, 'Hermione'), (2, 'Ron'), (1, 'Harry');
mysql> select * from mytable;
+----+----------+
| id | name |
+----+----------+
| 1 | Harry |
| 2 | Ron |
| 3 | Hermione |
+----+----------+
But if your query uses another index to read the table, like if you only access column(s) of a secondary index, you'll get rows in that order:
mysql> alter table mytable add key (name);
mysql> select name from mytable;
+----------+
| name |
+----------+
| Harry |
| Hermione |
| Ron |
+----------+
This shows it's reading the table by using an index-scan of that secondary index on name:
mysql> explain select name from mytable;
+----+-------------+---------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | mytable | index | NULL | name | 83 | NULL | 3 | Using index |
+----+-------------+---------+-------+---------------+------+---------+------+------+-------------+
In a more complex query, it can become very tricky to predict which index InnoDB will use for a given query. The choice can even change from day to day, as your data changes.
All this goes to show: You should just use ORDER BY if you care about the order of your query result set!
Bill's answer is good. But not complete.
If the query is a UNION, it will (I think) deliver first the results of the first SELECT (according to the rules), then the results of the second. Also, if the table is PARTITIONed, it is likely to do a similar thing.
GROUP BY may sort by the grouping expressions, thereby leading to a predictable order, or it may use a hashing technique, which scrambles the rows. I don't know how to predict which.
A derived table used to be an ordered list that propagates into the parent query's ordering. But recently, the ORDER BY is being thrown away in that subquery! (Unless there is a LIMIT.)
Bottom Line: If you care about the order, add an ORDER BY, even if it seems unnecessary based on this Q & A.
MyISAM, in contrast, starts with this premise: The default order is the order in the .MYD file. But DELETEs leave gaps, UPDATEs mess with the gaps, and INSERTs prefer to fill in gaps over appending to the file. So, the row order is rather unpredictable. ALTER TABLE x ORDER BY y temporarily sets the .MYD order; this 'feature' does not work for InnoDB.
In a given table I have a field (field_order) that will serve as way to define a custom order for showing the rows of the table. When inserting a new record
I would like to set that particular field with the numbers of rows in that table plus one
So if the table has 3 rows, at the time of inserting a new one, the default value for field_order should be 4.
What would be the best approach to set that value?
A simple select count inside the insert statement?
Is there a constant like CURRENT_TIMESTAMP for TIMESTAMP datatype that returns that value?
EDIT: The reason behind this is is to be able to sort the table by that particular field; and that field would be manipulated by a user in client side using jQuery's sortable
Okay, so the solutions surrounding this question actually involve a bit of nuance. Went ahead and decided to answer, but also wanted to address some of the nuance/details that the comments aren't addressing yet.
First off, I would very strongly advise you against using auto_increment on the primary key, if for no other reason that that it's very easy for those auto increment ids to get thrown off (for example, rolled back transactions will interfere with them MySQL AUTO_INCREMENT does not ROLLBACK. So will deletes, as #Sebas mentioned).
Second, you have to consider your storage engine. If you are using MyISAM, you can very quickly obtain a COUNT(*) of the table (because MyISAM always knows how many rows are in each table). If you're using INNODB, that's not the case. Depending on what you need this table for, you may be able to get away with MyISAM. It's not the default engine, but it is certainly possible that you could encounter a requirement for which MyISAM would be a better choice.
The third thing you should ask yourself is, "Why?" Why do you need to store your data that way at all? What does that actually give you? Do you in fact need that information in SQL? In the same table of the same SQL table?
And if the "Why" has an answer that justifies its use, then the last thing I'd ask is "how?" In particular, how are you going to deal with concurrent inserts? How are you going to deal with deletes or rollbacks?
Given the requirement that you have, doing a count star of the table is basically necessary... but even then, there's some nuance involved (deletes, rollbacks, concurrency) and also some decisions to be made (which storage engine do you use; can you get away with using MyISAM, which will be faster for count stars?).
More than anything, though, I'd be question why I needed this in the first place. Maybe you really do... but that's an awfully strange requirement.
IN LIGHT OF YOUR EDIT:
EDIT: The reason behind this is is to be able to sort the table by
that particular field; and that field would be manipulated by a user
in client side using jQuery's sortable
Essentially what you are asking for is metadata about your tables. And I would recommend storing those metadata in a separate table, or in a separate service altogether (Elastic Search, Redis, etc). You would need to periodically update that separate table (or Key value store). If you were doing this in SQL, you could use a trigger. Or you used something like Elastic Search, you could insert your data into SQL and ES at the same time. Either way, you have some tricky issues you need to contend with (for example, eventual consistency, concurrency, all the glorious things that can backfire when you are using triggers in MySQL).
If it were me, I'd note two things. One, not even Google delivers an always up to date COUNT(*). "Showing rows 1-10 out of approximately XYZ." They do that in part because they have more data that I imagine you do, and in part because it actually is impractical (and very quickly becomes infeasible and prohibitive) to calculate an exact COUNT(*) of a table and keep it up to date at all times.
So, either I'd change my requirement entirely and leverage a statistic I can obtain quickly (if you are using MyISAM for storage, go ahead and use count( * )... it will be very fast) or I would consider maintaining an index of the count stars of my tables that periodically updates via some process (cron job, trigger, whatever) every couple of hours, or every day, or something along those lines.
Inre the bounty on this question... there will never be a single, canonical answer to this question. There are tradeoffs to be made no matter how you decide to manage it. They may be tradeoffs in terms of consistency, latency, scalability, precise vs approximate solutions, losing INNODB in exchange for MyISAM... but there will be tradeoffs. And ultimately the decision comes down to what you are willing to trade in order to get your requirement.
If it were me, I'd probably flex my requirement. And if I did, I'd probably end up indexing it in Elastic Search and make sure it was up to date every couple of hours or so. Is that what you should do? That depends. It certainly isn't a "right answer" as much as it is one answer (out of many) that would work if I could live with my count(*) getting a bit out of date.
Should you use Elastic Search for this? That depends. But you will be dealing with tradeoffs which ever way you go. That does not depend. And you will need to decide what you're willing to give up in order to get what you want. If it's not critical, flex the requirement.
There may be a better approach, but all I can think of right now is to create a second table that holds the value you need, and use triggers to make the appropriate inserts / deletes:
Here's an example:
-- Let's say this is your table
create table tbl_test(
id int unsigned not null auto_increment primary key,
text varchar(50)
);
-- Now, here's the table I propose.
-- It will be related to your original table using 'Id'
-- (If you're using InnoDB you can add the appropriate constraint
create table tbl_incremental_values(
id int unsigned not null primary key,
incremental_value int unsigned not null default 0
);
-- The triggers that make this work:
delimiter $$
create trigger trig_add_one after insert on tbl_test for each row
begin
declare n int unsigned default 0;
set n = (select count(*) from tbl_test);
insert into tbl_incremental_values
values (NEW.id, (n));
end $$
-- If you're using InnoDB tables and you've created a constraint that cascades
-- delete operations, skip this trigger
create trigger trig_remove before delete on tbl_test for each row
begin
delete from tbl_incremental_values where id = OLD.id;
end $$
delimiter ;
Now, let's test it:
insert into tbl_test(text) values ('a'), ('b');
select a.*, b.incremental_value
from tbl_test as a inner join tbl_incremental_values as b using (id);
-- Result:
-- id | text | incremental_value
-- ---+------+------------------
-- 1 | a | 1
-- 2 | b | 2
delete from tbl_test where text = 'b';
select a.*, b.incremental_value
from tbl_test as a inner join tbl_incremental_values as b using (id);
-- Result:
-- id | text | incremental_value
-- ---+------+------------------
-- 1 | a | 1
insert into tbl_test(text) values ('c'), ('d');
select a.*, b.incremental_value
from tbl_test as a inner join tbl_incremental_values as b using (id);
-- Result:
-- id | text | incremental_value
-- ---+------+------------------
-- 1 | a | 1
-- 3 | c | 2
-- 4 | d | 3
This will work fine for small datasets, but as evanv says in his answer:
Why?" Why do you need to store your data that way at all? What does that actually give you? Do you in fact need that information in SQL? In the same table of the same SQL table?
If all you need is to output that result, there's a much easier way to make this work: user variables.
Let's now say that your table is something like this:
create table tbl_test(
id int unsigned not null auto_increment primary key,
ts timestamp,
text varchar(50)
);
insert into tbl_test(text) values('a');
insert into tbl_test(text) values('b');
insert into tbl_test(text) values('c');
insert into tbl_test(text) values('d');
delete from tbl_test where text = 'b';
insert into tbl_test(text) values('e');
The ts column will take the value of the date and time on which each row was inserted, so if you sort it by that column, you'll get the rows in the order they were inserted. But now: how to add that "incremental value"? Using a little trick with user variables it is possible:
select a.*
, #n := #n + 1 as incremental_value
-- ^^^^^^^^^^^^ This will update the value of #n on each row
from (select #n := 0) as init -- <-- you need to initialize #n to zero
, tbl_test as a
order by a.ts;
-- Result:
-- id | ts | text | incremental_value
-- ---+---------------------+------+----------------------
-- 1 | xxxx-xx-xx xx:xx:xx | a | 1
-- 3 | xxxx-xx-xx xx:xx:xx | c | 2
-- 4 | xxxx-xx-xx xx:xx:xx | d | 3
-- 5 | xxxx-xx-xx xx-xx-xx | e | 4
But now... how to deal with big datasets, where it's likely you'll use LIMIT? Simply by initializing #n to the start value of limit:
-- A dull example:
prepare stmt from
"select a.*, #n := #n + 1 as incremental_value
from (select #n := ?) as init, tbl_test as a
order by a.ts
limit ?, ?";
-- The question marks work as "place holders" for values. If you're working
-- directly on MySQL CLI or MySQL workbench, you'll need to create user variables
-- to hold the values you want to use.
set #first_row = 2, #nrows = 2;
execute stmt using #first_row, #first_row, #nrows;
-- ^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^
-- Initalizes The "floor" The number
-- the #n of the of rows
-- value LIMIT you want
--
-- Set #first_row to zero if you want to get the first #nrows rows
--
-- Result:
-- id | ts | text | incremental_value
-- ---+---------------------+------+----------------------
-- 4 | xxxx-xx-xx xx:xx:xx | d | 3
-- 5 | xxxx-xx-xx xx-xx-xx | e | 4
deallocate prepare stmt;
It seems like the original question was asking for an easy way to set a default sort order on a new record. Later on the user may adjust that "order field" value. Seems like DELETES and ROLLBACKS have nothing to do with this.
Here's a simple solution. For the sort order field, set your default value as 0, and use the primary key as your secondary sort. Simply change your sort order in the query to be DESC. If you want the default functionality to be "display most recently added first", then use:
SELECT * from my_table
WHERE user_id = :uid
ORDER BY field_order, primary_id DESC
If you want to "display most recently added last" use:
SELECT * from my_table
WHERE user_id = :uid
ORDER BY field_order DESC, primary_id
What I have done to avoid the SELECT COUNT(*) ... in the insert query is to have an unsorted state of the field_order column, let's say a default value of 0.
The select-query looks like:
SELECT * FROM my_table ... ORDER BY id_primary, field_order
As long as you don't apply a custom order, your query will result in chronological order.
When you want to apply custom sorting field_order should be re-setted by counting them from -X to 0:
id | sort
---+-----
1 | -2
2 | -1
3 | 0
When altering occurs the custom sort remains, and new rows will always be sorted chronoligicaly at end of the custom sorting already in place:
id | sort
---+-----
1 | -2
3 | 0
4 | 0
One of the things my app does a fair amount is:
select count(distinct id) from x;
with id the primary key for table x. With MySQL 5.1 (and 5.0), it looks like this:
mysql> explain SELECT count(distinct id) from x;
+----+-------------+----------+-------+---------------+-----------------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+-------+---------------+-----------------+---------+------+---------+-------------+
| 1 | SIMPLE | x | index | NULL | ix_blahblahblah | 1 | NULL | 1234567 | Using index |
+----+-------------+----------+-------+---------------+-----------------+---------+------+---------+-------------+
On InnoDB, this isn't exactly blazing, but it's not bad, either.
This week I'm trying out MySQL 5.5.11, and was surprised to see that the same query is many times slower. With the cache primed, it takes around 90 seconds, compared to 5 seconds before. The plan now looks like this:
mysql> explain select count(distinct id) from x;
+----+-------------+----------+-------+---------------+---------+---------+------+---------+-------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+-------+---------------+---------+---------+------+---------+-------------------------------------+
| 1 | SIMPLE | x | range | NULL | PRIMARY | 4 | NULL | 1234567 | Using index for group-by (scanning) |
+----+-------------+----------+-------+---------------+---------+---------+------+---------+-------------------------------------+
One way to make it go fast again is to use select count(id) from x, which is safe because id is a primary key, but I'm going through some abstraction layers (like NHibernate) that make this a non-trivial task.
I tried analyze table x but it didn't make any appreciable difference.
It looks kind of like this bug, though it's not clear what versions that applies to, or what's happening (nobody's touched it in a year yet it's "serious/high/high").
Is there any way, besides simply changing my query, to get MySQL to be smarter about this?
UPDATE:
As requested, here's a way to reproduce it, more or less. I wrote this SQL script to generate 1 million rows of dummy data (takes 10 or 15 minutes to run):
delimiter $$
drop table if exists x;
create table x (
id integer unsigned not null auto_increment,
a integer,
b varchar(100),
c decimal(9,2),
primary key (id),
index ix_a (a),
index ix_b (b),
index ix_c (c)
) engine=innodb;
drop procedure if exists fill;
create procedure fill()
begin
declare i int default 0;
while i < 1000000 do
insert into x (a,b,c) values (1,"one",1.0);
set i = i+1;
end while;
end$$
delimiter ;
call fill();
When it's done, I observe this behavior:
5.1.48
select count(distinct id) from x
EXPLAIN is: key: ix_a, Extra: Using index
takes under 1.0 sec to run
select count(id) from x
EXPLAIN is: key: ix_a, Extra: Using index
takes under 0.5 sec to run
5.5.11
select count(distinct id) from x
EXPLAIN is: key: PRIMARY, Extra: Using index for group-by
takes over 7.0 sec to run
select count(id) from x
EXPLAIN is: key: ix_a, Extra: Using index
takes under 0.5 sec to run
EDIT:
If I modify the query in 5.5 by saying
select count(distinct id) from x force index (ix_a);
it runs much faster. Indexes b and c also work (to varying degrees), and even forcing index PRIMARY helps.
I'm not making any promises that this will be better but, as a possible work around, you could try:
SELECT COUNT(*)
FROM (SELECT id
FROM x
GROUP BY id) t
I'm not sure why you need DISTINCT on a unique primary key. It looks like MySQL is viewing the DISTINCT keyword as an operator and losing the ability to make use of the index (as would any operation on a field.) Other SQL engines also sometimes don't optimize searches on expressions very well, so it's not a surprise.
I note your comment in another answer about this being an artifact of your ORM. Have you ever read the famous Leaky Abstractions blog by Joel Spolsky? I think you are there. Sometimes you end up spending more time straightening out the tool than you spend on the problem you're using the tool to solve.
I dont know if you have realiased, but counting the rows on a large database with InnoDB is slow, even without the distinct keyword. InnoDB does not cache the rowcount in the table metadata, MyISAM does.
I would suggest you do one of two things
1) create a trigger that inserts/updates distinct counts into another table on insertion.
2) slave another MySQL server to your database, but change the table type on the slave only, to MyISAM and perform your query there (this is probarbly overkill).
I may be missreading your question, but if id is the primary key of table x, then the following two queries are logically equivalent:
select count(distinct id) from x;
select count(*) from x;
...regardless of whether the optimizer realizes this. Distinct generally implies a sort or scanning the index in order, which is considerably slower than just counting the rows.
Creative use of autoincrement fields
Note that your id is autoincrement.
It will add +1 after each insert.
However it does not reuse numbers, so if you delete a row you need to track of that.
My idea goes something like this.
Count(rows) = Max(id) - number of deletions - starting(id) + 1
Scenario using update
Create a separate table with the totals per table.
table counts
id integer autoincrement primary key
tablename varchar(45) /*not needed if you only need to count 1 table*/
start_id integer default maxint
delete_count
Make sure you extract the starting_id before the first delete(!) into the table and do
INSERT INTO counts (tablename, start_id, delete_count)
SELECT 'x', MIN(x.id), 0
FROM x;
Now create a after delete trigger.
DELIMITER $$
CREATE TRIGGER ad_x_each AFTER DELETE ON x FOR EACH ROW
BEGIN
UPDATE counts SET delete_count = delete_count + 1 WHERE tablename = 'x';
END $$
DELIMITER ;
IF you want to have the count, you do
SELECT max(x.id) - c.start_id + 1 - c.delete_count as number_of_rows
FROM x
INNER JOIN counts c ON (c.tablename = 'x')
This will give you your count instantly, with requiring a trigger to count on every insert.
insert scenario
If you have lots of deletes, you can speed up the proces by doing an insert instead of an update in the trigger and selecting
TABLE count_x /*1 counting table per table to keep track of*/
id integer autoincrement primary key /*make sure this field starts at 1*/
start_id integer default maxint /*do not put an index on this field!*/
Seed the starting id into the count table.
INSERT INTO counts (start_id) SELECT MIN(x.id) FROM x;
Now create a after delete trigger.
DELIMITER $$
CREATE TRIGGER ad_x_each AFTER DELETE ON x FOR EACH ROW
BEGIN
INSERT INTO count_x (start_id) VALUES (default);
END $$
DELIMITER ;
SELECT max(x.id) - min(c.start_id) + 1 - max(c.id) as number of rows
FROM x
JOIN count_x as c ON (c.id > 0)
You'll have to test which approach works best for you.
Note that in the insert scenario you don't need delete_count, because you are using the autoincrementing id to keep track of the number of deletions.
select count(*)
from ( select distinct(id) from x)