Skip to content

Commit 5c248b5

Browse files
committed
More optimizations performance
1 parent 368e77f commit 5c248b5

13 files changed

+289
-156
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SpeedPuzzling\Web\Migrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
final class Version20260102230000 extends AbstractMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return 'Add composite indexes for query optimization';
15+
}
16+
17+
public function up(Schema $schema): void
18+
{
19+
// Composite index for player statistics and ranking queries
20+
$this->addSql('CREATE INDEX idx_pst_player_puzzle_type ON puzzle_solving_time (player_id, puzzle_id, puzzling_type)');
21+
22+
// Index for date-based monthly queries (used after EXTRACT->date range optimization)
23+
$this->addSql('CREATE INDEX idx_pst_tracked_at_type ON puzzle_solving_time (tracked_at, puzzling_type)');
24+
25+
// Partial index for fastest players/groups/pairs queries
26+
$this->addSql('CREATE INDEX idx_pst_type_time_valid ON puzzle_solving_time (puzzling_type, seconds_to_solve) WHERE seconds_to_solve IS NOT NULL AND suspicious = false');
27+
28+
// GIN index for JSONB containment on team column (custom_ prefix = Doctrine won't manage it)
29+
$this->addSql('CREATE INDEX custom_pst_team_puzzlers_gin ON puzzle_solving_time USING GIN ((team::jsonb->\'puzzlers\') jsonb_path_ops) WHERE team IS NOT NULL');
30+
}
31+
32+
public function down(Schema $schema): void
33+
{
34+
$this->addSql('DROP INDEX IF EXISTS idx_pst_player_puzzle_type');
35+
$this->addSql('DROP INDEX IF EXISTS idx_pst_tracked_at_type');
36+
$this->addSql('DROP INDEX IF EXISTS idx_pst_type_time_valid');
37+
$this->addSql('DROP INDEX IF EXISTS custom_pst_team_puzzlers_gin');
38+
}
39+
}

src/Query/GetBorrowedPuzzles.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,13 @@ public function unsolvedByHolderId(string $holderId): array
140140
JOIN puzzle p ON lp.puzzle_id = p.id
141141
LEFT JOIN manufacturer m ON p.manufacturer_id = m.id
142142
LEFT JOIN player owner ON lp.owner_player_id = owner.id
143-
LEFT JOIN puzzle_solving_time pst ON (
144-
pst.player_id = :holderId
145-
AND pst.puzzle_id = p.id
146-
)
147143
WHERE lp.current_holder_player_id = :holderId
148144
AND (lp.owner_player_id IS NULL OR lp.owner_player_id != :holderId)
149-
AND pst.id IS NULL
145+
AND NOT EXISTS (
146+
SELECT 1 FROM puzzle_solving_time pst
147+
WHERE pst.player_id = :holderId
148+
AND pst.puzzle_id = p.id
149+
)
150150
ORDER BY lp.lent_at DESC
151151
SQL;
152152

@@ -194,13 +194,13 @@ public function countUnsolvedByHolderId(string $holderId): int
194194
SELECT COUNT(*) as item_count
195195
FROM lent_puzzle lp
196196
JOIN puzzle p ON lp.puzzle_id = p.id
197-
LEFT JOIN puzzle_solving_time pst ON (
198-
pst.player_id = :holderId
199-
AND pst.puzzle_id = p.id
200-
)
201197
WHERE lp.current_holder_player_id = :holderId
202198
AND (lp.owner_player_id IS NULL OR lp.owner_player_id != :holderId)
203-
AND pst.id IS NULL
199+
AND NOT EXISTS (
200+
SELECT 1 FROM puzzle_solving_time pst
201+
WHERE pst.player_id = :holderId
202+
AND pst.puzzle_id = p.id
203+
)
204204
SQL;
205205

206206
$result = $this->database

