Divide each row by a windowed SUM. An empty OVER () is the grand total; OVER (PARTITION BY ...) is the group total. Same syntax in every dialect.
SELECT
category,
amount,
amount * 100.0 / SUM(amount) OVER () AS pct_of_grand_total,
amount * 100.0 / SUM(amount) OVER (PARTITION BY category) AS pct_of_category
FROM sales;
Integer-division trap. In PostgreSQL, MySQL and SQLite,
amount / SUM(...)with two integer columns does integer division and returns 0 for every fraction. Multiply by100.0(or cast one side to a decimal/float) before dividing, as above. BigQuery, Snowflake and DuckDB divide as floats by default, but* 100.0is harmless and keeps the query portable.
Round it for display:
SELECT
category, amount,
ROUND(amount * 100.0 / SUM(amount) OVER (), 1) AS pct
FROM sales;
Works in MySQL 8+, PostgreSQL, SQLite 3.25+, BigQuery, Snowflake and DuckDB.