Every DML statement you run has a cost. Salesforce counts them against a limit of 150
per transaction, and it counts a statement written inside a loop once for every pass
through that loop. Writing the same insert fifty times costs fifty statements. Writing
it once with a list of fifty records costs one.
The bulk form of insert takes a list:
List<Contact> records = new List<Contact>();
for (String name : lastNames) {
records.add(new Contact(LastName = name));
}
insert records;
That is ONE DML statement, no matter how many records the list holds. The loop that
builds the list is not DML, so it costs nothing against the limit. The insert is what
costs, and there is only one of it.
The loop version looks almost identical and is not:
for (Contact record : records) {
insert record;
}
This is a DML statement inside a loop, so it runs once per record. Measured on the
validation org, a bulk insert of three Contacts used one DML statement, and a loop
inserting those same three used three. Measured the same way on the production org,
the bulk insert still cost one, because the statement count belongs to your code and
not to the records.
The limit is 150 DML statements per transaction. A loop over 200 records does not
insert 200 records slowly. It fails outright:
System.LimitException: Too many DML statements: 151
One bulk insert of 200 records has no such problem. The limit counts statements, not
rows. A method that handles ten records and a method that handles ten thousand should
both spend the same single statement, and that is what makes a method safe to call
from a trigger, a flow, or a batch job.
Database.insert(records) does the same thing as the statement and hands you back a
List<Database.InsertResult>, one result per record, so you can see which row failed.
The statement form gives you nothing back, and one bad row rolls back all of them.
Passing false as the second argument keeps the good rows:
List<Database.InsertResult> results = Database.insert(records, false);
The usual mistake is building the list correctly and then inserting it inside the loop
anyway, which costs exactly what it cost before. The second is treating the record
count as the problem when it is the statement count that is limited. The third is
forgetting the empty list: inserting one is harmless and costs nothing, but a method
that promises to create rows and creates none has still failed.
Bulkification is the difference between a method that works on the ten records in a
demo and a method that still works when the org hands it ten thousand.