Database design for bus reservation

I'd probably go with a "brute force" structure similar to this basic idea:

enter image description here

(There are many more fields that should exist in the real model. This is only a simplified version containing the bare essentials necessary to establish relationships between tables.)

The ticket "covers" stops through TICKET_STOP table, For example, if a ticket covers 3 stops, then TICKET_STOP will contain 3 rows related to that ticket. If there are 2 other stops not covered by that ticket, then there will be no related rows there, but there is nothing preventing a different ticket from covering these stops.

Liberal usage or natural keys / identifying relationships ensures two tickets cannot cover the same seat/stop combination. Look at how LINE.LINE_ID "migrates" alongside both edges of the diamond-shaped dependency, only to be merged at its bottom, in the TICKET_STOP table.

This model, by itself, won't protect you from anomalies such as a single ticket "skipping" some stops - you'll have to enforce some rules through the application logic. But, it should allow for a fairly simple and fast determination of which seats are free for which parts of the trip, something like this:

SELECT *
FROM
    STOP CROSS JOIN SEAT
WHERE
    STOP.LINE_ID = :line_id
    AND SEAT.BUS_NO = :bus_no
    AND NOT EXIST (
        SELECT *
        FROM TICKET_STOP
        WHERE
            TICKET_STOP.LINE_ID = :line_id
            AND TICKET_STOP.BUS_ID = :bus_no
            AND TICKET_STOP.TRIP_NO = :trip_no
            AND TICKET_STOP.SEAT_NO = SEAT.SEAT_NO
            AND TICKET_STOP.STOP_NO = STOP.STOP_NO
    )

(Replace the parameter prefix : with what is appropriate for your DBMS.)

This query essentially generates all combinations of stops and seats for given line and bus, then discards those that are already "covered" by some ticket on the given trip. Those combinations that remain "uncovered" are free for that trip.

You can easily add: STOP.STOP_NO IN ( ... ) or SEAT.SEAT_NO IN ( ... ) to the WHERE clause to restrict the search on specific stops or seats.


From the perspective of bus company:

Usually one route is considered as series of sections, like A to B, B to C, C to D, etc. The fill is calculated on each of those sections separately. So if the bus leaves from A full, and people leave at C, then user can buy ticket at C.

We calculate it this way, that each route has ID, and each section belongs to this route ID. Then if user buys ticket for more than one section, then each section is marked. Then for the next passenger system checks if all sections along the way are available.