src/Query/GetFastestGroups.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ public function __construct(
2121
public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|CountryCode $countryCode): array
2222
{
2323
$query = <<<SQL
24-
WITH player_data AS (
24+
WITH candidate_times AS (
25+
SELECT pst.id
26+
FROM puzzle_solving_time pst
27+
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
28+
WHERE puzzle.pieces_count = :piecesCount
29+
AND pst.puzzling_type = 'team'
30+
AND pst.seconds_to_solve > 0
31+
AND pst.suspicious = false
32+
ORDER BY pst.seconds_to_solve ASC
33+
LIMIT 500
34+
),
35+
player_data AS (
2536
SELECT
2637
puzzle.id AS puzzle_id,
2738
puzzle.name AS puzzle_name,
@@ -32,15 +43,15 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count
3243
tracked_at,
3344
finished_at,
3445
finished_puzzle_photo,
35-
puzzle_solving_time.seconds_to_solve AS time,
46+
pst.seconds_to_solve AS time,
3647
player.name AS player_name,
3748
player.code AS player_code,
3849
player.country AS player_country,
3950
player.id AS player_id,
4051
manufacturer.name AS manufacturer_name,
4152
puzzle.identification_number AS puzzle_identification_number,
42-
puzzle_solving_time.id AS time_id,
43-
puzzle_solving_time.team ->> 'team_id' AS team_id,
53+
pst.id AS time_id,
54+
pst.team ->> 'team_id' AS team_id,
4455
first_attempt,
4556
player.is_private,
4657
competition.id AS competition_id,
@@ -56,18 +67,15 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count
5667
'is_private', p.is_private
5768
) ORDER BY player_elem.ordinality
5869
) AS players
59-
FROM puzzle_solving_time
60-
INNER JOIN puzzle ON puzzle.id = puzzle_solving_time.puzzle_id
61-
INNER JOIN player ON puzzle_solving_time.player_id = player.id
70+
FROM candidate_times ct
71+
INNER JOIN puzzle_solving_time pst ON pst.id = ct.id
72+
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
73+
INNER JOIN player ON pst.player_id = player.id
6274
INNER JOIN manufacturer ON manufacturer.id = puzzle.manufacturer_id
63-
LEFT JOIN competition on puzzle_solving_time.competition_id = competition.id,
64-
LATERAL json_array_elements(puzzle_solving_time.team -> 'puzzlers') WITH ORDINALITY AS player_elem(player, ordinality)
75+
LEFT JOIN competition ON pst.competition_id = competition.id,
76+
LATERAL json_array_elements(pst.team -> 'puzzlers') WITH ORDINALITY AS player_elem(player, ordinality)
6577
LEFT JOIN player p ON p.id = (player_elem.player ->> 'player_id')::UUID
66-
WHERE puzzle.pieces_count = :piecesCount
67-
AND puzzle_solving_time.puzzling_type = 'team'
68-
AND seconds_to_solve > 0
69-
AND puzzle_solving_time.suspicious = false
70-
GROUP BY puzzle.id, player.id, manufacturer.id, puzzle_solving_time.id, competition.id
78+
GROUP BY puzzle.id, player.id, manufacturer.id, pst.id, competition.id
7179
)
7280
SELECT *
7381
FROM player_data

src/Query/GetFastestPairs.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ public function __construct(
2121
public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|CountryCode $countryCode): array
2222
{
2323
$query = <<<SQL
24-
WITH player_data AS (
24+
WITH candidate_times AS (
25+
SELECT pst.id
26+
FROM puzzle_solving_time pst
27+
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
28+
WHERE puzzle.pieces_count = :piecesCount
29+
AND pst.puzzling_type = 'duo'
30+
AND pst.seconds_to_solve > 0
31+
AND pst.suspicious = false
32+
ORDER BY pst.seconds_to_solve ASC
33+
LIMIT 500
34+
),
35+
player_data AS (
2536
SELECT
2637
puzzle.id AS puzzle_id,
2738
puzzle.name AS puzzle_name,
@@ -32,15 +43,15 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count
3243
tracked_at,
3344
finished_at,
3445
finished_puzzle_photo,
35-
puzzle_solving_time.seconds_to_solve AS time,
46+
pst.seconds_to_solve AS time,
3647
player.name AS player_name,
3748
player.code AS player_code,
3849
player.country AS player_country,
3950
player.id AS player_id,
4051
manufacturer.name AS manufacturer_name,
4152
puzzle.identification_number AS puzzle_identification_number,
42-
puzzle_solving_time.id AS time_id,
43-
puzzle_solving_time.team ->> 'team_id' AS team_id,
53+
pst.id AS time_id,
54+
pst.team ->> 'team_id' AS team_id,
4455
first_attempt,
4556
player.is_private,
4657
competition.id AS competition_id,
@@ -56,18 +67,15 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count
5667
'is_private', p.is_private
5768
) ORDER BY player_elem.ordinality
5869
) AS players
59-
FROM puzzle_solving_time
60-
INNER JOIN puzzle ON puzzle.id = puzzle_solving_time.puzzle_id
61-
INNER JOIN player ON puzzle_solving_time.player_id = player.id
70+
FROM candidate_times ct
71+
INNER JOIN puzzle_solving_time pst ON pst.id = ct.id
72+
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
73+
INNER JOIN player ON pst.player_id = player.id
6274
INNER JOIN manufacturer ON manufacturer.id = puzzle.manufacturer_id
63-
LEFT JOIN competition ON puzzle_solving_time.competition_id = competition.id,
64-
LATERAL json_array_elements(puzzle_solving_time.team -> 'puzzlers') WITH ORDINALITY AS player_elem(player, ordinality)
75+
LEFT JOIN competition ON pst.competition_id = competition.id,
76+
LATERAL json_array_elements(pst.team -> 'puzzlers') WITH ORDINALITY AS player_elem(player, ordinality)
6577
LEFT JOIN player p ON p.id = (player_elem.player ->> 'player_id')::UUID
66-
WHERE puzzle.pieces_count = :piecesCount
67-
AND puzzle_solving_time.puzzling_type = 'duo'
68-
AND seconds_to_solve > 0
69-
AND puzzle_solving_time.suspicious = false
70-
GROUP BY puzzle.id, player.id, manufacturer.id, puzzle_solving_time.id, competition.id
78+
GROUP BY puzzle.id, player.id, manufacturer.id, pst.id, competition.id
7179
)
7280
SELECT *
7381
FROM player_data

