Stuck at optimizing "not in clause" for a medium-sized query across two tables
Consider the database that you're querying against. It contains a data model that someone designed. It likely contains indexes that someone added to improve query performance. It contains statistics maintained by the engine to improve query performance. You effectively throw all of that away when you join table variables to each other. SQL Server doesn't allow for statistics on table variables and those queries won't benefit from all of the work that went into improving your data model. The best use cases for table variables are to retain data after a transaction is rolled back or to avoid unnecessary recompiles in stored procedures that are executed hundreds of times per second or more.

Fundamentally, comparing 15000 rows to 2000 rows can be a very fast operation in SQL Server as long as you avoid fighting with the database. I mocked up some data similar to the quantities mentioned in the question:

CREATE TABLE dbo.Words (
    Id INT IDENTITY (1, 1),
    [Text] NVARCHAR(100),
    PRIMARY KEY (Id)
);

INSERT INTO dbo.Words WITH (TABLOCK)
SELECT DISTINCT LEFT(Text, 25)
FROM sys.messages;
-- (203013 rows affected)

CREATE TABLE dbo.LearnedWords (
    UserId INT,
    WordId INT
);

CREATE CLUSTERED INDEX CI ON dbo.LearnedWords (UserId);

INSERT INTO dbo.LearnedWords WITH (TABLOCK)
SELECT 1, Id
FROM dbo.Words
WHERE ID % 100 &lt;= 7;
-- (16247 rows affected)


Suppose that you were able to create a table that contains the distinct words used in each "text". I used the following:

CREATE TABLE dbo.TextWords (
    TextId INT,
    WordId INT
);

CREATE CLUSTERED INDEX CI ON dbo.TextWords (TextId);

INSERT INTO dbo.TextWords WITH (TABLOCK)
SELECT 1, Id
FROM dbo.Words
WHERE ID % 1000 &lt;= 9;
-- (2039 rows affected)


Finding new words can now be expressed with a single SELECT query without any table variables:

select w.[Text]
from TextWords t
INNER JOIN dbo.Words w ON t.WordId = w.Id
Where t.TextId = 1
AND NOT EXISTS (
    SELECT 1
    FROM LearnedWords l
    WHERE t.WordId = l.WordId
    AND l.UserId = 1
);


That query finishes in less than 20 ms on my machine. If I run your original code it takes 3800 ms in total. If you need to use the table variables then at the very least try adding a RECOMPILE hint to the final query. That drops the execution time to 1600 ms on my machine.

With the right data model you can meet your business requirements in a simple and efficient way. There's a bit of ambiguity in your statement about texts. Perhaps you aren't able to store unique words for each text in the database ahead of time. In that case the time taken for your procedure should entirely depend on how much time it takes to populate a temporary structure with the unique words used in that text. You don't have that code posted here so it's impossible for us to help you with that.