Skip to content

URLs Full URL Shrink Upgrade

Sam Betts edited this page Jun 16, 2026 · 3 revisions

URLs Full URL Shrink Upgrade

This release converts dbo.urls.full_url to nvarchar(850) and adds a non-clustered index IX_urls_full_url, plus snapshot-refresh migrations. They run automatically as part of the normal solution upgrade and are designed to be safe on customer databases with very large urls tables (tens of millions of rows), but on large tables the upgrade can take a long time. Read this page before you run the upgrade.

Why nvarchar, not varchar? An earlier build shipped this column as varchar(1700), which stores only a single code page and silently corrupted non-Latin (e.g. Greek) SharePoint URLs to ? — for example …/Καλημέρα κόσμε.pdf became …/?a??µ??a ??sµe.pdf. The column is now nvarchar(850): nvarchar holds the full Unicode range, and 850 Unicode characters = 1700 bytes = the SQL Server non-clustered index-key limit, so the column stays index-seekable. Both supported upgrade-from states reach nvarchar(850) losslessly and preserve Greek URLs (see §1).

The same information is intended for operators / admins with some technical background but who aren't SQL specialists.

Want the plain-English version of what happens to long URLs when data is imported (which links get shortened, and how)? See How URLs are shortened.


1. What the upgrade actually does

