Table design for efficiency [closed] - sql

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 8 years ago.
Improve this question
I have a requirement to develop a system. Part of the system is that, every day, a payment for a person is calculated (A person gets given $x.xx per day, based on some rules), and stored in a Transaction table, which at the moment, consists of the personId, the amount and the date.
CREATE TABLE DailyTransaction
(
DailyTransaction INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
PersonId INT NOT NULL,
TransactionDate DATE NOT NULL
)
There are 8,000 people in the system right now, so every day, 8,000 rows are written via a process to the table.
I think, storing the date, is repeating too much. And there may be queries on that date later. So, I'd like to create a 'DailyRun' table, which contains a date, and and id (and more columns later, if needed). So, when I do a daily payment insert run to populate the 8,000 rows, I first create a DailyRun record with the date, and then assign that ID for that row, to the Transaction table.
CREATE TABLE DailyRun
(
DailyRunId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
RunDate DATE NOT NULL
)
CREATE TABLE DailyTransaction
(
DailyTransaction INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
PersonId INT NOT NULL,
DailyRunId INT NOT NULL
)
So, if I wanted all the rows for a specific date, I find the date in the DailyRun table, and then get all the record associated to it, via a foreign key, to the Transaction Table.
That seems OK so far (Unless someone sees an issue?)
BUT, now the issue.
Another requirement is that during the day, an ad-hoc payment can be added to the transaction table. So, an extra one-off payment. But, it doesn't get created as a Run. So, no row in the DailyRun table. An idea is a nullable DailyRun ID in the Transaction table, but ... when I need to get all the transactions for a specific date - well, there's no date field... and now RunID set.. so it won't be found.
What I can do is allow for Null RunID, and also have a nullable 'AdHocPaymentDate' field. So, if it's an Adhoc payment, I can leave RunID null, and populate AdHocPaymentDate?
And if it's a Scheduled payment run, I populate the RunID, and leave the AdHocPaymentDate as null?
But, aren't nullable fields slow, or not recommended for some reason?
Is there a better way to handle this? Maybe a separate 'Ad Hock Payment' table, which holds the ad hoc payments, in addition to the table that holds the regular daily payments?

