Soru Milyonlarca girişle sıralama


Milyonlarca oyuncuyla başa çıkabilecek çevrimiçi bir oyun için sunucu üzerinde çalışıyorum. Şimdi oyunun skor tablolarına ihtiyacı var ve oyuncuların pozisyonunu ve oyuncuların pozisyonlarını ve mevcut oyuncu pozisyonunun yakınında bulunan diğer oyuncuları da göstermek isteyecek.

Şimdi bunu daha önce MySQL'de yaptım ve teknik olarak nasıl mümkün olabileceğini biliyorum, ancak bunun birçok çevrimiçi oyun için yaygın bir uygulama olduğunu düşündüğümden, özellikle bu amaçla mevcut kütüphaneler veya veritabanları olması gerektiğine inanıyorum?

Bu tür sorgulamalar için hangi veritabanının en iyisi olduğunu ve muhtemelen bu çalışmayı çoktan yapmış olan önceden var olan herhangi bir kütüphaneyi bana öneren var mı? API erişimine sahip bir üçüncü taraf hizmeti de iyi olurdu.

İyi tavsiye almak dileğiyle, teşekkürler!

Düzenle:

Açıklığa kavuşturmak için, milyonlarca giriş yapabilen bir veritabanına ihtiyacım var (şu ana kadar MySQL bunun için iyidir). Örneğin "leaderboard" tablosundan belirli bir satır alırsam satırın hangi sıralamasını bilmem gerekiyor. Bu sorgu, db'nin boyutuna bakılmaksızın 500 ms'nin altında olmalıdır.

Alternatif olarak, tabloyu geçerli sıralama bilgisiyle güncellemenin bir yolu, bu güncelleştirme sorgusu tüm tabloyu kilitlemediği ve güncelleme sorgusunun 30 saniyenin altında çalıştığı sürece çok uzun olacaktır.

Hangi veritabanı / mekanizma veya üçüncü taraf hizmetinin kullanılacağı ile ilgili herhangi bir fikir çok takdir edilecektir!


25
2018-03-25 17:55


Menşei


şemanızı gönderebilir ve mevcut puanlama sisteminiz nasıl çalışır? TIA - Jon Black
aynı zamanda maksimum eşzamanlı oyuncuya sahip olmayı beklediğiniz kaç oyuncudan bahseder misiniz? - Jon Black
Merhaba f00, şema ilgisizdir ve son amacın hizmetindedir - ki bu da 500 milyonun altındaki milyonlarca giriş arasında belirli bir sıralama elde etmektir. Eşzamanlı kullanıcılara gelince, bu girişlerin sayısıyla birlikte, yaklaşık 10.000 eşzamanlı kullanıcı hakkında 1 milyon kayıt eşit olacak (çok kaba tahmin) olacaktır. - Naatan
Yani (game_id, round_id, player_id) birleşik bir birincil anahtar olarak sahip olduğumu düşünmüyorsun. Sanırım oyunun yaşam döngüsünde birden fazla turunuz olabilir - ama hey, alakasız olduğu için (lol) sanırım önemli değil. - Jon Black
Bu çözüm ihtiyaçlarınız için çalışıyor mu. Nasıl ölçeklendirilir? Daha sonra mimariyi değiştirdin mi ..? - Reg Mem


Cevaplar:


Tek diskli arama yaklaşık 15ms'dir, belki sunucu sınıfı disklerde biraz daha azdır. 500 ms'den daha kısa bir yanıt süresi, yaklaşık 30 rasgele disk erişime sınırlar. Bu çok fazla değil.

Küçük dizüstü bilgisayarımda, bir geliştirme veritabanım var.

root@localhost [kris]> select @@innodb_buffer_pool_size/1024/1024 as pool_mb;
+--------------+
| pool_mb      |
+--------------+
| 128.00000000 |
+--------------+
1 row in set (0.00 sec)

ve yavaş bir dizüstü bilgisayar diski. Puan tablosu oluşturdum

root@localhost [kris]> show create table score\G
*************************** 1. row ***************************
       Table: score
