Вы немножко запутались, have_posts() это не цикл, а метод объекта WP_Query, проверка если ли посты в
текущем экземпляре WP_Query, независимо от того как данный экземпляр был вызван/создан - это основной запрос или произвольный. Что касается запросов, то все просто:
1. Есть основной запрос, который по умолчанию в WP всегда выполняется. Всегда в первую очередь смотрим, можно ли добиться нужного результата путем его модификации через хук
pre_get_posts. В большинстве случаев именно этим путем решается задача. Например, изменить количество постов на страницу на главной или в рубриках.
2. Если кроме основного запроса нужен дополнительный, например у вас на главной есть уже стандартный запрос, который выводит последние записи, а вы хотите еще отдельно вывести записи произвольного типа (например, вопросы из FAQ), то используете WP_Query. В принципе, в большинстве случае если нужен
отдельный от основного цикл / набор постов - используйте WP_Query. Но важно помнить, что в зависимости от задачи вы можете передать несколько важных параметров, которые повлияют на скорость. Например, по умолчанию WP_Query выполняет CALC_FOUND_ROWS (подсчет всего найденных строк), который нужен для пагинации. Если вам нужно всего лишь получить X постов, используйте параметр 'no_found_rows' => true в комбинации с указанием четкого количества постов в параметре posts_per_page - в этом случае общее количество не будет считаться. Также, по умолчанию WP_Query запрашивает таксономии и метаданные ко всем постам, и кеширует их. Это тоже можно отключать. Также, при выполнении WP_Query может быть затронут плагинами через фильтры - например posts_where (WHERE clause в MySQL). Еще важно понимать, что вызов new WP_Query возвращает новый объект WP_Query, по которому можно выполнить Loop с помощью while ( have_posts() ) : the_post() - в этом случае посты в итерации цикла будут попадать в глобальный scope.
3. get_posts - это как шорткат на WP_Query с определенными предустановленными параметрами. Если нужно просто получить несколько постов по каким-то параметрам - используйте эту функцию. Первое, и основное - эта функция внутри вызывает именно new WP_Query. Второе - эта функция возвращает массив постов (объектов WP_Post). Третье - эта функция вызывает WP_Query c предустановленными параметрами no_found_rows (не считать общее количество постов, соответствующих параметрам) и suppress_filters (что отключает все фильтры над запросом кроме pre_get_posts). Вот и вся разница.
4. query_posts - никогда, НИКОГДА, ни при каких обстоятельствах не использовать данную функцию. Считайте, что ее не существует. Точка.