Programming language used to interact with SQL Server databases
For a 20 GB database, I would not bother very much about fragmentation. Overall, fragmentation is a bit of a thing of the past. Particularly, the type of fragmentation where pages that are logically adjacent in an index are not physically adjacent on disk. This mattered a lot with spinning disks. Far less so with SSDs. Fragmentation in the sense that pages may only be half-full still matters. But again, for this small database, no I don't think is where you should put your focus. It may actually not matter at all.
You say that there are stored procedures that call other stored procedures. I get an unpleasant feeling of that this is code that is running one-row-at-a-time processing in loops. The best way is to rewrite the code into set-based operation, but that can be a major undertaking. When you run loops, indexes become even more crucial, also on smaller tables. A thing I've seen more than once is people who runs a poor man's cursor, by looping over a temp table like this:
WHILE EXISTS (SELECT * FROM #tmp WHERE done = 0)
BEGIN
SELECT TOP(1) @id = id, ...
FROM #tmp
WHERE done = 0
ORDER BY id
-- Do stuff
UPDATE #tmp
SET done = 1
WHERE id = @id
END
And the temp is entirely un-indexed, so all these operations require scans. It just takes a couple of thousands of rows to make this very costly.
Assuming that your code is running loops, a very good tool to troubleshoot this is sp_sqltrace, originally written by Lee Tudor, and which I am happy to host on my web site. You can simply say:
EXEC sp_sqltrace 'EXEC slow_sp', @order = 'Duration'
And it will start a trace, filtered for your spid, capture sp_statement_completed. When slow_sp has completed, it will analyse the trace, and if the same statement is executed multiple times, the data for those executions will be aggregated into a single row. The statement(s) that gets to the top is one you should look into. Beware that if the procedure runs for more than five minutes, you need to adjust the @trace_timeout parameter, because by default the trace stops after five minutes.
Then again, maybe you don't need the data from the full execution. You can also say:
EXEC sp_sqltrace 77, @order = 'Duration'
sp_sqltrace will now set up a trace filtered for spid 77 (or the spid where the slow procedure is running), and run that trace for 10 seconds (adjustable with the parameter @snoop_time) and then perform the aggregation.
You can also use sp_sqltrace to capture execution plans, would that be needed.
Even if I have given you an introduction and some examples, I still recommend that you read the manual page before you start playing.