Bir koleksiyon, sıralı dizine eklenmiş değerlere sahip belgeler içeriyorsa, Cloud Firestore yazma hızı saniyede 500 yazma işlemiyle sınırlandırılır. Bu sayfada, bu sınırı aşmak için doküman alanının nasıl parçalanacağı açıklanmaktadır. İlk olarak, "sıralı dizine eklenmiş alanlar" ile neyi kastettiğimizi tanımlayalım ve bu sınırın ne zaman geçerli olduğunu açıklayalım.
Sıralı dizine eklenmiş alanlar
"Sıralı dizine eklenmiş alanlar", monoton bir şekilde artan veya azalan dizine eklenmiş bir alan içeren tüm belge koleksiyonları anlamına gelir. Çoğu durumda bu, timestamp
alanı anlamına gelir ancak tekdüze şekilde artan veya azalan herhangi bir alan değeri, saniyede 500 yazma olan yazma sınırını tetikleyebilir.
Örneğin, uygulama user
değerlerini aşağıdaki gibi atarsa sınır, dizine eklenmiş alan userid
içeren userid
belgelerden oluşan bir koleksiyon için geçerlidir:
1281, 1282, 1283, 1284, 1285, ...
Öte yandan, tüm timestamp
alanları bu sınıra tabi değildir. Bir timestamp
alanı rastgele dağıtılmış değerleri izliyorsa yazma sınırı geçerli değildir. Alan değerinin gerçekte ne olduğu da önemli değildir. Yalnızca alanın tekdüze şekilde artması veya azalması gerekir. Örneğin, aşağıdaki monoton olarak artan alan değerlerinin her ikisi de yazma sınırını tetikler:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Zaman damgası alanını parçalama
Uygulamanızın, tekdüze olarak artan bir timestamp
alanı kullandığını varsayalım.
Uygulamanız hiçbir sorguda timestamp
alanını kullanmıyorsa zaman damgası alanını dizine eklemeyerek saniyede 500 yazma sınırını kaldırabilirsiniz. Sorgularınız için timestamp
alanı gerekiyorsa parçalanmış zaman damgaları kullanarak sınırı aşabilirsiniz:
timestamp
alanının yanına birshard
alanı ekleyin.shard
alanı için1..n
farklı değer kullanın. Bu işlem, koleksiyonun yazma sınırını500*n
'ya yükseltir ancakn
sorguyu toplamanız gerekir.- Yazma mantığınızı, her belgeye
shard
değerini rastgele atayacak şekilde güncelleyin. - Parçalanmış sonuç kümelerini toplamak için sorgularınızı güncelleyin.
- Hem
shard
alanı hem detimestamp
alanı için tek alanlı dizinleri devre dışı bırakın.timestamp
alanını içeren mevcut bileşik dizinleri silin. - Güncellenen sorgularınızı desteklemek için yeni bileşik dizinler oluşturun. Bir dizindeki alanların sırası önemlidir ve
shard
alanı,timestamp
alanından önce gelmelidir.timestamp
alanını içeren tüm dizinlershard
alanını da içermelidir.
Parçalanmış zaman damgalarını yalnızca saniyede 500'den fazla yazma işleminin yapıldığı sürekli yazma hızına sahip kullanım alanlarında uygulamanız gerekir. Aksi takdirde bu, erken optimizasyon olur. timestamp
alanının parçalanması, saniyede 500 yazma işlemi sınırını kaldırır ancak bu durumda istemci tarafı sorgu toplamaları gerekir.
Aşağıdaki örneklerde, timestamp
alanının nasıl parçalanacağı ve parçalanmış bir sonuç kümesinin nasıl sorgulanacağı gösterilmektedir.
Örnek veri modeli ve sorgular
Örneğin, para birimleri, adi hisse senetleri ve ETF'ler gibi finansal araçların neredeyse anlık analizini yapan bir uygulama düşünün. Bu uygulama, dokümanları instruments
koleksiyonuna şu şekilde yazar:
Node.js
async function insertData() { const instruments = [ { symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Bu uygulama aşağıdaki sorguları çalıştırır ve timestamp
alanına göre sıralar:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { return fs.collection('instruments') .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Yaptığınız araştırmanın ardından uygulamanın saniyede 1.000 ila 1.500 enstrüman güncellemesi alacağını belirliyorsunuz. Bu, dizine eklenmiş zaman damgası alanları içeren belgeler için izin verilen saniyede 500 yazma sınırını aşar. Yazma işleme hızını artırmak için 3 parçalama değerine ihtiyacınız vardır.
MAX_INSTRUMENT_UPDATES/500 = 3
. Bu örnekte x
, y
ve z
parçalama değerleri kullanılmaktadır. Parçalama değerleriniz için sayıları veya diğer karakterleri de kullanabilirsiniz.
Parçalama alanı ekleme
Dokümanlarınıza shard
alanı ekleyin. shard
alanını, koleksiyondaki yazma sınırını saniyede 1.500 yazmaya yükselten x
, y
veya z
değerlerine ayarlayın.
Node.js
// Define our 'K' shard values const shards = ['x', 'y', 'z']; // Define a function to help 'chunk' our shards for use in queries. // When using the 'in' query filter there is a max number of values that can be // included in the value. If our number of shards is higher than that limit // break down the shards into the fewest possible number of chunks. function shardChunks() { const chunks = []; let start = 0; while (start < shards.length) { const elements = Math.min(MAX_IN_VALUES, shards.length - start); const end = start + elements; chunks.push(shards.slice(start, end)); start = end; } return chunks; } // Add a convenience function to select a random shard function randomShard() { return shards[Math.floor(Math.random() * Math.floor(shards.length))]; }
async function insertData() { const instruments = [ { shard: randomShard(), // add the new shard field to the document symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Parçalı zaman damgasını sorgulama
shard
alanı eklemek için sorgularınızı güncelleyerek parçalanmış sonuçları toplamanız gerekir:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { // For each shard value, map it to a new query which adds an additional // where clause specifying the shard value. return Promise.all(shardChunks().map(shardChunk => { return fs.collection('instruments') .where('shard', 'in', shardChunk) // new shard condition .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); })) // Now that we have a promise of multiple possible query results, we need // to merge the results from all of the queries into a single result set. .then((snapshots) => { // Create a new container for 'all' results const docs = []; snapshots.forEach((querySnapshot) => { querySnapshot.forEach((doc) => { // append each document to the new all container docs.push(doc); }); }); if (snapshots.length === 1) { // if only a single query was returned skip manual sorting as it is // taken care of by the backend. return docs; } else { // When multiple query results are returned we need to sort the // results after they have been concatenated. // // since we're wanting the `limit` newest values, sort the array // descending and take the first `limit` values. By returning negated // values we can easily get a descending value. docs.sort((a, b) => { const aT = a.data().timestamp; const bT = b.data().timestamp; const secondsDiff = aT.seconds - bT.seconds; if (secondsDiff === 0) { return -(aT.nanoseconds - bT.nanoseconds); } else { return -secondsDiff; } }); return docs.slice(0, limit); } }); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Dizin tanımlarını güncelleme
Saniyede 500 yazma işlemi sınırını kaldırmak için timestamp
alanını kullanan mevcut tek alanlı ve birleşik dizinleri silin.
Bileşik dizin tanımlarını silme
Firebase Konsolu
Firebase konsolunda Cloud Firestore Bileşik Dizinler sayfasını açın.
timestamp
alanını içeren her dizin için düğmesini ve Sil'i tıklayın.
GCP Console
Google Cloud Console'da Veritabanları sayfasına gidin.
Veritabanları listesinden gerekli veritabanını seçin.
Gezinme menüsünde Dizinler'i, ardından Bileşik sekmesini tıklayın.
timestamp
alanını içeren dizin tanımlarını aramak için Filtre alanını kullanın.Bu indekslerin her biri için
düğmesini ve Sil'i tıklayın.
Firebase CLI
- Firebase CLI'yı ayarlamadıysanız CLI'yı yüklemek ve
firebase init
komutunu çalıştırmak için bu talimatları uygulayın.init
komutu sırasındaFirestore: Deploy rules and create indexes for Firestore
seçtiğinizden emin olun. - Kurulum sırasında Firebase CLI, mevcut dizin tanımlarınızı varsayılan olarak
firestore.indexes.json
adlı bir dosyaya indirir. timestamp
alanını içeren tüm dizin tanımlarını kaldırın. Örneğin:{ "indexes": [ // Delete composite index definition that contain the timestamp field { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }
Güncellenen dizin tanımlarınızı dağıtın:
firebase deploy --only firestore:indexes
Tek alanlı dizin tanımlarını güncelleme
Firebase Konsolu
Firebase konsolunda Cloud Firestore Tek Alanlı Dizine Ekleme sayfasını açın.
Muafiyet Ekle'yi tıklayın.
Collection ID (Koleksiyon kimliği) için
instruments
değişkenini girin. Alan yolu içintimestamp
değerini girin.Sorgu kapsamı bölümünde hem Koleksiyon'u hem de Koleksiyon grubu'nu seçin.
Sonraki'yi tıklayın.
Tüm dizin ayarlarını Devre dışı olarak değiştirin. Kaydet'i tıklayın.
Aynı adımları
shard
alanı için tekrarlayın.
GCP Console
Google Cloud Console'da Veritabanları sayfasına gidin.
Veritabanları listesinden gerekli veritabanını seçin.
Gezinme menüsünde Dizinler'i, ardından Tek Alan sekmesini tıklayın.
Tek Alan sekmesini tıklayın.
Muafiyet Ekle'yi tıklayın.
Collection ID (Koleksiyon kimliği) için
instruments
değişkenini girin. Alan yolu içintimestamp
değerini girin.Sorgu kapsamı bölümünde hem Koleksiyon'u hem de Koleksiyon grubu'nu seçin.
Sonraki'yi tıklayın.
Tüm dizin ayarlarını Devre dışı olarak değiştirin. Kaydet'i tıklayın.
Aynı adımları
shard
alanı için tekrarlayın.
Firebase CLI
Dizin tanımları dosyanızın
fieldOverrides
bölümüne aşağıdakileri ekleyin:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Güncellenen dizin tanımlarınızı dağıtın:
firebase deploy --only firestore:indexes
Yeni bileşik dizinler oluşturma
timestamp
karakterini içeren tüm önceki dizinleri kaldırdıktan sonra uygulamanızın gerektirdiği yeni dizinleri tanımlayın. timestamp
alanını içeren tüm dizinler shard
alanını da içermelidir. Örneğin, yukarıdaki sorguları desteklemek için aşağıdaki dizinleri ekleyin:
Toplama | Dizine eklenen alanlar | Sorgu kapsamı |
---|---|---|
enstrümanlar | shard, price.currency, timestamp | Toplama |
enstrümanlar | parçası, alışverişi, zaman damgası | Toplama |
enstrümanlar | parçası, instrumentType, zaman damgası | Toplama |
Hata Mesajları
Güncellenen sorguları çalıştırarak bu dizinleri oluşturabilirsiniz.
Her sorgu, Firebase Console'da gerekli dizini oluşturmak için bir bağlantı içeren hata mesajı döndürür.
Firebase CLI
Dizin tanımı dosyanıza aşağıdaki dizinleri ekleyin:
{ "indexes": [ // New indexes for sharded timestamps { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }
Güncellenen dizin tanımlarınızı dağıtın:
firebase deploy --only firestore:indexes
Sıralı dizine eklenmiş alanlar için yazma sınırını anlama
Sıralı dizine eklenmiş alanlar için yazma hızı sınırı, Cloud Firestore'nın dizin değerlerini nasıl depoladığından ve dizin yazma işlemlerini nasıl ölçeklendirdiğinden kaynaklanır. Her dizin yazma işlemi için Cloud Firestore, belge adını ve dizine eklenmiş her alanın değerini birleştiren bir anahtar/değer çifti girişi tanımlar. Cloud Firestore Bu dizin girişlerini tabletler adı verilen veri grupları halinde düzenler. Her Cloud Firestore sunucuda bir veya daha fazla tablet bulunur. Belirli bir tablete yazma yükü çok yüksek olduğunda Cloud Firestore, tableti daha küçük tabletlere bölerek ve yeni tabletleri farklı Cloud Firestore sunucularına dağıtarak yatay olarak ölçeklendirir.
Cloud Firestore, sözlükbilimsel olarak yakın dizin girişlerini aynı tablette yerleştirir. Bir tabletteki dizin değerleri çok yakınsa (ör. zaman damgası alanları için), Cloud Firestore tableti daha küçük tabletlere etkili bir şekilde bölemez. Bu durum, tek bir tabletin çok fazla trafik aldığı bir sıcak nokta oluşturur ve sıcak noktadaki okuma ve yazma işlemleri yavaşlar.
Bir zaman damgası alanını parçalayarak Cloud Firestore'ın iş yüklerini birden fazla tablet arasında verimli bir şekilde bölmesini sağlayabilirsiniz. Zaman damgası alanının değerleri birbirine yakın kalsa da birleştirilmiş parça ve dizin değeri, girişleri birden fazla tablet arasında bölmek için dizin girişleri arasında yeterli Cloud Firestore alan sağlar.
Sırada ne var?
- Ölçeklenebilir tasarım için en iyi uygulamalar başlıklı makaleyi okuyun.
- Tek bir dokümana yüksek yazma oranlarının olduğu durumlar için Dağıtılmış sayaçlar konusuna bakın.
- Cloud Firestore için standart sınırlara bakın.