Why does $post_id = $wp_query->get_queried_object_id(); return 0?

Plugins and themes are loaded pretty early in WordPress’s lifecycle. Assuming this code is a plugin file or a theme’s functions.php file, then it is executing before WordPress has set up the query, or even parsed the arguments in the request. The queried object ID is 0 because no query has taken place yet 🙂

Try running the code in a hook after WordPress has finished executing the main query instead:

function wpse408763_test() {
  echo get_the_post_id();
}

add_action( 'wp', 'wpse408763_test' );

It’s worth mentioning that WPDB::get_queried_object_id() can return values which do not correspond to a post at all – on a taxonomy term archive page, it will be the ID of the term, for example, which would result in the get_the_post_id() function returning a term ID when used outside the loop but the current post’s ID within the loop. You might want to consider only returning the value from get_queried_object_id() in the case of is_single() || is_page() and perhaps false otherwise in order to mitigate some headaches down the road.

There’s also a global wrapper such that you needn’t manually reference the $wpdb global yourself.