<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Conner Phillis</title>
    <description>The latest articles on Forem by Conner Phillis (@connerphillis).</description>
    <link>https://forem.com/connerphillis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1051657%2Fb017e3b2-1cbf-47e2-9adc-690d1c3d3927.jpeg</url>
      <title>Forem: Conner Phillis</title>
      <link>https://forem.com/connerphillis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/connerphillis"/>
    <language>en</language>
    <item>
      <title>No Indexes, No Parameters, No Problem</title>
      <dc:creator>Conner Phillis</dc:creator>
      <pubDate>Tue, 07 Apr 2026 20:56:16 +0000</pubDate>
      <link>https://forem.com/connerphillis/no-indexes-no-parameters-no-problem-73k</link>
      <guid>https://forem.com/connerphillis/no-indexes-no-parameters-no-problem-73k</guid>
      <description>&lt;p&gt;This story is from an organization I worked at a while back. A team had recently lost &lt;em&gt;all&lt;/em&gt; of its developers after they voluntarily left the company. Big red flag - but I was young and eager to prove myself, so I volunteered to take it on. The app had a multitude of issues: poor separation of concerns, copy-pasted code everywhere, awful performance, and outdated tech&lt;/p&gt;

&lt;p&gt;I could probably write a whole series of posts on that application - it ended up costing me quite a bit of hair - but I wanted to talk about one of the craziest things I found.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bug
&lt;/h3&gt;

&lt;p&gt;The first order of business was to fix the bugs that were preventing us from moving forward with customers that were using the software. There was a pretty standard create request that would fail somewhere inside of a 9,000 line function (yes... really...) for an unknown reason and customers were losing data. It had to be fixed.&lt;/p&gt;

&lt;p&gt;The debugging tooling was terrible. The server was at the company's data center with no remote debugger access, so I had to build locally, drop DLLs on the machine loaded with logger statements, and read the output to figure out what went wrong. To make matters worse, the API always returned a 200 after swallowing the exception regardless of whether it succeeded or failed. Unfortunately, the fastest way to verify my changes was to query the database directly and check if the row inserted.&lt;/p&gt;

&lt;p&gt;The primary key for the newly inserted record was something like &lt;code&gt;205&lt;/code&gt;, while the one previously was something like &lt;code&gt;190&lt;/code&gt;. This was weird - the entire time that I was debugging the application we hadn't even gotten to the point where we were inserting rows, there's no way we should have skipped that record.&lt;/p&gt;

&lt;p&gt;The issue was fixed - so I had time to figure out why it was doing that. I hadn't previously gone line by line on the function, in favor of focusing on the area of the code that was failing. At the start of the function, was an ominous looking function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DbUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNextId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TableName"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  dbo.Ids
&lt;/h3&gt;

&lt;p&gt;Weird - I thought, why would we need a function to get the next id? SQL will create that for us, right?&lt;/p&gt;

