Slow Query not using index in one of the tables
SELECT  n.*
    FROM  `reservation_notes` AS n
    INNER JOIN  `reservations` AS r  ON r.`id_hash` = n.`reservation_id_hash`
    INNER JOIN  `contacts` AS c  ON c.`id_hash` = r.`contact_id_hash`
    WHERE  c.`user_id` = 1 */ 


A guess:  user_id is VARCHAR??  It should be INT (or some other numeric datatype).  And c (Contacts) needs INDEX(user_id).  (Please don't use those awfully long names for indexes; it clutters things unnecessarily.)

That would let the Optimizer start with c and do the filtering (user_id=1) first.

Also, provide INDEX(user_id, id_hash), in this order.  That will make the index "covering".  This means that the processing can be done entirely in the index's BTree, without touching the Data BTree.  This is an extra optimizaiton.

Then r (Reservations) needs INDEX(contact_id_hash, id_hash).  The first column is for reaching into the table.  The extra column is to make it "covering".

Then n (Notes) needs INDEX(reservation_id_hash), which you apparently have.

With these changes, EXPLAIN will list the table in a more optimal order:  c, r, n.  (I prefer to write JOINs in the order that the Optimizer will perform the query.  However, the Optimizer will rearrange the tables as it sees fit.)

When you get millions of rows, you may find that "hashes" slow things down.  But that is another topic.