As a fan of the Valorant Champions Tour (VCT), I wanted to combine my interest in esports with my developing data skills by exploring the 2026 VCT season through SQL.
I wanted to find a dataset that would allow me to ask meaningful questions about the events, matches and player performances, and then use SQL to find the answers within the data. While there are several websites containing VCT statistics, I struggled to find a ready-made dataset that I could query directly.
To solve this, with the help of Claude AI, I used Python to scrape the data I needed from VLR.gg, a website that contains a comprehensive collection of VCT event, match and player statistics.
Once the data had been collected, I cleaned and restructured it into a relational database schema, creating the relationships needed to connect events, matches, teams and player statistics. I then uploaded the resulting database to Supabase, where it was ready for analysis using SQL.
This project gave me the opportunity to combine my interest in VCT with practical experience in Python, web scraping, data modelling, SQL and relational databases, while working through the process of turning raw web data into a structured dataset that could be used to answer real questions.
As my Python knowledge is not yet at a level where I could code a web scraper myself, I used Claude AI to help me with this part of the project. I started by giving it a prompt to scrape a single matchup and return the data for each map, including the player statistics.
It took a few attempts to get the code to return the correct data, but once it was working as expected, I then adapted it to scrape a full event, returning each matchup, map and player statistics.
After checking that the results were accurate and matched the data on the website, I then moved on to scraping all of the remaining events from the 2026 season.
The raw data collected by the web scraper was initially split across four tables: events, maps, matches and player_stats. Before importing the data into Supabase, these tables needed to be cleaned and, in some cases, split into additional tables to create a more efficient relational model.
For example, the matches table contained both the event_id and event_name, while the player_stats table contained the player, player_id, team and team_tag. This resulted in the same information being repeated across multiple tables, something I wanted to minimise when designing the database.
I therefore spent some time designing a database schema, considering how the different pieces of data should relate to each other. Once I was happy with the structure, I reorganised the data into tables that matched the schema. This allowed me to create relationships between the tables using unique IDs, resulting in a more organised and efficient database that was ready to be imported into Supabase and queried using SQL.
Now that the structure has been finalised, my next step was to create the tables in Supabase using PostgreSQL. Below are some samples of the SQL code used. As you may see, there are no relationships being created here. The reason being that I simply forgot to create them at the time of creating the tables. However, I used the built-in **Add Foreign Keys** feature within Supabase to add the relationships afterwards.
CREATE TABLE players (
player_id INT PRIMARY KEY
handle TEXT,
country TEXT
);
CREATE TABLE player_map_stats (
id INT PRIMARY KEY,
match_id INT,
map_number INT,
player_id INT,
team_id INT,
agent_primary TEXT,
agents_all TEXT,
rating FLOAT,
acs FLOAT,
kills INT,
deaths INT,
assists INT,
plus_minus INT,
kast_pct FLOAT,
adr FLOAT,
hs_pct FLOAT,
fk INT,
fd INT,
fk_fd_diff INT
);
CREATE TABLE matches (
id INT PRIMARY KEY,
match_id INT,
event_id INT,
stage TEXT,
region TEXT,
round_name TEXT,
sub_event TEXT,
match_date DATE,
match_time TEXT,
team1_id INT,
team2_id INT,
score1 INT,
score2 INT,
winner_team_id INT,
best_of INT,
status TEXT
);
With the data cleaned and restructured, and the database created, the next step was to import the data into the database so it was ready to be queried. As Supabase provides a quick and simple import tool that allows data to be imported via CSV, I converted the restructured tables into CSV files and used this tool to import them. I then used some simple SQL to ensure the data was imported correctly.
SELECT *
FROM players;
SELECT *
FROM player_map_stats;
SELECT *
FROM matches;
With the above imported data returning as expected, i wrote one final SQL query to check if the relationships were working by asking myself a question regarding the data and using SQL to find the answer.
Question: Show me the top 5 teams by win rate, how many games they have played, and how many they have won.
SELECT
team_name,
COUNT(*) AS games_played,
SUM(CASE WHEN team_id = winner_team_id THEN 1 ELSE 0 END) AS wins,
ROUND(
100.0 * SUM(CASE WHEN team_id = winner_team_id THEN 1 ELSE 0 END) / COUNT(*),
2
) AS win_rate
FROM (
SELECT
m.team1_id AS team_id,
t.name AS team_name,
m.winner_team_id
FROM matches m
JOIN teams t ON m.team1_id = t.team_id
UNION ALL
SELECT
m.team2_id AS team_id,
t.name AS team_name,
m.winner_team_id
FROM matches m
JOIN teams t ON m.team2_id = t.team_id
) AS games
GROUP BY team_name
ORDER BY win_rate DESC
limit 5;
Result
Now that I have confirmed everything is setup and running correctly, my next steps are to further analyse the data and create visualisations for topics such as;
Player performance
Team performance
Full event analysis