SQL Lab'ı: SQL Server 2025'i Docker'da Çalıştırmak, VS Code MSSQL Eklentisi ve Bookstore Örnek Veritabanı
SQL Server 2025'i mcr.microsoft.com/mssql/server:2025-latest image'ıyla, parolayı environment variable'da tutarak ve volume ile Docker'da çalıştırıyor; sqlcmd, VS Code mssql eklentisi ve SSMS 22 ile bağlanıyor, seri boyunca kullanacağımız 10 table'lık Bookstore veritabanını yüklüyoruz. Edition seçimi, Apple Silicon'da Rosetta emülasyonu ve TrustServerCertificate'in yalnız lab için olduğu da işleniyor.
Sürüm notu: Bu yazıdaki SQL (Structured Query Language) sorguları SQL Server 2025 (17.0, Cumulative Update 10) üzerinde, macOS 27.0.1 ve Docker Desktop 4.64.0 ile çalıştırılmıştır; container içindeki sqlcmd sürümü 18.7.
Önce Tahmin Et: Apple Silicon'daki Container Hangi İşlemciyi Görüyor?
Bu serinin bütün sorguları, M serisi işlemcili bir Mac'te Docker içinde çalışan SQL Server 2025'te koşuyor. Mac'in işlemcisi ARM64 (Arm'ın 64 bit işlemci mimarisi) üzerinde çalışıyor. Şimdi container'a bağlanıp sunucuya kim olduğunu soruyoruz. Sence sürüm satırının sonunda hangi mimari yazar?
SELECT @@VERSION AS version;
version
--------------------------------------------------------------------------
Microsoft SQL Server 2025 (RTM-CU10) (KB5125471) - 17.0.5035.5 (X64)
Oct 15 2026 19:55:26
Copyright (C) 2025 Microsoft Corporation
Enterprise Developer Edition (64-bit) on Linux (Ubuntu 24.04.5 LTS) <X64>
(1 rows affected)
X64. Sunucu kendini Intel ya da AMD (Advanced Micro Devices) işlemcili bir Ubuntu makinesinde sanıyor. Container'ın içindeki işlemci bilgisi ise hikâyenin geri kalanını anlatıyor:
docker exec bookstore-sql cat /proc/cpuinfo | grep -m1 'model name'
model name : VirtualApple @ 2.50GHz
"VirtualApple", Apple'ın Rosetta çeviri katmanı. SQL Server'ın Linux image'ı yalnız x86-64 için yayımlanıyor; Apple Silicon'da çalışabilmesinin tek sebebi, Docker'ın her x86 komutunu Rosetta ile ARM64'e çevirmesi. Bu yazıda bu laboratuvarı adım adım kuracağız: hangi edition, hangi Docker komutu, parolayı nerede tutmalı, hangi araçla bağlanmalı ve serinin tamamında kullanacağımız Bookstore veritabanı nasıl yüklenir. Emülasyonun bir de yan etkisi var; onu "Üretimde Dikkat" bölümünde konuşacağız.
Hangi Edition? Express, Standard Developer ve Enterprise Developer
SQL Server 2025 ile edition listesi değişti. Daha önce tek bir Developer edition vardı; artık iki tane var. Lab için önemli olanlar:
| Edition | Lisans | Önemli sınırlar |
|---|---|---|
| Express | Ücretsiz, production'da kullanılabilir | Veritabanı başına 50 GB, en fazla 1 socket veya 4 core, buffer pool için 1.410 MB |
| Standard Developer | Ücretsiz, yalnız geliştirme ve test | Standard edition'ın özellik seti |
| Enterprise Developer | Ücretsiz, yalnız geliştirme ve test | Enterprise edition'ın bütün özellikleri |
| Evaluation | 180 günlük deneme | Enterprise edition'ın bütün özellikleri |
Express'in veritabanı sınırı 2025'te 10 GB'tan 50 GB'a çıktı ve eskiden "Express with Advanced Services" adıyla ayrı dağıtılan özellikler artık Express'in içinde. Yine de Express'i lab için seçmek için bir sebep yok. Production'da Standard kullanacaksan Standard Developer, Enterprise kullanacaksan Enterprise Developer seç. Böylece geliştirirken kullandığın bir özelliğin production edition'ında olmadığını son anda öğrenmezsin. Container'da edition'ı MSSQL_PID environment variable'ı seçer; 2025 için geçerli değerler arasında Express, StandardDeveloper ve EnterpriseDeveloper var. Microsoft'un quickstart örneği Developer değerini kullanıyor ve bu laboratuvarda o değer Enterprise Developer'a karşılık geliyor:
SELECT SERVERPROPERTY('Edition') AS edition,
SERVERPROPERTY('ProductVersion') AS product_version,
SERVERPROPERTY('ProductUpdateLevel') AS update_level,
SERVERPROPERTY('Collation') AS server_collation;
edition product_version update_level server_collation
------------------------------------- --------------- ------------ ----------------------------
Enterprise Developer Edition (64-bit) 17.0.5035.5 CU10 SQL_Latin1_General_CP1_CI_AS
(1 rows affected)
ProductUpdateLevel sütunundaki CU10 (Cumulative Update 10), sunucuya kurulu son toplu güncellemeyi gösteriyor. SQL Server'ın bakım modeli artık service pack değil, düzenli aralıklarla çıkan cumulative update'ler. 2025-latest tag'i her zaman en son cumulative update'i çeker.
SQL Server 2025'i Docker'da Çalıştırmak
Önce parolayı ayarla. Microsoft'un varsayılan parola politikası en az 8 karakter ve büyük harf, küçük harf, rakam, sembol gruplarından en az üçünü istiyor; uymazsan container kurulumu yarıda bırakıp kapanıyor ve sebebi yalnız docker logs'ta görünüyor. Parolayı komut satırına yazma, shell history'ye düşer. Gizli okuyup environment variable olarak tut:
# parolayı ekrana yazdırmadan oku
read -rs MSSQL_SA_PASSWORD
export MSSQL_SA_PASSWORD
docker run -d --name bookstore-sql --hostname bookstore-sql \
--platform linux/amd64 \
-e "ACCEPT_EULA=Y" -e MSSQL_SA_PASSWORD -e "MSSQL_PID=Developer" \
-p 1433:1433 \
-v bookstore-sql-data:/var/opt/mssql \
mcr.microsoft.com/mssql/server:2025-latest
Üç ayrıntıya dikkat. -e MSSQL_SA_PASSWORD değer içermiyor; Docker bu yazımda değeri senin shell'indeki aynı isimli değişkenden alır, böylece parola ne komutta ne de history'de görünür. --platform linux/amd64 Apple Silicon'da Docker'a hangi image varyantını istediğini açıkça söyler; zaten tek varyant bu, ama açık yazınca uyarı mesajı görmezsin. -v bookstore-sql-data:/var/opt/mssql ise veritabanı dosyalarını isimli bir volume'a koyar. Volume olmadan container'ı sildiğin anda bütün veritabanların da gider. Bu yazının çıktılarını aldığım laboratuvar container'ı aynı image'la ve aynı MSSQL_PID=Developer ayarıyla, ama başka bir isimle ve volume'suz çalışıyor; tek kullanımlık bir test ortamı için bu bilinçli bir tercih, kendi lab'ın için değil.
Container ayağa kalkınca hazır olup olmadığını log'dan anlarsın:
docker logs bookstore-sql 2>&1 | grep -m1 'ready for client'
2026-11-07 17:57:05.21 spid43s SQL Server is now ready for client connections. This is an informational message; no user action is required.
Log'un başında bir satır daha dikkat çekici: "SQL Server 2025 will run as non-root by default." 2025 image'ı process'i root yerine mssql kullanıcısıyla başlatıyor. Bir host klasörünü (-v ~/sqldata:/var/opt/mssql gibi) bağlarsan, o klasörün bu kullanıcıya yazma izni vermesi gerekir; isimli volume'da bu sorun çıkmaz.
Bağlanmak: sqlcmd, VS Code ve SSMS
İlk bağlantı için ek bir şey kurmana gerek yok. Image'ın içinde ODBC (Open Database Connectivity) Driver 18 tabanlı mssql-tools18 paketi geliyor:
export SQLCMDPASSWORD="$MSSQL_SA_PASSWORD"
docker exec -it -e SQLCMDPASSWORD bookstore-sql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -C -W -Q "SELECT SUSER_SNAME() AS login_name, DB_NAME() AS db"
login_name db
---------- --
sa master
sqlcmd, -P verilmezse parolayı SQLCMDPASSWORD environment variable'ından okur; böylece parola process listesinde de görünmez. -C ise sunucunun sertifikasına koşulsuz güvenmek demek. Driver 18 ile bağlantılar varsayılan olarak şifreli ve container'ın sertifikası self-signed olduğu için bu bayrak olmadan bağlantı sertifika doğrulamasında düşer. Aynı şeyin .NET tarafındaki karşılığı connection string'deki TrustServerCertificate=True. İkisi de yalnız kendi makinendeki lab için kabul edilebilir.
Günlük çalışma için iki grafik araç var. Visual Studio Code'da Microsoft'un "SQL Server (mssql)" eklentisi macOS, Linux ve Windows'ta çalışıyor: connection dialog'una server olarak localhost,1433, authentication olarak SQL Login, kullanıcı olarak sa girip "Trust server certificate" seçeneğini işaretliyorsun. Query results grid'i, execution plan görüntüleyici, table designer ve schema designer eklentinin içinde; eklenti artık doğrudan local SQL Server container'ı oluşturmayı da destekliyor. Windows'taysan SSMS (SQL Server Management Studio) 22, 11 Kasım 2025'ten beri genel kullanımda ve en eksiksiz yönetim aracı. Bir de Go ile yeniden yazılmış yeni sqlcmd var; Homebrew ya da winget ile kurulup sqlcmd create mssql komutuyla container da açabiliyor. Ancak Microsoft'un dokümanına göre bu komut MSSQL_PID'i desteklemiyor ve her zaman Developer edition kuruyor.
Bookstore Örnek Veritabanı
Serinin bütün örnekleri aynı veritabanı üzerinde çalışıyor: kitap satan bir online kitapçı. Şema ve seed verisi tek bir script'te, SCHEMA.sql'de duruyor. Script idempotent: çalıştırdığın veritabanındaki on Bookstore table'ını silip yeniden oluşturuyor ve her seferinde birebir aynı row'ları ekliyor. Bir örneği bozduğunda script'i tekrar çalıştırman yeter.

