meta_query is a join per clause

Each condition in a meta_query becomes another self-join against a table that is usually the largest in the database.

// four joins against wp_postmeta
$q = new WP_Query( array(
    'post_type'  => 'product',
    'meta_query' => array(
        'relation' => 'AND',
        array( 'key' => 'price', 'value' => 5000, 'compare' => '<',
               'type' => 'NUMERIC' ),
        array( 'key' => 'in_stock', 'value' => '1' ),
    ),
) );

// a taxonomy term is one join and an index. use one.

Modelling a filterable attribute as a taxonomy rather than as meta is the change that makes these queries affordable, because the term relationship tables are indexed for exactly this and meta is not. The type argument is required for any numeric comparison and is routinely omitted, producing a string comparison that is wrong in a way that looks right for small numbers. Anything with more than two meta conditions on a public page deserves a rethink.