Here is a more generalized approach to binning in SQL:
SELECT
concat(
binsize * floor(diff / binsize),
' - ',
binsize * floor(diff / binsize) + binsize - 1
) as range,
count(*) as number_of_rows
FROM
new_table,
(
SELECT
21 as binsize
FROM dual
) as prm
GROUP BY 1
ORDER BY floor(diff / binsize)
This way, you only have to provide the size of your range (called bins) once, in the sub query from dual.
The sub query returns a table of size 1 in both dimensions, a single row with a single column. This table is cross tabulated with each row of the other table, so its value is accessible in each row of the first table. This works without specifying a join condition.
As long as you only return a single row, you can add parameters to your sub query. For example, you can define upper and lower bounds to exclude certain features from your result this way:
SELECT
concat(
binsize * floor(diff / binsize),
' - ',
binsize * floor(diff / binsize) + binsize - 1
) as range,
count(*) as number_of_rows
FROM
new_table,
(
SELECT
21 as binsize,
21 as above,
83 as below
FROM dual
) as prm
WHERE
diff >= above
AND diff <= below
GROUP BY 1
ORDER BY floor(diff / binsize)
If your RDBMS supports it, consider restructuring your query to a CTE (Common Table Expression), which helps in making the expression look neater and more tidy by putting the declaration of parameters right to the start of the whole statement:
WITH prms as (
SELECT
21 as binsize,
21 as above,
83 as below
FROM dual
)
SELECT
concat(
binsize * floor(diff / binsize),
' - ',
binsize * floor(diff / binsize) + binsize - 1
) as range,
count(*) as number_of_rows
FROM
new_table, prms
WHERE
diff >= above
AND diff <= below
GROUP BY 1
ORDER BY floor(diff / binsize)