Ben her yazı için ayrı bir veritabanı açıyorum (bu yazıda sqlt_02); sen tek bir bookstore veritabanıyla da ilerleyebilirsin:
docker exec -i -e SQLCMDPASSWORD bookstore-sql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -C -Q "CREATE DATABASE sqlt_02;"
docker exec -i -e SQLCMDPASSWORD bookstore-sql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -C -d sqlt_02 -f 65001 -I < SCHEMA.sql
Hata yoksa ikinci komut hiçbir şey yazmaz; script SET NOCOUNT ON ile başlıyor ve her şeyi tek bir transaction içinde yapıyor. -f 65001 dosyanın Unicode Transformation Format 8 bit (UTF-8) olduğunu söyler. Kataloğun dört Türkçe kitabı (Kürk Mantolu Madonna, İnce Memed, Tutunamayanlar, Saatleri Ayarlama Enstitüsü) Türkçe karakter içeriyor. Linux'taki sqlcmd'de benim denememde -f olmadan da doğru okundu, ama Windows'ta komut satırının code page'i farklı olabildiği için bu bayrağı alışkanlık haline getir. -I ise QUOTED_IDENTIFIER ayarını açar; sqlcmd'de bu ayar varsayılan olarak kapalı, oysa filtered index ve computed column gibi bazı özellikler açık olmasını şart koşuyor.
Yükleme bitince her table'da kaç row olduğuna bakalım:
SELECT t.name AS table_name, SUM(p.rows) AS row_count
FROM sys.tables AS t
JOIN sys.partitions AS p
ON p.object_id = t.object_id AND p.index_id IN (0, 1)
GROUP BY t.name
ORDER BY t.name;
table_name row_count
------------ ---------
authors 69
book_authors 83
books 77
categories 13
customers 40
order_items 680
orders 379
payments 422
reviews 181
stock_items 69
(10 rows affected)
Sayılar küçük ama bilinçli seçildi. 77 kitap, 20'lik sayfalarla sayfalama yapmaya yetiyor; 379 sipariş 2025 Ocak ile 2026 Haziran arasına yayılıyor ve aylık rapor, running total ve window function örnekleri için yeterince ay ve gün sunuyor. Verinin içine öğretici tuzaklar da yerleştirildi: 71 kitabın alt başlığı yok, 2 kitabın kategorisi yok, 4 müşteri hiç sipariş vermedi, 2 yazarın katalogda kitabı yok, bazı siparişler gece yarısına yakın UTC (Coordinated Universal Time) saatinde verilmiş. Bunların her biri ilerideki bir yazının konusu.
Table'lar arasındaki ilişkileri sistem view'larından da okuyabilirsin:
SELECT fk.name AS foreign_key,
OBJECT_NAME(fk.parent_object_id) AS child_table,
COL_NAME(fkc.parent_object_id, fkc.parent_column_id) AS child_column,
OBJECT_NAME(fk.referenced_object_id) AS parent_table,
fk.delete_referential_action_desc AS on_delete
FROM sys.foreign_keys AS fk
JOIN sys.foreign_key_columns AS fkc
ON fkc.constraint_object_id = fk.object_id
ORDER BY child_table, foreign_key;
foreign_key child_table child_column parent_table on_delete
----------------------- ------------ ------------------ ------------ ---------
FK_book_authors_authors book_authors author_id authors NO_ACTION
FK_book_authors_books book_authors book_id books CASCADE
FK_books_categories books category_id categories NO_ACTION
FK_categories_parent categories parent_category_id categories NO_ACTION
FK_order_items_books order_items book_id books NO_ACTION
FK_order_items_orders order_items order_id orders CASCADE
FK_orders_customers orders customer_id customers NO_ACTION
FK_payments_orders payments order_id orders NO_ACTION
FK_reviews_books reviews book_id books NO_ACTION
FK_reviews_customers reviews customer_id customers NO_ACTION
FK_stock_items_books stock_items book_id books NO_ACTION
(11 rows affected)
Üç yapı özellikle önemli. categories kendine referans veriyor: parent_category_id ile Fiction → Science Fiction → Dystopian gibi bir ağaç kuruluyor; bunu self join (9. yazı) ve recursive CTE (12. yazı) ile gezeceğiz. book_authors bir many-to-many köprüsü: Good Omens'ın iki, Design Patterns'ın dört yazarı var. Ve iki foreign key CASCADE: bir sipariş silinirse kalemleri de silinir. Bu seçimlerin gerekçesini constraint'ler yazısında (4. yazı) tartışacağız.
İlk Sorgu
Lab'ın uçtan uca çalıştığını görmek için Türkçe kitapları fiyata göre listeleyelim:
SELECT TOP (5) b.title, c.name AS category, b.list_price
FROM dbo.books AS b
JOIN dbo.categories AS c
ON c.category_id = b.category_id
WHERE b.language_code = 'tr'
ORDER BY b.list_price DESC;
title category list_price
--------------------------- -------- ----------
Tutunamayanlar Classics 620.90
İnce Memed Classics 480.90
Saatleri Ayarlama Enstitüsü Classics 400.50
Kürk Mantolu Madonna Classics 350.00
(4 rows affected)
TOP (5) istedik, 4 row geldi; çünkü katalogda 4 Türkçe kitap var. TOP bir üst sınırdır, garanti değil. Türkçe karakterler de bozulmadan geldi; script'i doğru encoding'le yüklediğimizin kanıtı bu. Son kontrol olarak sipariş verisinin hangi aralığı kapsadığına bakalım:
SELECT MIN(ordered_at) AS first_order, MAX(ordered_at) AS last_order, COUNT(*) AS order_count
FROM dbo.orders;
first_order last_order order_count
------------------- ------------------- -----------
2025-01-02 09:19:25 2026-06-29 21:05:58 379
(1 rows affected)
Verinin "bugün"ü 30 Haziran 2026 ve bütün zamanlar UTC. Tarih sorgularında bunu hatırlaman gerekecek: UTC 21:05'te verilen bir sipariş İstanbul saatiyle ertesi gün 00:05'e denk geliyor.
Üretimde Dikkat
Emülasyon, performans ölçümlerini anlamsız kılar. Rosetta altında çalışan bir SQL Server'da bir sorgunun kaç milisaniye sürdüğü, gerçek bir x86 sunucusunda ne kadar süreceği hakkında bir şey söylemez. Bu yüzden seri boyunca süre yerine logical read sayısı, row sayısı ve plan operator'leri gibi makineden bağımsız ölçüler kullanacağız. Süre ölçmen gerekiyorsa gerçek bir x86-64 makine ya da bulut kullan; Azure SQL Database'in ücretsiz teklifi her veritabanı için ayda 100.000 vCore saniyesi ve 32 GB depolama veriyor.
Apple Silicon desteği resmî değil. Microsoft'un dokümanı container image'larının yalnız Intel ve AMD x86-64 işlemcili Linux host'larda desteklendiğini, Rosetta 2 gibi emülasyon katmanlarının test edilmediğini açıkça yazıyor. Pratikte bunun bir bedeli de görüldü: Ekim 2025'te macOS 26 Tahoe'ya geçen kullanıcılar SQL Server 2025 RC (release candidate) container'larının açılmadığını bildirdi; AVX (Advanced Vector Extensions) komutlarıyla ilgili hata Microsoft'un mssql-docker GitHub deposunda bir issue olarak takip edildi. Bu yazının ortamı macOS 27.0.1 ve Docker Desktop 4.64.0; container sorunsuz çalışıyor. Yine de bir macOS ya da Docker güncellemesinden sonra container açılmıyorsa önce docker logs'a, sonra o GitHub deposuna bak.
Parolalar ve connection string'ler repository'ye girmez. Lab'da bile .env dosyası kullanıyorsan .gitignore'a ekle; .NET tarafında connection string'i user-secrets ya da environment variable'dan oku. Son olarak edition lisansını unutma: Developer edition'lar ücretsiz ama production'da çalıştırılamaz. "Lab'daki container'ı production'a taşıdık" cümlesi bir lisans ihlalidir.
Sık Düşülen Tuzaklar
- Parolayı
docker runkomutuna açık yazıp shell history'de ya da bir README'de bırakmak.-e MSSQL_SA_PASSWORDyazımı değeri environment'tan alır. - Parola politikasına uymayan bir parola verip container'ın neden kapandığını anlamamak. Sebep
docker logs'ta yazıyor. - Volume kullanmadan container'ı silmek ve bütün veritabanlarını kaybetmek.
TrustServerCertificate=Trueya da-C'yi lab'dan production connection string'ine taşımak. Production'da sunucunun gerçek bir sertifikası olmalı ve doğrulanmalı.- Emülasyon altında ölçülen süreleri "SQL Server şu kadar hızlı" diye yorumlamak.
- Seed script'ini
-f 65001olmadan, yanlış encoding'le yüklemek ve Türkçe kitap adlarını bozuk karakterlerle görmek.
Küçük Bir Deneme
★☆☆ Lab'ı kur, SCHEMA.sql'i yükle ve row sayısı sorgusunu çalıştır. Senin sayıların buradakilerle birebir aynı olmalı; değilse script'i yarıda kesmiş olabilirsin.
★★☆ SCHEMA.sql'i aynı veritabanında ikinci kez çalıştır ve row sayılarının değişmediğini doğrula. Sonra dbo.books'a elle bir row ekle, script'i tekrar çalıştır ve o row'a ne olduğuna bak. Script'in idempotent olması tam olarak ne anlama geliyor?
★★★ Container'ı volume olmadan başlat, bir veritabanı oluştur, container'ı sil ve aynı komutla yeniden başlat. Veritabanın nerede? Aynı denemeyi isimli volume ile tekrarla ve docker volume inspect çıktısında verinin host'ta nerede durduğunu bul.
Lab hazır ve Bookstore yüklü. Bir sonraki yazıda bu table'ları oluşturan CREATE TABLE cümlelerine yakından bakacağız: neden list_price için DECIMAL(10,2) seçildi, email neden VARCHAR ama title NVARCHAR, ve "İnce Memed"i büyük harfe çevirdiğinde collation'ın Türkçe "i" harfine ne yaptığı.