So if you can deal with some conversion on the application side:
DECLARE @ZoneCounts TABLE
(
Zone nvarchar(max),
sessionId int
)
INSERT INTO @ZoneCounts
values
('Zone 1', 1),
('Zone 2', 1),
('Zone 3', 1),
('Zone 1', 2),
('Zone 2', 2)
select v3.down, v3.across, convert(decimal(13,2),(CONVERT(decimal(13,2), v3.matchingSessions) / v3.sessionsVisitingDown) * 100) as percentage
from
(SELECT v2.down, v2.across, v2. matchingSessions, max(v2.matchingSessions) over (partition by v2.down) as sessionsVisitingDown
FROM (select down, across, matchingSessions from
(select lhs.Zone as down, rhs.Zone as across, count(lhs.sessionId) over (partition by lhs.Zone, rhs.Zone) as matchingSessions
from @ZoneCounts as lhs inner join @ZoneCounts as rhs on lhs.sessionId = rhs.sessionId) AS v1
GROUP BY down, across, matchingSessions) AS v2)
as v3
gives you
NB - uses MS SQL but should convert
Ah, Postgres isn't quite the same
SELECT v3.down, v3.across, cast(cast(v3.matchingSessions as decimal) / cast(v3.sessionsVisitingDown as decimal) * 100 as decimal(13,2)) AS percentage
FROM
(SELECT v2.down, v2.across, v2. matchingSessions, max(v2.matchingSessions) over (partition BY v2.down) AS sessionsVisitingDown
FROM (SELECT down, across, matchingSessions FROM
(SELECT lhs.name AS down, rhs.name AS across, count(lhs.id) over (partition BY lhs.name, rhs.name) AS matchingSessions
FROM ItemList AS lhs INNER JOIN ItemList AS rhs ON lhs.id = rhs.id) AS v1
GROUP BY down, across, matchingSessions) AS v2)
AS v3
here's a SQL fiddle: http://sqlfiddle.com/#!15/f379c/13/0 using PostgreSQL