A string table is a grouping of const string references, so it’s exactly the size it needs to be to contain all of the characters in the strings. Additionally it’s really meant for localization containing only the component parts of FText (Namespace, Key, localized string value), not just being a bunch of game-ready strings. It’s a lookup for the game itself to know what to display to the end user based on the language settings.
Data Tables on the other hand have to be of at least a certain size based on the data types used regardless of the data that’s actually saved in it. More data does expand the size of certain types, but a float in a struct in a data table will always take up 4 bytes, while that number represented as a string can take up as little as 1 byte (if the representation is a single digit). That said, the memory overhead for a BP struct is higher than the overhead of FText even with all its component parts considered.
When these tables are in CSV form, they’re just text and nothing more. When they come into the engine they’re made into discrete types and the memory usage depends entirely on the data types used. One float would be 4 bytes in size (plus struct overhead) per row while a string table would expect three strings (Namespace, Key, localized string value), each string likely longer than a single character and easily far surpassing 4 bytes.
What it all comes down to is that you shouldn’t worry about this kind of memory management and just use the table that applies to your use case. If you’re doing localization, use a string table. If you need any other kind of data ingression from CSV, use Data Table. Even if the data is all in string format, it’s better to be able to define what that text is for your use case rather than trying to fit it all into the component parts of FText.
If I may plug my uh, plugin, Runtime DataTable works in a manner similar to DataTable but allows you to import csv at runtime. You can update data to structs (like regular DataTable) or directly on UObjects themselves completely bypassing the need to create structs. If you do the latter, you also avoid the memory usage incurred by creating a struct per row; you simply change the values of variables on the objects you define. Not that you should worry about that memory in 99% of cases, but it’s something to consider if you’re working with very large data sets.