src/Query/GetLastSolvedPuzzle.php

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
{
1414
public function __construct(
1515
private Connection $database,
16-
private GetTeamPlayers $getTeamPlayers,
1716
) {
1817
}
1918

@@ -52,7 +51,18 @@ public function forPlayer(string $playerId, int $limit): array
5251
competition.id AS competition_id,
5352
competition.shortcut AS competition_shortcut,
5453
competition.name AS competition_name,
55-
competition.slug AS competition_slug
54+
competition.slug AS competition_slug,
55+
CASE WHEN puzzle_solving_time.team IS NOT NULL THEN
56+
(SELECT JSON_AGG(JSON_BUILD_OBJECT(
57+
'player_id', elem.player ->> 'player_id',
58+
'player_name', COALESCE(p.name, elem.player ->> 'player_name'),
59+
'player_code', p.code,
60+
'player_country', p.country,
61+
'is_private', p.is_private
62+
) ORDER BY elem.ordinality)
63+
FROM json_array_elements(puzzle_solving_time.team -> 'puzzlers') WITH ORDINALITY AS elem(player, ordinality)
64+
LEFT JOIN player p ON p.id = (elem.player ->> 'player_id')::UUID)
65+
ELSE NULL END AS players
5666
FROM puzzle_solving_time
5767
INNER JOIN puzzle ON puzzle.id = puzzle_solving_time.puzzle_id
5868
INNER JOIN player ON puzzle_solving_time.player_id = player.id
@@ -71,13 +81,7 @@ public function forPlayer(string $playerId, int $limit): array
7181
])
7282
->fetchAllAssociative();
7383

