Hash indexes are faster than btree indexes?
amitkapila16.blogspot.com
amitkapila16.blogspot.com
For any real production database hash indexes should be used with extreme caution if used at all.
> PostgreSQL supports Hash Index from a long time, but they are not much used in production mainly because they are not durable.
And there's a good reason for bringing them up anyway in the second sentence:
> Now, with the next version of PostgreSQL, they will be durable.
# CREATE INDEX ON t USING hash (id);
WARNING: hash indexes are not WAL-logged and their use is discouraged
CREATE INDEXI think I'm missing the important point.
PostgreSQL 10 which will be released this autumn will include durable hash index, do not use hash indexes until then unless you really know what you are doing.
In the case of a primary key, a row is inserted or deleted but the power goes out before the hash index is updated. When the power comes back, the hash index still points to rows were deleted and is missing rows that were inserted.
Hash tables are good, for small (relative to available space) static sets that don't change. Dynamic production environments will introduce an escalating probablity for performance degradation, and this can detrimental for critical systems.
For something like this, the cost of reindexing is trivial, and the full data is usually in a .sql file somewhere, so even if there's data loss it takes all of about 10 seconds to restore it.
Why would you do this instead of hardcoding this in code, storing it in a local hashtable, or putting it in Redis/Memcached? Bunch of reasons. These tables can be shared across all clients of the database, so if you have multiple apps (in different languages!) that require common validation, they can all make a SQL query instead of recoding the logic. If you don't otherwise need Redis/Memcached, you can avoid adding that dependency to your stack. You can use foreign key constraints to ensure that the values in real data tables match the valid values in the validation table. You can throw an admin interface on top of the tables and let customers edit the set of available options, rather than forcing all changes to go through the developers.
And also with updates to hash tables (e.g. transactional workloads), they're very quick, versus btree updates require traversing the tree structure.
Rehashing can be done in the background, but then you had added code complexity, you'll be doing significantly more I/O during the rehashing (did you think about rehashing when sizing your machines?) and in pathological cases, you can run out of hash table space before the resize is complete. Now what?
1: Allocate a new table of size at least 2X. 2: All accesses to the table hit both the old and new tables. 3: Inserts only go in new. 4: Any change (insert, update, delete) also moves 2 other elements.
You can prevent a O(n) rescan at 4 by keeping a single persistent cursor that scans through the old table from top to bottom. There, still O(1) throughout (C may go up by 2-3 but that's still small). And only uses 1.5x memory.
Not saying that's what postgres does here (it uses WAL, so probably not exactly).
What's the disk usage of hash indexes v.s. regular btrees?
Does it depend on the underlying type or is it a fixed size regardless of the key fields?
How's this compare to the poor man's hash index, i.e. BTREE(HASH(keys...))?
EDIT: I got curious so I ran a test of building btrees vs hash indexes. This is done on 9.6 (not HEAD with the new WAL logging supported hash indexes). Looks like the sweet spot is when the key size is around 20 bytes. After that the fixed size of the hash index gives it a win (on space usage) v.s. the btree which would have to repeatedly store the key values.
CREATE TABLE IF NOT EXISTS hash_test_results (
key_size int,
btree_size bigint,
hash_size bigint
);
CREATE OR REPLACE FUNCTION pg_temp.test_hash_index(p_key_size int)
RETURNS void
AS
$BODY$
DROP TABLE IF EXISTS index_test;
CREATE TABLE index_test as select lpad('' || x, p_key_size, '0') AS a FROM generate_series(1,100000) x;
CREATE INDEX index_test_ix_btree ON index_test USING btree (a);
CREATE INDEX index_test_ix_hash ON index_test USING hash (a);
INSERT INTO hash_test_results
(key_size, btree_size, hash_size)
SELECT
p_key_size AS key_size,
(SELECT pg_relation_size(c.oid) FROM pg_class c WHERE c.relname = 'index_test_ix_btree') AS btree_size,
(SELECT pg_relation_size(c.oid) FROM pg_class c WHERE c.relname = 'index_test_ix_hash') AS hash_size;
$BODY$
LANGUAGE SQL;
SELECT pg_temp.test_hash_index(x * 8) FROM generate_series(1, 16) x;
=> SELECT
key_size,
pg_size_pretty(btree_size) AS btree_size,
pg_size_pretty(hash_size) AS hash_size
FROM hash_test_results;
key_size | btree_size | hash_size
----------+------------+-----------
8 | 3104 kB | 4112 kB
16 | 3992 kB | 4112 kB
24 | 4880 kB | 4112 kB
32 | 5792 kB | 4112 kB
40 | 6648 kB | 4112 kB
48 | 7592 kB | 4112 kB
56 | 8464 kB | 4112 kB
64 | 9352 kB | 4112 kB
72 | 10 MB | 4112 kB
80 | 11 MB | 4112 kB
88 | 12 MB | 4112 kB
96 | 13 MB | 4112 kB
104 | 14 MB | 4112 kB
112 | 15 MB | 4112 kB
120 | 15 MB | 4112 kB
128 | 16 MB | 4112 kBMore to the point: even if you're just talking indexes you can't load/use half a hash table, so wouldn't the point also apply to isolated use of a hash table.
I know it's not what was asked for, just a related point.
I think the primary use case is O(1) lookup by key, i.e. similar to how key/value stores work.
I'm not aware of the planner being able to reuse an existing hash index to perform a hash join. AFAIK the hash buckets are created on the fly. That does sound like a cool idea though.
> More the point, even if you're just talking indexes you can't load/use half a hash table, so wouldn't the point also apply to isolated use of a hash table.
What do you mean by half a hash table?
>> What do you mean by half a hash table?
What I'm trying to get across is my impression that you don't have to load an entire B-tree index into memory to use it... you can traverse it. Whereas a hash index can't be "streamed" or traversed like that, you have to use the whole thing.
I read some papers on hash indexes and hash joins several months ago, and know little about the Postgres specifics... just wanting to wrap my head around it, so please help me understand if I'm off base with this.
That means you don't load it into memory at all, you just jump to Bucket[Hash(KEY)] and then check there for KEY (it may or may not be there due to false positives and the bucket being full).
Right, of course. You're making me realize this really is my confusion between hash indexes and hash joins. The issue I'm thinking of is all about dynamically constructed hash joins. Sorry, you called it right from the start but it took me a minute to see... thanks for helping talk through it.
Before this work was done, hash indexes could literally be corrupted by power outages, etc.
Hash index operations are not presently WAL-logged, so hash indexes might need to be rebuilt with REINDEX after a database crash if there were unwritten changes. Also, changes to hash indexes are not replicated over streaming or file-based replication after the initial base backup, so they give wrong answers to queries that subsequently use them. For these reasons, hash index use is presently discouraged.
That is the warning in the current version of Postgres about hash indexes.
EDIT: Isn't that the point of btree ?
As far as I can tell, the only real way to do efficient iteration over postgres tables is to continually bound ID at either end and then take an offset. (SELECT * from <foo table> where id > <largest id from last batch> limit 10). With btrees on UUID you get lexicographic ordering here I imagine, so it'll work. With hash indexes no dice.
Cursors don't end up working out because if you kill long-running queries or idle connections (which most websites should, in production) you'll kill applications using cursors. With pgbouncer, at least, reading from cursors doesn't seem to bump idle timeouts.
(but range queries / being able to iterate are generally why I prefer btrees, unless I have a good reason. Keep in mind that the O(log n) on B-trees is log with a huge base — it's not power of two. It takes very few disk reads for a b-tree to find its query; it's just not the O(1) of a hash table.)
It isn't that I am doubting the results, but without some trail on how to get the results I can't make this useful information. For example if I were architecting a system and was using this as information I have no idea if I run this in a VM that I need enough memory to get these results.
Quote:
To start with let us see the impact of work done to improve the performance of hash indexes. Below is the performance data of the pgbench read-only workload to compare the performance difference of Hash indexes between 9.6 and HEAD on IBM POWER-8 having 24 cores, 192 hardware threads, 492GB RAM.
The workload is such that all the data fits in shared buffers (scale factor is 300 (~4.5GB) and shared_buffers is 8GB).
And chart itself says it is a median of 3 5 minute runs.
Why would I assume that Concurrent runs == Seperate Runs or that other caching mechanisms aren't in place or really anything. Computers do really odd things trying to optimize and making assumptions that your system is the same over a period of 30 minutes when you don't even know it is the same 30 minutes is concurrent. There is all sorts of stuff that gets in the way of performance tests and I would like to know how it is mitigated. Again, I am sure there is proper process, but why wouldn't know want to know what that is?
Tuning/optimization starts with using the right schema.
They aren't used in Postgres because their implementation there has many problems. One of them is that they are slower than the theoretically suboptimal B-trees. In the process of fixing those problems, people will compare both indexes many times.
Trees don't and are easier to reason about and prove. Esp. Important for critical systems or projects on massive scales,ud want a tree ADT on that Mars rover.
That's the gist of my conversation with my algorithms tutor about why my use of hashing to solve problems, while correct, is flawed.
I did get the points for those exercises and my lower bounds were alot better than what was demanded, but the problem lies in the fact that it's not guaranteed.