Mert Özen Her Satırda Daha İleri
Veritabanı SQL Temelleri — #02

SQL Lab'ı: SQL Server 2025'i Docker'da Çalıştırmak, VS Code MSSQL Eklentisi ve Bookstore Örnek Veritabanı

Mert Özen 8 Kas 2026 11 dk 3 görüntülenme
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:

EditionLisansÖnemli sınırlar
ExpressÜcretsiz, production'da kullanılabilirVeritabanı 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 testStandard edition'ın özellik seti
Enterprise DeveloperÜcretsiz, yalnız geliştirme ve testEnterprise edition'ın bütün özellikleri
Evaluation180 günlük denemeEnterprise 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.

02-bookstore-er-tr.png

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 run komutuna açık yazıp shell history'de ya da bir README'de bırakmak. -e MSSQL_SA_PASSWORD yazı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=True ya 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 65001 olmadan, 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ığı.