页内整理
This commit is contained in:
1 parent
157372ae37
commit
03a1f2bfb2
7 files changed
+199
-3
No files matched your search
@@ -294,6 +294,11 @@ impl Catalog {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 这张表还有隐藏列字节吗
|
||||
pub fn has_dropped_columns(&self, table: &str) -> bool {
|
||||
self.tables.get(table).is_some_and(|e| !e.dropped_columns.is_empty())
|
||||
}
|
||||
|
||||
/// 还有隐藏列字节待整理的表
|
||||
pub fn dropped_tables(&self) -> Vec<String> {
|
||||
self.tables
|
||||
|
||||
@@ -199,6 +199,55 @@ impl HeapPage {
|
||||
pub fn iter_slots(page: &Page) -> Vec<u16> {
|
||||
(0..Self::slot_count(page)).collect()
|
||||
}
|
||||
|
||||
/// 页内死空间字节数:行区里除活行以外的部分
|
||||
pub fn dead_bytes(page: &Page) -> usize {
|
||||
let n = Self::slot_count(page);
|
||||
let mut live = 0;
|
||||
for i in 0..n as usize {
|
||||
let slot_off = SLOT_DIR_START + i * SLOT_SIZE;
|
||||
live += read_u32(&page.data, slot_off + 4) as usize;
|
||||
}
|
||||
(page.data.len() - Self::free_end(page)).saturating_sub(live)
|
||||
}
|
||||
|
||||
/// 页内整理:活行挪到页尾,槽位号不动,返回回收字节数
|
||||
pub fn compact_page(page: &mut Page) -> usize {
|
||||
let reclaimed = Self::dead_bytes(page);
|
||||
if reclaimed == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let n = Self::slot_count(page);
|
||||
let mut live: Vec<(usize, Vec<u8>)> = Vec::new();
|
||||
for i in 0..n as usize {
|
||||
let slot_off = SLOT_DIR_START + i * SLOT_SIZE;
|
||||
let off = read_u32(&page.data, slot_off) as usize;
|
||||
let len = read_u32(&page.data, slot_off + 4) as usize;
|
||||
if len > 0 {
|
||||
live.push((i, page.data[off..off + len].to_vec()));
|
||||
} else {
|
||||
write_u32(&mut page.data, slot_off, 0);
|
||||
}
|
||||
}
|
||||
|
||||
let mut end = page.data.len();
|
||||
for (i, bytes) in live {
|
||||
end -= bytes.len();
|
||||
page.data[end..end + bytes.len()].copy_from_slice(&bytes);
|
||||
write_u32(&mut page.data, SLOT_DIR_START + i * SLOT_SIZE, end as u32);
|
||||
}
|
||||
reclaimed
|
||||
}
|
||||
|
||||
/// 死空间超过四分之一就整理这一页
|
||||
pub fn compact_if_fragmented(page: &mut Page) -> usize {
|
||||
if Self::dead_bytes(page) * 4 >= page.data.len() {
|
||||
Self::compact_page(page)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NamedIndex {
|
||||
@@ -442,6 +491,7 @@ impl HeapTable {
|
||||
let last_id = n - 1;
|
||||
let mut slot = None;
|
||||
self.file.update_page(last_id, |p| {
|
||||
HeapPage::compact_page(p);
|
||||
slot = HeapPage::insert_tuple(p, &tuple);
|
||||
})?;
|
||||
if let Some(s) = slot {
|
||||
@@ -556,7 +606,13 @@ impl HeapTable {
|
||||
/// 按位置删一行并返回它
|
||||
pub fn delete_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
|
||||
let row = self.read_at(page_id, slot)?;
|
||||
let existed = self.file.update_page(page_id, |p| HeapPage::delete_tuple(p, slot))?;
|
||||
let existed = self.file.update_page(page_id, |p| {
|
||||
let ok = HeapPage::delete_tuple(p, slot);
|
||||
if ok {
|
||||
HeapPage::compact_if_fragmented(p);
|
||||
}
|
||||
ok
|
||||
})?;
|
||||
if !existed {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -702,6 +758,19 @@ impl HeapTable {
|
||||
self.file.flush()
|
||||
}
|
||||
|
||||
/// 整理整张表的页内碎片,返回回收字节数
|
||||
pub fn compact_pages(&mut self) -> io::Result<usize> {
|
||||
let n = self.file.num_pages()?;
|
||||
let mut reclaimed = 0;
|
||||
for i in 0..n {
|
||||
if self.file.with_page(i, HeapPage::dead_bytes)? == 0 {
|
||||
continue;
|
||||
}
|
||||
reclaimed += self.file.update_page(i, HeapPage::compact_page)?;
|
||||
}
|
||||
Ok(reclaimed)
|
||||
}
|
||||
|
||||
/// 当前缓存页数
|
||||
pub fn cached_pages(&self) -> usize { self.file.cached_pages() }
|
||||
/// 命中次数
|
||||
|
||||
@@ -817,8 +817,22 @@ impl Server {
|
||||
self.write_via_wal(&op)
|
||||
}
|
||||
|
||||
/// 整理一张表:按当前列定义重写堆页,回收隐藏列占的空间
|
||||
/// 整理一张表:原地回收页内碎片,有隐藏列时重写堆页
|
||||
fn compact_table(&self, table: &str) -> std::io::Result<()> {
|
||||
let rewrite = {
|
||||
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
|
||||
c.has_dropped_columns(table)
|
||||
};
|
||||
if !rewrite {
|
||||
let reclaimed = self.with_existing_table(table, |t| {
|
||||
let n = t.compact_pages()?;
|
||||
t.flush()?;
|
||||
Ok(n)
|
||||
})?;
|
||||
log_info!(core, "compact {} reclaimed {} bytes", table, reclaimed);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = self.with_existing_table(table, |t| t.scan())?;
|
||||
let op = WalOp::Compact {
|
||||
table: table.to_string(),
|
||||
|
||||
@@ -304,3 +304,77 @@ fn secondary_index_is_maintained() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 整表整理要收回删行留下的页内空洞
|
||||
#[test]
|
||||
fn compact_pages_reclaims_dead_space() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
|
||||
let pad = "x".repeat(200);
|
||||
for i in 0..10 {
|
||||
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
|
||||
}
|
||||
let pages = t.num_pages().unwrap();
|
||||
|
||||
t.delete_by_keys(&[3]).unwrap();
|
||||
assert_eq!(t.num_pages().unwrap(), pages, "删一行不该动页数");
|
||||
let reclaimed = t.compact_pages().unwrap();
|
||||
assert!(reclaimed > 0, "整理要回收删掉的 {} 字节", 200);
|
||||
assert_eq!(t.compact_pages().unwrap(), 0, "整理过就没有空洞了");
|
||||
|
||||
let rows = t.scan().unwrap();
|
||||
assert_eq!(rows.len(), 9);
|
||||
assert!(!rows.iter().any(|r| r["id"] == json!(3)));
|
||||
assert_eq!(rows[0]["pad"], json!(pad));
|
||||
}
|
||||
|
||||
/// 删行的页内空间要能被后续插入复用
|
||||
#[test]
|
||||
fn deletes_compact_pages_so_space_is_reused() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
|
||||
let pad = "y".repeat(80);
|
||||
for i in 0..30 {
|
||||
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
|
||||
}
|
||||
assert_eq!(t.num_pages().unwrap(), 1, "30 行要先塞进一页");
|
||||
|
||||
// 删掉中间的行,只留最靠页尾的那一行
|
||||
let keys: Vec<i64> = (10..29).collect();
|
||||
t.delete_by_keys(&keys).unwrap();
|
||||
|
||||
for i in 100..119 {
|
||||
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
|
||||
}
|
||||
assert_eq!(t.num_pages().unwrap(), 1, "删掉的空间要能被复用");
|
||||
assert_eq!(t.scan().unwrap().len(), 30);
|
||||
}
|
||||
|
||||
/// 页内整理之后索引里的位置仍然指向原来的行
|
||||
#[test]
|
||||
fn compact_pages_keeps_index_positions_valid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let data = dir.path().join("t.db");
|
||||
let index = dir.path().join("t.idx");
|
||||
let mut t = HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
|
||||
|
||||
let pad = "z".repeat(80);
|
||||
for i in 0..30 {
|
||||
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
|
||||
}
|
||||
let keys: Vec<i64> = (5..25).collect();
|
||||
t.delete_by_keys(&keys).unwrap();
|
||||
t.compact_pages().unwrap();
|
||||
|
||||
for i in [0, 1, 2, 3, 4, 25, 26, 27, 28, 29] {
|
||||
let found = t.get_by_column_key("id", i).unwrap();
|
||||
assert_eq!(found.as_ref().map(|r| r["pad"].clone()), Some(json!(pad)), "整理后 id={} 仍要能点查", i);
|
||||
}
|
||||
for i in 5..25 {
|
||||
assert!(t.get_by_column_key("id", i).unwrap().is_none(), "删掉的行不该再查出来");
|
||||
}
|
||||
}
|
||||
@@ -480,6 +480,38 @@ fn startup_compaction_reclaims_dropped_columns() -> Result<(), Box<dyn std::erro
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 没有隐藏列时整理表只原地回收页内空间
|
||||
#[test]
|
||||
fn compact_defragments_pages_without_dropped_columns() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, data) = start_server();
|
||||
let mut c = connect()?;
|
||||
let pad = "p".repeat(200);
|
||||
for id in 1..=5 {
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert","table":"items","row":{"id":id,"note":pad}}))?;
|
||||
}
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert","table":"items","row":{"id":6,"note":"doomed-row-marker"}}))?;
|
||||
for id in 7..=11 {
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert","table":"items","row":{"id":id,"note":pad}}))?;
|
||||
}
|
||||
request_ok(&mut c, serde_json::json!({"method":"delete_keys","table":"items","keys":[6]}))?;
|
||||
|
||||
let heap = data.path().join("databases/main/items.db");
|
||||
assert!(
|
||||
std::fs::read(&heap)?.windows(17).any(|w| w == &b"doomed-row-marker"[..]),
|
||||
"删行只留墓碑,字节还在页里"
|
||||
);
|
||||
request_ok(&mut c, serde_json::json!({"method":"compact","table":"items"}))?;
|
||||
assert!(
|
||||
!std::fs::read(&heap)?.windows(17).any(|w| w == &b"doomed-row-marker"[..]),
|
||||
"原地整理要把墓碑字节收掉"
|
||||
);
|
||||
assert_eq!(std::fs::metadata(data.path().join("databases/main/wal.log"))?.len(), 0);
|
||||
|
||||
let rows: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"items"}"#))?;
|
||||
assert_eq!(rows["rows"].as_array().map(Vec::len), Some(10), "rows survive compaction: {}", rows);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 非法库名被拒;system 是唯一保留库
|
||||
#[test]
|
||||
fn databases_reject_unsafe_names_and_keep_only_system_reserved() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
Reference in new issue
Block a user