If you're concerned about time or memory limits, use a batch class. This is how I would perform the export of a potentially unknown size (10k+ records).
global class X implements Database.Batchable<SObject>, Database.Stateful {
string[] Lines;
global Database.QueryLocator start(Database.BatchableContext bc) {
lines = new String[0];
return Database.getQueryLocator( your-query-here );
}
global void execute(Database.BatchableContext bc, SObject[] scope) {
for(Sobject record: scope) {
String line = '';
// build your csv line here
lines.add(line);
}
}
global void finish(Database.batchablecontext bc) {
String finalResults = String.join(lines, '\n');
lines = null; // Free temp memory to avoid heap errors
// proceed to send attachment by email
}
}
The best performance I've gotten for a synchronous export (that is, a direct query, plus processing about 30 fields per record) is about 5,000 records before a CPU timeout exception, so if this is a concern, definitely go with a batch. If you're confident that you can beat the timeout, you could perform the query and translation directly in the scheduler class itself without resorting to a batch.