It seems that there is a business requirement to distinguish the ad hoc payments from the daily runs. I guess you could work with this kind of solution. Using 'DailyRun' and 'AdHoc' as transaction types and then making the business intelligence insert the transaction properly from daily runs (service) and adhocs (user interaction)
CREATE TABLE [TransactionType]
(
[Id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Name] NVARCHAR(40)
)
CREATE TABLE [Transaction]
(
[Id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[TransactionTypeId] INT NOT NULL REFERENCES TransationType
[PersonId] INT NOT NULL REFERENCES person
[Date] DATE NOT NULL
)

I'm not sure how to approach your concern about repeating values in rows. The best I can offer is that there's no principle of normalization that says, "Hunt for values that appear in more than one row. If you find them, replace then with integers and another table." Anyway, if you do that, you'll have row after row of repeating integers.
Also, since both "run" and ad hoc payments have a date, there's no logical reason to remove the date from the table of transactions.
How I'd start . . .
By your description, this isn't a table of transactions. It's a table of payments.
create table payments (
payment_id integer not null,
payment_date date not null,
payment_amount decimal(12, 2) not null
check (payment_amount > 0),
-- For identifying a(d hoc) and r(un) payments
payment_type char(1) not null
check (payment_type in ('a', 'r')),
person_id integer not null, -- references a table not shown
primary key (payment_id),
unique (payment_id, payment_type),
unique (payment_date, payment_type, person_id)
);
The unique constraint on {payment_date, payment_type, person_id} is important. Your description seems to require no more than one "run" payment per date per person, and no more than one ad hoc payment per date per person. In any case, you need a unique constraint on the real data in addition to a primary key constraint on the surrogate ID number.
The unique constraint on {payment_id, payment_type} is also important. If you need to store more details about either run payments or about ad hoc payments, you can build a table like this.
create table payments_ad_hoc (
payment_id integer not null,
payment_type char(1) not null
default 'a'
check (payment_type = 'a'),
other_columns_go_here char(1) not null
default 'x',
primary key (payment_id),
foreign key (payment_id, payment_type)
references payments (payment_id, payment_type)
);
The combination of the check constraint and the foreign key constraint guarantees that rows in this table will reference an ad hoc payment, not just any payment. Structure for "run" payments would be similar.

Related

Two postgresql tables referencing each other

Question may be basic, I don't have any experience with databases.
I have a postgres db with some tables. Two of them are dates and accounts.
The date table has an account_id field referencing an id table in an account table and a balance field that represents the balance that account had at that date. So, many date entities may reference one account entity, many-to-one, okay.
But an account table also has an actual_date field, that must reference the date entity, with actual balance this account has. One account entity may reference one actual date entuty, but date entity can have one or zero account entities referncing it. And if it does have an account referencing it with it's actual_date, it will always be the same account, date itself referencing with account_id.
What kind of relathinship is this? Is it even possible to implement? And if it is, how do I do it?
I came up with this piece of code, but I have no clue if it does what I think it does.
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users,
actual_date_id DATE UNIQUE REFERENCES dates
);
CREATE TABLE dates (
id SERIAL PRIMARY KEY,
account_id INT REFERENCES accounts,
date DATE,
balance INT,
unconfirmed_balance INT
);
P.S. I create tables with init.sql but work with them with sqlalchemy and it would be greate if someone could also show how to define such model with it.
As written the SQL script would never work for two reasons:
a foreign key can only reference the primary key of a table, not any arbitrary column in it. So actual_date_id should be an integer in order to be able to reference the primary key of the dates table.
you can't reference a table that hasn't been created yet, so the foreign key between accounts and dates must be created after both tables are created.
With circular foreign keys it's usually easier to define at least one of them as deferrable, so that you can insert them without the need of e.g. an intermediate NULL value.
So something along the lines (assuming that users already exists)
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users,
actual_date_id integer UNIQUE -- note the data type
);
CREATE TABLE dates (
id SERIAL PRIMARY KEY,
account_id INT REFERENCES accounts,
date DATE,
balance INT,
unconfirmed_balance INT
);
-- now we can add the foreign key from accounts to dates
alter table accounts
add foreign key (actual_date_id)
references dates (id)
deferrable initially deferred;
It might be better to avoid the circular reference to begin with. As you want to make sure that only one "current balance" exists for each account, this could be achieved by adding a flag in the dates table and getting rid of the actual_date_id in the accounts table.
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users
);
CREATE TABLE dates (
id SERIAL PRIMARY KEY,
account_id INT REFERENCES accounts,
is_current_balance boolean not null default false,
date DATE,
balance INT,
unconfirmed_balance INT
);
-- this ensures that there is exactly one row with "is_current_balance = true"
-- for each account
create unique index only_one_current_balance
on dates (account_id)
where is_current_balance;
Before you change a row in dates to be the "current one", you need to reset the existing one to false.
Unrelated, but:
With modern Postgres versions it's recommended to use identity columns instead of serial

Restrict the number of entries in a relation based on conditions across several relations

I am using PostgreSQL and am trying to restrict the number of concurrent loans that a student can have. To do this, I have created a CTE that selects all unreturned loans grouped by StudentID, and counts the number of unreturned loans for each StudentID. Then, I am attempting to create a check constraint that uses that CTE to restrict the number of concurrent loans that a student can have to 7 at most.
The below code does not work because it is syntactically invalid, but hopefully it can communicate what I am trying to achieve. Does anyone know how I could implement my desired restriction on loans?
CREATE TABLE loan (
id SERIAL PRIMARY KEY,
copy_id INTEGER REFERENCES media_copies (copy_id),
account_id INT REFERENCES account (id),
loan_date DATE NOT NULL,
expiry_date DATE NOT NULL,
return_date DATE,
WITH currentStudentLoans (student_id, current_loans) AS
(
SELECT account_id, COUNT(*)
FROM loan
WHERE account_id IN (SELECT id FROM student)
AND return_date IS NULL
GROUP BY account_id
)
CONSTRAINT max_student_concurrent_loans CHECK(
(SELECT current_loans FROM currentStudentLoans) BETWEEN 0 AND 7
)
);
For additional (and optional) context, I include an ER diagram of my database schema.
You cannot do this using an in-line CTE like this. You have several choices.
The first is a UDF and check constraint. Essentially, the logic in the CTE is put in a UDF and then a check constraint validates the data.
The second is a trigger to do the check on this table. However, that is tricky because the counts are on the same table.
The third is storing the total number in another table -- probably accounts -- and keeping it up-to-date for inserts, updates, and deletes on this table. Keeping that value up-to-date requires triggers on loans. You can then put the check constraint on accounts.
I'm not sure which solution fits best in your overall schema. The first is closest to what you are doing now. The third "publishes" the count, so it is a bit clearer what is going on.

