I missed this when it came out, and my addition is too long to comment, so I'll add another answer here just to share what the documentation on [SOQL For Loops](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_loops_for_SOQL.htm) recommends (emphasis mine):

> ### SOQL For Loops Versus Standard SOQL Queries
>
> SOQL for loops differ from standard SOQL statements because of the method they use to retrieve sObjects. While the standard queries discussed in [SOQL and SOSL Queries](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_SOQL.htm) can retrieve either the count of a query or a number of object records, SOQL for loops retrieve all sObjects, using efficient chunking with calls to the query and queryMore methods of the SOAP API. ***Developers should always use a SOQL for loop to process query results that return many records, to avoid the limit on [heap size](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm#total_heap_size_limit_desc).***
>
> Note that queries including an [aggregate function](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_SOQL_agg_fns.htm) don't support queryMore. A run-time exception occurs if you use a query containing an aggregate function that returns more than 2,000 rows in a for loop.