&lt;p&gt;The function definition contained a bit of SQL that looked something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;table-name&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;followed by&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;table-name&amp;gt;`
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To date, I have yet to see anything in my career that made my jaw hit the floor that badly. I quickly selected against &lt;code&gt;dbo.ids&lt;/code&gt; and found that &lt;em&gt;every single table&lt;/em&gt; in the database had an entry in this singular table. Every single entity had to call &lt;code&gt;DbUtils.GetNextId&lt;/code&gt; to get its next id. &lt;/p&gt;

&lt;p&gt;Not only were we not using auto-incrementing primary keys, the operation to get the next id wasn't even atomic. If two calls came in at the same time and selected before one updated, two entities got the same id!!&lt;/p&gt;

&lt;p&gt;Of course, I thought, at least the primary key constraint on the destination table should enforce uniqueness and keep us from getting into a bad state like that - that's just common sense, right?&lt;/p&gt;

&lt;p&gt;Just in case, I opened SSMS &amp;amp; expanded the target table's indexes. &lt;/p&gt;

&lt;p&gt;And it was empty.&lt;/p&gt;

&lt;p&gt;I did a double-take, reloaded SSMS, and still empty. I checked my permissions to the database, nothing wrong - I was logged in as an SA. I checked another table - and empty there too.&lt;/p&gt;

&lt;p&gt;Not one index in the entire database. Not a foreign key, a unique constraint, a primary key, nothing.&lt;/p&gt;

&lt;p&gt;The dev prior to me hadn't known what an index was - and instead of doing research to figure out if there was a better way to do what he wanted, he just crapped out something that seemed like it worked.&lt;/p&gt;

&lt;p&gt;Customers had always complained that bulk operations took forever - and now I knew why. Every single row insertion required at least three round trips to the database: two for the manual ID generation, and one for the actual insert. If only there was a better way...&lt;/p&gt;

&lt;h3&gt;
  
  
  The Aftermath
&lt;/h3&gt;

&lt;p&gt;I ended up doing an audit of the entire codebase after that. It got worse. Not a single database parameter in the whole application. Every query was built with raw string concatenation. We were wide open to SQL injection.&lt;/p&gt;

&lt;p&gt;There were enough bad findings in that application that I had to talk my boss in to putting an immediate moratorium on further sales. We had to rebuild the entire thing from scratch. &lt;/p&gt;

&lt;p&gt;There were further issues to fix at customers we'd already deployed to, and the rewrite took at ton of work - but at the end of day cleaning up that hot pile of garbage taught me more than any class in my university could have. As many sleepless nights as it took - I would have to say that without that experience I wouldn't have learned nearly as much as I know today. In the end, it led me to a career I enjoy that keeps me happy and fed.&lt;/p&gt;




&lt;p&gt;Anyways - I've always enjoyed reading software horror stories. Figured it was about time that I wrote my own worst software story. Thanks for reading - feel free to tell me about your own in the comments. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;no AI was used to write this post, this is just how I type lol&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>database</category>
    </item>
    <item>
      <title>Optimizing CI/CD Pipelines: How Dynamic JavaScript Configuration Streamlined Our Deployments</title>
      <dc:creator>Conner Phillis</dc:creator>
      <pubDate>Mon, 17 Feb 2025 14:00:12 +0000</pubDate>
      <link>https://forem.com/connerphillis/one-build-multiple-environments-how-we-optimized-cicd-with-settingsjs-4fjb</link>
      <guid>https://forem.com/connerphillis/one-build-multiple-environments-how-we-optimized-cicd-with-settingsjs-4fjb</guid>
      <description>&lt;p&gt;I used to frequently run in to problems building CI / CD pipelines at both my previous and my current organization. &lt;/p&gt;

&lt;p&gt;Often times, I would be building a website that would need to be deployed multiple times, either internally or externally. Years ago, when I was researching how to support rollouts like this online, the general guidance that I found was that I should be creating a .env file for each environment, and then letting my build process sub in those environment variables.&lt;/p&gt;

&lt;p&gt;Years ago, the general advice I came across was to use a &lt;code&gt;.env&lt;/code&gt; file for each environment, and have the build process substitute the appropriate environment variables. While this worked in some cases, I quickly realized that managing separate builds for each environment was inefficient. Especially when deploying the same website to multiple locations. This approach was increasing the cost of our rented CI/CD agents and artifact storage, and adding unnecessary complexity to our workflows&lt;/p&gt;

&lt;p&gt;One possible solution that we piloted was to try to load the configuration into an API that would be fetched at runtime. While this seemed like a viable solution, it felt problematic. The primary issue was that we'd need to run an additional fetch call in our app, which slowed down our load times. Plus, we had to modify every API request or introduce guards to ensure that the configuration was loaded beforehand. This added unnecessary complexity and potential failure points.&lt;/p&gt;

&lt;p&gt;What I eventually settled on was creating a JavaScript file to inject these settings into globalThis. By including this file at the start of the document, we ensure that the configuration is available as soon as the app code begins executing. This method provides a few key benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single Build, Multiple Deployments:&lt;/strong&gt; Since you only need to run one build, you can generate your deployment artifacts once, and then customize the settings for each environment by using unique &lt;code&gt;settings.js&lt;/code&gt; files for each deployment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Need for Extra Checks:&lt;/strong&gt; With the configuration already loaded when the app starts, there's no need to worry about checking whether the configuration has been successfully loaded. The app can rely on it being available immediately, which simplifies the code and removes unnecessary validation steps."&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="c"&gt;&amp;lt;!-- index.html --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./assets/favicon.svg"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/x-icon"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/settings.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/main.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- remaining site assets ...  --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, you can see that the &lt;code&gt;settings.js&lt;/code&gt; file is loaded in the same breath as the &lt;code&gt;main.js&lt;/code&gt; file, inside of that &lt;code&gt;settings.js&lt;/code&gt; file we have the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// settings.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;siteSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// sso&lt;/span&gt;
  &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;client-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;tenant-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audienceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;audience-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// api&lt;/span&gt;
  &lt;span class="na"&gt;apiAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;api-address&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// auditing&lt;/span&gt;
  &lt;span class="na"&gt;metricsEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;metrics-endpoint&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;siteSettings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this script is loaded, it declares a new property against globalThis which holds all of our site specific configuration.&lt;/p&gt;

&lt;p&gt;A few notes that I should add:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NEVER EVER store sensitive configuration information in your settings.js files&lt;/strong&gt; - If you do this, it's essentially broadcasting your secrets to the world. This probably goes without saying, but its better that I spell this out before someone blames me for their secrets getting leaked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Potential Namespace Pollution&lt;/strong&gt; - I recommend that you choose a unique name (not configuration) when assigning to globalThis or window. There is no guarantee that one of your dependencies hasn't decided that &lt;em&gt;they&lt;/em&gt; want to use &lt;code&gt;globalThis.configuration&lt;/code&gt; to hold some global state that it needs. You should use a &lt;code&gt;globalThis&lt;/code&gt; key that gives you reasonable assurance that no other code is writing to it. Think &lt;code&gt;globalThis.&amp;lt;my-app-name&amp;gt;_settings&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it lean&lt;/strong&gt; - don't put anything in your &lt;code&gt;settings.js&lt;/code&gt; file that you wouldn't put in a &lt;code&gt;.env&lt;/code&gt; file. This file should be lightweight, no massive base64 strings for your favicon, or other weird stuff like that. Keep it to keys and values so your users have no idea that there was a couple lines of extra JS loaded.&lt;/p&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;I've found this approach great for simplifying CI/CD pipelines. It has reduced the total executions of our build pipeline and made it much easier for us to manage our release builds. &lt;/p&gt;

&lt;p&gt;It does come with trade-offs; there are some security risks (particularly with exposing sensitive configuration values) and potential conflicts in the global namespace. However, it is important to note that these are not risks introduced by the method, rather they are risks inherent to developing public applications. &lt;/p&gt;

&lt;p&gt;By ensuring sensitive data is handled separately and by implementing best practices around naming and performance, these drawbacks can be mitigated.&lt;/p&gt;

&lt;p&gt;As always, feel free to critique this in the comments. I am always happy to create revisions and offer corrections. I recognize that this is a rather niche option that most people wouldn't be able to take advantage of, but I still think it should be published for people with similar issues.&lt;/p&gt;




&lt;p&gt;note - My organization uses a template &lt;code&gt;settings.js&lt;/code&gt; file and replaces the individual keys of the settings using environment variables. If there is interest in this approach I can post and link that approach as well.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Sequential GUIDs in Entity Framework Core Might Not Be Sequential</title>
      <dc:creator>Conner Phillis</dc:creator>
      <pubDate>Fri, 24 Mar 2023 16:15:57 +0000</pubDate>
      <link>https://forem.com/connerphillis/sequential-guids-in-entity-framework-core-might-not-be-sequential-3408</link>
      <guid>https://forem.com/connerphillis/sequential-guids-in-entity-framework-core-might-not-be-sequential-3408</guid>
      <description>&lt;p&gt;&lt;em&gt;Edit April 27, 2023 -&lt;/em&gt; The Entity Framework Core team has since opened &lt;a href="https://github.com/dotnet/efcore/issues/30753" rel="noopener noreferrer"&gt;this issue&lt;/a&gt; in response to this article. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Background
&lt;/h3&gt;

&lt;p&gt;Our customers more often than not chose to host our application on their own machines, so we frequently get asked what the minimum hardware requirements are. We based the estimates we provide on the requirements of similar applications.&lt;/p&gt;

&lt;p&gt;With our latest release we decided we'd get a conclusive answer for ourselves, so we put some resources into running benchmarks. We settled on a simple setup. Write a simple script that would simulate a series of requests that would run through hot paths, and see how many operations we could complete in a fixed time frame. The script would run X number of concurrent requests for N minutes, log the statistics to a CSV file and export our results to a CSV file for analysis.&lt;/p&gt;

&lt;p&gt;We architected our test server to simulate an organization stressed for resources. On a single virtual machine we installed SQL Server, IIS, and our application. For the hardware behind the virtual machine we used an Azure F4s v2 (4 vCPU, 8GB).&lt;/p&gt;

&lt;p&gt;For our warm up, we ran the script with 20 concurrent tasks for 10 minutes, the results that we got?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;263724 total requests completed
67431 form submissions in 10 minutes across 20 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this may not seem like a lot for some, this was great for us. We consider our workloads somewhat computationally expensive, and didn't imagine we would get these sort of numbers out of our code. Especially when hosting the server and database on the same machine.&lt;/p&gt;

&lt;p&gt;Our logs indicated that we were on average consuming about 70% of the CPU. The data that we got was plenty for us to determine our hardware requirements, but just for fun we decided to see how far we could push it. We resized the VM to an F8s V2 (8 vCPU 16GB) expecting linear results.&lt;/p&gt;

&lt;p&gt;The script was set, 50 concurrent tasks instead of 20 to account for the increase in core count, running for ten minutes. The results?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;275532 total requests completed
68883 form submissions in 10 minute across 50 tasks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;What!?!?&lt;/em&gt;&lt;/strong&gt; We doubled the hardware, 2.5x'd the number of concurrent runs, and ended up with only ~3% more completed requests. This set off an alarm for us, we obviously had a large issue with the scalability of our application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Investigating the Issue
&lt;/h3&gt;

&lt;p&gt;The first thing that we theorized was that the increased number of tasks was causing problems with IIS, causing connections to stay open for longer than they should. We altered our the parameters of our test script to use 20 tasks over 10 minutes, mirroring the test against the F4s machine. After 10 minutes, the results were...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;275916 total requests completed
68979 form submissions in 10 minutes across 20 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;The same??&lt;/em&gt;&lt;/strong&gt; There was only a marginal difference in the results. Less than 1% from the original run. The test machine was hardly using a fraction of the processing power and network it could utilize. Something bigger was afoot.&lt;/p&gt;

&lt;p&gt;We started a Remote Desktop session with the server and ran another test, 10 minutes, 20 cores. We observed SQL Server start by consuming ~30% of our CPU time, and watched it move up to as much as 60% of the CPU by the end of the run. Over time, our performance was getting &lt;em&gt;worse&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;On a whim, we ran a query to check for index fragmentation of the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7jtah0y6v1rqyh8ppob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7jtah0y6v1rqyh8ppob.png" alt="Production Index Fragmentation" width="800" height="765"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The index fragmentation was far above what could be expected out of a healthy database. North of 50% for some indexes. While we can't &lt;em&gt;prove&lt;/em&gt; right now that this is what is causing our scaling issue&lt;sup&gt;1&lt;/sup&gt; it does explain how SQL server can continuously need more resources. As the size of the data grows, SQL is having to spend more time doing table scans and expending more resources on IO.&lt;/p&gt;

&lt;p&gt;We found this puzzling, we were using Entity Framework Core's &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.valuegeneration.sequentialguidvaluegenerator?view=efcore-7.0" rel="noopener noreferrer"&gt;&lt;code&gt;Sequential Guid Value Generator&lt;/code&gt;&lt;/a&gt; With the &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.schema.databasegeneratedoption?view=net-7.0" rel="noopener noreferrer"&gt;&lt;code&gt;DatabaseGeneratedOption.Identity&lt;/code&gt;&lt;/a&gt; option. The documentation states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generates sequential Guid values optimized for use in Microsoft SQL server clustered keys or indexes, yielding better performance than random values. This is the default generator for SQL Server Guid columns which are set to be generated on add.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's important to note in addition to this documentation for those that aren't aware, setting a column to use a GUID as a key with &lt;code&gt;DatabaseGeneratedOption.Identity&lt;/code&gt; &lt;strong&gt;does not mean that it will be generated by the database&lt;/strong&gt;. Instead, EF Core generates the sequential GUID itself, and then inserts it into the database (&lt;a href="https://weblogs.asp.net/ricardoperes/current-limitations-of-entity-framework-core#:~:text=For%20GUIDs%2C%20EF%20Core%20automatically%20generates%2C%20on%20the,makes%20it%20database-specific%20%E2%80%93%20currently%2C%20SQL%20Server%20only." rel="noopener noreferrer"&gt;read here&lt;/a&gt;). This can be observed when comparing GUIDs generated normally to those generated by &lt;code&gt;NEWSEQUENTIALID&lt;/code&gt; later in this post.&lt;/p&gt;

&lt;p&gt;Additionally, &lt;a href="https://github.com/dotnet/efcore/pull/20528#issuecomment-612889464" rel="noopener noreferrer"&gt;this issue&lt;/a&gt; in the EF core repository shows that EF core generates GUIDs &lt;em&gt;better&lt;/em&gt; than SQL Server does. The documentation wasn't lining up with what we were seeing, it was time to recreate the EF tests, and see if we could simulate the behavior we were getting from our server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running our Own Benchmarks
&lt;/h3&gt;

&lt;p&gt;The first thing we did was see if we could reproduce the test done by &lt;a href="https://github.com/roji" rel="noopener noreferrer"&gt;roji&lt;/a&gt; on the EF core team with 100000. And...&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Average page space used in %&lt;/th&gt;
&lt;th&gt;Average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NEWSEQUENTIALID&lt;/td&gt;
&lt;td&gt;99.91 %&lt;/td&gt;
&lt;td&gt;1.04 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF Core Sequential Guid Value Generator&lt;/td&gt;
&lt;td&gt;99.86 %&lt;/td&gt;
&lt;td&gt;0.56 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same results as the team found. The EF Core value generator is still generating GUIDs optimally as of SQL Server 2022.&lt;/p&gt;

&lt;p&gt;But wait... this isn't really how a web server works. Entities aren't just inserted one after another when coming from a web server. Entries are created in response to user activity, and that can happen whenever. Database activity happens spontaneously, whenever a user performs an action, and different user hardware can mean these operations can take different amounts of time. What if we modify the test, instead to simulate a large degree of parallel actions rather than pure sequential inserts? &lt;/p&gt;

&lt;p&gt;We altered our script, instead of inserting 100,000 sequential ids into the database, we created 20 tasks, and told each of those tasks to insert 5000 rows into the database. Once this was done we looked at index fragmentation again.&lt;/p&gt;

&lt;h5&gt;
  
  
  Parallel Entity Framework Sequential Guid Generation
&lt;/h5&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;average page space used in %&lt;/th&gt;
&lt;th&gt;average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;57.93 %&lt;/td&gt;
&lt;td&gt;44.53 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Multithreaded Simulation Code&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BlogContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureDeletedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCreatedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;List&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BlogContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Interlocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Blog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Foo"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DbSet&lt;/span&gt; &lt;span class="n"&gt;Blogs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnConfiguring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptionsBuilder&lt;/span&gt; &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Server=.;Database=Testing;Trusted_Connection=true;Encrypt=false;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Blog&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The top 10 results returned when querying the database illuminate the issue: &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuyhgc7qg7toj7y0fovt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuyhgc7qg7toj7y0fovt.png" alt="Top 10 Results From SequentialGuidValueGenerator" width="508" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our conclusion? &lt;strong&gt;Entity Framework seeks to create an efficient value generation strategy optimized for SQL Server, but after the network stack has its say, its likely that some rows will be inserted out of their original order.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compare that to the results that you get when running the same code, but setting &lt;code&gt;HasDefaultValueSql("NEWSEQUENTIALID()")&lt;/code&gt; in the &lt;code&gt;OnModelCreating&lt;/code&gt; method in the database context:&lt;/p&gt;
&lt;h5&gt;
  
  
  Parallel Guid Generation with NEWSEQUENTIALID()
&lt;/h5&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;average page space used in %&lt;/th&gt;
&lt;th&gt;average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;96.03 %&lt;/td&gt;
&lt;td&gt;7.67 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fragmentation percentage is still not as good as inserting the rows one after the other, and the average page space used is a bit lower, but I think we can all agree that it's better than generating the IDs in memory with Entity Framework Core.&lt;/p&gt;

&lt;p&gt;This method has drawbacks too, however. Looking at the GUIDs that SQL generates it's hard to say that they have the same uniqueness guarantee that standard GUIDs have. It appears that the leading bits of the GUIDs are all that change when taking a sample of the first 10 inserted in the database after our concurrent test:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74vgm9xj7u3rnih586q5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74vgm9xj7u3rnih586q5.png" alt="NEWSEQUENTIALID Results" width="608" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(in case anyone is curious, generating the GUIDs randomly led to a fragmentation percentage of almost 99%)&lt;/p&gt;
&lt;h3&gt;
  
  
  Studying the Issue
&lt;/h3&gt;

&lt;p&gt;There were two main benefits that initially brought us to use GUIDs as primary keys in our database.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We sometimes have to export data across servers, so the (near) uniqueness guarantee meant that it should be trivial to merge the data&lt;/li&gt;
&lt;li&gt;Certain actions don't require our users to be connected to our server all the time as long as they do a periodic sync. In this case we could let the client generate IDs and after the sync turn the IDs into sequential ones. Once we were done with the transformation we just had to inform the client of the new IDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unfortunately, the SQL server GUIDs don't seem like they would be able to cut it for us, as it seems likely that a collision could occur when exporting from one server to another.&lt;/p&gt;

&lt;p&gt;This led us to a tough crossroad. Do we&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep going, knowing that scaling up our application leads to highly diminishing returns necessitating expensive hardware OR&lt;/li&gt;
&lt;li&gt;Lose the benefits GUIDs give us in favor of another primary key format that would be better suited for parallel inserts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ultimately, we decided that our best path forward was to go with a hybrid approach. We would alter our tables to have two IDs where GUIDs are required. This involved using an integer primary key generated by the database, and GUID value as a non-clustered index with a unique constraint. These GUIDs would use the &lt;code&gt;SequentialGuidValueGenerator&lt;/code&gt; to try to "presort" some of the items in the non-clustered index, but we wouldn't enforce that it had to be a sequential GUID.&lt;/p&gt;

&lt;p&gt;After performing our parallel benchmark, we ended up with the following results:&lt;/p&gt;
&lt;h6&gt;
  
  
  Hybrid Key Generation Approach
&lt;/h6&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;average page space used in %&lt;/th&gt;
&lt;th&gt;average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;94.15 %&lt;/td&gt;
&lt;td&gt;10.38 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Just in case we ran the benchmark again with only an integer primary key, that yielded a fragmentation percentage of almost exactly 12%. It really just seems that some fragmentation is unavoidable in a parallel context.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Great Key Migration
&lt;/h3&gt;

&lt;p&gt;Armed with the results of the benchmarks we had ran, we decided that we would make a gamble. Every table that we had that used a GUID primary key we would alter to contain an auto-incrementing integer primary key, and a GUID UniqueId column with a unique constraint enforced. We would still use the Entity Framework Core GUID value generator to create these unique Ids so to reduce the amount of work SQL would have to do maintaining the unique constraint.&lt;/p&gt;

&lt;p&gt;In the end, it took roughly two weeks of work, and by the end we had modified 600 files according to Git. We ran the benchmark again with the new composite keys and our test script outputted the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;334192 total requests completed
83548 form submissions in 10 minutes across 20 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This absolutely shocked us. We had more than doubled our throughput, obtaining a total boost of &lt;em&gt;~24%&lt;/em&gt; by changing our code to use integer primary keys instead of GUIDs.&lt;/p&gt;

&lt;p&gt;Furthermore, our 8 core results showed a near-linear increase of 153,076 submissions, and further analysis showed that the processor wasn't being 100% utilized in this benchmark. Some may say the time investment or the risk involved isn't worth it, but in our minds, the tradeoff we got was more than worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing
&lt;/h3&gt;

&lt;p&gt;I'd like to end this post with a couple of acknowledgements.&lt;/p&gt;

&lt;p&gt;First, I don't believe that using the sequential id generator strategy is bad. The Entity Framework Core team's benchmarks show that it does great work in a purely sequential workload. As long as you aren't expecting a high degree of parallelism, it seems that they are perfectly fine as a primary key. Even if you do have a parallel workload, its still possible to reorganize your clustered indexes.&lt;/p&gt;

&lt;p&gt;Second, I want to acknowledge that its totally possible that this is all a coincidence, and that the GUIDs weren't the cause of the performance issues that we were seeing in SQL Server. It's our belief that it's the culprit. It's also of secondary importance for us to raise awareness that the assumption that we made, that because &lt;code&gt;SequentialGUidValueGenerator&lt;/code&gt; uses a strategy optimized for sequential access in SQL server, that GUIDs aren't always going to be &lt;em&gt;inserted&lt;/em&gt; sequentially.&lt;/p&gt;

&lt;p&gt;Lastly, I encourage anyone who reads this to look into the methods enclosed and run their own benchmarks to draw their own conclusions. If there is a flaw in my methods I'm happy to make an edit or publish a correction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thank You!
&lt;/h3&gt;

&lt;p&gt;Thank you for reading my first blog post, please let me know what worked, and what didn't&lt;/p&gt;

&lt;p&gt;-- Conner&lt;/p&gt;

&lt;p&gt;&lt;a&gt; 1&lt;/a&gt; It still perplexes us as to how it didn't show up on the smaller machine. It's possible (spoiler) that since we had less cores we had a lesser degree of parallelism, so rows were not being inserted out of order as bad.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>sqlserver</category>
      <category>entityframeworkcore</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
