Oddbean new post about | logout
 This absolute monster. It's not the draft events as I originally thought. It's this:

REQ [{"kinds":[0,10002],"authors":["ede3d957774e5d5c79664b5b50f53170e9024d76c90d99fca9324a3cc9795382"]},{"kinds":[0,30315,10002],"authors":["8e14131498c6cc263b8a78f4639328c825d394bc0ab8cc58d2e297652d6443ba"]},{"kinds":[1984],"#p":["8e14131498c6cc263b8a78f4639328c825d394bc0ab8cc58d2e297652d6443ba"]},{"kinds":[0,30315,10002],"authors":["f7f4e30857d26187e4840d764e94a464a8f8552db8bc507e1a9ae59fb44d67f4","3d5e66f4a87dae4e4e1cd56aa9eb7d4316062bc6a19b2891bfa890e7544bd26c","87e02be9ae3894742a3fedda2e6b33675b642800633ab8c7ac1a306f107ac81c"]},{"kinds":[1984],"#p":["f7f4e30857d26187e4840d764e94a464a8f8552db8bc507e1a9ae59fb44d67f4","3d5e66f4a87dae4e4e1cd56aa9eb7d4316062bc6a19b2891bfa890e7544bd26c","87e02be9ae3894742a3fedda2e6b33675b642800633ab8c7ac1a306f107ac81c"]},{"kinds":[0,30315,10002],"authors":["36fd6d2fbf3c9c95fdf4287b7a473217bd3f36c6a44bf150baa254e589368175"]},{"kinds":[1984],"#p":["36fd6d2fbf3c9c95fdf4287b7a473217bd3f36c6a44bf150baa254e589368175"]},{"kinds":[0,30315,10002],"authors":["b5db2c16e1037cece8fcb4e8591723a6dfc97dec76dbce250a8484ec25730230","d738af7476d45de503e3133ac0d6c909e6db184ad6482b675cc58037b2367d94"]},{"kinds":[1984],"#p":["b5db2c16e1037cece8fcb4e8591723a6dfc97dec76dbce250a8484ec25730230","d738af7476d45de503e3133ac0d6c909e6db184ad6482b675cc58037b2367d94"]},{"kinds":[0,30315,10002],"authors":["7364379f93f1ca4f9d7f3610d9aa609d97e2510778f91bdb062c3708c1344976"]},{"kinds":[1984],"#p":["7364379f93f1ca4f9d7f3610d9aa609d97e2510778f91bdb062c3708c1344976"]}] 
 Whoever made this goes straight to jail. 
 I am also getting a ton like this:

REQ [{"since":1718038930,"until":1718038990}]

fuck you scrapers. Give me your tired, your poor, Your huddled masses yearning to breathe free 
 Yeah, since and until aren't us :)  
 Reject those, specially if the delta is too big. 
 Listen to Cameri 
 Putting a timeout seems to be helping a lot because it's a sweeping fix. That combined with rate-limiting ws messages covers the basics. But I think I need to also punish slow queries similar to rate-limiting, so that if you spend too much query time over a period you get blocked for a short period. 
 In chorus, every connection is IP blocked for 1 second after disconnection.  That is to prevent rapid reconneciton loops.  That block last longer on bad behavior. 
 btw there's no limit on any of these, and they need to be returned in reverse-chronological order. It results in this monster query with 6 joins and 55 parameters. Christ.

