Your second approach is the right one to take, with a few additional notes.
There's no need for a separate ID token. Just hash the session token and pass the hash digest to the DB (e.g. SELECT * FROM Sessions WHERE hashed_token = ?
). You can, if you want faster lookups, create an index on the sessions table (though that makes creating and deleting sessions more expensive).
IIRC cryptographically safe hashing functions are intended to be slow
Not at all! Quite the opposite, in fact. For example, in SSL/TLS cipher suites that aren't using authenticated encryption modes, ever single packet you send or receive has been HMACed at both ends, each of which requires two instances of a secure hash function. While it's true that insecure hashes can be faster than secure ones tend to be, good performance is an essential property of a successful secure hash algorithm.
You're probably confusing secure hash functions in general with password hashing functions. Password hashing functions exist primarily because normal secure hashing functions are too fast, so it's possible to brute-force the low entropy of a memorable secret in a very short time (a single modern graphics card can compute billions, even tens of billions, of secure hashes per second). However, that's not relevant here. You definitely shouldn't use a password hashing function for session tokens, but you also don't need to. Instead of the (generously) 20-40 bits of entropy that a human-generated password might have, a machine-generated random token will (and should) generally be at least 128 bits of entropy, and can easily be much more. Brute-forcing that at mere billions, or even a trillion, of guesses per second will take... oh, about 710 million times the age of the universe. You'll be fine.
A bunch of people seem to be taking issue with the use of "session" here, but realistically you should be doing this with any and all persistent but opaque client-supplied blobs. It doesn't matter in the least if they're "classic" session tokens, refresh tokens, API access keys (assuming the client passes the key itself e.g. as a bearer token), and so on.
As a side note, it's funny that you said JWTs are "probably faster than hashing". What do you think the signature of a JWT is? It's either an HMAC (twice as expensive as a single secure hash iteration) or a digital signature (orders of magnitude slower than a secure hash iteration). The only way JWTs are faster is that you don't have to do a DB lookup (no I/O, no search time), which matters a lot sometimes (if the DB is the bottleneck) but is often totally acceptable (and you can integrate this DB lookup into the DB query you'd be doing anyhow to service the request, if you really want to). You also really, REALLY must never use JWTs for long-term sessions. They can't be revoked (because the entire point is that they're stateless and nothing lives on the server), so if the JWT ever becomes compromised, either it's valid until its expiration date (you can clean it off a cooperating client, but you can't make the attacker forget it), or you have to rotate the signing key (invalidating all the JWTs issued with it). Instead, JWTs should have a very short lifetime - a few minutes is common - and the client also gets a long-lived "refresh token" which is just an opaque blob identical to a session token except you only need it every few minutes instead of on every request. (You should hash the refresh token before storing it in the DB and when the client presents it for use.)
Hashing session tokens (and refresh tokens, and bearer tokens, and some kinds of API keys*, and every other client-supplied opaque value with a security significance) is very nearly as important as password hashing, for nearly all the same reasons (the lone exception being that the same token won't work on other sites, while the same password might), and the cost of doing so is actually much lower (because you can use a fast hash instead of a deliberately slow one).
Many threats against tokens/API keys/etc. stored in the DB are such that access to those tokens barely matters, due to full compromise of the DB (via high-privilege SQL injection, compromise of the DB server software or host machine, weak DB credentials for an exposed DB server, or a malicious DB admin). In such a case, the attacker has direct access to all the data, and also can inject their own credentials for any user that they want to log in as, so hashing tokens provides only trivial additional security. However, many less-complete compromises exist. A common one is compromise of a DB snapshot, such as via an insecure cloud storage or physical theft of backup tapes. An attacker might also achieve only limited SQL injection (restricted to read-only access, or to only certain tables). In such a case, hashing tokens can easily be the difference between totally compromising arbitrary users' accounts, and merely having a limited (and often outdated) information disclosure.
* Obviously you can't hash API keys that are used to sign requests, such as in AWS SigV4; the server needs to know the actual API key in that case. But if the key is presented directly - in the URL, Authorization header, cookie, custom header, body, etc. - and the server is checking it against a stored value (rather than reading a meaningful value out of the token, as in a JWT), then the server should be hashing it before storing (or checking for) it.