EF migrations against the urls table. Which ones run depends on the build you're upgrading from:

  1. 202606011710001_ShrinkUrlsFullUrlColumn — does the real work: converts full_url to nvarchar(850) and creates the supporting non-clustered index IX_urls_full_url. It is idempotent and converts from any earlier shape ((n)varchar(max) or the superseded varchar(1700)). This is the migration that runs when you upgrade from a build whose full_url is still (n)varchar(max).
  2. 202606011739254_UrlFullUrlVarcharMappingUp/Down are empty (superseded — it was the snapshot refresh for the old varchar(1700) form). Runs in milliseconds; ignore it for capacity planning.
  3. 202606141000001_UrlFullUrlNvarchar — replays the idempotent converter and refreshes the EF Code-First model snapshot to nvarchar(850) (so the Url.FullUrl mapping doesn't trigger AutomaticDataLossException at every AnalyticsEntitiesContext construction). For databases already on the earlier varchar(1700) build this is the migration that performs the varchar(1700) → nvarchar(850) conversion; for everyone else it's a millisecond no-op.

Two supported upgrade-from states, both lossless — Greek/Unicode URLs are preserved:

  • From a pre-shrink build (full_url = (n)varchar(max)): ShrinkUrlsFullUrlColumn widens straight to nvarchar(850). A pure Unicode-preserving change.
  • From the varchar(1700) build: UrlFullUrlNvarchar converts varchar(1700) → nvarchar(850) — a lossless widen. (Anything that reached that build had already passed the old build's representability check, so its varchar data is fully representable and converts faithfully.)

urls.full_url is the join / de-duplication key for every staging-table merge in the importers (Migrate Hits Import into Hits.sql, Insert Activity from Staging Table.sql, the Copilot SharePoint variant, and the App Insights clicks variant). As an (n)varchar(max) LOB column it cannot be a B-tree index key, so every one of those joins is forced into a full scan of the (largest dimension) urls table with LOB string comparisons. 1700 bytes is the SQL Server single-column non-clustered index-key limit, and the column must be nvarchar (not varchar) so it can hold any Unicode URL (e.g. Greek) without corruption — nvarchar is 2 bytes/char, so nvarchar(850) (= 1700 bytes) is the widest indexable Unicode URL column.

In plain English, here's what happens step-by-step

For each step the actual time depends on how many rows are in urls.

  1. Skip-if-already-done check. If the column is already nvarchar(850) and IX_urls_full_url already exists, the migration writes a one-line message and exits in milliseconds. Safe to re-run.

  2. Pre-flight data check. The migration scans urls once before changing anything for too-long URLs — rows where LEN(full_url) > 850, which wouldn't fit the new column and would otherwise be truncated. There is no longer any "lossy conversion" check: the target is nvarchar, which represents every Unicode character, so Greek (and any other script) converts faithfully. The representability abort that blocked Greek customers on the old varchar build is gone.

    If the scan finds rows, the migration lists up to 50 of them (id + full_url), prints the exact diagnostic query, and aborts without changing anything. See "If the migration aborts" below.

  3. Convert the column. ALTER COLUMN full_url nvarchar(850) NOT NULL. This is a size-of-data rewrite: SQL Server has to read every URL and re-store it inline in the clustered index leaf pages. It holds a schema-modification lock on urls for its full duration. This is the dominant cost.

  4. Build the index. CREATE NONCLUSTERED INDEX IX_urls_full_url ON urls(full_url). On Enterprise / Azure SQL DB / Azure SQL MI this is built ONLINE = ON, meaning normal reads and writes continue while it's building. On Standard / Web / Express / Developer SQL Server editions it's built offline (writes wait).

All steps stream live progress + per-step timing via RAISERROR ... WITH NOWAIT — watch them in the SSMS / Azure Data Studio Messages tab or in the SQL session running the migration.

What you end up with

Before After
urls.full_url is nvarchar(MAX) (LOB column, can't be an index key) urls.full_url is nvarchar(850) NOT NULL (Unicode-safe)
No supporting index on full_url IX_urls_full_url (non-clustered)
Every staging-merge full-scans urls with LOB comparisons Staging merges seek IX_urls_full_url
EF emits parameters that don't match the column → implicit conversion would defeat the index Url.FullUrl is mapped as nvarchar(850) → seeks the index

What it does NOT do

  • It does not delete any rows. If pre-flight finds offending rows it aborts the whole migration — the operator fixes the data and re-runs. It will never silently truncate or corrupt URLs.
  • It does not touch hits, event_meta_sharepoint, or any other table that references urls(id) via url_id. Foreign-key references continue to point at the same urls.id rows.
  • It is not reversible without losing the index. A Down migration is provided (drops the index, widens the column back to nvarchar(MAX)), but there is no data to undo because nothing about row identity was changed.

2. Measured baseline performance

The upgrade was benchmarked against a freshly seeded database in a controlled environment. These figures were measured on the superseded varchar(1700) form of the migration. The operation for nvarchar(850) is the same shape, with two differences: (1) the lossy/representability pre-flight scan no longer runs, so the pre-flight is faster; and (2) nvarchar stores 2 bytes/char, so for typical (mostly-ASCII) URLs the rewritten column and index are larger — expect somewhat more transaction log, tempdb and final on-disk size than the numbers below, and budget headroom accordingly. Treat the timings as a representative order-of-magnitude guide, not a precise prediction.

Property Value
urls row count 10,000,000
Distribution All clean ASCII URLs, average 152 characters each (realistic SharePoint paths). None over 850 chars.
Pre-migration urls size on disk 3.27 GB (LOB-dominated)
Post-migration urls size on disk 8.19 GB (clustered PK now holds the URLs inline; new NC index = 1.68 GB)
SQL Server SQL Server 2025 (RTM-GDR, 17.0.1115.1), Standard Developer Edition (EngineEdition = 2), default localhost instance
Host Windows VM, Standard HDD disks (~500 IOPS, ~60 MB/s, ~10 ms latency — the slowest Azure storage tier)
ONLINE index? NoEngineEdition = 2 doesn't qualify, so IX_urls_full_url was built offline. This is the pessimistic / non-Enterprise customer case.
Total ShrinkUrlsFullUrlColumn elapsed 6 min 6 sec (366 sec)
Per-row cost ~36.6 µs/row (4.3 µs scans + 13.3 µs ALTER + 19.0 µs CREATE INDEX)
Post-state Column is nvarchar(850) NOT NULL, IX_urls_full_url present, row count unchanged

Per-step breakdown

Step Duration % of total
Pre-flight "too long" scan (LEN(full_url) > 850) 2.5 sec <1%
ALTER COLUMN full_url nvarchar(850) NOT NULL 2 min 12.7 sec ~41%
CREATE INDEX IX_urls_full_url (OFFLINE) 3 min 9.9 sec ~58%
Total ~5 min 25 sec 100%

The original varchar(1700) benchmark also ran a "lossy varchar" pre-flight scan (40.9 sec) that no longer existsnvarchar needs no representability check — so removing it brought the measured total down from 6 min 6 sec to ~5 min 25 sec. The ALTER COLUMN / CREATE INDEX durations shown are from the varchar run; on nvarchar(850) they will be somewhat longer because the rewritten data and index hold 2 bytes/char.

Why this is a "best-case at 10M, worst-case at 100M+" baseline. The test VM has Standard HDD storage, which is the slowest disk type Azure offers — about 500 IOPS and 60 MB/s with high (~10 ms) latency. At 10M rows the pre-migration urls table is only ~3 GB and fits comfortably in the SQL Server buffer pool, so the pre-flight scans were served almost entirely from memory and the slow HDD barely mattered. As the table grows past what fits in memory (somewhere between 25M and 50M rows on a typical SQL instance), the Standard HDD becomes the bottleneck and the per-row cost gets dramatically worse. Note also that on Standard Developer Edition the index is built offline — Enterprise / Azure SQL DB / Azure SQL MI build it ONLINE and finish in a comparable wall-clock time but without blocking writers. All Azure SQL DB tiers (even the cheapest) use SSD, so most Azure SQL DB customers will see times in line with — or better than — the Standard tier table in §4 below.


2a. How many rows do you have?

Before you look at the time estimates, run this against your database to see which row in the tables you should be looking at. It also surfaces how many URLs would cause the migration to abort, and whether the column/index are already in place (so the upgrade would be a no-op).

-- Instant estimate (reads metadata only, no table scan - safe on any DB size)
SELECT
    OBJECT_NAME(p.object_id) AS table_name,
    SUM(p.rows)              AS estimated_row_count
FROM sys.partitions p
WHERE p.object_id = OBJECT_ID('dbo.urls')
  AND p.index_id IN (0, 1)   -- heap or clustered index only
GROUP BY p.object_id;

-- Already-applied check - if both come back 1, the upgrade is a no-op
SELECT
    (SELECT CASE WHEN t.name = 'nvarchar' AND c.max_length = 1700 THEN 1 ELSE 0 END
       FROM sys.columns c
       INNER JOIN sys.types t ON c.user_type_id = t.user_type_id
       WHERE c.object_id = OBJECT_ID('dbo.urls') AND c.name = 'full_url') AS column_already_nvarchar850,
    (SELECT COUNT(*) FROM sys.indexes
       WHERE object_id = OBJECT_ID('dbo.urls') AND name = 'IX_urls_full_url') AS index_already_present;
-- (nvarchar(850) reports max_length = 1700 bytes, i.e. 850 chars x 2 bytes.)

-- Will the migration abort? (full scan - may take a while on huge tables;
-- skip if you just want time estimates and trust that your URLs are well-formed)
-- Only one condition can abort the upgrade: a URL longer than 850 characters.
-- (There is no "representable as varchar" check any more - nvarchar stores all Unicode,
--  so Greek and other non-Latin URLs are preserved, not rejected.)
SELECT COUNT_BIG(*) AS urls_longer_than_850_chars
FROM dbo.urls WHERE LEN(full_url) > 850;

What to do with the numbers:

  • urls estimated_row_count → pick the corresponding row in §3 (if you self-host SQL on a VM) or §4 (if you're on Azure SQL DB).
  • column_already_nvarchar850 = 1 AND index_already_present = 1 → the migration will detect this and exit in seconds. No action needed.
  • urls_longer_than_850_chars > 0 → fix the offending rows before kicking off the upgrade; otherwise the migration will abort (safely, without changing anything) and you'll have to fix them and re-run. See §6.

3. Expected upgrade time vs table size

The work the upgrade does is essentially linear in the number of rows in urlsas long as the table fits in memory. Once it doesn't, every page has to be read from disk and the per-row cost balloons on slow storage. The pre-migration urls table is roughly (avg URL length + 30) × rowcount bytes for the data and a constant ~50% overhead for the LOB structures.

On the test VM (Azure VM, Standard HDD — the slowest option)

urls rows Estimated upgrade time Notes
1,000,000 (1M) ~40 sec Fits in memory
5,000,000 (5M) ~3 min Fits in memory
10,000,000 (10M) ~6 min (measured) Fits in memory; HDD largely bypassed
25,000,000 (25M) ~15 – 25 min Starting to spill from memory
50,000,000 (50M) ~45 min – 1.5 hr HDD becomes painful
100,000,000 (100M) ~3 – 6 hr Mostly disk-bound; 500-IOPS / 60 MB/s HDD is the bottleneck
250,000,000 (250M) ~10 – 18 hr Sequential scan throughput-limited by HDD
500,000,000 (500M) ~1.5 – 2 days HDD throughput is the wall; not realistic without better storage

If you're running SQL Server on an Azure VM and you have 25M+ urls, the single highest-impact change you can make is to switch the data disk from Standard HDD to Premium SSD (P30 or above) before applying the migration. This alone would shave 5–10× off the times above. Even Standard SSD would be a major improvement over Standard HDD for tables that no longer fit in memory.

If you're on Standard / Web / Express / Developer Edition the new index is built offline (the CREATE INDEX line above). On Enterprise / Azure SQL DB / Azure SQL MI that same step runs ONLINE = ON — wall-clock is similar, but other reads and writes against urls aren't blocked.


4. Expected upgrade time on Azure SQL DB (DTU model, Standard tier)

Good news for Azure SQL DB customers: every Azure SQL DB tier — even the cheapest — uses SSD storage under the hood. So the "table doesn't fit in memory" cliff that hits hard on a Standard HDD test VM is much gentler in Azure SQL DB. The dominant cost on Azure SQL DB is the DTU cap on CPU and log throughput (because both ALTER COLUMN and CREATE INDEX are fully logged), not raw disk IO.

These are estimates with a wide error margin (±50%). Real-world numbers depend on how busy your DB is, what's already cached, log throttling, and other concurrent workload. Treat them as orders of magnitude.

Tier DTUs Max log throughput Max data IOPS Notes
S3 100 ~2.5 MB/s ~320 ALTER COLUMN log writes will be the bottleneck
S4 200 ~5 MB/s ~640
S6 400 ~10 MB/s ~1,280 A reasonable sweet spot for a one-off upgrade
S7 800 ~20 MB/s ~2,560 Recommended if you have 50M+ rows

Estimated upgrade time by row count and tier

For context: the 10M-row baseline writes roughly 6 GB of transaction log (3.3 GB ALTER COLUMN row rewrite + 1.7 GB index leaf + overhead). This scales linearly with urls rowcount.

Rows S3 (100 DTU) S4 (200 DTU) S6 (400 DTU) S7 (800 DTU)
1M ~5 – 8 min ~3 – 5 min ~1 – 2 min <1 min
10M ~45 – 70 min ~25 – 40 min ~12 – 20 min ~6 – 10 min
50M ~4 – 6 h ~2 – 3 h ~1 – 1.5 h ~30 – 50 min
100M ~8 – 12 h ~4 – 6 h ~2 – 3 h ~1 – 1.5 h
250M ~20 – 30 h ~10 – 15 h ~5 – 7 h ~2.5 – 3.5 h
500M ~1.5 – 2.5 days ~20 – 30 h ~10 – 14 h ~5 – 7 h

How to read this table: if you're seeing hours-on-end (let alone days), that's a strong signal to scale up before the upgrade — see §5. The cost of running on a higher tier for a few hours is trivial compared to a multi-day outage window.

Premium tier is dramatically faster. If your urls table is in the 50M+ range, scaling temporarily to Premium (P2 / P4 / P6) is the best move. Premium has order-of-magnitude better IOPS (P2 = 4,000 IOPS, P6 = 16,000 IOPS) and log throughput. As a rough guide, P2 will finish the upgrade in roughly half the time of S7, and P6 in roughly a quarter — and you can scale back down immediately afterwards.


5. Pre-upgrade recommendations

Do these before you start the upgrade, in this order:

5.1 Stop the analytics web jobs

The web jobs continuously hit urls from the staging-table merges. Leaving them running during the migration will (a) make the ALTER COLUMN much slower because of the schema-modification lock contention, and (b) almost certainly cause the web jobs themselves to time out and fail.

  • In the Azure Portal, navigate to your App Service / Function App and stop the analytics web jobs (WebJob.Office365ActivityImporter, WebJob.AppInsightsImporter, and any other importer jobs).
  • Wait ~30 seconds for any in-flight batches to finish committing.

5.2 Pause / unschedule Power BI dataset refreshes

A Power BI refresh against a busy urls/hits/audit_events workload during the migration will:

  • Compete for IO with the ALTER COLUMN rewrite and the CREATE INDEX sort.
  • Probably fail anyway because the new index/column type temporarily change query plans.

Open the dataset(s) in the Power BI Service, disable scheduled refresh (or change the schedule to skip the upgrade window), and cancel any refresh that's already in progress.

5.3 Free up disk / data-file headroom

This migration is storage-intensive. Post-migration urls grows because the URLs move from LOB pages into the clustered PK leaf pages and are now stored as 2-byte-per-char nvarchar. On the superseded varchar(1700) form this measured ~2.5× the pre-migration size; for nvarchar(850) budget roughly 3.5–4× (the benchmark figures elsewhere on this page were measured on varchar). Before kicking off:

  • Data file: ensure the data file (or auto-grow) has headroom for ~4 × current urls size. Example: a 30 GB urls table can need ~120 GB total post-migration, so ~90 GB free in the data file (or in the volume the data file can grow into).
  • Transaction log: allow free log space on the order of 2–3 × urls data size. On SIMPLE recovery the log auto-truncates between steps; on FULL recovery take a log backup before the upgrade and another straight after.
  • tempdb: the CREATE INDEX sort spills to tempdb. Make sure tempdb has at least ~2 GB free per 10M URLs being sorted.

5.4 Temporarily scale up your Azure SQL Database

If your DB is on a lower DTU tier (S3 / S4 / S6) and you have more than ~25M urls, scale up before the upgrade. The cost of running S7 (or even Premium) for a few hours is trivial compared to the time saved.

A reasonable pattern:

  1. Scale up to S9 (1600 DTU) or a Premium P2 / P4 tier just before starting.
  2. Apply the migration.
  3. Verify the new column type and index are present (see §7).
  4. Scale back down to your normal tier.

Scaling Azure SQL DB up or down is online — your DB stays available during the resize, with a brief (single-digit-second) reconnect at the end. Plan to do the scale-up at least a few minutes before kicking off the migration so it's settled in.

5.5 Make sure you have a recent backup

Azure SQL DB takes automatic backups, but confirm the most recent backup is recent (last few hours) before starting. If anything goes wrong you want a clean rollback point.

5.6 Allow plenty of time in your maintenance window

Use the estimates in §3 and §4 and multiply by 2 when picking the maintenance window. Things you don't want to discover at the 90% mark:

  • The DB was busier than you thought.
  • A long-running query held a lock on urls and the ALTER COLUMN was waiting on it.
  • The auto-generated tempdb / log file growth was throttled.
  • The pre-flight scans found offending URLs and aborted, and now you need to fix them and re-run.

6. If the migration aborts (URLs longer than 850 chars)

If the pre-flight scan finds any URL that would be truncated (>850 chars), the migration aborts before changing anything and prints up to 50 offending rows on the SQL session messages. The database is left in exactly the state it was in before — there is no partial-apply state to clean up. (Non-Latin characters such as Greek do not abort the migration — nvarchar(850) stores them faithfully; only length matters.)

To find every offending row:

-- Too long: would be truncated to 850 chars
SELECT id, LEN(full_url) AS length, full_url
FROM dbo.urls
WHERE LEN(full_url) > 850
ORDER BY length DESC;

For each offending row, decide whether to:

  • Delete the URL row (and the hits / event_meta_sharepoint / etc. rows that reference it via url_id). Usually appropriate for obsolete or test URLs.
  • Correct the URL — e.g. trim the query string that pushed it past 850 chars.

Then re-run the upgrade. The migration is idempotent: it will pick up where the previous run left off.


7. During the upgrade

  • Watch the SQL session running the migration. The script uses RAISERROR ... WITH NOWAIT to surface live progress — you'll see messages like:
    ShrinkUrlsFullUrlColumn: dbo.urls row estimate = 10000000 ...
    ShrinkUrlsFullUrlColumn: pre-flight "too long" scan completed in 2511ms; 0 offending row(s).
    ShrinkUrlsFullUrlColumn: altering full_url to nvarchar(850) NOT NULL...
    ShrinkUrlsFullUrlColumn: ALTER COLUMN completed in 132690ms.
    ShrinkUrlsFullUrlColumn: EngineEdition=2, ONLINE=0. Creating [IX_urls_full_url]...
    ShrinkUrlsFullUrlColumn: [IX_urls_full_url] created in 189933ms.
    ShrinkUrlsFullUrlColumn: finished in 325136ms.
    
  • It is safe to leave it running, however long it takes. The installer's EF migration runner sets CommandTimeout = 0 (infinite), so the upgrade will not give up part-way through ALTER COLUMN even if it runs for many hours. The migration also runs outside the EF migration transaction (suppressTransaction: true), so each step commits as it completes. If the connection drops mid-way through CREATE INDEX, you can simply re-run the upgrade — the early steps are idempotent (they check whether they've already been done) and SQL Server will rebuild any partially-built index from scratch.

8. After the upgrade — quick sanity check

Run this against your DB:

SELECT
    (SELECT COUNT_BIG(*) FROM dbo.urls) AS urls_total,
    (SELECT t.name + '(' + CASE WHEN c.max_length = -1 THEN 'max'
              WHEN t.name LIKE 'n%' THEN CAST(c.max_length / 2 AS varchar(10))
              ELSE CAST(c.max_length AS varchar(10)) END + ')'
       FROM sys.columns c
       INNER JOIN sys.types t ON c.user_type_id = t.user_type_id
       WHERE c.object_id = OBJECT_ID('dbo.urls') AND c.name = 'full_url'
    )                                                                AS full_url_type,
    -- should be 'nvarchar(850)'
    (SELECT COUNT(*) FROM sys.indexes
       WHERE object_id = OBJECT_ID('dbo.urls') AND name = 'IX_urls_full_url'
    )                                                                AS ix_present,
    -- should be 1
    (SELECT COUNT_BIG(*) FROM dbo.urls WHERE LEN(full_url) > 850)    AS still_too_long;
    -- should be 0

Expected: full_url_type = 'nvarchar(850)', ix_present = 1, still_too_long = 0, urls_total unchanged from before.

Then check that the IX_urls_full_url index is actually being used by the staging merges (this is the whole reason for the migration). After the first importer run completes, the plan for the staging-merge join should show an index seek on IX_urls_full_url rather than a clustered-index scan. The simplest way to confirm:

SELECT TOP 5
    s.user_seeks, s.user_scans, s.last_user_seek, s.last_user_scan
FROM sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i ON s.object_id = i.object_id AND s.index_id = i.index_id
WHERE s.database_id = DB_ID()
  AND i.name = 'IX_urls_full_url';

user_seeks should be incrementing every time the App Insights / Activity / Copilot importers run.

Once that's confirmed:

  1. Re-enable Power BI scheduled refresh.
  2. Restart the web jobs.
  3. Scale the DB back down to its normal tier.

9. Quick reference summary

  • What changes: urls.full_url becomes nvarchar(850) NOT NULL (Unicode-safe — preserves Greek and any non-Latin URLs) and gets a new non-clustered index IX_urls_full_url. Row count is identical before and after.
  • Companion migrations: UrlFullUrlVarcharMapping (empty, superseded) and UrlFullUrlNvarchar (refreshes the EF model snapshot to nvarchar(850)). Both are millisecond no-ops — except when upgrading from the old varchar(1700) build, where UrlFullUrlNvarchar performs the varchar(1700) → nvarchar(850) conversion.
  • Time: ~37 µs per row when the table fits in memory; significantly worse once it doesn't on slow storage like Standard HDD. Azure SQL DB (all tiers use SSD) degrades much more gracefully.
  • Storage cost: post-migration urls grows because URLs move from LOB pages into the clustered PK leaf pages and are stored as 2-byte nvarchar. Budget ~3.5–4× current urls size in data-file headroom (the ~2.5× figure elsewhere on this page was measured on the old varchar(1700) form).
  • Worst-case time: ~2 days for 500M rows on S3 (100 DTU), or longer on a Standard HDD VM. Scale up.
  • Best ROI tweak:
    • Azure SQL DB customers: temporarily scale to S7 or Premium (P2 / P4 / P6) for the upgrade — the runtime drops several-fold.
    • VM-hosted SQL customers: if your data disk is Standard HDD or Standard SSD and you have 25M+ rows, switch the data disk to Premium SSD before the upgrade.
  • Before: stop web jobs, disable Power BI refresh, free up data-file headroom, scale up, confirm backup, check for URLs longer than 850 chars so the migration doesn't abort (non-Latin / Greek URLs are fine — only length matters).
  • During: watch the SQL session messages; safe to leave running for as long as it takes — the installer's EF migration command timeout is infinite, so it won't abort mid-rewrite.
  • After: verify full_url is nvarchar(850), IX_urls_full_url is present, row count is unchanged, then scale back and re-enable jobs.

Based on a controlled benchmark of DatabaseUpgrader.CheckDbUpgraded against a freshly seeded database with 10,000,000 urls rows (avg URL 152 chars) on an Azure VM with Standard HDD disks and SQL Server 2025 Standard Developer Edition (the offline-index case). The benchmark was measured on the superseded varchar(1700) form (total 6 min 6 sec, including a ~41 sec "lossy varchar" scan that no longer runs). The nvarchar(850) operation is the same shape, with somewhat larger storage / log because nvarchar is 2 bytes/char.

Clone this wiki locally