Pages with only small parts of interactive contents can not take advantage of browser caching.
Put all interactive content into images with headers for immediate expiration. These images must of course come from scripts and you will probably need something like mod_perl to handle the resulting number of script hits. Then let your database pages send Last-Modified headers. You can achieve this by using SSI (see server side frames) with XBitHack full and touching the frame if you update the data.
A serious drawback of this method is that the information is image only. So people without the ability to see those images will not get it.
Unfortunately Microsoft seems to have skipped support for If-Modified-Since with IE5. They consider it a waste of time and let their client make the caching decisions. As a consequence the client does not request cached pages at all as long as you are doing GET requests to the server they come from. The moment you do a POST request it invalidates the cached pages. So you might want to change server state with a GET request, if only the interactive images are affected. This is of course bad for it violates an RFC MUST. The way to do it in a hopefully safe and at least reload stable way is to use a skript that changes server state and does a redirect (possibly with an expire to prevent caching) afterwards.
The most common example of the above is a product catalog that displays shopping cart information. Behind every product there appears a little number if the user puts some of it in his cart.
Performance on the users end. If you have static database pages you can consider using a local proxy to take load off your database.