select * from (select "nostr_events".* from "nostr_events" where "kind" in ($1, $2) and "pubkey" in ($3) limit $4) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" where "kind" in ($5, $6, $7) and "pubkey" in ($8, $9)) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" inner join "nostr_tags" as "tag0" on "tag0"."event_id" = "nostr_events"."id" where "kind" in ($10) and "tag0"."name" = $11 and "tag0"."value" in ($12, $13) order by "created_at" desc) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" where "kind" in ($14, $15, $16) and "pubkey" in ($17)) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" inner join "nostr_tags" as "tag0" on "tag0"."event_id" = "nostr_events"."id" where "kind" in ($18) and "tag0"."name" = $19 and "tag0"."value" in ($20) order by "created_at" desc) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" where "kind" in ($21, $22, $23) and "pubkey" in ($24, $25, $26)) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" inner join "nostr_tags" as "tag0" on "tag0"."event_id" = "nostr_events"."id" where "kind" in ($27) and "tag0"."name" = $28 and "tag0"."value" in ($29, $30, $31) order by "created_at" desc) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" where "kind" in ($32, $33, $34) and "pubkey" in ($35)) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" inner join "nostr_tags" as "tag0" on "tag0"."event_id" = "nostr_events"."id" where "kind" in ($36) and "tag0"."name" = $37 and "tag0"."value" in ($38) order by "created_at" desc) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" where "kind" in ($39, $40, $41) and "pubkey" in ($42, $43)) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" inner join "nostr_tags" as "tag0" on "tag0"."event_id" = "nostr_events"."id" where "kind" in ($44) and "tag0"."name" = $45 and "tag0"."value" in ($46, $47) order by "created_at" desc) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" where "kind" in ($48, $49, $50) and "pubkey" in ($51)) as "nostr_events" union all select * from (select "nostr_events".* from "nostr_events" inner join "nostr_tags" as "tag0" on "tag0"."event_id" = "nostr_events"."id" where "kind" in ($52) and "tag0"."name" = $53 and "tag0"."value" in ($54) order by "created_at" desc) as "nostr_events" limit $55 
 Looks like some of the 'order by' statements are pre-union. You should only sort after the unions. 
 I will try that and benchmark it. Thank you for the suggestion! 
 I think it would only work if each filter doesn't have a limit. 
 Since this is postgres, can you do an EXPLAIN on that query, or preferably any similarly slow but not-so-hairy query? 
 Also, do you have an multi-column index on (kind, pubkey) as a pair?  It may sound superfluous but it is faster than having two separate indexes (which you still need however).  Because under the hood the data is laid out in such an index like this:

  kind1,author1 -> offsets of events
  kind1,author2 -> offsets of events 
  kind1,author3 -> offsets of events
  kind2, author1 -> offsets of events
  ...

and it can jump to the matching kind and author in one fell swoop without doing a hashjoin (for example). 
 Unless Im reading this wrong (which I could very well be) the performance would likely be increased dramatically if the unions were removed, as this creates a temporary table in postgresql (or atleast it does in MYSQL which im used to) which adds a lot of overhead. I guess this is probably hard to get rid of however, as Im going to assume this is dynamically generated SQL?

Another performance optimization would be using EXISTS in a few of the queries where the INNER JOIN on nostr_tags is used just to filter (most of them) 

Id also suggest if you make the EXISTS change that you create a composite index on (event_id, name, value)

This is my experience with MYSQL though, I know postgres is more efficent in some manners so maybe all of this is automatically done under the hood

If you want me to have a look further into this I would be happy to, but Id probably need to spin up a database (and therefore would need some data) 
 tags to events is a many-to-many relationship, so the joins are necessary. I've never heard of EXIST before 🤔  
 Again, I could very well be reading this wrong, but the joins dont look necassary to me, it could look something like this

SELECT *
FROM nostr_events ne
WHERE (kind IN ($1, $2) AND pubkey IN ($3)) 
	AND EXISTS (
           SELECT 1
           FROM nostr_tags tag0
           WHERE tag0.event_id = ne.id
             AND tag0.name = $4
             AND tag0.value IN ($5, $6)
     ))

(this isnt directly taken from your query below, this is just an example)

This is also why i recommended the use of the composite index (event_id, name, value), which I would image would give a huge performance boost on that EXISTS check, but without being able to test it (and make sure it returns the correct results) I could be talking out of my ass 
 select [...] where exists (select 1 from blah where cond); 
 This looks like Amethyst. When then user scrolls it adds and removes authors to the sub. But is the issue that there is no event in your DB for those authors? Otherwise, it would have a `since` based on the previous EOSE. 

Do you return EOSE when nothing matches the filter? 
 I do return EOSE. But now if the query times out, I just return CLOSE. 
 I'm questioning if multiple filters in a REQ was ever a good idea. I treat a single REQ as one database query. I could break them up and treat them like separate requests (but so could you). I'm not sure whose responsibility that is. If I do, I will need to rate-limit the number per interval of filters instead of the number of websocket messages. 
 Let me try to get to the same filter. Amethyst is supposed to group those into bigger filters. But I am wondering if there is a bug when since is not present in these filters. 