SQL/ORACLE- FOREIGN KEY using two columns from other tables

I'm trying to create an table in SQL*Plus that consults two columns from another table. For example,
If table A looks something like this:
CREATE TABLE Customers
(Customer_ID int NOT NULL PRIMARY KEY,
NAME Varchar(30) NOT NULL,
PHONE Varchar(12) NOT NULL,
OUTSTANDING_FEES Varchar(10) NULL);
And if my table B looks something like this:
CREATE TABLE Customer_Fees
(Fee_ID int NOT NULL PRIMARY KEY,
FEE_TYPE Varchar(20) NOT NULL,
AMOUNT Varchar(10) NOT NULL,
CUSTOMER_ID int NOT NULL);
I want to populate the OUTSTANDING_FEES in table A with the AMOUNT in table B, where the CUSTOMER_ID matches among the tables. For my purposes I can assume that any single Customer_ID in table B will only appear once in the table.
I've tried creating both tables, with the table A OUTSTANDING_FEES field being null and then making it a FOREIGN KEY that references table B's AMOUNT field, but it's not working since I need to make sure it also cross references the CUSTOMER_ID fields in both tables.
Thanks if you can help!
You can not create foreign key on a non-primary column from another table(s).
You'll have to create FK on FEE_ID.
CREATE TABLE Customers (
Customer_ID int NOT NULL PRIMARY KEY,
NAME Varchar(30) NOT NULL,
PHONE Varchar(12) NOT NULL,
OUTSTANDING_FEES Varchar(10) FOREIGN KEY REFERENCES Customer_Fees(FEE_ID)
);
You can use AMOUNT field in your select clause.
Select A.Customer_ID,B.AMOUNT from Customers A Join Customer_Fees B
on A.OUTSTANDING_FEES = B.FEE_ID
There are several things wrong with the data model posed in the question.
Defining OUTSTANDING_FEES and AMOUNT as varchar2columns is bad data modelling as both are surely intended to be numeric (monetary) values. Good practice is always to use the most appropriate datatype for the attribute we're modelling.
Building a foreign key between OUTSTANDING_FEES and AMOUNT is wrong because they are not unique identifiers. The amount of money owed by one customer can be the same as the amount of money owed by any other - even all - customers (at the start of term all students owe the same amount of tuition fees). So, a foreign which "references the CUSTOMER_ID fields in both tables" is all that is needed.
The data model doesn't provide any attribute which allows us to distinguish between fees which have been paid and fees which haven't.
The questioner states that "I can assume that any single Customer_ID in table B will only appear once in the table" but in real life we would expect Customers to have multiple fee records, unpaid and paid. Why not model that? Otherwise if there is truly a 1:1 relationship between Customer and Fee then there is no need for two tables.
So, here is an improved model. It uses proper datatype for monetary values; it enforces the foreign key between the two tables using CUSTOMER_ID; consequently it supports a one-to-many relationship between Customer and Fee; finally it tracks paid and unpaid fees.
create table customers
( customer_id integer not null constraint cust_pk primary key
, name varchar2(30) not null
, phone varchar2(12) not null
)
/
create table customer_fees
( fee_id integer not null constraint fees_pk primary key
, fee_type varchar2(20) not null
, amount number not null
, invoice_date date not null
, paid_date date null
, customer_id integer not null constraint fees_cust_fk references customers
)
/
Ah, but what about OUTSTANDING_FEES? Well, that information is derivable from the data in the two tables. There are many ways of writing this query, this approach is just a choice:
select cust.customer_id
, cust.name
, cust.phone
, fees.outstanding_fees
from customers cust
left outer join
( select fees.customer_id
, sum(case when fees.paid_date is null then fees.amount
else 0 end) as outstanding_fees
from customer_fees fees
group by fees.customer_id ) fees on fees.customer_id = cust.customer_id
/
Generally it is better to calculate aggregated values on demand rather than re-calculate them in every transaction. It scales better, certainly with OLTP volumes of data; the physics of a data warehouse is different, but I don't think that's what we're dealing with in this case.

