2

I am currently working on a project for the management of oil distribution, and i need the receipts of every bill to get stored in a database. I am thinking of building a smart key for the receipts which will contain the first 2 letters of the city, the gas station id, the auto increment number, first letter of the month and the last 2 digits of the year. So it will be somewhat like this: "AA-3-0001-J15". What i am wondering is how to make the AI number to go back at 0001 when the month changes. Any suggestions?

Rhys Jones
  • 5,348
  • 1
  • 23
  • 44
xhulio
  • 1,093
  • 1
  • 13
  • 32
  • 2
    Don't store that number. Just build it in your select query. – juergen d Jan 25 '15 at 10:30
  • i need it to be stored for accounting purposes – xhulio Jan 25 '15 at 10:32
  • 1
    I would like to know the reasoning for such a key. I've never used anything like this and it is interesting to know in what cases such approach is useful. – Vladimir Baranov Jan 25 '15 at 10:34
  • well in my case i would like to know where this receipt came from, what city, what gas station and at what time. the database should deal with multiple gas stations at the same time which may cause an error if i just put a normal pk there in case 2 gas stations make a transaction in the same time. that's why i created this key to prove its uniqueness at least for the next 100 years. another example are the isbn. they show the vendor, publisher and the book id (this often causes for a book to have different isbn's) – xhulio Jan 25 '15 at 10:40
  • 2
    Yes, you can have a need for maintaining sequenced numbers this way. Problem 1 - multiple months have the same first letter (unless you use something unusual for March vs May, say). Problem 2 - repeat for cities. Problem 3 - The only way to do this safely requires blocking until a `COMMIT` happens, which makes this kind of thing a major bottleneck for your registers (thankfully, your groups are pretty small). I go over such a setup [in this answer](http://stackoverflow.com/questions/24184749/sql-server-unique-composite-key-of-two-field-with-second-field-auto-increment/24196374#24196374). – Clockwork-Muse Jan 25 '15 at 11:00
  • thanks i didn't thought the months before, as for the cities that problem is already solved and i'll try your setup. thanks again – xhulio Jan 25 '15 at 11:09
  • side problem - 10,000 transactions per month seems kind of small, especially if the gas station is in a highly frequented area (say, a rest stop on a major highway). Are you sure that's a reasonable limit? 10k/month -> 300/day -> 30/hour (assuming 10-hour day) limit. Generating identical receipt ids can cause people to have kittens. At minimum, you _do_ need to store the base information separately, as in @Ronald's answer (and probably with more detail, like the full timestamp). – Clockwork-Muse Jan 25 '15 at 11:22
  • well i can add another digit there, but the problem stays at when it reaches the limit, full 9, and the time stamp is on every table to keep the timeline of the transactions. however i am facing with multiple gas stations, which means multiple transactions at the same time. and i dont want to store again what information is already stored in different tables – xhulio Jan 25 '15 at 11:27

3 Answers3

2

Do you want to store all that in one column? That sounds to me like a composite key over four columns...

Which could look like the following:

CREATE TABLE receipts (
  CityCode VARCHAR2(2),
  GasStationId NUMERIC,
  AutoKey NUMERIC,
  MonthCode VARCHAR2(2),
  PRIMARY KEY (CityCode, GasStationId, AutoKey, MonthCode)
);

Which DBMS are you using? (MySQL, MSSQL, PostgreSQL, ...?)

If it's MySQL you could have a batch-job which runs on the month's first which executes:

 ALTER TABLE tablename AUTO_INCREMENT = 1

But that logic would be on application layer instead of DB-layer...

Ronald Duck
  • 323
  • 1
  • 11
  • i am using mssql and the idea is to create a concatenated key with just a substring of the other columns not all the columns – xhulio Jan 25 '15 at 10:23
  • the table receipt will only have these attributes: the concatenated key, fuel-type, amount, price, total – xhulio Jan 25 '15 at 10:28
  • @xhulio Regardless of storing a customer-facing key (which may very well be concatenated into the desired form), you're going to want to store the backing data like this. And store the complete timestamp, please. – Clockwork-Muse Jan 25 '15 at 10:45
  • i just want an idea on how to reset the AI number because i know that i cannot do it with triggers and i doubt procedures will help on that. i already created the primary key. – xhulio Jan 25 '15 at 10:49
  • Well, if it's just that, have a look at the following post -> http://stackoverflow.com/questions/510121/reset-autoincrement-in-sql-server-after-delete Maybe that's helping you... – Ronald Duck Jan 25 '15 at 10:51
  • i saw that when i was searching, it's not the same case because i'm not deleting anything and that function does not work on a concatenated keys, it only removes the keys with empty rows and reuses them. – xhulio Jan 25 '15 at 10:55
  • 1
    ...besides the fact that Accountants get very nervous if people start deleting rows out of a `Receipt` table. Like "maybe go to jail", nervous... – Clockwork-Muse Jan 25 '15 at 11:04
  • 1
    I can imagine that... What about solving that issue on application layer instead? How many concurrent clients are accessing the DB for inserting purposes? (what would be a more dirty way, imho...) – Ronald Duck Jan 25 '15 at 11:11
  • well basically every terminal manager would add new employees to that location, new tanker trucks or even smaller gas stations. and each terminal would have the rights to insert data only for some tables, whilst only the pump would insert data in the receipts table. and there will be a general manager who will have access to the whole database but still it will be limited in the receipt table – xhulio Jan 25 '15 at 11:22
  • So, that means, there is just one process running in the whole rollout infrastructure, which inserts into that specific table? Who/what is the general manager in form of artefact in your software architecture? Is he inserting new rows into that table and is another process? Where is that software part running? Probably not running on the server you control? – Ronald Duck Jan 25 '15 at 11:29
  • well if you call all the gas stations as one process running to insert data in that table than yes. the managers will only update the tables that are not related to the sales. they can read it but not alter or inserting a record in the receipt – xhulio Jan 25 '15 at 11:39
  • No, I mean: where is all your code running which is inserting into that table (and has write access)... is it on several machines or just on one big server which is under control (so there are no wanted changes in the running code)? The problem with multiple processes running inserting into that table is, that all clients need to be synchronised - generating that specific primary key. Which is no big deal if there is just one client to that table, which can hold it's last calculated sequence and/or key in memory and begin calculation of the new sequence and/or key for the next insert... – Ronald Duck Jan 25 '15 at 11:46
  • well each gas station has it's own sequence, and the gas station has a maximum of 6 pumps. that means that are at most 6 machines reading the last sequence of that particular station and than if necessary writing in the database a new sequence. than the accountant reads it as receipt number for managerial purposes. it is a not a primary key but it must be unique for each station. – xhulio Jan 25 '15 at 12:01
2

To answer the direct question - how to make the number restart at 1 at the beginning of the month.

Since it is not a simple IDENTITY column, you'll have to implement this functionality yourself.

To generate such complex value you'll have to write a user-defined function or a stored procedure. Each time you need a new value of your key to insert a new row in the table you'll call this function or execute this stored procedure.

Inside the function/stored procedure you have to make sure that it works correctly when two different sessions are trying to insert the row at the same time. One possible way to do it is to use sp_getapplock.

You didn't clarify whether the "auto increment" number is the single sequence across all cities and gas stations, or whether each city and gas station has its own sequence of numbers. Let's assume that we want to have a single sequence of numbers for all cities and gas stations within the same month. When month changes, the sequence restarts.

The procedure should be able to answer the following question when you run it: Is the row that I'm trying to insert the first row of the current month? If the generated value is the first for the current month, then the counter should be reset to 1.

One method to answer this question is to have a helper table, which would have one row for each month. One column - date, second column - last number of the sequence. Once you have such helper table your stored procedure would check: what is the current month? what is the last number generated for this month? If such number exists in the helper table, increment it in the helper table and use it to compose the key. If such number doesn't exist in the helper table, insert 1 into it and use it to compose the key.

Finally, I would not recommend to make this composite value as a primary key of the table. It is very unlikely that user requirement says "make the primary key of your table like this". It is up to you how you handle it internally, as long as accountant can see this magic set of letters and numbers next to the transaction in his report and user interface. Accountant doesn't know what a "primary key" is, but you do. And you know how to join few tables of cities, gas stations, etc. together to get the information you need from a normalized database.

Oh, by the way, sooner or later you will have more than 9999 transactions per month.

Vladimir Baranov
  • 31,799
  • 5
  • 53
  • 90
  • well each gas station has its own sequence, and yes, it might not be a primary key, but it still has to be unique as the id of the receipt, and you're right the transaction number can be more than 9999 but that's easily fixed. what i want is how to reset it in the next month. i will try to create a store procedure to check the month and automatically reset it. thanks you gave me a more clear idea – xhulio Jan 25 '15 at 11:36
  • @xhulio If each gas station has its own sequence, then the helper table would have one more column: GasStationID and it would have one row for each month for each gas station. – Vladimir Baranov Jan 25 '15 at 11:42
  • @xhulio I still can't understand why you have to reset the counter every month. What's wrong with having one sequence for each gas station for the whole life time of that station? – Vladimir Baranov Jan 25 '15 at 11:43
  • that sequence will be the receipt id for managerial purposes not for database. which means when the accountant sees it they for sure would not want to see something like AA-B-15684654896....55464-F20 – xhulio Jan 25 '15 at 11:48
  • @xhulio You think that there will be less than 10,000 transactions per month, so for a year you can have not more than 120,000, for 10 years you'll have 1,200,000, i.e. 7 digits. 7 digits (9,999,999) would last for 1000 months, which is 83 years. At the moment you are going to use 4 digits plus one character for month plus 2 for year. So, you already are using 7 characters. You'll have to use more than 1 character for month, because January, June and July in English language start with the same letter. Plus another character for dash. So, simple plain number would be shorter... – Vladimir Baranov Jan 25 '15 at 11:57
  • as i said, this is not a primary key of the table, but it must be unique, primary key of this table will be a composite key with several columns, however i need this specific column to show let's say, how many transactions have been in a month, in what city and what gas station.and yes maybe the month would be in numbers not in letters to solve the repetition issue – xhulio Jan 25 '15 at 12:06
  • 1
    @xhulio I see. I personally try to fulfill such user requirements not at a database design level. If user wants to know how many transactions they had per month - I'll make a separate report for them. Anyway, the outlined approach should do what you need. I hope it will be helpful for you to work out the final solution, whatever it will be. – Vladimir Baranov Jan 25 '15 at 12:27
  • well since the company wants to automate most of the process, and based on what you said last time, i suggested the team that we coded the algorithm of the receipt id in the fuel pumps and from there to write it in the database. many thanks – xhulio Jan 25 '15 at 13:27
1

In such cases, it is best to use a User-Defined function to generate this key and then store it. Like :

Create Function MyKeyGenerator(
@city varchar(250) = '',
 @gas_station_id varchar(250) = '')

AS


/*Do stuff here

*/

My guess is , you may need another little table that keeps the last generated auto-number for the month and you may need to update it for the first record that generates during the month. For the next records, during the month, you will fetch from there and increment by 1. You can alse use a stored procedure that returns an Integer as a return code, just for the autonumber part and then do the rest in a function.


Btw, you may want to note that, using the first letter of the month has pitfalls, because two months can have the same first letter. May be try the the two-digit-numeric for the month or the first three letters of the month name.


If you ready not to insist the the AI number exactly be of identity type, you can have another table, where it is a non-identity regular integer, and then run an SQL Server Agent Task calling a stored procedure that'll do the incrementing business.

Whirl Mind
  • 884
  • 1
  • 9
  • 18
  • It's worse than that. The only time I can think of where you're _required_ to have per-group sequences usually means you can't have any gaps either (exercise - logically, why?). Which means your procedure has to be resilient in the face of rollbacks, and block until the row is committed. Yes, you need a table storing this. And quite a bit more... – Clockwork-Muse Jan 25 '15 at 11:05
  • I think keys like these, are user-friendly to the end user, such as an Accountant, who can look at the key and find out which month etc, it is. And usually, Accountants are sacrosanct about keys, LoL, they just want it. – Whirl Mind Jan 25 '15 at 11:08
  • 1
    Uh, the accountant will probably need to know which _day_ it happened on, plus most of the other data anyways. If your receipt table doesn't maintain that kind of information, you're going to have lots of other issues. This kind of id on a receipt isn't really for origin-company accountants, it's to aid manual entry for customer-facing lookups, etc. – Clockwork-Muse Jan 25 '15 at 11:12
  • I already created the function @WhirlMind. what i wanted is to avoid the AI number reach 9999 that's why i want to reset it at the end of each month. and finally i found out someone who knows what pain in the ass accountants can be about these things :p – xhulio Jan 25 '15 at 11:13
  • @Clockwork-Muse i want to avoid re-entering the same data for each gas station and secondly these are the bloody user requirements – xhulio Jan 25 '15 at 11:15
  • 1
    Edited my answer towards the end, on AI number gen part. – Whirl Mind Jan 25 '15 at 11:26