A normal WP_Query hydrates every result into a WP_Post object and primes the meta and term caches for all of them. When the only thing you want is a list of ids — to exclude from another query, or to feed a count — that is a lot of work discarded.
$ids = get_posts( array(
'post_type' => 'product',
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
) );
fields => ids returns a flat array of integers and skips the object hydration entirely. There is also id=>parent, which returns both columns. Anything else is ignored — passing an arbitrary column name silently gives you full post objects, which makes the optimisation look like it worked when it did not.