Normalizing a table with duplicate rows and many-to-many relationships

I am designing the database for an accounting system, currently working on the Expenses table.
According to IRS rules, whenever you update a row in any accounting table, you need to cancel out the existing row by negating its values, and create a new row with the modified information, like so:
Set the row's Status flag to "Modified"
Create an identical copy of this row, with all Money fields negated, so that the sum of the two rows is 0
Create a 3rd row, identical to the first one, with the modified data
Each expense has an identity field called ID for internal identification purposes, and an ExpenseID field, which identifies the transaction to the users. The two cannot be the same, because
ExpenseID can be repeated twice if the transaction was modified and its row was duplicated.
ExpenseIDs MUST be consecutive and NEVER have gaps, while identity fields can skip numbers if a transaction is rolled back and the identity is not reseeded.
In general, I believe the primary key should have no business meaning whatsoever.
My problem is that there are other tables used to link these expenses Many-To-Many to other objects in our system. E.g.: each expense can be linked to documents, folders, users, etc.
So it looks something like this:
create table Expenses (
ID int not null identity(1,1),
ExpenseID int not null,
Amount Money not null,
Status tinyint not null,
[...]
)
create table Expenses_Users (
ExpenseID int not null,
UserID int not null
)
alter table Expenses_Users add constraint FK_Expenses_Users_Expenses
foreign key (ExpenseID) references Expenses (ID)
alter table Expenses_Users add constraint FK_Expenses_Users_Users
foreign key (UserID) references Users (ID)
Now, because of the IRS guidelines, I have to duplicate not only rows in the Expenses table, but also in Expenses_Users, and any other table that links Expenses to other tables.
I have two ideas on how to solve this:
Option One: Normalize Expenses like this:
create table Expenses (
ID int not null identity(1,1),
ExpenseID int not null,
Status tinyint not null,
[...]
)
create table ExpensesNormalized (
ExpenseID int not null,
Amount Money not null
)
alter table ExpensesNormalized add constraint FK_ExpensesNormalized_Expenses
foreign key (ExpenseID) references Expenses(ExpenseID)
This means I'll only have to link external tables to Expenses, not ExpensesNormalized. Also, when updating an expense, I'll only duplicate and negate the data in ExpensesNormalized, which means I'll have far less redundant data in the Expenses table.
However, I'll have to use a JOIN clause every single time I SELECT from Expenses. I fear a performance hit because of this.
Option Two: Use the same tables I use now, but have the field Expenses_Users.ExpenseID point to the field Expenses.ExpenseID. This means that I won't have to duplicate any external objects because they'll point to ExpenseID, which may occur several times.
However, this will not be a real foreign key because SQL Server does not allow foreign keys to non-unique fields, so I'll have to implement foreign key logic in a trigger.
I'm having a hard time deciding between these two options. Any feedback would be appreciated.

How do you store business activities in a SQL database?

