Updated:

1. Build the track

Boats are much faster on ice, and blue ice is faster than packed ice. Use blue ice on straights and packed ice on corners.

/fill ~ ~-1 ~ ~20 ~-1 ~4 blue_ice

Lay long tracks in several parts because fill has a block limit. See the fill command.

2. Setup (once)

/scoreboard objectives add cp dummy
/scoreboard objectives add lap dummy "Lap"
/scoreboard objectives add time dummy
/scoreboard objectives setdisplay sidebar lap
/scoreboard players set #20 time 20

3. Place checkpoint markers

/summon marker ~ ~ ~ {Tags:["cp1"]}
/summon marker ~ ~ ~ {Tags:["cp2"]}
/summon marker ~ ~ ~ {Tags:["goal"]}

4. Start signal

/scoreboard players set @a cp 0
/scoreboard players set @a lap 0
/scoreboard players set @a time 0
/tag @a add racing
/title @a title {"text":"GO!","color":"green"}

Scores must be set to 0 first, or scores={cp=0} will not match. Hand out boats with /give @a oak_boat.

5. Checkpoints in order (repeating)

/execute at @e[type=marker,tag=cp1] as @a[distance=..4,scores={cp=0}] run scoreboard players set @s cp 1
/execute at @e[type=marker,tag=cp2] as @a[distance=..4,scores={cp=1}] run scoreboard players set @s cp 2
/execute at @e[type=marker,tag=goal] as @a[distance=..4,scores={cp=2}] run scoreboard players add @s lap 1
/execute at @e[type=marker,tag=goal] as @a[distance=..4,scores={cp=2}] run scoreboard players set @s cp 0

6. Timer (repeating)

/scoreboard players add @a[tag=racing] time 1

This counts ticks, 20 per second, and stops when a player finishes.

7. Finish (repeating)

/tag @a[tag=racing,scores={lap=3..}] add goal_now
/tag @a[tag=goal_now] remove racing
/execute as @a[tag=goal_now] run scoreboard players operation @s time /= #20 time
/execute as @a[tag=goal_now] run tellraw @a [{"selector":"@s"},{"text":" finished in "},{"score":{"name":"@s","objective":"time"}},{"text":" s"}]
/tag @a[tag=goal_now] remove goal_now

Troubleshooting

Check your command

The Command Error Doctor checks common syntax mistakes in your browser.