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

Command icon 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

mably created an issue. See original summary.

mably’s picture

Title: The free-place read repeats within a request, and its exclusion may plan badly » Measure whether the free-place read repeats, and whether its exclusion plans badly
Issue summary: View changes

Measured. 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:

  • a party of three on one rate: 1 read
  • a party of three spanning two rates, one request: 1 read, not one per rate
  • a basket taking seats on two sessions: 2 reads, one per session
  • draw the map, then take a named seat on it: 0 reads

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:

  • 0 taken: NOT IN 2.29, NOT EXISTS 1.92, LEFT JOIN 2.78
  • 50 taken: NOT IN 2.07, NOT EXISTS 1.92, LEFT JOIN 2.58
  • 350 taken: NOT IN 1.86, NOT EXISTS 1.91, LEFT JOIN 2.55
  • 700 taken: NOT IN 1.70, NOT EXISTS 1.91, LEFT JOIN 2.59
  • 1100 taken: NOT IN 1.44, NOT EXISTS 1.88, LEFT JOIN 2.51
  • 1400 taken: NOT IN 1.24, NOT EXISTS 1.87, LEFT JOIN 2.49

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.

mably’s picture

Status: Active » Needs review

  • mably committed c8545896 on 1.x
    task: #3614625 Measure whether the free-place read repeats, and whether...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.