74-
// TODO: optimize, filter out rows without teams
75-
/** @var array<string> $timeIds */
76-
$timeIds = array_column($data, 'time_id');
77-
78-
$players = $this->getTeamPlayers->byIds($timeIds);
79-
80-
return array_map(static function (array $row) use ($players): SolvedPuzzle {
84+
return array_map(static function (array $row): SolvedPuzzle {
8185
/**
8286
* @var array{
8387
* time_id: string,
@@ -104,11 +108,10 @@ public function forPlayer(string $playerId, int $limit): array
104108
* competition_name: null|string,
105109
* competition_shortcut: null|string,
106110
* competition_slug: null|string,
111+
* players: null|string,
107112
* } $row
108113
*/
109114

110-
$row['players'] = $players[$row['time_id']] ?? null;
111-
112115
return SolvedPuzzle::fromDatabaseRow($row);
113116
}, $data);
114117
}
@@ -143,7 +146,18 @@ public function limit(int $limit): array
143146
competition.id AS competition_id,
144147
competition.shortcut AS competition_shortcut,
145148
competition.name AS competition_name,
146-
competition.slug AS competition_slug
149+
competition.slug AS competition_slug,
150+
CASE WHEN puzzle_solving_time.team IS NOT NULL THEN
151+
(SELECT JSON_AGG(JSON_BUILD_OBJECT(
152+
'player_id', elem.player ->> 'player_id',
153+
'player_name', COALESCE(p.name, elem.player ->> 'player_name'),
154+
'player_code', p.code,
155+
'player_country', p.country,
156+
'is_private', p.is_private
157+
) ORDER BY elem.ordinality)
158+
FROM json_array_elements(puzzle_solving_time.team -> 'puzzlers') WITH ORDINALITY AS elem(player, ordinality)
159+
LEFT JOIN player p ON p.id = (elem.player ->> 'player_id')::UUID)
160+
ELSE NULL END AS players
147161
FROM puzzle_solving_time
148162
INNER JOIN puzzle ON puzzle.id = puzzle_solving_time.puzzle_id
149163
INNER JOIN player ON puzzle_solving_time.player_id = player.id
@@ -160,14 +174,7 @@ public function limit(int $limit): array
160174
])
161175
->fetchAllAssociative();
162176

163-
164-
// TODO: optimize, filter out rows without teams
165-
/** @var array<string> $timeIds */
166-
$timeIds = array_column($data, 'time_id');
167-
168-
$players = $this->getTeamPlayers->byIds($timeIds);
169-
170-
return array_map(static function (array $row) use ($players): SolvedPuzzle {
177+
return array_map(static function (array $row): SolvedPuzzle {
171178
/**
172179
* @var array{
173180
* time_id: string,
@@ -194,11 +201,10 @@ public function limit(int $limit): array
194201
* competition_name: null|string,
195202
* competition_shortcut: null|string,
196203
* competition_slug: null|string,
204+
* players: null|string,
197205
* } $row
198206
*/
199207

200-
$row['players'] = $players[$row['time_id']] ?? null;
201-
202208
return SolvedPuzzle::fromDatabaseRow($row);
203209
}, $data);
204210
}
@@ -257,7 +263,18 @@ public function ofPlayerFavorites(int $limit, string $playerId): array
257263
competition.id AS competition_id,
258264
competition.shortcut AS competition_shortcut,
259265
competition.name AS competition_name,
260-
competition.slug AS competition_slug
266+
competition.slug AS competition_slug,
267+
CASE WHEN pst.team IS NOT NULL THEN
268+
(SELECT JSON_AGG(JSON_BUILD_OBJECT(
269+
'player_id', elem.player ->> 'player_id',
270+
'player_name', COALESCE(p.name, elem.player ->> 'player_name'),
271+
'player_code', p.code,
272+
'player_country', p.country,
273+
'is_private', p.is_private
274+
) ORDER BY elem.ordinality)
275+
FROM json_array_elements(pst.team -> 'puzzlers') WITH ORDINALITY AS elem(player, ordinality)
276+
LEFT JOIN player p ON p.id = (elem.player ->> 'player_id')::UUID)
277+
ELSE NULL END AS players
261278
FROM
262279
filtered_puzzle_solving_time fpt
263280
INNER JOIN puzzle_solving_time pst ON pst.id = fpt.id
@@ -276,13 +293,7 @@ public function ofPlayerFavorites(int $limit, string $playerId): array
276293
])
277294
->fetchAllAssociative();
278295

279-
// TODO: optimize, filter out rows without teams
280-
/** @var array<string> $timeIds */
281-
$timeIds = array_column($data, 'time_id');
282-
283-
$players = $this->getTeamPlayers->byIds($timeIds);
284-
285-
return array_map(static function (array $row) use ($players): SolvedPuzzle {
296+
return array_map(static function (array $row): SolvedPuzzle {
286297
/**
287298
* @var array{
288299
* time_id: string,
@@ -309,11 +320,10 @@ public function ofPlayerFavorites(int $limit, string $playerId): array
309320
* competition_name: null|string,
310321
* competition_shortcut: null|string,
311322
* competition_slug: null|string,
323+
* players: null|string,
312324
* } $row
313325
*/
314326

315-
$row['players'] = $players[$row['time_id']] ?? null;
316-
317327
return SolvedPuzzle::fromDatabaseRow($row);
318328
}, $data);
319329
}

0 commit comments

Comments
 (0)