SQL Server: High performance inserts
altdevblogaday.com
altdevblogaday.com
First up, the non-SQL-injection code doesn't specify the parameter types. As such, each length of monster_class will result in a different execution plan being compiled and cached by SQL Server. Granted, there's only 50 different kinds - but just specify the parameter types to avoid it.
Sure, you could setup a VLAN, but we're lightyears from having that affect our performance at this point.
Parser overhead is not an issue as long as parameter types are specified. The plan will be compiled and cached. The only advantage to stored procedures at this point is reduced network data as well as management.
Page faults - Sure, but for inserts the data will most likely be in memory already as we're writing new data. And if there's enough data, the relevant b-tree pages will most certainly be hot.
Varchars - Granted, avoid them unless you're storing string data. I have no idea where the "hundreds of bytes of of memory" comes from. SQL Server caches all data as pages, exact copies of what's stored on disk. Variable-length varchars have an overhead of exactly two bytes + 2/#varlengthCOlumns in total.
Disk IO - The transaction log is never merged with the main data file. SQL Server uses write ahead logging, writing to the log first while modifying the data pages in memory. Now and then a checkpoint is run, writing all the dirty pages to the main data file. A log is only "merged" with the data file if the server has crashed and recovery is being run automatically during startup.
"This increases the number of rows you can insert before SQL Server succumbs to memory pressure" - Since the article only deals with inserts, why are we talking about memory pressure? You'll get disk contention for sure, but memory won't realistically be an issue unless you're reading as well.
There's an alternative to keeping the potentially long running transaction open - avoiding both SqlBulkCopy and transferring datatables. The BCL has a CommandSet notion that utilitizes a TDS feature that allows batches to be streamed to SQL Server. Unfortunately you'll have to use reflection to get at it (https://github.com/ayende/rhino-etl/blob/master/Rhino.Etl.Co...), but the code has been stable for several releases and is very simple to interface. It basically allows you to create normal SqlCommands and just send a large batch of them at once, all being executed atomically with minimal locking.
Finally, with the micro optimizations being mentioned, I'm surprised he doesn't recommend to "SET NOCOUNT OFF".
Overall, some good points with some inaccuracies. I have a hard time with the closing statement "As you can see, you don’t need to abandon SQL Server to get massive performance improvements.". These are decent improvements, but this is such a simple scenario that I find it pretty optimistic to let this define whether to abandon SQL Server or not. The RDBMS problem is rarely with inserting enough data, but with reading data at the same time while avoiding locking.
That is not the case if you have indexes. Most secondary indexes are high-entropy (if they weren't, you probably wouldn't be storing them, is an emotional proof), so inserts on larger-than-memory data sets almost always incur a disk I/O, in a b-tree, at least.
Shameless plug: I have a talk about how to deal with this: http://www.youtube.com/watch?v=q6BnG74FZMQ
The mdf pages will get marked "dirty" at the time of insert, and they will get flushed to disk eventually - either when checkpint time comes, or if memory gets scarce and the dirty pages get evicted.
It might seem that postponing the write until checkpoint/eviction is not meaningful, but it actually serves a purpose - for one, the transaction is able to finish and return confirmation to the user very quickly, and then if the same page is touched twice (or more) between two flushes, all but the first touches are "free".
1. All data fits in RAM 2. Active data fits in RAM, but inactive data does not 3. Active data does not fit in RAM
Case #1 is trivial, though still widely used for e.g. web sites.
Case #2 is where the relational databases have their sweet spot. For example you could have a 200Gb database with all customers and history of orders, but only a subset of those customers are placing orders at any one time, and all the new orders are stored close to each other, so only 8Gb of data is hot, and the rest is accessed occasionally. Assuming you have more than 8Gb of RAM, there will be no IO at the time of the insert, there will be some sequential IO in log file at the end of the transaction, and there will be some "random" IO come at checkpoint time. The latter will not be totally random as SQL Server will reorder all delayed writes in the most sequential manner possible to reduce the seeks (hence there is a good reason to space out checkpoints). The log flush is also sometimes shared between several adjacent transactions if they end up committing at the same time, so under load you can get less than one IO per transactions.
Case #3 is the one you refer to. In this case each btree update will push out some other old dirty page to disk, causing IO elsewhere in the database.
Now what you are saying in this comment is that because there exists case #3, there is no case #2. I think you severely underestimate the importance of the case #2.
I recommend a copy of "Inside SQL Server" by Kalen Delaney to learn more about SQL Server.
First up, in his example we're talking about 10k rows with a size of 35 bytes. At 8050 bytes of available space per page, that gives a fan-out of 8050/35 = 230. 10k/230*8kb = 350kb. At 350kb of inserts (hobt data, he doesn't go into secondary indexes), memory is completely irrelevant - the only force going on here is latency on writing the log to disk.
If we had a huge data set (as in, did not fit in memory, at all) with high cardinality - sure, we'd have a lot of cold leaf level pages. With no further info on his case, I can only assume most of the hobt and secondary indexes will fit in memory. At worst we'll have to read a cold leaf level page into memory to perform the addition in-memory.
As is there's no mention of even a clustered index, causing all of this to be heap inserts which is arguably one of the fastest insert methods there are (barring certain very special cases).
If you have a large enough data set that you have to do a leaf read per insert (or close to that often), it will kill your throughput.
I have to give it to the author though for a genuine attempt at one of the important things when dealing with databases. There is lot of room for performance improvement in application code, before you start blaming the platform for not scaling.
In regards to your last sentence, he could change the isolation mode if he didn't care so much about consistency.
One warning about this is that at times you should consider having a new plan generated - especially if the data changes over time.
In my experience, it tends to pull ahead of table-valued parameters somewhere between 5 and 10,000 rows. On the other hand, the way it does locking makes it less suitable for concurrent scenarios.
[1]http://msdn.microsoft.com/en-us/library/system.data.sqlclien...
Also, unless I'm mistaken, if you want to, you can enroll a bulk copy into a custom transaction for maximum control and essentially replicate the table-valued parameter behavior.
Although, in practice, when importing complex data in real-time there's a lot of experimentation required to determine where the real bottlenecks are and this article skims the surface of the complexity involved in a real import.
For example, do you do your lookups inside your stored procedure or does your application layer deal with all of that and send off pre-prepared data? If the latter, you reduce the amount of time the table is locked whilst adding rows since there are no joins and lookups in the insert, and this may be more performant overall than the alternative.
You can also reduce the duration of any transaction lock you may need to hold for importing all the data which ultimately makes the system more responsive during large imports. Furthermore the "prepare-first" approach makes error reporting more practical since it's generally easier to do and report validation in the application layer.
-- No longer has a DEFAULT constraint; this means we have to insert the date
-- ourselves, but it reduces the work on SQL server
kill_time datetime
My gut reaction is that this is unnecessary micro-optimization. Does making SQL server come up with the date and time really make that big a performance difference?I suspect it's mostly just necessary to permit the delay between data generation and storage.
May still be unnecessary, but seems like a fair optimization.
And, the need to define your UDTs is a bit of a pain. But to prevent a glut of UDTs in the db, I've tried to make common UDTs that would commonly be used, such as
CREATE TYPE [dbo].[udt_IntTable] AS TABLE(
[Item_Key] [int] NULL
)
One more point: it's probably best practice to use the TypeName property when using these, as it's required when using a UDT against a parameterized statement that's not a sproc. param.TypeName = "item_drop_bulk_table_rev4";My guess would it's because inserting into table variables isn't logged; doubt it has anything to do with constraints. This turns all those individual statements into a single bulk insert, a single larger log operation instead of many small ones.
Do you have a source to back your statement up? That way I could stop looking for mine :)
SQL is quickly becoming a legacy product.