In a sense, the distinction between global variables and a database is similar to the distinction between private and public members of an object (assuming anybody still uses public fields). If you think of the entire program as an object, then the globals are the private variables, and the database is the public fields.
They key distinction here is one of assumed responsibility.
When you write an object, it is assumed that anyone who maintains the member methods will ensure private fields remain well behaved. But you already give up any assumptions about the state of public fields and treat them with extra care.
The same assumption applies at a wider level to globals v/s database. Also, the programming language/ecosystem guarantees access restrictions on private v/s public in the same was as it enforces them on (nonshared memory) globals v/s database.
When multithreading comes into play, the concept of private v/s public v/s global v/s database is merely distinctions along a spectrum.
static int global; // within process memory space
static int dbvar; // mirrors/caches data outside process memory space
class Cls {
public: static int class_public; // essentially the same as global
private: static int class_private; // but public to all methods in class
private: static void method() {
static int method_private; // but public to all scopes in method
// ...
{
static int scope1_private; // mutex guarded
int the_only_truly_private_data;
}
// ...
{
static int scope2_private; // mutex guarded
}
}
}