To anyone interested this is the query that worked for me:
INSERT INTO $wpdb->postmeta (post_id,meta_key,meta_value)
SELECT p.ID, 'score' AS meta_key, 0 AS meta_value
FROM $wpdb->posts AS p
WHERE p.post_type="my_cpt"
AND p.ID NOT IN (
SELECT p2.ID
FROM $wpdb->posts AS p2
INNER JOIN $wpdb->postmeta AS pm
ON pm.post_id = p2.ID
WHERE pm.meta_key = 'score'
)
But after reading this article I preferred to split the query in two for better performance:
global $wpdb;
//Get all posts which already have the metadata
$not_in = $wpdb->get_results(
"
SELECT p.ID
FROM $wpdb->posts AS p
INNER JOIN $wpdb->postmeta AS pm
ON pm.post_id = p.ID
WHERE pm.meta_key = 'score'
AND p.post_type="my-cpt"
AND p.post_status NOT IN ('draft,auto-draft')",
ARRAY_A
);
//Make results into string
$not_in = ! empty($not_in) ? implode( ',', array_column( $not_in, 'ID' ) ) : '';
//Insert new metadata
$wpdb->query(
"
INSERT INTO $wpdb->postmeta (post_id,meta_key,meta_value)
SELECT p.ID, 'score' AS meta_key, 0 AS meta_value
FROM $wpdb->posts AS p
WHERE p.post_type="my-cpt"
AND p.ID NOT IN ( $not_in )
"
);