Anywhere that an array of SObjects is queried - e.g. your `Select Id from Contacts` - those data rows are included in the response and so have to be transferred and consume a bit of heap.

An aggregate query allows the count to be established inside the database and just that number to be returned. It also signals your intent more clearly which is important to help others understand your code in the future.

So yes sometimes you can use either approach, but aggregate queries are worth learning about so you can apply them when they are by far the best choice e.g. large row counts or other operations such as these are needed:

 - AVG
 - COUNT
 - COUNT_DISTINCT
 - MIN
 - MAX
 - SUM

or more exotic operations such as [GROUP BY ROLLUP](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_groupby_rollup.htm).

(In the past counting via aggregate queries was still subject to the 50,000 row governor limit but that is no longer the case.)

Using aggregate queries gets the work done close to the data where the work can be optimised.