I highly recommend collecting results from your OPTIONAL MATCHES as needed, and using WITH to break up your queries and narrow down the columns you're interested in to keep your rows down between your subqueries. As explained in the question comments, MATCHES and OPTIONAL MATCHES can build up result rows, which can make queries that SEEM like they should be quick costlier.
For example, I will add comments to analyze the original query inline:
MATCH (u:User)-[:CAN_ADMINISTER]->(cs:CustomerSite)
WHERE u.id="1234" WITH cs
MATCH r1=(cs)<-[:AFFECTS_SITE]-(t:Ticket)
WHERE not(t.status = "COMPLETE")
// we have 1 row per User at a CustomerSite
OPTIONAL MATCH r2=(t)-[:HAS_EVENTS]->(te:TicketEvent)
// now, 1 row per User @ CustomerSite per TicketEvent
OPTIONAL MATCH r3=(t)-[:CREATED_BY]->(u:User)
// the above OPTIONAL MATCH had to iterate over each User/CS/TE row instead of just each distinct TICKET
OPTIONAL MATCH r4=(te)<-[:HAS_EVENTS]-(u2:User)
// now, 1 row per User @ CustomerSite per User on each Ticket Event
OPTIONAL MATCH r5=(t)-[:AFFECTS_SITE]->(cs)<-[:HAS_SITE]-(c:Customer)
// now, 1 row per User @ CustomerSite per User on each Ticket Event per Customer at each Customer Site
RETURN r1, r2, r3, r4, r5
While it changes the format of the returned data, doing COLLECTS along the way, and ordering your OPTIONAL MATCHES better, should improve query speed. Here is one way you could do this:
MATCH (u:User)-[:CAN_ADMINISTER]->(cs:CustomerSite)
WHERE u.id="1234" WITH cs
MATCH (cs)<-[:AFFECTS_SITE]-(t:Ticket)
WHERE not(t.status = "COMPLETE")
// should be 1 creator per ticket, so best to do this first
OPTIONAL MATCH (t)-[:CREATED_BY]->(creator:User)
OPTIONAL MATCH (cs)<-[:HAS_SITE]-(affectedCustomer:Customer)
// collection of affected customers for each ticket (and their creator) affecting a customer site
WITH cs, t, creator, COLLECT(affectedCustomer) as affectedCustomers
OPTIONAL MATCH (t)-[:HAS_EVENTS]->(te:TicketEvent)<-[:HAS_EVENTS]-(userOnEvent:User)
WITH cs, t, creator, affectedCustomers, te, COLLECT(userOnEvent) as usersOnEvent
RETURN cs, t, creator, affectedCustomers, COLLECT({ticketEvent:te, usersOnEvent:usersOnEvent}) as ticketEventsAndUsers
Each row will correspond with a ticket at a customer site, the ticket's creator, the collection of affected customers at the site, and a collection of ticket events for the ticket and for the event, the users on that event.
Give that a try and see how the performance compares. If it looks better, you'll have to change how you parse through the returned data, but it's nothing a little for loop or two can't handle.