Create Table: CREATE TABLE `score` (
  `player_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `score` int(11) NOT NULL,
  PRIMARY KEY (`player_id`),
  KEY `score` (`score`)
) ENGINE=InnoDB AUTO_INCREMENT=2490316 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

rasgele tam sayı ve ardışık player_id değerleri. Sahibiz

root@localhost [kris]> select count(*)/1000/1000 as mrows from score\G
*************************** 1. row ***************************
mrows: 2.09715200
1 row in set (0.39 sec)

Veritabanı çifti koruyor (score, player_id) içinde score dizinde sipariş score, bir InnoDB dizinindeki veriler bir BTREE içinde saklandığından ve satır işaretçisi (veri işaretçisi) birincil anahtar değerdir, böylece tanım KEY (score) sona erer KEY(score, player_id) içten. Bir puan alma için sorgu planına bakarak bunu kanıtlayabiliriz:

root@localhost [kris]> explain select * from score where score = 17\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: score
         type: ref
possible_keys: score
          key: score
      key_len: 4
          ref: const
         rows: 29
        Extra: Using index
1 row in set (0.00 sec)

Gördüğünüz gibi key: score ile kullanılıyor Using indexveri erişiminin gerekli olmadığı anlamına gelir.

Belirli bir sabit için sıralama sorgusu player_id dizüstü bilgisayarımda tam 500ms alır:

root@localhost [kris]>  select p.*, count(*) as rank 
    from score as p join score as s on p.score < s.score 
   where p.player_id = 479269\G
*************************** 1. row ***************************
player_id: 479269
    score: 99901
     rank: 2074
1 row in set (0.50 sec)

Daha fazla bellek ve daha hızlı bir kutuda daha hızlı olabilir, ancak hala oldukça pahalı bir işlemdir, çünkü plan berbat:

root@localhost [kris]> explain select p.*, count(*) as rank from score as p join score as s on p.score < s.score where p.player_id = 479269;
+----+-------------+-------+-------+---------------+---------+---------+-------+---------+--------------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows    | Extra                    |
+----+-------------+-------+-------+---------------+---------+---------+-------+---------+--------------------------+
|  1 | SIMPLE      | p     | const | PRIMARY,score | PRIMARY | 4       | const |       1 |                          |
|  1 | SIMPLE      | s     | index | score         | score   | 4       | NULL  | 2097979 | Using where; Using index |
+----+-------------+-------+-------+---------------+---------+---------+-------+---------+--------------------------+
2 rows in set (0.00 sec)

Gördüğünüz gibi, plandaki ikinci tablo bir indeks taramasıdır, bu nedenle sorgu, oyuncuların sayısı ile doğrusal olarak yavaşlar.

Tam bir liderlik tablosunu istiyorsanız, tümceyi nereye bırakmanız gerektiğini ve daha sonra iki tarama ve ikinci kez yürütme süreleri almanız gerekir. Yani bu plan tamamen çöker.

Burada prosedür usulü gitme zamanı:

root@localhost [kris]> set @count = 0; 
    select *, @count := @count + 1 as rank from score where score >= 99901 order by score desc ;
...
|   2353218 | 99901 | 2075 |
|   2279992 | 99901 | 2076 |
|   2264334 | 99901 | 2077 |
|   2239927 | 99901 | 2078 |
|   2158161 | 99901 | 2079 |
|   2076159 | 99901 | 2080 |
|   2027538 | 99901 | 2081 |
|   1908971 | 99901 | 2082 |
|   1887127 | 99901 | 2083 |
|   1848119 | 99901 | 2084 |
|   1692727 | 99901 | 2085 |
|   1658223 | 99901 | 2086 |
|   1581427 | 99901 | 2087 |
|   1469315 | 99901 | 2088 |
|   1466122 | 99901 | 2089 |
|   1387171 | 99901 | 2090 |
|   1286378 | 99901 | 2091 |
|    666050 | 99901 | 2092 |
|    633419 | 99901 | 2093 |
|    479269 | 99901 | 2094 |
|    329168 | 99901 | 2095 |
|    299189 | 99901 | 2096 |
|    290436 | 99901 | 2097 |
...

Bu bir prosedür planı olduğu için kararsızdır:

  • LIMIT kullanamazsınız, çünkü bu sayacı dengeleyecektir. Bunun yerine tüm bu verileri indirmeniz gerekiyor.
  • Gerçekten sıralayamazsın. Bu ORDER BY deyim çalışır, çünkü sıralamaz, ancak bir dizin kullanır. Gördüğünüz anda using filesortSayaç değerleri çılgınca olacak.

Yine de, bir NoSQL (okuma: prosedürel) veri tabanının bir yürütme planı olarak ne yapacağına en yakın olan çözümdür.

NoSQL'i bir alt sorgunun içinde stabilize edebilir ve sonra da ilgilendiğimiz parçayı dilimleyebiliriz;

root@localhost [kris]> set @count = 0; 
    select * from ( 
        select *, @count := @count + 1 as rank 
          from score 
         where score >= 99901 
      order by score desc 
    ) as t 
    where player_id = 479269;
Query OK, 0 rows affected (0.00 sec)
+-----------+-------+------+
| player_id | score | rank |
+-----------+-------+------+
|    479269 | 99901 | 2094 |
+-----------+-------+------+
1 row in set (0.00 sec)

root@localhost [kris]> set @count = 0; 
    select * from ( 
        select *, @count := @count + 1 as rank 
          from score 
         where score >= 99901 
      order by score desc 
    ) as t 
    where rank between 2090 and 2100;
Query OK, 0 rows affected (0.00 sec)
+-----------+-------+------+
| player_id | score | rank |
+-----------+-------+------+
|   1387171 | 99901 | 2090 |
|   1286378 | 99901 | 2091 |
|    666050 | 99901 | 2092 |
|    633419 | 99901 | 2093 |
|    479269 | 99901 | 2094 |
|    329168 | 99901 | 2095 |
|    299189 | 99901 | 2096 |
|    290436 | 99901 | 2097 |
+-----------+-------+------+
8 rows in set (0.01 sec)

Alt sorgu, önceki sonucu, t adlı bir ad-hoc tablosu olarak, dış sorguda erişebildiğimiz şekilde somutlaştırır. Ad-hoc bir tablo olduğundan, MySQL'de dizin olmayacaktır. Bu, dış sorguda etkili olanı sınırlar.

Her iki sorgunun da zamanlama kısıtlamalarınızı nasıl karşıladığını unutmayın. İşte plan:

root@localhost [kris]> set @count = 0; explain select * from ( select *, @count := @count + 1 as rank from score where score >= 99901 order by score desc ) as t where rank between 2090 and 2100\G
Query OK, 0 rows affected (0.00 sec)

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <derived2>
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2097
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: DERIVED
        table: score
         type: range
possible_keys: score
          key: score
      key_len: 4
          ref: NULL
         rows: 3750
        Extra: Using where; Using index
2 rows in set (0.00 sec)

Her iki sorgu bileşeni (iç, DERIVED sorgu ve dış BETWEEN kısıtlama), kötü sıradaki oyuncular için yavaşlayacak, ancak zamanlama kısıtlamalarınızı büyük ölçüde ihlal edecektir.

root@localhost [kris]> set @count = 0; select * from ( select *, @count := @count + 1 as rank from score where score >= 0 order by score desc ) as t;
...
2097152 rows in set (3.56 sec)

Tanımlayıcı yaklaşım için yürütme süresi sabittir (yalnızca tablo boyutuna bağlıdır):

root@localhost [kris]> select p.*, count(*) as rank 
   from score as p join score as s on p.score < s.score 
   where p.player_id = 1134026;
+-----------+-------+---------+
| player_id | score | rank    |
+-----------+-------+---------+
|   1134026 |     0 | 2097135 |
+-----------+-------+---------+
1 row in set (0.53 sec)

Araman.


31
2018-03-30 19:13



Müthiş cevap lsotopp için teşekkürler! Sayıların benimkiyle iyi uyuşuyor gibi görünüyor. Bunu birçok kez gözden geçirdim ve optimizasyon yollarını aradım ama orada hiçbir şekilde bir yol yok gibi görünüyor. Yapacağım şey temel olarak önerilen yönteminizi kullanmaktır, ancak skor belli bir sayının altındaysa, ideal olarak değil, ama eğer rütbeniz # 5000 ise, çok az farkederse çok önemli değil. Neyse, ayrıntılı yanıt için çok teşekkürler! İnanılmaz derecede yararlı! - Naatan
Rica ederim. Makale, ilk olarak iki MySQL AB meslektaşından, Kai Voigt (şimdi Cloudera) ve Jan Kneschke'den (şimdi Oracle) yardım aldığım bir temanın bir çeşididir. Görmek mysqldump.azundris.com/archives/... Orijinal yazı için. - Isotopp
Biraz kafa karıştırıcı: Tablo adı ve sütun adı için "skor" kullandınız, böylece hem (nispeten karmaşık) sorguları birbirinden ayırt etmek zordur. - aberaud
50M satırınız olduğunda ve skoru sıfır olan son oyuncuyu sorguladığınızda alt sorgu en iyi 1000 oyuncuyu sorgulamak kadar hızlı mı olacak? - Daniel
Menzili manuel olarak vermek yerine, oyuncu_araçı -2 ve player_rank + 2 gibi güncel oyuncu sıralamasını kullanarak menzili vermek mümkündür. - Kumar KS


Bunun eski bir soru olduğunu biliyorum, ama bu tür problemlere bakmayı seviyorum. Veri -> sorgulama hızının gerekli olduğu göz önünde bulundurulduğunda, daha fazla kodlama çalışması gerektiren geleneksel olmayan hileler kullanılabilir, ancak sorgulama performansına gerçekten bir destek verebilir.

Puanlama kovaları

Başlangıç ​​olarak, puanları kovalarla izlemeliyiz. Kova listesinin (ne harika bir isim!) Hafızada kolayca tutulacak kadar küçük olmasını ve kovaların sık sık (nispeten konuşma) etkilenmediği kadar büyük olmasını istiyoruz. Bu, kilitleme sorunlarından kaçınmak için daha büyük bir eşzamanlılık sağlar.

Bu kepçelerin yükünüze göre nasıl ayrılacağını yargılamanız gerekecek, ancak bence kolayca belleğe sığacak ve hızlı bir şekilde eklenebilecek kadar çok kovaya sahip olmak istiyorsunuz.

Buna uyum sağlamak için score_buckets tablo aşağıdaki yapıya sahip olacaktır:

minscore, maxscore, usercount; PK(minscore, maxscore)

Kullanıcı tablosu

Kullanıcılarımızı izlemeliyiz ve muhtemelen şu şekilde yapılmalı:

userid, score, timestamp
#(etc., etc. that we don't care about for this part of the problem)

Skoru saymak için bunu etkin bir şekilde yineleyebilmek için skor üzerinde bir endekse ihtiyacımız var. Zaman damgası benim örneğimde kravat kırmak için attığım bir şeydir, böylece kesin bir siparişim olur. İhtiyacın yoksa, hendek - boşluk kullanıyor ve sorgu süresini etkileyecek. Şu anda: endeks (skor, zaman damgası).

Kullanıcıları ve puanlarını Ekleme / Güncelleme / Silme

Kullanıcı tablosuna tetikleyiciler ekleyin. Ekleme tarihinde:

update score_buckets sb
    set sb.usercount = sb.usercount + 1
    where sb.minscore <= NEW.score
    and sb.maxscore >= NEW.score

Güncellemede

update score_buckets sb
    set sb.usercount = sb.usercount - 1
    where sb.minscore <= OLD.score
    and sb.maxscore >= OLD.score
update score_buckets sb
    set sb.usercount = sb.usercount + 1
    where sb.minscore <= NEW.score
    and sb.maxscore >= NEW.score

Silme üzerine

update score_buckets sb
    set sb.usercount = sb.usercount - 1
    where sb.minscore <= OLD.score
    and sb.maxscore >= OLD.score

Sıralama Belirleniyor

$usersBefore = select sum(usercount)
    from score_buckets
    where maxscore < $userscore;
$countFrom = select max(maxscore)
    from score_buckets
    where maxscore < $userscore;
$rank = select count(*) from user
    where score > $countFrom
    and score <= $userscore
    and timestamp <= $userTimestamp

Notları kapatma

Çeşitli sayıdaki kepçelerle kıyaslama, her seferinde ikiye katlama veya yarıya indirme. Bunu test etmenize izin vermek için hızlıca bir kova ikiye katlama / yarı yarıya yazabilirsiniz. Daha fazla kovalar, kullanıcı puan endeksini daha az taramak ve puanları güncellerken daha az kilit / işlem çekişmesi yapmakta. Daha fazla kova daha fazla bellek tüketir. Başlamak için bir numara seçmek için 10.000 paket kullanın. İdeal olarak, kovalarınız tüm puan aralığını kapsayacak ve her bir kova, kabaca aynı sayıda kullanıcıya sahip olacaktır. Eğer dağılım grafiği alırsanız, bir çeşit eğri izlerseniz, kova dağılımınızı bu eğriyi takip edin.

Bu teori iki kademeli atlama listesi.


16
2017-10-20 04:56



Bu yaklaşımı seviyorum. - Sorter
İki sentim, eğer çok fazla kullanıcı aynı puana sahipse, hatta eşiği aşarlar ama yine de kepçeyi ayıramazsınız. Bu, bir oyun çok popüler değil ve birçok oyuncu 10 dakika içinde çıkmak, ancak 100 gibi bir başlangıç ​​puanı bırakın olur. Yani, hem skor hem de ts ile kovalar inşa etmek daha iyidir. score_buckets üç sütun, score_ts_from, score_ts_to ve sayım olurdu. - Daniel


Son zamanlarda Redis ile bu tür bir problemi çözmek için bir makale okudum. MySQL'i temel mağazanız olarak kullanabilirsiniz, ancak Redis'te sıralanmamış sonuçları önbelleğe alır ve sıralamayı gerçek zamanlı olarak güncellersiniz. Bağlantı olabilir burada bulundu. Makalenin son üçte biri, sıralama listenizde olduğu gibi anahtarlanmış türlerle ilgilidir.


3
2018-03-25 18:14



Teşekkürler David, maalesef skor tablolarım milyonlarca giriş yapabildiğinden gerçekten işe yaramıyor. Bunu önbelleğe almak için biraz fazla. Sıralamayı sql sorguları ile alabilirim ancak bu sorgular daha uzun süre masanın çalışmasına neden olur. Sorgu yürütme süresinin çok fazla artmasına gerek kalmadan büyük DB'lerle ölçeklenecek bir şeye ihtiyacım var. - Naatan
@Naatan: Ben David yanıtını destekliyorum, NoSQL sorunsuz bir şekilde ele almanın iyi bir yoludur. "Sorgu yürütme süresinin çok fazla artmasına gerek kalmadan daha büyük DB'lerle ölçeklenecek bir şey" dir. Milyonlarca kullanıcı için ölçeklendiren bir veritabanınız varsa, bazı kullanıcı paramızları için paralel Redis sunucusuna sahip olmak sorun olmaz. - w.k
@Naatan: Muhtemelen bu makaleye göz atmalısın. Tartışma, tanımladığınız şey olan milyonlarca set için Redis'i kullanmaktır. Belki de bunu düşünmenin bir yolu, MySQL'in ayarlanmış işlemler için optimize edilmesi ve Redis'in çok büyük veri kümelerinin hızlı bir şekilde önbelleğe alınması için optimize edilmesidir. - David Richards
Teşekkürler David, bir sonraki birkaç ay boyunca ulaşamayacağımız, yaklaşık 5 milyon girişe kadar çalışacak olan ilk kullanım sürümümün çalışmasına başladığımda bir kez vereceğim. - Naatan


Milyonlarca girişin sıralanması çok fazla iş gibi görünebilir, ancak açıkça görülmemektedir. 10 ^ 6 tamamen rasgele girişleri sıralamak bilgisayarımda yaklaşık 3 saniye sürüyor (sadece Atom CPU'lu eski bir EeePC (ilk nesil sanırım), 1.6GHz).

Ve iyi bir sıralama algoritması ile, sıralama en kötü durumda O (n * log (n)) sahiptir, bu yüzden 10 ^ 9 veya daha fazla girdiniz varsa gerçekten sorun olmaz. Ve çoğu zaman sıra listesi zaten bir önceki sıralamaya göre (neredeyse bir önceki sıralamaya göre) sıralanacaktır ve sonuçta O (n) olması daha muhtemeldir.

Yani, bunun için endişelenmeyi bırak! Tek gerçek sorun, çoğu DBMS'nin 1000'inci girdiye doğrudan erişememesidir. Yani, gibi bir sorgu SELECT ... LIMIT 1000, 5 En az 1005 girişi sorgulamak ve ilk 1000'i atlamak zorunda kalacak. Ama buradaki çözüm de çok basit. Sadece depola rank Her satırın yedekli bir sütunu olarak, ona bir dizin ekleyin ve her 15 dakikada bir (ya da her 5 dakika, 30 dakika, 1 saat veya uygulamanız için anlamlı olan her şeyi) hesaplayın. Bununla, rütbeye göre tüm sorgular sadece ikincil dizin aramalarıdır (yaklaşık O (log (N))), ki bu da son derece hızlıdır ve sorgu başına sadece birkaç milisaniye (ağ buradaki veri tabanı değil, darboğazdır) olacaktır.

Not: Başka bir cevaba, sıralanmış girdileri önbelleğe alamadığınızı, çünkü belleğiniz için çok büyük olduklarını belirttiniz. Önbelleğe aldığınızı varsayarak (user_id, rütbe) iki 64 bit tamsayı ile tuples (32 bit de fazlasıyla yeterli olur!), 10 ^ 6 girişi saklamak için 8 MB'den daha az bellek gerekir. Bunun için yeterli RAM olmadığından emin misin?

Öyleyse, açıkça bir darboğaz (henüz) olmayan bir şey optimize etmeye çalışmayın lütfen ...


2
2018-03-28 22:48



Tux21b yanıtı için teşekkürler! Çok bilgilendirici! Ne yazık ki, 15, 30 ya da 60 dakikayı her zaman söylediğinizi ve güncellemeyi yapmak, sorgunun tüm satırları yeni sıralarıyla güncellemek için yaklaşık 1 dakika çalışmasına neden olacaktır; bu, veritabanı büyüdükçe hala artacaktır. Ona başvurmak zorunda kalabilirim ama cron işinde bu kadar ağır sorguları çalıştırmamayı tercih ederim. Sonuçların önbelleğe alınmasına bakacağım, bu iyi bir çözüm olabilir. - Naatan
Sıralamayı kendi masasında saklamak nasıl olur? (Kullanıcı_kimliği, sıralaması, sıralamadaki dizin)? Her N dakika, boş ve yeniden yükle. - Philip Kelley


Her bir oyuncunun sıralamasını oyuncu tablosunda yedekli olarak saklayabilirsiniz, böylece herhangi bir katılma işlemi yapmanız gerekmez. Her seferinde, skor tabloları yeniden hesaplandığında, oynatıcı tabloları da güncellenmelidir.


1
2018-03-28 17:46



Yapabileceğimi biliyorum ancak bu güncelleme, yürütme süresini önemli ölçüde azaltan bir veritabanı / güncelleme mekanizması bilmediğiniz için milyonlarca girişle sonsuza kadar sürecek bir şekilde devam ediyor? - Naatan
Denebilirsin tetikleyiciler - Alp
Bir tetikleyici, tetiklenecek sorgulamanın yürütme süresini nasıl azaltır? Ayrıca, bu rütbeden sonra gelen satırların tüm sıralarını dengeleyeceği için eklenen / güncellenen satırın rütbesini basitçe güncelleyemiyorum. - Naatan


Bu probleme yaklaşmanın iki yolunu düşünebilirim:

İlk yaklaşım: Toplu işlerde güncelleme:

  • Puanları sırala, sıralama elde et
  • Oyuncuları player0-player999, player1000-player1999, vb. Gibi gruplara ayırın.
  • Her bir toplu iş için mevcut tablodaki yeni verilerle çakışan girdileri silin. Bu, geçerli gruptaki oyunculara ait mevcut girişlerin silinmesi veya şu anda geçerli toplu işlemde güncellenen sıralar aralığında yer alması anlamına gelir. Ardından, toplu iş için sıralama verilerini veritabanına yükler ve 0,1 sn sonra bir sonraki partiye atlarsınız.

İkinci yaklaşım: Yeni masa

  • Mevcut sıralama tablonuz gibi yeni bir tablo oluşturun (veya kırpın).
  • yeni sıralamayı hesaplayın ve verilerinizi ekleyin
  • Tabloları değiştirin (tercihen onları kilitledikten sonra). Bu bir saniyeden daha az sürmeli.

0
2017-11-30 18:24