What's the relay url to test this out?  
 wss://gleasonator.dev/relay 
 Yep, this one. Thank you Mr. Patrick! 🍀  
 Ok I am getting `error: something went wrong` on that filter. Is that the timeout error?  
 There is something weird going on. Your relay doesn't like the kind 1984 with only the ptag filter. 

This simple one takes forever. 
```
[
   {
      "kinds":[
         1984
      ],
      "#p":[
         "99bb5591c9116600f845107d31f9b59e2f7c7e09a1ff802e84f1d43da557ca64",
         "8867bed93e89c93d0d8ac98b2443c5554799edb9190346946b12e03f13664450"
      ]
   }
]``` 
 Actually, just one p-tag runs into the same error: 


``` 
[ "REQ", "aa2dd",
   {
      "#p":[
         "8867bed93e89c93d0d8ac98b2443c5554799edb9190346946b12e03f13664450"
      ]
   }
]```

```
[
  "CLOSED",
  "aa2dd",
  "error: something went wrong"
]
```
 
 I bet there is a record between this since and until that you cannot reassemble to send back: 

[ "REQ", "aa2dd",
   {
      "since": 1719870040,
      "until": 1719870100,
      "#p":[
         "8867bed93e89c93d0d8ac98b2443c5554799edb9190346946b12e03f13664450"
      ]
   }
] 
 I increased the timeout a lot and it still gets cancelled, yikes. I can't get into postgres to "explain analyze" from this hotel wifi. Will try to see later. Thanks for your help. 
 Hi Vitor, is this just a sample request or there is indeed at least 1 event that matches this tags and kinds? I have a relay called "wss://slime.church/relay" and it's the same code as "wss://gleasonator.dev/relay" (both run ditto).

However, I couldn't reproduce this issue with slime.church, the request is fast and the response is empty. 
 I don't know if there is an event. But the filter bug is real. It doesn't work even if I ask through Websocket testing sites. 
 Got it, I just don't understand why this doesn't happen with wss://slime.church/relay, perhaps there's a specific configuration in wss://gleasonator.dev/relay that is messing things up, I'll take a look, thanks 
 It's the amount of data that's the problem. Because with ORDER BY, it has to go through everything to figure out which is the most recent before it can pull even 1 result. 
 Ok well that suprises me a lot. When it comes to further performance optimisations the only thing throwing a curveball is the LIMIT on some of the subqueries, otherwise this could easily become one or two queries (reducing the overall performance overhead of the UNION, which runs as an individual query, creates a temporary table,  has to be sorted (in some cases), and then remerged.

An example of what I mean is

SELECT *
FROM (
    SELECT *
    FROM nostr_events
    WHERE kind IN ($5, $6, $7) 
      AND pubkey IN ($8, $9)
) AS nostr_events

UNION ALL

SELECT *
FROM (
    SELECT *
    FROM nostr_events
    WHERE kind IN ($14, $15, $16)
      AND pubkey IN ($17)
) AS nostr_events

UNION ALL

SELECT *
FROM (
    SELECT *
    FROM nostr_events
    WHERE kind IN ($21, $22, $23)
      AND pubkey IN ($24, $25, $26)
) AS nostr_events;

Becomes

SELECT nostr_events.*
FROM nostr_events
WHERE 
    (kind IN ($5, $6, $7) AND pubkey IN ($8, $9))
    OR (kind IN ($14, $15, $16) AND pubkey IN ($17))
    OR (kind IN ($21, $22, $23) AND pubkey IN ($24, $25, $26))


OR 

SELECT * 
FROM (
    SELECT * 
    FROM nostr_events 
    WHERE kind IN ($1, $2) 
      AND pubkey IN ($3) 
    LIMIT $4
) AS nostr_events

UNION ALL

SELECT * 
FROM (
    SELECT * 
    FROM nostr_events 
    WHERE kind IN ($5, $6, $7) 
      AND pubkey IN ($8, $9)
) AS nostr_events

can easily become 

SELECT * 
FROM nostr_events 
WHERE (kind IN ($1, $2) AND pubkey IN ($3))
   OR (kind IN ($5, $6, $7) AND pubkey IN ($8, $9))
LIMIT $4

if that limit is not required.

Infact all 6 of those queries could be merged into one, but it would likely return a slightly different result set due to the exclusion of LIMIT, but a lot of the other queries do not have a limit and would be easier to do than others

Please check my SQL btw, it may not return the correct result set, or it may just be straight up incorrect, it is 1.30am here.