SQL Server'da Veri Taşıma: BULK INSERT, bcp, OPENROWSET, BACPAC Export/Import ve Veritabanını Kopyalamak
BULK INSERT'te MAXERRORS her hatayı atlamaz: kolon sayısı bozuk tek bir CSV satırı 200.000 satırlık yüklemeyi sıfırda durdurdu. Dosya yolunun sunucuya göre çözülmesi, Linux'ta desteklenmeyen CODEPAGE, CRLF dosyasında son kolona yapışan CR, staging ve doğrulama, BULK_LOGGED ve TABLOCK ile 43 MB'tan 0,5 MB'a inen log, OPENROWSET ve bcp, anonimleştirilmiş kopyadan SqlPackage ile BACPAC export/import.
Sürüm notu: Bu yazıdaki örnekler SQL Server 2025 CU10 (17.0.5035.5, Enterprise Developer Edition) üzerinde, Linux container'da serinin Bookstore laboratuvar veritabanıyla; bcp, mssql-tools18 ile aynı container'da; BACPAC (şemayı ve veriyi birlikte taşıyan dışa aktarım dosyası) işlemleri SqlPackage 170.5.96 ile çalıştırılmıştır.
Önce Tahmin Et: 200 Bin Satırda Tek Hatalı Satır
Sitedeki serilerin ortak örnek uygulaması Bookstore'a (bir online kitapçı) bir yayınevi her ay fiyat listesini CSV (virgülle ayrılmış değerler) olarak gönderiyor: 200.000 satır, başlık satırı, UTF-8 kodlaması (Unicode'un en yaygın kodlaması), Türkçe kitap adları, tırnak içindeki virgüllü adlar ("Savaş ve Barış, Cilt 1"). Dosyayı önce bütün kolonları metin olan bir staging tablosuna yüklüyoruz:
CREATE TABLE dbo.PriceListStaging
(
Isbn nvarchar(20) NULL,
Title nvarchar(200) NULL,
ListPrice nvarchar(20) NULL,
Currency nvarchar(10) NULL,
ValidFrom nvarchar(20) NULL
);
Bu ayki dosyanın 8. satırında yayınevinin sistemi fiyatı Türkçe ondalık ayracıyla ve tırnaksız yazmış: 9780000000007,"Çalıkuşu #7",18,05,TRY,2026-12-01. Güvende olmak için 100 hataya kadar izin veriyor ve hatalı satırları bir dosyaya yazdırıyoruz:
TRUNCATE TABLE dbo.PriceListStaging;
BULK INSERT dbo.PriceListStaging
FROM '/var/opt/mssql/import/prices_v1.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2, MAXERRORS = 100,
ERRORFILE = '/var/opt/mssql/import/prices_v1_errors.txt');
GO
SELECT COUNT(*) AS rows_loaded FROM dbo.PriceListStaging;
Kaç satır yüklenir: 199.999 mu, 200.000 mü?
Msg 4879, Level 16, State 1, Server sql1, Line 2
Bulk load failed due to invalid column value in CSV data file /var/opt/mssql/import/prices_v1.csv in row 8, column 5.
Msg 7399, Level 16, State 1, Server sql1, Line 2
The OLE DB provider "BULK" for linked server "(null)" reported an error. The provider did not give any information about the error.
Msg 7330, Level 16, State 2, Server sql1, Line 2
Cannot fetch a row from OLE DB provider "BULK" for linked server "(null)".
rows_loaded
-----------
0
9780000000007,"Çalıkuşu #7",18,05,TRY
Row 8 File Offset 397 ErrorFile Offset 0 - HRESULT 0x80004005
Sıfır. Hata dosyası satırı yakaladı, ama yükleme yine de tamamen durdu. MAXERRORS bir değeri hedef tipe çeviremeyen satırları atlamak içindir; bir satırın kolon sayısı yanlışsa CSV ayrıştırıcısı dosyanın geri kalanına güvenemez ve durur. Yayınevi aynı fiyatı "18,05" diye tırnak içinde yazsaydı satır yüklenir, sorun ancak doğrulamada ortaya çıkardı. Bu yazıda BULK INSERT, bcp ve OPENROWSET ile dosyadan veri yüklemeyi, satır sonu ve karakter kodlaması tuzaklarını, minimal logging'i ve veritabanının tamamını SqlPackage ile BACPAC olarak taşımayı ölçerek inceliyoruz.
BULK INSERT: Dosya Kimin Makinesinde?
BULK INSERT dosyayı SQL Server'ın kendisi okur; yol, sorguyu gönderdiğin makineye göre değil, SQL Server'ın çalıştığı makineye göre çözülür. Container'da bu, container'ın dosya sistemi demektir:
BULK INSERT dbo.PriceListStaging
FROM 'C:\Users\user\Downloads\prices.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2);
Msg 4860, Level 16, State 1, Server sql1, Line 1
Cannot bulk load. The file "C:\Users\user\Downloads\prices.csv" does not exist or you don't have file access rights. See https://go.microsoft.com/fwlink/?LinkId=2374938 for more information.
Dosyayı container'a docker cp ile kopyaladım (ya da bir volume'a koyabilirsin); Windows'taki bir SQL Server ise ağdaki dosyayı UNC (ağ paylaşımı yolu, \\server\share\file) ile okur ve SQL Server servis hesabının o paylaşıma erişimi olmalıdır. Yetki olarak INSERT ve ADMINISTER BULK OPERATIONS gerekir. Linux'ta bu yetki ve bulkadmin rolü SQL Server 2025 CU3'ten (Cumulative Update 3; üçüncü toplu güncelleme) beri destekleniyor; daha önce yalnız sysadmin yükleyebiliyordu.
FORMAT = 'CSV' (SQL Server 2017'den beri) CSV biçimini tanımlayan RFC (internet standartları belgesi) 4180'in kurallarını uygular: tırnak içindeki virgül alan ayracı sayılmaz, tırnak karakteri FIELDQUOTE ile değiştirilebilir. FIRSTROW = 2 başlık satırını atlar. Bu seçenek olmadan yalnız FIELDTERMINATOR = ',' ile yüklenen bir CSV, "Savaş ve Barış, Cilt 1" adını iki kolona böler.
Karakter Kodlaması ve Satır Sonu
Windows'taki SQL Server'da bir UTF-8 dosyası CODEPAGE = '65001' ile okunur; belirtmezsen varsayılan OEM kod sayfası kullanılır ve Türkçe karakterler bozulur. Aynı seçenek Linux'ta yok:
TRUNCATE TABLE dbo.PriceListStaging;
BULK INSERT dbo.PriceListStaging
FROM '/var/opt/mssql/import/prices.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2, CODEPAGE = '65001');
Msg 16202, Level 15, State 3, Server sql1, Line 4
Keyword or statement option 'CODEPAGE' is not supported on the 'Linux' platform.
Linux'ta CODEPAGE'siz yükleme bu dosyada Türkçe karakterleri doğru okudu (aşağıdaki çıktılarda görülüyor). Yani aynı script iki platformda aynı çalışmıyor: Windows için CODEPAGE şart, Linux'ta hata. Script'ini hangi sunucuda çalışacağını bilerek yaz.
İkinci tuzak satır sonu. Linux ve macOS'ta üretilen dosyalar satırı yalnız LF (satır besleme karakteri, 0x0A) ile, Excel'in CSV UTF-8 çıktısı CRLF (satır başı ve satır besleme, 0x0D0A) ile bitirir. Excel'den gelen dosyayı LF ayracıyla yükledim:
TRUNCATE TABLE dbo.PriceListStaging;
BULK INSERT dbo.PriceListStaging
FROM '/var/opt/mssql/import/prices_excel.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2, ROWTERMINATOR = '0x0a');
SELECT TOP (2) Isbn, Title, ValidFrom, LEN(ValidFrom) AS len,
CAST(RIGHT(ValidFrom, 1) AS varbinary(2)) AS last_char, TRY_CONVERT(date, ValidFrom) AS valid_from
FROM dbo.PriceListStaging WHERE Isbn IN ('9780000000001', '9780000000002');
Isbn Title ValidFrom len last_char valid_from
------------- ------------------ ----------- --- --------- ----------
9780000000001 Şeker Portakalı #1 2026-12-01 11 0x0D00 NULL
9780000000002 Küçük Prens #2 2026-12-01 11 0x0D00 NULL
200.000 satırın hepsi yüklendi ve hiçbir hata yok, ama son kolon 11 karakter: ekranda görünmeyen CR (0x0D) değerin sonunda kaldı ve tarih dönüşümü sessizce NULL verdi. Doğru ayraçla:
TRUNCATE TABLE dbo.PriceListStaging;
BULK INSERT dbo.PriceListStaging
FROM '/var/opt/mssql/import/prices_excel.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2, ROWTERMINATOR = '0x0d0a');
SELECT TOP (2) Isbn, Title, ValidFrom, LEN(ValidFrom) AS len, TRY_CONVERT(date, ValidFrom) AS valid_from
FROM dbo.PriceListStaging WHERE Isbn IN ('9780000000001', '9780000000002');
Isbn Title ValidFrom len valid_from
------------- ------------------ ---------- --- ----------
9780000000001 Şeker Portakalı #1 2026-12-01 10 2026-12-01
9780000000002 Küçük Prens #2 2026-12-01 10 2026-12-01
Ayracı onaltılık yazmanın nedeni Microsoft Learn'deki bir not: Windows'ta \n otomatik olarak \r\n'ye çevrilir, yani ROWTERMINATOR = '\n' aynı script'te Windows'ta CRLF, Linux'ta LF demektir. 0x0a ve 0x0d0a her yerde aynı anlamdadır. Excel dosyasının başındaki UTF-8 BOM (dosyanın kodlamasını belirten görünmez ilk üç byte) bu denemelerde sorun çıkarmadı: başlık satırı atlandığında BOM da atlanıyor, başlıksız bir dosyada da ilk ISBN'e (kitapların uluslararası standart numarası) karışmadı. Yine de doğrulamada ilk satırın ilk değerine bak.
Staging, Doğrulama ve Uygulama
Yayınevinin düzelttiği dosyayı (fiyat artık "18,05" tırnak içinde) yükledik. Staging tablosu heap ve bütün kolonları metin; hiçbir satır tip yüzünden reddedilmiyor, doğrulama SQL'de yapılıyor:
TRUNCATE TABLE dbo.PriceListStaging;
BULK INSERT dbo.PriceListStaging
FROM '/var/opt/mssql/import/prices.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2, ROWTERMINATOR = '0x0a', TABLOCK);
SELECT
COUNT(*) AS total_rows,
SUM(CASE WHEN NULLIF(s.Isbn, '') IS NULL THEN 1 ELSE 0 END) AS missing_isbn,
SUM(CASE WHEN TRY_CONVERT(decimal(10,2), s.ListPrice) IS NULL THEN 1 ELSE 0 END) AS bad_price,
SUM(CASE WHEN TRY_CONVERT(date, s.ValidFrom) IS NULL THEN 1 ELSE 0 END) AS bad_date,
SUM(CASE WHEN b.BookId IS NOT NULL THEN 1 ELSE 0 END) AS known_books,
SUM(CASE WHEN b.BookId IS NULL AND NULLIF(s.Isbn, '') IS NOT NULL THEN 1 ELSE 0 END) AS new_titles
FROM dbo.PriceListStaging AS s
LEFT JOIN dbo.Books AS b ON b.Isbn = s.Isbn;
SELECT TOP (3) s.Isbn, s.Title, s.ListPrice
FROM dbo.PriceListStaging AS s
WHERE TRY_CONVERT(decimal(10,2), s.ListPrice) IS NULL;
total_rows missing_isbn bad_price bad_date known_books new_titles
---------- ------------ --------- -------- ----------- ----------
200000 4 10 0 39999 159997
Isbn Title ListPrice
------------- --------------- ---------
9780000000007 Çalıkuşu #7 18,05
9780000020007 Çalıkuşu #20007 37,51
9790000040007 Çalıkuşu #40007 24,33
Doğrulama raporu tek sorguda: 4 satırda ISBN yok, 10 fiyat ondalık virgülle yazılmış, 39.999 kitap katalogda var, 159.997 başlık bizde satılmıyor. Rapor yayınevine geri gider; geçerli satırlar uygulanır:
SET XACT_ABORT ON;
BEGIN TRANSACTION;
UPDATE b
SET b.Price = TRY_CONVERT(decimal(10,2), s.ListPrice)
FROM dbo.Books AS b
JOIN dbo.PriceListStaging AS s ON s.Isbn = b.Isbn
WHERE TRY_CONVERT(decimal(10,2), s.ListPrice) > 0
AND s.Currency = 'TRY'
AND b.Price <> TRY_CONVERT(decimal(10,2), s.ListPrice);
SELECT @@ROWCOUNT AS prices_updated;
COMMIT;
prices_updated
--------------
39997
39.999 bilinen kitaptan ikisinin fiyatı hatalıydı; 39.997 fiyat güncellendi. Bu akışın uygulama tarafındaki karşılığı, yüklemeyi kullanıcının sitesinden alıp satır bazında hata dosyası üreten tasarım, Çözüm Desenleri serisinin "Toplu Import ve Validation Raporu" yazısında; UPDATE ... FROM ve MERGE ile upsert'ün ayrıntıları SQL Temelleri serisinin 15. yazısında. Uygulama dosyayı kendisi okuyorsa veriyi sunucuya göndermenin en hızlı yolu SqlBulkCopy'dir (Dapper serisinin performans yazısı); sunucu tarafında dosya yolu ve yetki sorunu da ortadan kalkar.
TABLOCK ve Minimal Logging
Aynı 200.000 satırı dört kez yükleyip her yüklemenin ürettiği log'u ölçtüm:
SET NOCOUNT ON;
ALTER DATABASE sqls_23 SET RECOVERY FULL;
BACKUP DATABASE sqls_23 TO DISK = '/var/opt/mssql/backup/sqls_23_full.bak' WITH COMPRESSION, INIT;
DECLARE @r TABLE (recovery_model nvarchar(20), options varchar(20), log_mb decimal(8,1));
DECLARE @i int = 1, @rm nvarchar(20), @tl bit;
WHILE @i <= 4
BEGIN
SELECT @rm = CASE WHEN @i <= 2 THEN N'FULL' ELSE N'BULK_LOGGED' END, @tl = @i % 2;
IF @i = 3 ALTER DATABASE sqls_23 SET RECOVERY BULK_LOGGED;
TRUNCATE TABLE dbo.PriceListStaging;
BACKUP LOG sqls_23 TO DISK = '/var/opt/mssql/backup/sqls_23_log.trn' WITH INIT;
IF @tl = 1
BULK INSERT dbo.PriceListStaging FROM '/var/opt/mssql/import/prices.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2, ROWTERMINATOR = '0x0a', TABLOCK);
ELSE
BULK INSERT dbo.PriceListStaging FROM '/var/opt/mssql/import/prices.csv'
WITH (FORMAT = 'CSV', FIRSTROW = 2, ROWTERMINATOR = '0x0a');
INSERT @r SELECT @rm, CASE WHEN @tl = 1 THEN 'TABLOCK' ELSE '-' END, log_since_last_log_backup_mb FROM sys.dm_db_log_stats(DB_ID());
SET @i += 1;
END;
ALTER DATABASE sqls_23 SET RECOVERY SIMPLE;
SELECT * FROM @r;
recovery_model options log_mb
-------------- ------- ------
FULL TABLOCK 26.3
FULL - 43.6
BULK_LOGGED TABLOCK .5
BULK_LOGGED - 43.4
Minimal logging (satırlar yerine yalnız page allocation'larının log'lanması) için koşullar birlikte sağlanmalı: recovery model SIMPLE ya da BULK_LOGGED, hedef heap (ya da belirli koşullarda boş bir index), TABLOCK. Üçü bir aradayken 200.000 satır yarım MB log üretti; biri eksikken 43 MB. FULL'de TABLOCK log'u azalttı ama satırlar yine log'lanıyor. Bu yüzden staging tablosu heap ve index'siz; toplu yükleme için BULK_LOGGED'a geçip dönmek, 20. yazıda gördüğümüz gibi o aralığı kapsayan log backup'ında point-in-time restore'u kaybettirir. Staging tablosunu ayrı, SIMPLE recovery'deki bir veritabanında tutmak bu bedeli ortadan kaldırır.
OPENROWSET(BULK): Dosyayı Yüklemeden Sorgulamak
OPENROWSET(BULK ...) dosyayı bir tablo gibi okur; yüklemeden önce dosyaya göz atmak, filtrelemek ya da katalogla birleştirmek için kullanışlıdır. Kolonları tarif etmek için bir format dosyası gerekir; bcp staging tablosundan üretir:
bcp sqls_23.dbo.PriceListStaging format /dev/null -c -t, -r 0x0a -f prices.fmt -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -u
14.0
5
1 SQLCHAR 0 40 "," 1 Isbn SQL_Latin1_General_CP1_CI_AS
2 SQLCHAR 0 400 "," 2 Title SQL_Latin1_General_CP1_CI_AS
3 SQLCHAR 0 40 "," 3 ListPrice SQL_Latin1_General_CP1_CI_AS
4 SQLCHAR 0 20 "," 4 Currency SQL_Latin1_General_CP1_CI_AS
5 SQLCHAR 0 40 "\n" 5 ValidFrom SQL_Latin1_General_CP1_CI_AS
Linux'taki bcp XML format dosyası (-x) üretmiyor; bu klasik metin biçimi. Fiyatı en çok artacak beş kitabı yüklemeden sorguladım:
SELECT TOP (5) p.Isbn, p.Title, b.Price AS current_price, p.ListPrice AS new_price
FROM OPENROWSET(BULK '/var/opt/mssql/import/prices.csv',
FORMAT = 'CSV', FIRSTROW = 2,
FORMATFILE = '/var/opt/mssql/import/prices.fmt') AS p
JOIN dbo.Books AS b ON b.Isbn = p.Isbn
ORDER BY TRY_CONVERT(decimal(10,2), p.ListPrice) - b.Price DESC;
Isbn Title current_price new_price
------------- ------------------------------- ------------- ---------
9780000000431 Şeker Portakalı #431 49.99 53.99
9780000003493 Savaş ve Barış, Cilt 1 #3493 49.95 53.95
9780000004044 Yüzyıllık Yalnızlık #4044 49.96 53.96
9780000004675 Gurur ve Önyargı #4675 49.97 53.97
9780000005728 İnce Memed #5728 49.99 53.99
Sayılar doğru, metin bozuk. Format dosyasındaki SQLCHAR alanları UTF-8 byte'larını tek byte'lık bir kod sayfasıyla okudu; format dosyasındaki collation'ı bir UTF-8 collation'ına (Latin1_General_100_CI_AS_SC_UTF8) çevirmek bu denemede sonucu değiştirmedi. Aynı dosya BULK INSERT ile nvarchar staging'e doğru yüklendiği için, Linux'ta OPENROWSET'i ISBN, fiyat ve tarih gibi ASCII (İngilizce harf ve rakamlardan oluşan temel karakter kümesi) kolonları kontrol etmek için kullan, metni staging'den oku. Windows'ta OPENROWSET da CODEPAGE = '65001' alır.
bcp: İstemci Tarafında Toplu Kopyalama
bcp, BULK INSERT'ün tersine dosyayı kendisinin çalıştığı makinede okur ve yazar; veriyi sunucuya ağdan gönderir. Klasik kitapları sekmeyle ayrılmış bir dosyaya aktarıp başka bir tabloya geri yükledim:
bcp "SELECT Isbn, Title, Price FROM dbo.Books WHERE Category = 'Classic'" queryout classics.tsv -c -t "\t" -d sqls_23 -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -u -b 5000
bcp dbo.ClassicsCopy in classics.tsv -c -t "\t" -d sqls_23 -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -u -b 5000 -h "TABLOCK"
Starting copy...
1000 rows successfully bulk-copied to host-file. Total received: 1000
...
22000 rows copied.
Starting copy...
5000 rows sent to SQL Server. Total sent: 5000
...
22000 rows copied.
-c karakter modu, -t alan ayracı, -b 5000 her 5.000 satırda bir commit (büyük bir yükleme yarıda kesilirse en çok bir batch kaybedilir), -h "TABLOCK" minimal logging ipucu, -u ise mssql-tools18'de sunucu sertifikasına güvenmek içindir. bcp CSV tırnaklarını anlamaz; tırnaklı CSV için BULK INSERT ... FORMAT = 'CSV' kullan, bcp'yi kendi ürettiği sekmeli ya da native (-n) dosyalar için sakla. Komut satırındaki -P parolası süreç listesinde görünür; gerçek bir sunucuda Windows Authentication ya da Microsoft Entra kimliği tercih et.
Veritabanını Taşımak: BACPAC ve Anonimleştirilmiş Geliştirici Kopyası
Geliştirme ekibi gerçeğe yakın veri istiyor. Üretim verisini olduğu gibi geliştiricinin makinesine koymak hem güvenlik hem de KVKK (Kişisel Verilerin Korunması Kanunu) açısından yanlış; kişisel veri, geliştirme ortamında üretimdeki korumaların hiçbiri olmadan durur. Akış şu: üretimin COPY_ONLY bir backup'ını yan bir veritabanına restore et (20. yazı), orada kişisel verileri anonimleştir, BACPAC'i oradan al:
UPDATE dbo.Customers
SET FirstName = CONCAT(N'Customer', CustomerId),
LastName = N'Test',
Email = CONCAT('customer', CustomerId, '@example.test'),
City = N'-';
SELECT TOP (2) CustomerId, FirstName, LastName, Email, City FROM dbo.Customers ORDER BY CustomerId;
CustomerId FirstName LastName Email City
---------- --------- -------- ---------------------- ----
1 Customer1 Test customer1@example.test -
2 Customer2 Test customer2@example.test -
sqlpackage /Action:Export /SourceServerName:"localhost,14333" /SourceDatabaseName:sqls_23_anon /SourceUser:sa /SourcePassword:"$MSSQL_SA_PASSWORD" /SourceTrustServerCertificate:True /TargetFile:bookstore-dev.bacpac
sqlpackage /Action:Import /TargetServerName:"localhost,14333" /TargetDatabaseName:sqls_23_dev /TargetUser:sa /TargetPassword:"$MSSQL_SA_PASSWORD" /TargetTrustServerCertificate:True /SourceFile:bookstore-dev.bacpac
Connecting to database 'sqls_23_anon' on server 'localhost,14333'.
...
Successfully exported database and saved it to file '/home/user/bookstore/bookstore-dev.bacpac'.
Importing to database 'sqls_23_dev' on server 'localhost,14333'.
...
Disabling index 'UX_Customers_Email'.
...
Enabling index 'UX_Customers_Email'.
...
Successfully imported database.
SELECT d.name, CAST(SUM(f.size) * 8 / 1024 AS int) AS data_mb, COUNT(*) AS files,
(SELECT COUNT(*) FROM sqls_23_dev.dbo.Orders) AS orders,
(SELECT TOP (1) Email FROM sqls_23_dev.dbo.Customers ORDER BY CustomerId) AS first_email
FROM sys.databases AS d JOIN sys.master_files AS f ON f.database_id = d.database_id AND f.type = 0
WHERE d.name = 'sqls_23_dev'
GROUP BY d.name;
name data_mb files orders first_email
----------- ------- ----- ------- ----------------------
sqls_23_dev 456 1 1000000 customer1@example.test
BACPAC, şemayı ve her tablonun verisini içeren bir zip dosyası; bu veritabanı için 75 MB'tı (20. yazıdaki sıkıştırılmış full backup 112 MB). Import sırasında SqlPackage önce index'leri kapatıp veriyi yüklüyor, sonra index'leri yeniden oluşturuyor. Anonimleştirmenin ne kadarının yeterli olduğu (e-posta ve isim yetmez; adres, telefon, doğum tarihi, serbest metin notları da kişisel veridir) ilerideki Veri Gizliliği ve KVKK serisinin konusu.
| Yöntem | Ne taşır | Ne zaman | Dikkat |
|---|---|---|---|
| Backup/restore | Veritabanının tamamı, page olarak | Aynı ya da daha yeni sürümdeki SQL Server'a hızlı kopya | Daha eski sürüme ve Azure SQL Database'e restore edilemez |
| BACPAC (SqlPackage) | Şema ve veri, mantıksal olarak | Azure SQL Database'e taşıma, sürümler arası, seçilmiş tablolar | Export sırasında yazma varsa tutarlı değildir; büyük veritabanında yavaştır |
| DACPAC (yalnız şemayı taşıyan paket dosyası) | Yalnız şema | Şema karşılaştırma ve deploy (24. yazı) | Veri taşımaz |
BULK INSERT, bcp | Tek tablo, dosyadan/dosyaya | Dış sistemlerle veri alışverişi | Kodlama, satır sonu, dosya yolu |
Tablodaki tutarlılık uyarısı önemli: Microsoft'un SqlPackage belgesine göre export, sırasında yazma olmayan bir veritabanından ya da tutarlı bir kopyadan alınmalı; yoksa tablolar farklı anlarda okunur ve foreign key'leri tutmayan satırlar oluşabilir. Yukarıdaki akışta export'u restore edilmiş kopyadan almamızın ikinci nedeni bu.
SQL Server 2019/2022'de farkı: FORMAT = 'CSV' 2017'den beri var. Linux'ta CODEPAGE 2019'da yalnız 'RAW' değerini kabul ediyordu, bu yazıda 2025'te '65001' reddedildi. BULK INSERT Azure Blob Storage'daki dosyaları 2017'den beri okuyabiliyor; 2022'de veri sanallaştırmayla S3 (Amazon'un object storage API'si) uyumlu storage'daki CSV ve Parquet dosyaları da sorgulanabilir oldu; 2025'te CSV, Parquet ve Delta dosyalarını sorgulamak için ayrı bir PolyBase servisi gerekmiyor.
Üretimde Dikkat
Yüklemeleri staging'e yap, doğrula, sonra tek bir transaction'da uygula. Doğrulama raporunu yükleme kadar önemli bir çıktı olarak sakla.
Büyük yüklemeyi batch'lere böl (BATCHSIZE, bcp -b). Tek bir dev transaction log'u büyütür ve yarıda kalırsa her şey geri alınır.
Dosyanın kodlamasını ve satır sonunu yüklemeden önce kontrol et ve yayınevinden gelen dosyanın biçimini bir sözleşme olarak yazılı hâle getir: ayraç, tırnak, ondalık ayracı, kodlama.
Sık Düşülen Tuzaklar
MAXERRORS'ın her hatayı atlayacağını sanmak. Kolon sayısı bozuk bir CSV satırı yüklemeyi tamamen durdurur.
Dosya yolunun SQL Server'ın makinesine göre çözüldüğünü unutmak. Container'da yol, container'ın dosya sistemidir; dosyayı bir volume'a ya da docker cp ile içeri koy.
Satır sonu farkını gözden kaçırmak. CRLF dosyası LF ile yüklenince hata vermez; her satırın son kolonunun sonunda görünmez bir CR kalır ve dönüşümler sessizce NULL döner.
Aynı script'in Windows ve Linux'ta aynı çalışacağını varsaymak. CODEPAGE Windows'ta gerekli, Linux'ta hata; \n Windows'ta CRLF, Linux'ta LF.
Üretim verisini anonimleştirmeden geliştirme ortamına kopyalamak.
Küçük Bir Deneme
★☆☆ Fiyat listesini FORMAT = 'CSV' olmadan, yalnız FIELDTERMINATOR = ',' ile yükle. "Savaş ve Barış, Cilt 1" satırı nasıl görünüyor?
★★☆ Staging tablosuna bir clustered index ekle ve BULK_LOGGED + TABLOCK ölçümünü tekrarla. Tablo boşken ve doluyken log nasıl değişiyor?
★★★ SqlPackage'ın /p:TableData özelliğiyle yalnız katalog tablolarını (Authors, Books, StockItems) içeren bir BACPAC al ve boyutunu tam BACPAC'le karşılaştır. Foreign key'li bir tabloyu dışarıda bırakınca import ne diyor?
Veriyi taşımayı gördük; geriye şemayı taşımak kaldı. Bir sonraki ve serinin son yazısında şema değişikliğini SQL Database Project ve DACPAC ile (state-based) ve DbUp ile Flyway ile (migration-based) yönetmeyi karşılaştıracağız.