Pertimbangan kunci primer non-integer

16

Konteks

Saya sedang merancang basis data (pada PostgreSQL 9.6) yang akan menyimpan data dari aplikasi terdistribusi. Karena sifat aplikasi terdistribusi, saya tidak dapat menggunakan bilangan bulat kenaikan-otomatis ( SERIAL) sebagai kunci utama saya karena kondisi ras yang potensial.

Solusi alami adalah dengan menggunakan UUID, atau pengidentifikasi unik secara global. Postgres dilengkapi dengan built-in UUIDjenis , yang cocok.

Masalah yang saya miliki dengan UUID terkait dengan debugging: ini adalah string yang tidak ramah terhadap manusia. Pengenal tidak ff53e96d-5fd7-4450-bc99-111b91875ec5memberi tahu saya apa pun, sedangkan ACC-f8kJd9xKCd, meskipun tidak dijamin unik, memberi tahu saya bahwa saya sedang berurusan dengan suatu ACCobjek.

Dari perspektif pemrograman, biasanya untuk men-debug permintaan aplikasi yang berhubungan dengan beberapa objek yang berbeda. Misalkan programmer salah mencari objek ACC(akun) di ORDtabel (urutan). Dengan pengenal yang bisa dibaca manusia, programmer langsung mengidentifikasi masalah, saat menggunakan UUID dia akan menghabiskan waktu mencari tahu apa yang salah.

Saya tidak membutuhkan keunikan UUID yang "dijamin"; Saya memang membutuhkan ruang untuk membuat kunci tanpa konflik, tetapi UUID berlebihan. Juga, skenario terburuk, itu tidak akan menjadi akhir dunia jika terjadi tabrakan (database menolaknya dan aplikasi dapat pulih). Jadi, dipertimbangkan pertukaran, pengenal yang lebih kecil namun ramah manusia akan menjadi solusi ideal untuk kasus penggunaan saya.

Mengidentifikasi objek aplikasi

Identifier yang saya buat memiliki format berikut:, di {domain}-{string}mana {domain}diganti dengan domain objek (akun, pesanan, produk) dan {string}merupakan string yang dibuat secara acak. Dalam beberapa kasus, bahkan mungkin masuk akal untuk memasukkan {sub-domain}sebelum string acak. Mari kita mengabaikan panjangnya {domain}dan {string}untuk tujuan menjamin keunikan.

Format dapat memiliki ukuran tetap jika membantu kinerja pengindeksan / pencarian.

Masalah

Mengetahui bahwa:

  • Saya ingin memiliki kunci utama dengan format seperti ACC-f8kJd9xKCd.
  • Kunci primer ini akan menjadi bagian dari beberapa tabel.
  • Semua kunci ini akan digunakan pada beberapa sambungan / hubungan, pada basis data 6NF.
  • Sebagian besar tabel memiliki ukuran sedang hingga besar (rata-rata ~ 1M baris; yang terbesar dengan ~ 100M baris).

Mengenai kinerja, apa cara terbaik untuk menyimpan kunci ini?

Di bawah ini adalah empat solusi yang mungkin, tetapi karena saya memiliki sedikit pengalaman dengan database, saya tidak yakin yang mana (jika ada) yang terbaik.

Solusi yang dipertimbangkan

1. Simpan sebagai string ( VARCHAR)

(Postgres tidak membuat perbedaan antara CHAR(n)dan VARCHAR(n), jadi saya mengabaikan CHAR).

Setelah beberapa penelitian, saya telah menemukan bahwa perbandingan string dengan VARCHAR, khusus pada bergabung dengan operasi, lebih lambat daripada menggunakan INTEGER. Ini masuk akal, tetapi apakah itu sesuatu yang harus saya khawatirkan pada skala ini?

2. Simpan sebagai biner ( bytea)

Tidak seperti Postgres, MySQL tidak memiliki UUIDtipe asli . Ada beberapa pos yang menjelaskan cara menyimpan UUID menggunakan bidang 16-byte BINARY, bukan 36-byte VARCHAR. Posting ini memberi saya ide untuk menyimpan kunci sebagai biner ( byteadi Postgres).

Ini menghemat ukuran, tapi saya lebih mementingkan kinerja. Saya kurang beruntung menemukan penjelasan perbandingan mana yang lebih cepat: biner atau string. Saya percaya perbandingan biner lebih cepat. Jika ya, maka byteamungkin lebih baik daripada VARCHAR, meskipun programmer sekarang harus menyandikan / mendekode data setiap saat.

