1) É realmente necessário, sempre que usar INNER JOIN + GROUP BY, colocar em todos os campos uma função matemática (como SUM,MAX, MIN, etc)?
Eu tentei rodar o seu código e não funcionou. O problema apareceu justamente no
GROUP BY
    nome,
    mes_ano -- erro nessa linha
eu vou responder isso na 2), já que esse é o motivo do erro.
Mas quanto à 'necessidade' de usar funções matemáticas com GROUP BY, não é bem assim que a banda toca.
O GROUP BY faz com que os dados sejam unificados seguindo tais regras, ou seja, uma linha é reservada para os campos que se conectam:
SELECT A, B, C FROM TABELA
GROUP BY A, B, C
Aqui teremos várias combinações de diferentes As com diferentes Bs com diferentes Cs, mas esse GROUP BY vai fazer com que todas as linhas que têm combinações A, B, C iguais se transformem em UMA só. Nós então usamos o COUNT() para saber quantas vezes essa mesma combinação foi encontrada, o SUM() para somar todos os valores do campo _ de todas as linhas que apresentaram tal combinação, o MAX() para saber qual foi o maior valor  encontrado entre todas essas linhas com essa combinação particular e MIN(_) para o mesmo intuito.
A palavra para isso pode ser unificação, até porque não será raro você usar 
SELECT COUNT(*), SUM(QUANTIDADE), MAX(QUANTIDADE), MIN(QUANTIDADE) FROM ITENS_NOTAS_FISCAIS;
em uma querry já que você sabe que ela retorna apenas uma linha. 
E é por isso que você não precisa especificar essas funções matemáticas no GROUP BY você usa essas funções matemáticas porque sabe que a consulta retorna apenas uma linha.
2) Não entendi porque não se pode usar o alias MES_ANO no GROUP BY, é obrigatório ou apenas uma boa prática?
Alias é nada mais que um apelido, para ele se tornar um 'campo' real, você teria que fazer uma subquerry (subconsulta), farei isso na próxima pergunta.
O que acontece é o seguinte: o GROUP BY alinha os dados e apresenta fazendo uma agrupação (veja só) entre os dados, esse tipo de agrupação ocorre antes da apresentação de dados, da mesma forma que o WHERE seleciona o que vai ser apresentado, mas o ORDER BY já realiza esse processo um pouco depois e por isso:
SELECT * FROM TABELA
WHERE ALIAS_QUALQUER <> 0   --- ERRORRRR!!!!!
GROUP BY ALIAS_QUALQUER     --- ERRORRRR! EXQUECE, PAPAI, VOCÊ SeQueLou
ORDER BY ALIAS_QUALQUER     --- IRÁ FUNCIONAR NORMALMENTE PORQUE O ORDER BY ACONTECE POR ÚLTIMO
[Copiei de uma resposta do StackOverFlow e traduzi aqui]
FROM Clause é processada e todas as tabelas requisitadas são identificadas, depois de resolver possíveis definições de views.
WHERE clause é processada, aplicando condições às linhas da tabela.
GROUP BY clause é processa, aplicando funções agregadas às linhas remanescentes depois do WHERE.
HAVING clause é processada, aplicando condições adicionais após o GROUP BY.
Apenas agora o SELECT lista as linhas escolhidas e atribuindo os aliases às linhas.
ORDER BY clause é processada, mas como ela só acontece após o SELECT, ela já tem, finalmente, acesso aos aliases atribuídos no seu código.
3) Acabei não usando a subconsulta, isso piora a performance do meu código?
Geralmente, join vai ser mais rápida que uma subquerry. Mas isso não quer dizer que subquerries são inúteis ou inutilizáveis.
Por exemplo, usando o seu próprio código com subquerry, poderíamos resolver o problema de usar aliases no ORDER BY dessa maneira.
Eu escrevi em ORACLE, então para funcionar no seu caso você precisa comentar o TO_CHAR e descomentar a linha de baixo.
SELECT
    X.nome
    ,X.mes_ano
    ,X.qtd_total
    ,X.qtd_limite
FROM
    (SELECT
        TC.NOME
        ,TO_CHAR(NF.DATA_VENDA, 'YYYY-MM')  AS MES_ANO
        --,date_format(nf.data_venda, '%Y-%m')
        ,SUM(quantidade)                    AS qtd_total
        ,volume_de_compra                   AS qtd_limite
        ,( CASE WHEN SUM(quantidade) <= volume_de_compra 
            THEN 'VÁLIDA'
            ELSE 'INVÁLIDA' 
            END) AS STATUS
    FROM
         tabela_de_clientes tc
    INNER JOIN notas_fiscais nf
        ON tc.cpf = nf.cpf
    INNER JOIN itens_notas_fiscais inf
        ON nf.numero = inf.numero
    GROUP BY
        TC.NOME
        ,TO_CHAR(NF.DATA_VENDA, 'YYYY-MM')
        --,date_format(nf.data_venda, '%Y-%m')
        ,volume_de_compra
    ) X
ORDER BY
    X.nome
    ,X.mes_ano
Eu entendo que o código não mudou tanto do seu, e embora tenha resolvido o problema do alias, mas o ponto aqui é explicar como aliases funcionam (ou deixam de funcionar).