WP_Query->is_main_query() method source (which function of same name calls) is very short and simple:
function is_main_query() {
global $wp_the_query;
return $wp_the_query === $this;
}
The main query is the query that is stored in $wp_the_query global. But what is that global? When WP sets up main query it stores it in two places: $wp_the_query and $wp_query. Latter is more known because that variable is what you commonly use to work with main query and what query_posts() changes.
However query_posts() works like this:
function query_posts($query) {
$GLOBALS['wp_query'] = new WP_Query();
return $GLOBALS['wp_query']->query($query);
}
It break link between $wp_query and $wp_the_query. And the reverse can be performend by wp_reset_query() to re-establish that:
function wp_reset_query() {
$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
wp_reset_postdata();
}
So main query is the one that WP set up during core load.
It is typically what $wp_query holds, unless it was modified not to be main query anymore.