The goal is to store activities such as inserting, updating, and deleting business records.
One solution I'm considering is to use one table per record to be tracked. Here is a simplified example:
CREATE TABLE ActivityTypes
(
TypeId int IDENTITY(1,1) NOT NULL,
TypeName nvarchar(50) NOT NULL,
CONSTRAINT PK_ActivityTypes PRIMARY KEY (TypeId),
CONSTRAINT UK_ActivityTypes UNIQUE (TypeName)
)
INSERT INTO ActivityTypes (TypeName) VALUES ('WidgetRotated');
INSERT INTO ActivityTypes (TypeName) VALUES ('WidgetFlipped');
INSERT INTO ActivityTypes (TypeName) VALUES ('DingBatPushed');
INSERT INTO ActivityTypes (TypeName) VALUES ('ButtonAddedToDingBat');
CREATE TABLE Activities
(
ActivityId int IDENTITY(1,1) NOT NULL,
TypeId int NOT NULL,
AccountId int NOT NULL,
TimeStamp datetime NOT NULL,
CONSTRAINT PK_Activities PRIMARY KEY (ActivityId),
CONSTRAINT FK_Activities_ActivityTypes FOREIGN KEY (TypeId)
REFERENCES ActivityTypes (TypeId),
CONSTRAINT FK_Activities_Accounts FOREIGN KEY (AccountId)
REFERENCES Accounts (AccountId)
)
CREATE TABLE WidgetActivities
(
ActivityId int NOT NULL,
WidgetId int NOT NULL,
CONSTRAINT PK_WidgetActivities PRIMARY KEY (ActivityId),
CONSTRAINT FK_WidgetActivities_Activities FOREIGN KEY (ActivityId)
REFERENCES Activities (ActivityId),
CONSTRAINT FK_WidgetActivities_Widgets FOREIGN KEY (WidgetId)
REFERENCES Widgets (WidgetId)
)
CREATE TABLE DingBatActivities
(
ActivityId int NOT NULL,
DingBatId int NOT NULL,
ButtonId int,
CONSTRAINT PK_DingBatActivities PRIMARY KEY (ActivityId),
CONSTRAINT FK_DingBatActivities_Activities FOREIGN KEY (ActivityId)
REFERENCES Activities (ActivityId),
CONSTRAINT FK_DingBatActivities_DingBats FOREIGN KEY (DingBatId)
REFERENCES DingBats (DingBatId)
CONSTRAINT FK_DingBatActivities_Buttons FOREIGN KEY (ButtonId)
REFERENCES Buttons (ButtonId)
)
This solution seems good for fetching all activities given a widget or dingbat record id, however it doesn't seem so good for fetching all activities and then trying to determine to which record they refer.
That is, in this example, all the account names and timestamps are stored in a separate table, so it's easy to create reports focused on users and focused on time intervals without the need to know what the activity is in particular.
However, if you did want to report on the activities by type in particular, this solution would require determining to which type of activity the general activity table refers.
I could put all my activity types in one table, however the ID's would not be able to be constrained by a foreign key, instead the table name might be used as an id, which would lead me to use dynamic queries.
Note in the example that a DingBatActivity has an optional button Id. If the button name were to have been edited after being added to the dingbat, the activity would be able to refer to the button and know its name, so if a report listed all activities by dingbat and by button by name, the button name change would automatically be reflected in the activity description.
Looking for some other ideas and how those ideas compromise between programming effort, data integrity, performance, and reporting flexibility.
The way that I usually architect a solution to this problem is similar to inheritance in objects. If you have "activities" that are taking place on certain entities and you want to track those activities then the entities involved almost certainly have something in common. There's your base table. From there you can create subtables off of the base table to track things specific to that subtype. For example, you might have:
CREATE TABLE Objects -- Bad table name, should be more specific
(
object_id INT NOT NULL,
name VARCHAR(20) NOT NULL,
CONSTRAINT PK_Application_Objects PRIMARY KEY CLUSTERED (application_id)
)
CREATE TABLE Widgets
(
object_id INT NOT NULL,
height DECIMAL(5, 2) NOT NULL,
width DECIMAL(5, 2) NOT NULL,
CONSTRAINT PK_Widgets PRIMARY KEY CLUSTERED (object_id),
CONSTRAINT FK_Widgets_Objects
FOREIGN KEY (object_id) REFERENCES Objects (object_id)
)
CREATE TABLE Dingbats
(
object_id INT NOT NULL,
label VARCHAR(50) NOT NULL,
CONSTRAINT PK_Dingbats PRIMARY KEY CLUSTERED (object_id),
CONSTRAINT FK_Dingbats_Objects
FOREIGN KEY (object_id) REFERENCES Objects (object_id)
)
Now for your activities:
CREATE TABLE Object_Activities
(
activity_id INT NOT NULL,
object_id INT NOT NULL,
activity_type INT NOT NULL,
activity_time DATETIME NOT NULL,
account_id INT NOT NULL,
CONSTRAINT PK_Object_Activities PRIMARY KEY CLUSTERED (activity_id),
CONSTRAINT FK_Object_Activities_Objects
FOREIGN KEY (object_id) REFERENCES Objects (object_id),
CONSTRAINT FK_Object_Activities_Activity_Types
FOREIGN KEY (activity_type) REFERENCES Activity_Types (activity_type),
)
CREATE TABLE Dingbat_Activities
(
activity_id INT NOT NULL,
button_id INT NOT NULL,
CONSTRAINT PK_Dingbat_Activities PRIMARY KEY CLUSTERED (activity_id),
CONSTRAINT FK_Dingbat_Activities_Object_Activities
FOREIGN KEY (activity_id) REFERENCES Object_Activities (activity_id),
CONSTRAINT FK_Dingbat_Activities_Buttons
FOREIGN KEY (button_id) REFERENCES Object_Activities (button_id),
)
You can add a type code to the base activity if you want to for the type of object which it is affecting or you can just determine that by looking for existence in a subtable.
Here's the big caveat though: Make sure that the objects/activities really do have something in common which relates them and requires you to go down this path. You don't want to store disjointed, unrelated data in the same table. For example, you could use this method to create a table that holds both bank account transactions and celestial events, but that wouldn't be a good idea. At the base level they need to have something in common.
Also, I assumed that all of your activities were related to an account, which is why it's in the base table. Anything in common to ALL activities goes in the base table. Things relevant to only a subtype go in those tables. You could even go multiple levels deep, but don't get carried away. The same goes for the objects (again, bad name here, but I'm not sure what you're actually dealing with). If all of your objects have a color then you can put it in the Objects table. If not, then it would go into sub tables.
I'm going to go out on a limb and take a few wild guesses about what you're really trying to accomplish.
You say you're trying to track 'store activities' I'm going to assume you have the following activities:
Purchase new item
Sell item
Write off item
Hire employee
Pay employee
Fire employee
Update employee record
Ok, for these activities, you need a few different tables: one for inventory, one for departments, and one for employees
The inventory table could have the following information:
inventory:
item_id (pk)
description (varchar)
number_in_stock (number)
cost_wholesale (number)
retail_price (number)
dept_id (fk)
department:
dept_id (pk)
description (varchar)
employee
emp_id (pk)
first_name (varchar)
last_name (varchar)
salary (number)
hire_date (date)
fire_date (date)
So, when you buy new items, you will either update the number_in_stock in inventory table, or create a new row if it is an item you've never had before. When you sell an item, you decriment the number_in_stock for that item (also for when you write off an item).
When you hire a new employee, you add a record from them to the employees table. When you pay them, you grab their salary from the salary column. When you fire them, you fill in that column for their record (and stop paying them).
In all of this, the doing is not done by the database. SQL should be used for keeping track of information. It's fine to write procedures for doing these updates (a new invoice procedure that updates all the items from an invoice record). But you don't need a table to do stuff. In fact, a table can't do anything.
When designing a database, the question you need to ask is not "what do I need to do?" it is "What information do I need to keep track of?"
New answer, based on an different interpretation of the question.
Are you just trying to keep a list of what has happened? If you just need a ordered list of past events, you just need 1 table for it:
action_list
action_list_id (pk)
action_desc (varchar)
event_log:
event_log_id (pk)
event_time (timestamp)
action_list_id (fk)
new_action_added (fk)
action_details_or_description (varchar)
In this, the action_list would be something like:
1 'WidgetRotated'
2 'WidgetFlipped'
3 'DingBatPushed'
4 'AddNewAction'
5 'DeleteExistingAction'
The event_log would be a list of what activities happened, and when. One of your actions would be "add new action" and would require the 'new_action_added' column to be filled in on the event table anytime the action taken is "add new action".
You can create actions for update, remove, add, etc.
EDIT:
I added the action_details_or_description column to event. In this way, you can give further information about an action. For example, if you have a "product changes color" action, the description could be "Red" for the new color.
More broadly, you'll want to think through and map out all the different types of actions you'll be taking ahead of time, so you can set up your table(s) in a way that can accurately contain the data you want to put into them.
How about the SQL logs?
The last time I needed a database transaction logger I used an Instead Of trigger in the database so that it would instead of just updating the record, the database would insert a new record into the log table. This technique meant that I needed an additional table to hold the log for each table in my database and the log table had an additional column with a time stamp. Using this technique you can even store the pre and post update state of the record if you want to.