Why does $_GET return values but get_query_var does not?

Because they do different things and source their data from different places.

$_GET is for URL parameters, values passed via HTTP GET. get_query_var is for fetching the query variables of the main post query aka the WP_Query object. Query variables and GET values are not the same thing.

Query variables usually get their values either from query objects, e.g. the parameters and arguments passed to new WP_Query or get_posts. Another place they can come from are rewrite rules. This may be where confusion is coming from as query variables can be set using URL parameters, but this does not mean that all URL parameters are query values. Query values can only have certain values.

Should you use get_query_var to fetch URL parameters/GET values?

No, you should not, that’s not what that function is for.

Instead use standard PHP methods such as filter_input or $_GET

Notes on Query Variables

  • Query variables have a whitelist, so p is a valid name for a query variable, but banana is not. There is a filter to add these but it’s rarely used.
    • It’s main use is when writing custom rewrite rules in order to pass information along, For example lets say I had a plugin to redirect to stack exchange answers that had /?stackanswer=417665 and I wanted a pretty URL e.g./tomsanswers/417665 where 417665 is a stack exchange ID, I’d add a rewrite rule with a regex to map that to index.php?stackanswer=match[1], and I’ll then look to see if the stackanswer query variable has a value, but I’d need to add stackanswer to the list of allowed query variables or it’ll be stripped out. In this sitaution $_GET['stackanswer'] would not exist
    • you can set query variables on any page via URL/GET variables because ugly permalinks take the form index.php?queryvar=value, and pretty permalinks are just regexes that turn pretty URLs into ugly ones. Note that with pretty permalinks those ugly URLs are purely hypothetical and internal, no redirects are happening, and no server mapping happens. It’s not the same as doing it in an Apache/Nginx config, the URL hasn’t actually changed. This is why you can add ?s=searchterm to any archive in WP to search just that archive rather than the entire site.
  • Query variables, or query arguments can be changed but you must do this before the query runs. An action is provided for this named pre_get_posts that will provide the query in question as a function argument.
    • Similar actions exist for terms/comments/users e.g. pre_get_users
    • these run for all queries, not just the main query, and not just the frontend
    • You can’t use this action from a theme template if you want to modify the main query because it’s too late and would require a small amount of time travel
    • If you have query_posts or new WP_Query at the top of every template, switching it to instead use pre_get_posts can provide huge performance boosts, and avoid lots of common errors, e.g. pagination problems
      • this also fixes RSS feeds not showing the right posts, 404’s when your custom template has 10 pages but the original query only has 6 pages of content and the user requests page 7, etc etc