Choosing seats reads every free place of the session that the rates can price, and builds a value object per row. An auditorium of 1422 seats therefore costs about 1422 rows to seat a party of three, once per hold. That read is inherent, since the best contiguous run cannot be found without knowing which seats are free, but two things around it were worth measuring rather than assuming.
1. Does the read repeat within one request? There is no per-slot cache, so a surface asking twice pays twice. The engine already solved the same shape for the session overrides with a small per-request lookup service, and that treatment, keyed by session and by the grades asked for, is what this would want if the repeat is real.
2. Does the exclusion plan badly? It is written as NOT IN a subquery, and the worry was that MySQL plans that as a dependent subquery rather than an anti-join, where NOT EXISTS or a LEFT JOIN with an IS NULL test plans well. The index on the booking side covers the session, the place and the state, so the plan may already be fine and a rewrite would then be noise.
Measure first, on a venue of a real size. A small test venue answers nothing here, and neither does reasoning about the plan: the point of the issue is to get the numbers and then decide.
How to measure it. Both halves are answerable without new tooling. For the repeat, open a query log around each surface and count the statements touching the place table. For the plan, EXPLAIN the candidate read on a venue of a realistic size, around fifteen hundred places, and compare the NOT IN form against NOT EXISTS and against a LEFT JOIN with an IS NULL test, at several levels of fill. A venue of a hundred seats will pick plans that mean nothing, so the size matters more than the data.
Answered below, and the answer to both halves is that nothing needs rewriting. The read already runs once per session per hold, and the subquery is the fastest of the three forms on a hall-sized venue. What ships is the measurement of the repeat, as a test, because nothing in the code enforces the single read.
Not introduced by #3614361: Stepping a quantity up places one seat at a time, so a party is spread around the venue, which is what took this read from once per rate per click down to once per hold. It was already improved once by #3614075: PlaceAssignment loads every place of the venue to propose a few seats, which replaced loading place entities with the column read it does now.
Issue fork yoyaku-3614625
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedMeasured. Neither half needs a code change, so what the branch carries is the measurement of the first half as a test, plus the docs.
1. The read does not repeat
One read of the free places per session per hold. The request is grouped by session and each session is composed once, and the seating strategies rank the candidate list they were handed instead of going back to the database. The map path never makes the read at all: the visitor names the seat, so nothing has to be proposed.
Counted by putting a query log around each surface and classifying the statements that read the place table:
So there is nothing for a per-request cache to save, and adding one would introduce a staleness question the current code does not have: the read happens inside the hold, and a memo spanning a write in the same request could propose a seat just taken.
The count ships as
FreePlaceReadCountTest. Nothing in the code enforces the single read, only how the request is composed and the fact that strategies work from the list they were given. A regression to one read per rate, or per unit, would leave every functional test green, because those only check that the party got seated.One detail worth recording:
hasFreePlace()excludes taken places with the same NOT IN shape but stops at the first row. It is counted separately, since lumping the two together would report a picker page asking whether anything is left as making a hall-sized read.2. The exclusion plans well as written
The premise does not hold on MySQL 8. It does not plan the subquery as a dependent one: it materializes it once, with deduplication, and applies it to the place scan before the sections are joined. Both candidate rewrites plan as an anti-join over every place instead, joining the sections for all of them, and both measure slower.
Measured on a schema seeded with 1422 places over six sections, carrying the indexes the module installs, statistics analyzed, all three forms returning identical result sets, twenty runs each. Median milliseconds, by how much of the hall is sold:
The shipping form is the only one that gets faster as the hall fills, because the exclusion is what narrows the scan: at 1400 taken it reads 22 rows through a filter, where the join forms still walk 1422 and anti-join each one. Rewriting it would have cost about a third of the read at every level of fill. So it stays.
The whole read is between one and three milliseconds on a hall-sized venue, which is the other half of the answer: this was never the expensive thing.
What the measurement did find
The IS NOT NULL guard inside the subquery is load-bearing, not defensive. Remove it and the read returns 0 free places instead of 722: an area sold as a pool holds bookings that name no place, and a single NULL makes NOT IN answer with the empty set, so a hall with one pool in it would offer no seats at all. That was a code comment; it is now measured, and written into the docs next to the numbers.
Comment #4
mably commentedComment #6
mably commented