Saya mungkin salah, tapi saya pikir keduanya byteadan VARCHARakan membandingkan (kesetaraan) byte demi byte (atau karakter demi karakter). Apakah ada cara untuk "melewatkan" perbandingan ini selangkah demi selangkah dan hanya membandingkan "semuanya"? (Saya rasa tidak, tetapi tidak perlu biaya pengecekan).

Saya pikir menyimpan sebagai byteasolusi terbaik, tapi saya ingin tahu apakah ada alternatif lain yang saya abaikan. Juga, kekhawatiran yang sama yang saya ungkapkan pada solusi 1 benar: apakah biaya overhead pada perbandingan cukup yang harus saya khawatirkan?

Solusi "Kreatif"

Saya datang dengan dua solusi yang sangat "kreatif" yang dapat bekerja, saya hanya tidak yakin pada tingkat mana (yaitu jika saya akan mengalami kesulitan meningkatkan mereka ke lebih dari beberapa ribu baris dalam sebuah tabel).

3. Simpan sebagai UUIDtetapi dengan "label" yang melekat padanya

Alasan utama untuk tidak menggunakan UUID adalah agar programmer dapat lebih baik men-debug aplikasi. Tetapi bagaimana jika kita dapat menggunakan keduanya: database menyimpan semua kunci sebagai UUIDs saja, tetapi membungkus objek sebelum / setelah query dibuat.

Sebagai contoh, programmer meminta ACC-{UUID}, database mengabaikan ACC-bagian itu, mengambil hasilnya, dan mengembalikan semuanya sebagai {domain}-{UUID}.

Mungkin ini bisa dilakukan dengan peretasan dengan prosedur atau fungsi tersimpan, tetapi beberapa pertanyaan muncul di benak:

  • Apakah ini (menghapus / menambahkan domain pada setiap permintaan) overhead yang besar?
  • Apakah ini mungkin?

Saya tidak pernah menggunakan prosedur atau fungsi tersimpan sebelumnya, jadi saya tidak yakin apakah ini mungkin. Adakah yang bisa menjelaskan? Jika saya dapat menambahkan lapisan transparan antara programmer dan data yang tersimpan, sepertinya solusi yang sempurna.

4. Simpan (favorit saya) sebagai IPv6 cidr

Ya, Anda membacanya dengan benar. Ternyata format alamat IPv6 menyelesaikan masalah saya dengan sempurna .

  • Saya dapat menambahkan domain dan sub-domain pada beberapa oktet pertama, dan menggunakan yang tersisa sebagai string acak.
  • The kemungkinan tabrakan yang OK. (Saya tidak akan menggunakan 2 ^ 128, tetapi masih OK.)
  • Perbandingan kesetaraan (semoga) dioptimalkan, jadi saya mungkin mendapatkan kinerja yang lebih baik daripada hanya menggunakan bytea.
  • Saya benar-benar dapat melakukan beberapa perbandingan yang menarik, seperti contains, tergantung pada bagaimana domain dan hierarki mereka diwakili.

Misalnya, saya menggunakan kode 0000untuk mewakili domain "produk". Kunci 0000:0db8:85a3:0000:0000:8a2e:0370:7334akan mewakili produk 0db8:85a3:0000:0000:8a2e:0370:7334.

Pertanyaan utama di sini adalah: dibandingkan dengan bytea, apakah ada kelebihan atau kekurangan utama dalam menggunakan cidrtipe data?

Renato Siqueira Massaro
sumber
5
Berapa banyak node terdistribusi yang dimungkinkan? Apakah Anda tahu nomor (dan nama) mereka sebelumnya? Apakah Anda mempertimbangkan PK komposit (multikolom)? Domain (tergantung pada pertanyaan pertama saya), ditambah kolom serial sederhana mungkin paling kecil, paling sederhana, dan tercepat ...
Erwin Brandstetter
@ Phil terima kasih! @ErwinBrandstetter Mengenai aplikasi, ini dirancang untuk skala otomatis sesuai dengan beban, jadi ada sangat sedikit informasi sebelumnya. Saya sudah berpikir untuk menggunakan (domain, UUID) sebagai PK, tetapi ini akan mengulangi "domain" di seluruh, domain masih akan menjadi varcharsalah satu masalah lainnya. Saya tidak tahu tentang domain pg, yang bagus untuk dipelajari. Saya melihat domain yang digunakan untuk memvalidasi jika kueri yang diberikan menggunakan objek yang benar, tetapi masih bergantung pada memiliki indeks non-integer. Tidak yakin apakah ada cara "aman" untuk menggunakan di serialsini (tanpa satu langkah kunci).
Renato Siqueira Massaro
1
Domain tidak harus berupa a varchar. Pertimbangkan untuk menjadikannya FK integertipe dan tambahkan tabel pencarian untuknya. Dengan begitu Anda dapat memiliki keterbacaan manusia dan Anda akan melindungi komposit Anda PKdari menyisipkan / memperbarui anomali (menempatkan domain yang tidak ada).
yemet
1
Saya ingin memiliki kunci utama dengan format suka ACC-f8kJd9xKCd. ”← Tampaknya itu adalah pekerjaan untuk komposit PRIMARY KEY tua yang baik .
MDCCL

