A single field would be enough but if you manage your way with 3 that’s good as well.
I would do a loop through the results and end up with a structure like the following:
$years = array(
'2015' => array(
<WP_Post object>,
<WP_Post object>,
etc...
),
'2016' => array(
etc...
)
);
Then you could go through this arrays again to build your html.
A lot of different technics can be used in that case. Just use your favorite I guess. I just think doing one database call is better than 3.
Not sure if I answer your question but I hope it helps.
EDIT:
Since you are ordering by date, you don’t need to re-order your result so it should be as easy as :
/* Your custom query */
$args = array(
'cat' => 14,
'post_type' => 'post',
'posts_per_page' => -1,
'orderby' => 'meta_value_num',
'order' => 'DESC',
'meta_key' => 'p2f_date'
);
$archive = new WP_Query( $args );
$current_year = null;
/* if our query has posts */
if( $archive->have_posts() ){
/* loop through them */
while( $archive->have_posts() ){
$archive->the_post();
$p2f_date = get_post_meta( get_the_id(), 'p2f_date' );
/* Here I skip if the meta is not defined but you might want to handle that differently */
if(empty($p2f_date)){
continue;
}
/* Get the year - Depending how you handle the post meta you might not need the index [0] */
$year = date('Y', strtotime( $p2f_date[0] ));
if( $current_year === null ){
/* First item */
?>
<ul class="year year--<?php echo $year; ?>">
<?php
}else if( $current_year !== $year ){
/* New section */
?>
</ul>
<ul class="year year--<?php echo $year; ?>">
<?php
}
?>
<li>
<a href="https://wordpress.stackexchange.com/questions/178156/<?php the_permalink(); ?>"><?php the_title(); ?></a>
</li>
<?php
$current_year = $year;
}
?>
</ul>
<?php
}
/* Reset your page data in case you need it after the custom query */
wp_reset_postdata();
Hope that makes sense and answers your question.
Here I used a custom WP_Query
but you could just change the main query if this is relevant in your case.