@Serg solution is very good and should be the accepted one, here is another one with simple SQL statements:
WITH aggr_new as (
SELECT `new_position`, COUNT(*) as pos_in
FROM `positions`
GROUP BY `new_position`
),
aggr_old as (
SELECT `old_position`, COUNT(*) as pos_out
FROM `positions`
GROUP BY `old_position`
)
SELECT IFNULL(t.`new_position`, t.`old_position`) as `position`, IFNULL(t.pos_in, 0) pos_in, IFNULL(t.pos_out, 0) pos_out
FROM (
SELECT *
FROM aggr_new n
LEFT OUTER JOIN aggr_old o ON n.`new_position` = o.`old_position`
UNION
SELECT *
FROM aggr_new n
RIGHT OUTER JOIN aggr_old o ON n.`new_position` = o.`old_position`
) t
The first 2 CTE are based on your initial idea to count the positions separately, but then you can join them with a FULL OUTER JOIN (that is emulated with a UNION of left and right joins). In this way, you are sure that all the positions are present. NULL values are replaced with 0 for pos_in and pos_out when not present in the full join.
Tested on:
CREATE TABLE IF NOT EXISTS `positions` (
`id` int(6) unsigned NOT NULL,
`new_position` char(1) NOT NULL,
`old_position` char(1) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `positions` (`id`, `new_position`, `old_position`) VALUES
(1, 'A', 'B'),
(2, 'B', 'C'),
(3, 'A', 'C'),
(4, 'C', 'B'),
(5, 'A', 'B');