Jawaban:

5

Menggunakan ltree

Jika IPV6 berfungsi, bagus. Itu tidak mendukung "ACC". ltreetidak.

Jalur label adalah urutan nol atau lebih label yang dipisahkan oleh titik, misalnya L1.L2.L3, yang mewakili jalur dari akar hierarki pohon ke simpul tertentu. Panjang jalur label harus kurang dari 65 kB, tetapi lebih rendah dari 2 kB lebih disukai. Dalam praktiknya ini bukan batasan utama; misalnya, jalur label terpanjang dalam katalog DMOZ ( http://www.dmoz.org ) adalah sekitar 240 byte.

Anda akan menggunakannya seperti ini,

CREATE EXTENSION ltree;
SELECT replace('ACC-f8kJd9xKCd', '-', '.')::ltree;

Kami membuat data sampel.

SELECT x, (
  CASE WHEN x%7=0 THEN 'ACC'
    WHEN x%3=0 THEN 'XYZ'
    ELSE 'COM'
  END ||'.'|| md5(x::text)
  )::ltree
FROM generate_series(1,10000) AS t(x);

CREATE INDEX ON foo USING GIST (ltree);
ANALYZE foo;


  x  |                ltree                 
-----+--------------------------------------
   1 | COM.c4ca4238a0b923820dcc509a6f75849b
   2 | COM.c81e728d9d4c2f636f067f89cc14862c
   3 | XYZ.eccbc87e4b5ce2fe28308fd9f2a7baf3
   4 | COM.a87ff679a2f3e71d9181a67b7542122c
   5 | COM.e4da3b7fbbce2345d7772b0674a318d5
   6 | XYZ.1679091c5a880faf6fb5e6087eb1b2dc
   7 | ACC.8f14e45fceea167a5a36dedd4bea2543
   8 | COM.c9f0f895fb98ab9159f51fd0297e236d

Dan biola ..

                                                          QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=103.23..234.91 rows=1414 width=57) (actual time=0.422..0.908 rows=1428 loops=1)
   Recheck Cond: ('ACC'::ltree @> ltree)
   Heap Blocks: exact=114
   ->  Bitmap Index Scan on foo_ltree_idx  (cost=0.00..102.88 rows=1414 width=0) (actual time=0.389..0.389 rows=1428 loops=1)
         Index Cond: ('ACC'::ltree @> ltree)
 Planning time: 0.133 ms
 Execution time: 1.033 ms
(7 rows)

Lihat dokumen untuk info lebih lanjut dan operator

Jika Anda membuat id produk, saya akan ltree. Jika Anda membutuhkan sesuatu untuk membuatnya, saya akan menggunakan UUID.

Evan Carroll
sumber
1

Mengenai perbandingan kinerja dengan bytea. perbandingan jaringan dilakukan dalam 3 langkah: pertama pada bit umum dari bagian jaringan, kemudian pada panjang bagian jaringan, dan kemudian pada seluruh alamat kedok terbuka. lihat: network_cmp_internal

jadi itu harus sedikit lebih lambat dari byte yang digunakan untuk memcmp. Saya telah menjalankan tes sederhana di atas meja dengan 10 juta baris mencari satu:

  • menggunakan numeric id (integer) saya butuh 1000ms.
  • menggunakan cidr butuh 1300 ms.
  • menggunakan bytea butuh 1250 ms.

Saya tidak bisa mengatakan ada banyak perbedaan antara bytea dan cidr (walaupun celahnya tetap konsisten) Hanya ifpernyataan tambahan - tebak itu tidak terlalu buruk untuk tupel 10m.

Semoga ini membantu - akan senang mendengar apa yang akhirnya Anda pilih.

cohenjo
sumber